2025-03-30 03:10:29 +02:00
import ImageProvider , { PanoramaView , ProvidedImage } from "./ImageProvider"
2022-09-08 21:40:48 +02:00
import BaseUIElement from "../../UI/BaseUIElement"
import { Utils } from "../../Utils"
import { LicenseInfo } from "./LicenseInfo"
import Constants from "../../Models/Constants"
2023-12-14 18:25:35 +01:00
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import MapillaryIcon from "./MapillaryIcon.svelte"
2025-03-30 03:10:29 +02:00
import { Feature , Point } from "geojson"
2025-06-07 01:56:15 +02:00
import { Store , UIEventSource } from "../UIEventSource"
2025-06-27 18:36:02 +02:00
import { ServerSourceInfo } from "../../Models/SourceOverview"
2020-10-17 00:37:45 +02:00
2021-09-29 23:56:59 +02:00
export class Mapillary extends ImageProvider {
2022-09-08 21:40:48 +02:00
public static readonly singleton = new Mapillary ( )
2024-07-27 12:59:38 +02:00
public readonly name = "Mapillary"
2021-10-01 02:57:41 +02:00
private static readonly valuePrefix = "https://a.mapillary.com"
2022-09-08 21:40:48 +02:00
public static readonly valuePrefixes = [
Mapillary . valuePrefix ,
"http://mapillary.com" ,
"https://mapillary.com" ,
"http://www.mapillary.com" ,
2025-04-15 18:18:44 +02:00
"https://www.mapillary.com" ,
2022-09-08 21:40:48 +02:00
]
2021-11-07 16:34:51 +01:00
defaultKeyPrefixes = [ "mapillary" , "image" ]
2021-09-15 01:33:52 +02:00
2022-05-06 12:41:24 +02:00
/ * *
* Indicates that this is the same URL
* Ignores 'stp' parameter
2022-09-08 21:40:48 +02:00
*
2022-05-06 12:41:24 +02:00
* const a = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s1024x768&ccb=10-5&oh=00_AT-ZGTXHzihoaQYBILmEiAEKR64z_IWiTlcAYq_D7Ka0-Q&oe=6278C456&_nc_sid=122ab1"
* const b = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s256x192&ccb=10-5&oh=00_AT9BZ1Rpc9zbY_uNu92A_4gj1joiy1b6VtgtLIu_7wh9Bg&oe=6278C456&_nc_sid=122ab1"
* Mapillary . sameUrl ( a , b ) = > true
* /
static sameUrl ( a : string , b : string ) : boolean {
if ( a === b ) {
return true
}
try {
const aUrl = new URL ( a )
const bUrl = new URL ( b )
if ( aUrl . host !== bUrl . host || aUrl . pathname !== bUrl . pathname ) {
2022-09-08 21:40:48 +02:00
return false
2022-05-06 12:41:24 +02:00
}
2022-09-08 21:40:48 +02:00
let allSame = true
2022-05-06 12:41:24 +02:00
aUrl . searchParams . forEach ( ( value , key ) = > {
if ( key === "stp" ) {
// This is the key indicating the image size on mapillary; we ignore it
return
}
if ( value !== bUrl . searchParams . get ( key ) ) {
allSame = false
return
}
} )
2022-09-08 21:40:48 +02:00
return allSame
2022-05-06 12:41:24 +02:00
} catch ( e ) {
2022-06-03 01:33:41 +02:00
console . debug ( "Could not compare " , a , "and" , b , "due to" , e )
2022-05-06 12:41:24 +02:00
}
2022-09-08 21:40:48 +02:00
return false
2022-05-06 12:41:24 +02:00
}
2023-12-07 01:04:43 +01:00
static createLink (
location : {
lon : number
lat : number
} = undefined ,
zoom : number = 17 ,
pKey? : string
) {
2023-12-02 03:12:34 +01:00
const params = {
focus : pKey === undefined ? "map" : "photo" ,
2023-12-05 18:35:18 +01:00
lat : location?.lat ,
lng : location?.lon ,
2023-12-02 03:12:34 +01:00
z : location === undefined ? undefined : Math . max ( ( zoom ? ? 2 ) - 1 , 1 ) ,
2025-04-15 18:18:44 +02:00
pKey ,
2023-12-02 03:12:34 +01:00
}
const baselink = ` https://www.mapillary.com/app/? `
2023-12-07 01:04:43 +01:00
const paramsStr = Utils . NoNull (
Object . keys ( params ) . map ( ( k ) = >
params [ k ] === undefined ? undefined : k + "=" + params [ k ]
)
)
2023-12-02 03:12:34 +01:00
return baselink + paramsStr . join ( "&" )
}
2021-11-11 17:35:24 +01:00
/ * *
* Returns the correct key for API v4 . 0
2025-04-09 17:10:33 +02:00
*
* Mapillary . ExtractKeyFromURL ( "999924810651016" ) // => 999924810651016
2021-11-11 17:35:24 +01:00
* /
private static ExtractKeyFromURL ( value : string ) : number {
2022-09-08 21:40:48 +02:00
let key : string
2021-11-07 16:34:51 +01:00
2024-01-02 18:06:25 +01:00
if ( value . startsWith ( "http" ) ) {
try {
const url = new URL ( value . toLowerCase ( ) )
if ( url . searchParams . has ( "pkey" ) ) {
const pkey = Number ( url . searchParams . get ( "pkey" ) )
if ( ! isNaN ( pkey ) ) {
return pkey
}
}
} catch ( e ) {
console . log ( "Could not parse value for mapillary:" , value )
}
}
if ( value . startsWith ( Mapillary . valuePrefix ) ) {
2021-11-11 17:35:24 +01:00
key = value . substring ( 0 , value . lastIndexOf ( "?" ) ) . substring ( value . lastIndexOf ( "/" ) + 1 )
2022-05-06 12:41:24 +02:00
} else if ( value . match ( "[0-9]*" ) ) {
2022-09-08 21:40:48 +02:00
key = value
2021-06-18 01:25:13 +02:00
}
2021-09-26 17:36:39 +02:00
2021-11-11 17:35:24 +01:00
const keyAsNumber = Number ( key )
if ( ! isNaN ( keyAsNumber ) ) {
return keyAsNumber
2021-09-15 01:33:52 +02:00
}
2021-11-07 16:34:51 +01:00
2021-11-11 17:35:24 +01:00
return undefined
2021-06-18 01:25:13 +02:00
}
2025-06-27 18:36:02 +02:00
apiUrls ( ) : ServerSourceInfo [ ] {
return [ "https://mapillary.com" , "https://www.mapillary.com" , "https://graph.mapillary.com" ] . map (
url = > ( {
url ,
category : "core" ,
trigger : [ "always" ] ,
sourceAvailable : "proprietary server" , selfhostable : false , openData : true ,
description : "Mapillary is an online service which hosts streetview-imagery. It is used to query and show nearby images. Owned by Meta Inc. (Facebook). MapComplete does only use data, but does not recommend contributing data to Mapillary (instead, we recommend uploading to a panoramax-instance)" ,
moreInfo : [ "https://www.mapillary.com/about" , "https://wiki.openstreetmap.org/wiki/Mapillary" ] ,
} ) ,
)
2023-12-02 03:12:34 +01:00
}
2023-12-07 01:04:43 +01:00
SourceIcon (
2024-10-19 14:44:55 +02:00
img : { id : string ; url : string } ,
2023-12-07 01:04:43 +01:00
location ? : {
lon : number
lat : number
}
) : BaseUIElement {
2023-12-14 18:25:35 +01:00
let url : string = undefined
2024-09-30 01:08:07 +02:00
const id = img . id
2023-12-14 18:25:35 +01:00
if ( id ) {
url = Mapillary . createLink ( location , 16 , "" + id )
2023-12-02 03:12:34 +01:00
}
2023-12-14 18:25:35 +01:00
return new SvelteUIElement ( MapillaryIcon , { url } )
2021-06-18 01:25:13 +02:00
}
2024-09-28 02:04:14 +02:00
async ExtractUrls ( key : string , value : string ) : Promise < ProvidedImage [ ] > {
const img = await this . PrepareUrlAsync ( key , value )
return [ img ]
2021-09-29 23:56:59 +02:00
}
2025-03-30 03:10:29 +02:00
/ * *
* Download data necessary for the 360 ° - viewer
* /
2025-04-15 18:18:44 +02:00
public async getPanoramaInfo ( image : {
id : number | string
} ) : Promise < Feature < Point , PanoramaView > > {
2025-03-30 03:10:29 +02:00
const pkey = image . id
const metadataUrl =
"https://graph.mapillary.com/" +
pkey +
"?fields=computed_compass_angle,geometry,is_pano,thumb_2048_url,thumb_original_url&access_token=" +
Constants . mapillary_client_token_v4
2025-04-15 18:18:44 +02:00
const response = await Utils . downloadJsonCached < {
computed_compass_angle : number
geometry : Point
2025-03-30 03:10:29 +02:00
2025-04-15 18:18:44 +02:00
is_pano : boolean
thumb_2048_url : string
thumb_original_url : string
id : string
} > ( metadataUrl , 60 * 60 )
2025-03-30 03:10:29 +02:00
return {
type : "Feature" ,
geometry : response.geometry ,
properties : {
url : response.thumb_2048_url ,
2025-04-15 18:18:44 +02:00
northOffset : response.computed_compass_angle ,
2025-06-05 11:40:56 +02:00
provider : this ,
2025-06-27 18:36:02 +02:00
imageMeta : image ,
2025-04-15 18:18:44 +02:00
} ,
2025-03-30 03:10:29 +02:00
}
}
2024-07-21 10:52:51 +02:00
public async DownloadAttribution ( providedImage : { id : string } ) : Promise < LicenseInfo > {
2024-04-01 02:00:48 +02:00
const mapillaryId = providedImage . id
const metadataUrl =
"https://graph.mapillary.com/" +
mapillaryId +
"?fields=thumb_1024_url,thumb_original_url,captured_at,creator&access_token=" +
Constants . mapillary_client_token_v4
2025-04-09 17:10:33 +02:00
const response = await Utils . downloadJsonCached < {
2025-04-15 18:18:44 +02:00
thumb_1024_url : string
thumb_original_url : string
captured_at
creator : string
2025-04-09 17:10:33 +02:00
} > ( metadataUrl , 60 * 60 )
2024-04-01 02:00:48 +02:00
2021-11-11 17:35:24 +01:00
const license = new LicenseInfo ( )
2024-04-01 02:00:48 +02:00
license . artist = response [ "creator" ] [ "username" ]
2022-09-08 21:40:48 +02:00
license . license = "CC BY-SA 4.0"
2021-11-11 17:35:24 +01:00
// license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
2022-09-08 21:40:48 +02:00
license . attributionRequired = true
2025-05-12 11:37:26 +02:00
const date = response [ "captured_at" ]
try {
license . date = new Date ( date )
} catch ( e ) {
2025-06-04 00:21:28 +02:00
console . warn (
"Could not parse captured_at date from mapillary image. The date is:" ,
date
)
2025-05-12 11:37:26 +02:00
}
2021-09-26 17:36:39 +02:00
return license
2020-10-17 00:37:45 +02:00
}
2021-11-07 16:34:51 +01:00
private async PrepareUrlAsync ( key : string , value : string ) : Promise < ProvidedImage > {
2021-11-11 17:35:24 +01:00
const mapillaryId = Mapillary . ExtractKeyFromURL ( value )
if ( mapillaryId === undefined ) {
2022-09-08 21:40:48 +02:00
return undefined
2021-11-07 16:34:51 +01:00
}
2022-09-08 21:40:48 +02:00
const metadataUrl =
"https://graph.mapillary.com/" +
mapillaryId +
2025-04-09 23:30:39 +02:00
"?fields=thumb_1024_url,thumb_original_url,captured_at,compass_angle,geometry,computed_geometry,creator,camera_type&access_token=" +
2022-09-08 21:40:48 +02:00
Constants . mapillary_client_token_v4
2025-04-09 17:10:33 +02:00
const response = await Utils . downloadJsonCached < {
2025-04-15 18:18:44 +02:00
thumb_1024_url : string
thumb_original_url : string
captured_at
compass_angle : number
creator : string
computed_geometry : Point
geometry : Point
2025-04-09 23:30:39 +02:00
camera_type : "equirectangular" | "spherical" | string
2025-04-09 17:10:33 +02:00
} > ( metadataUrl , 60 * 60 )
2022-09-08 21:40:48 +02:00
const url = < string > response [ "thumb_1024_url" ]
2023-12-07 01:04:43 +01:00
const url_hd = < string > response [ "thumb_original_url" ]
2024-04-01 02:00:48 +02:00
const date = new Date ( )
2025-04-09 23:30:39 +02:00
const rotation : number = ( 720 - Number ( response . compass_angle ) ) % 360
const geometry : Point = response . computed_geometry ? ? response . geometry
date . setTime ( response . captured_at )
2024-04-13 02:40:21 +02:00
return < ProvidedImage > {
2023-12-02 03:12:34 +01:00
id : "" + mapillaryId ,
2023-12-07 01:04:43 +01:00
url ,
url_hd ,
2021-11-11 17:35:24 +01:00
provider : this ,
2024-04-01 02:00:48 +02:00
date ,
2023-12-07 01:04:43 +01:00
key ,
2024-09-12 01:31:00 +02:00
rotation ,
2025-04-15 18:18:44 +02:00
isSpherical :
response . camera_type === "spherical" || response . camera_type === "equirectangular" ,
2024-09-12 01:31:00 +02:00
lat : geometry.coordinates [ 1 ] ,
2025-04-15 18:18:44 +02:00
lon : geometry.coordinates [ 0 ] ,
2025-06-18 21:40:01 +02:00
originalAttribute : { key , value } ,
2021-11-07 16:34:51 +01:00
}
}
2025-04-23 21:35:43 +02:00
2025-05-03 23:48:35 +02:00
public visitUrl (
image : { id : string } ,
location ? : { lon : number ; lat : number }
) : string | undefined {
2025-04-23 21:35:43 +02:00
if ( ! image . id ) {
return
}
return Mapillary . createLink ( location , 17 , image . id )
}
2025-06-07 01:56:15 +02:00
/ * *
* Returns true if we are in firefox strict mode ( or if we are offline )
* @private
* /
private static async checkStrictMode ( ) : Promise < boolean > {
try {
2025-06-18 21:40:01 +02:00
const result = await fetch (
"https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/Xn8-ISUUYQyBD9FyACzPFRGZnBJRqIFmnQ_yd7FU6vxFYwD21fvAcZwDQoMzsScxcQyCWeBviKpWO4nX8yf--neJDvVjC4JlQtfBYb6TrpXQTniyafSFeZeePT_NVx3H6gMjceEvXHyvBqOOcCB_xQ?stp=c2048.2048.2000.988a_s1000x1000&_nc_gid=E2oHnrAtHutVvjaIm9qDLg&_nc_oc=AdkcScR9HuKt1X_K5-GrUeR5Paj8d7MsNFFYEBSmgc0IiBey_wS3RiNJpflWIKaQzNE&ccb=10-5&oh=00_AfNJ1Ki1IeGdUMxdFUc3ZX9VYIVFxVfXZ9MUATU3vj_RJw&oe=686AF002&_nc_sid=201bca"
)
2025-06-07 01:56:15 +02:00
console . log ( "Not blocked, got a forbidden" , result . status )
return false
} catch ( e ) {
console . log ( "Mapillary blocked - probably a scriptblocker" )
return true
}
}
private static _isInStrictMode : UIEventSource < boolean > = undefined
/ * *
* Creates a store which contains true if strict tracking protection is probably enabled .
* This will attempt to fetch a bit of content from mapillary - as that is probably the main culprit
* Loads lazy , so will only attempt to fetch the _first time_ this method is called
* /
public static isInStrictMode ( ) : Store < boolean > {
if ( this . _isInStrictMode === undefined ) {
this . _isInStrictMode = UIEventSource . FromPromise ( this . checkStrictMode ( ) )
}
return this . _isInStrictMode
}
2022-09-08 21:40:48 +02:00
}