forked from MapComplete/MapComplete
Add support for mapillary api v4, fixes #364
This commit is contained in:
parent
5d81e7d792
commit
c8eacaa409
9 changed files with 117 additions and 63 deletions
|
@ -100,7 +100,7 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]>
|
||||||
|
|
||||||
public static construct(tags: UIEventSource<any>, imagePrefix = "image", loadSpecial = true): ImageSearcher {
|
public static construct(tags: UIEventSource<any>, imagePrefix = "image", loadSpecial = true): ImageSearcher {
|
||||||
const key = tags.data["id"] + " " + imagePrefix + loadSpecial;
|
const key = tags.data["id"] + " " + imagePrefix + loadSpecial;
|
||||||
if (ImageSearcher._cache.has(key)) {
|
if (tags.data["id"] !== undefined && ImageSearcher._cache.has(key)) {
|
||||||
return ImageSearcher._cache.get(key)
|
return ImageSearcher._cache.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default abstract class ImageAttributionSource {
|
||||||
public abstract SourceIcon(backlinkSource?: string): BaseUIElement;
|
public abstract SourceIcon(backlinkSource?: string): BaseUIElement;
|
||||||
|
|
||||||
/*Converts a value to a URL. Can return null if not applicable*/
|
/*Converts a value to a URL. Can return null if not applicable*/
|
||||||
public PrepareUrl(value: string): string {
|
public PrepareUrl(value: string): string | UIEventSource<string>{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,39 +8,92 @@ import {Utils} from "../../Utils";
|
||||||
export class Mapillary extends ImageAttributionSource {
|
export class Mapillary extends ImageAttributionSource {
|
||||||
|
|
||||||
public static readonly singleton = new Mapillary();
|
public static readonly singleton = new Mapillary();
|
||||||
|
|
||||||
|
private static readonly v4_cached_urls = new Map<string, UIEventSource<string>>();
|
||||||
|
|
||||||
|
private static readonly client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
|
||||||
|
private static readonly client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ExtractKeyFromURL(value: string) {
|
private static ExtractKeyFromURL(value: string): {
|
||||||
|
key: string,
|
||||||
|
isApiv4?: boolean
|
||||||
|
} {
|
||||||
if (value.startsWith("https://a.mapillary.com")) {
|
if (value.startsWith("https://a.mapillary.com")) {
|
||||||
return value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1);
|
const key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1)
|
||||||
|
return {key:key, isApiv4: !isNaN(Number(key))};
|
||||||
}
|
}
|
||||||
const matchApi = value.match(/https?:\/\/images.mapillary.com\/([^/]*)/)
|
const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/)
|
||||||
if (matchApi !== null) {
|
if (newApiFormat !== null) {
|
||||||
return matchApi[1];
|
return {key: newApiFormat[1], isApiv4: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/)
|
||||||
|
console.log("Mapview matched ", value, mapview)
|
||||||
|
if(mapview !== null){
|
||||||
|
const key = mapview[1]
|
||||||
|
return {key:key, isApiv4: !isNaN(Number(key))};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (value.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) {
|
if (value.toLowerCase().startsWith("https://www.mapillary.com/map/im/")) {
|
||||||
// Extract the key of the image
|
// Extract the key of the image
|
||||||
value = value.substring("https://www.mapillary.com/map/im/".length);
|
value = value.substring("https://www.mapillary.com/map/im/".length);
|
||||||
}
|
}
|
||||||
return value;
|
|
||||||
|
const matchApi = value.match(/https?:\/\/images.mapillary.com\/([^/]*)(&.*)?/)
|
||||||
|
if (matchApi !== null) {
|
||||||
|
return {key: matchApi[1]};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {key: value, isApiv4: !isNaN(Number(value))};
|
||||||
}
|
}
|
||||||
|
|
||||||
SourceIcon(backlinkSource?: string): BaseUIElement {
|
SourceIcon(backlinkSource?: string): BaseUIElement {
|
||||||
return Svg.mapillary_svg();
|
return Svg.mapillary_svg();
|
||||||
}
|
}
|
||||||
|
|
||||||
PrepareUrl(value: string): string {
|
PrepareUrl(value: string): string | UIEventSource<string> {
|
||||||
const key = Mapillary.ExtractKeyFromURL(value)
|
const keyV = Mapillary.ExtractKeyFromURL(value)
|
||||||
return `https://images.mapillary.com/${key}/thumb-640.jpg?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
|
if (!keyV.isApiv4) {
|
||||||
|
return `https://images.mapillary.com/${keyV.key}/thumb-640.jpg?client_id=${Mapillary.client_token_v3}`
|
||||||
|
} else {
|
||||||
|
const key = keyV.key;
|
||||||
|
if(Mapillary.v4_cached_urls.has(key)){
|
||||||
|
return Mapillary.v4_cached_urls.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataUrl ='https://graph.mapillary.com/' + key + '?fields=thumb_1024_url&&access_token=' + Mapillary.client_token_v4;
|
||||||
|
const source = new UIEventSource<string>(undefined)
|
||||||
|
Mapillary.v4_cached_urls.set(key, source)
|
||||||
|
Utils.downloadJson(metadataUrl).then(
|
||||||
|
json => {
|
||||||
|
console.warn("Got response on mapillary image", json, json["thumb_1024_url"])
|
||||||
|
return source.setData(json["thumb_1024_url"]);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected DownloadAttribution(url: string): UIEventSource<LicenseInfo> {
|
protected DownloadAttribution(url: string): UIEventSource<LicenseInfo> {
|
||||||
|
|
||||||
const key = Mapillary.ExtractKeyFromURL(url)
|
const keyV = Mapillary.ExtractKeyFromURL(url)
|
||||||
|
if(keyV.isApiv4){
|
||||||
|
const license = new LicenseInfo()
|
||||||
|
license.artist = "Contributor name unavailable";
|
||||||
|
license.license = "CC BY-SA 4.0";
|
||||||
|
// license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||||
|
license.attributionRequired = true;
|
||||||
|
return new UIEventSource<LicenseInfo>(license)
|
||||||
|
|
||||||
|
}
|
||||||
|
const key = keyV.key
|
||||||
|
|
||||||
const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
|
const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2`
|
||||||
const source = new UIEventSource<LicenseInfo>(undefined)
|
const source = new UIEventSource<LicenseInfo>(undefined)
|
||||||
Utils.downloadJson(metadataURL).then(data => {
|
Utils.downloadJson(metadataURL).then(data => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {Utils} from "../Utils";
|
||||||
|
|
||||||
export default class Constants {
|
export default class Constants {
|
||||||
|
|
||||||
public static vNumber = "0.9.11";
|
public static vNumber = "0.9.12";
|
||||||
|
|
||||||
// The user journey states thresholds when a new feature gets unlocked
|
// The user journey states thresholds when a new feature gets unlocked
|
||||||
public static userJourney = {
|
public static userJourney = {
|
||||||
|
|
|
@ -4,11 +4,15 @@ import BaseUIElement from "../BaseUIElement";
|
||||||
export default class Img extends BaseUIElement {
|
export default class Img extends BaseUIElement {
|
||||||
private _src: string;
|
private _src: string;
|
||||||
private readonly _rawSvg: boolean;
|
private readonly _rawSvg: boolean;
|
||||||
|
private _options: { fallbackImage?: string };
|
||||||
|
|
||||||
constructor(src: string, rawSvg = false) {
|
constructor(src: string, rawSvg = false, options?: {
|
||||||
|
fallbackImage?: string
|
||||||
|
}) {
|
||||||
super();
|
super();
|
||||||
this._src = src;
|
this._src = src;
|
||||||
this._rawSvg = rawSvg;
|
this._rawSvg = rawSvg;
|
||||||
|
this._options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
static AsData(source: string) {
|
static AsData(source: string) {
|
||||||
|
@ -23,7 +27,7 @@ export default class Img extends BaseUIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerConstructElement(): HTMLElement {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
const self = this;
|
||||||
if (this._rawSvg) {
|
if (this._rawSvg) {
|
||||||
const e = document.createElement("div")
|
const e = document.createElement("div")
|
||||||
e.innerHTML = this._src
|
e.innerHTML = this._src
|
||||||
|
@ -35,6 +39,15 @@ export default class Img extends BaseUIElement {
|
||||||
el.onload = () => {
|
el.onload = () => {
|
||||||
el.style.opacity = "1"
|
el.style.opacity = "1"
|
||||||
}
|
}
|
||||||
|
el.onerror = () => {
|
||||||
|
if (self._options?.fallbackImage) {
|
||||||
|
if(el.src === self._options.fallbackImage){
|
||||||
|
// Sigh... nothing to be done anymore
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.src = self._options.fallbackImage
|
||||||
|
}
|
||||||
|
}
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,26 @@ import Combine from "../Base/Combine";
|
||||||
import Attribution from "./Attribution";
|
import Attribution from "./Attribution";
|
||||||
import Img from "../Base/Img";
|
import Img from "../Base/Img";
|
||||||
import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource";
|
import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
|
|
||||||
|
|
||||||
export class AttributedImage extends Combine {
|
export class AttributedImage extends Combine {
|
||||||
|
|
||||||
constructor(urlSource: string, imgSource: ImageAttributionSource) {
|
constructor(urlSource: string, imgSource: ImageAttributionSource) {
|
||||||
urlSource = imgSource.PrepareUrl(urlSource)
|
const preparedUrl = imgSource.PrepareUrl(urlSource)
|
||||||
super([
|
let img: BaseUIElement;
|
||||||
new Img(urlSource),
|
let attr: BaseUIElement
|
||||||
new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())
|
if (typeof preparedUrl === "string") {
|
||||||
]);
|
img = new Img(urlSource);
|
||||||
|
attr = new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())
|
||||||
|
} else {
|
||||||
|
img = new VariableUiElement(preparedUrl.map(url => new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'})))
|
||||||
|
attr = new VariableUiElement(preparedUrl.map(url => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
super([img, attr]);
|
||||||
this.SetClass('block relative h-full');
|
this.SetClass('block relative h-full');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,12 @@ export class ImageCarousel extends Toggle {
|
||||||
return new Img(url);
|
return new Img(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AttributedImage(url, attrSource)
|
try {
|
||||||
|
return new AttributedImage(url, attrSource)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not create an image: ", e)
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
53
test.ts
53
test.ts
|
@ -1,41 +1,16 @@
|
||||||
import {UIEventSource} from "./Logic/UIEventSource";
|
const client_token = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||||
import DirectionInput from "./UI/Input/DirectionInput";
|
|
||||||
import Loc from "./Models/Loc";
|
|
||||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
|
||||||
import Minimap from "./UI/Base/Minimap";
|
|
||||||
|
|
||||||
|
const image_id = '196804715753265';
|
||||||
const location = new UIEventSource<Loc>({
|
const api_url = 'https://graph.mapillary.com/' + image_id + '?fields=thumb_1024_url&&access_token=' + client_token;
|
||||||
zoom: 18,
|
fetch(api_url,
|
||||||
lat: 51.2,
|
{
|
||||||
lon: 4.3
|
headers: {'Authorization': 'OAuth ' + client_token}
|
||||||
})
|
|
||||||
DirectionInput.constructMinimap = options => new Minimap(options)
|
|
||||||
|
|
||||||
new DirectionInput(
|
|
||||||
AvailableBaseLayers.SelectBestLayerAccordingTo(location, new UIEventSource<string | string[]>("map")),
|
|
||||||
location
|
|
||||||
).SetStyle("height: 250px; width: 250px")
|
|
||||||
.SetClass("block")
|
|
||||||
.AttachTo("maindiv")
|
|
||||||
|
|
||||||
/*
|
|
||||||
new VariableUiElement(Hash.hash.map(
|
|
||||||
hash => {
|
|
||||||
let json: {};
|
|
||||||
try {
|
|
||||||
json = atob(hash);
|
|
||||||
} catch (e) {
|
|
||||||
// We try to decode with lz-string
|
|
||||||
json =
|
|
||||||
Utils.UnMinify(LZString.decompressFromBase64(hash))
|
|
||||||
}
|
|
||||||
return new Combine([
|
|
||||||
new FixedUiElement("Base64 decoded: " + atob(hash)),
|
|
||||||
new FixedUiElement("LZ: " + LZString.decompressFromBase64(hash)),
|
|
||||||
new FixedUiElement("Base64 + unminify: " + Utils.UnMinify(atob(hash))),
|
|
||||||
new FixedUiElement("LZ + unminify: " + Utils.UnMinify(LZString.decompressFromBase64(hash)))
|
|
||||||
]).SetClass("flex flex-col m-1")
|
|
||||||
}
|
}
|
||||||
))
|
).then(response => {
|
||||||
.AttachTo("maindiv")*/
|
return response.json()
|
||||||
|
}).then(
|
||||||
|
json => {
|
||||||
|
const thumbnail_url = json["thumb_1024"]
|
||||||
|
console.log(thumbnail_url)
|
||||||
|
}
|
||||||
|
)
|
|
@ -19,9 +19,7 @@ export default class ImageSearcherSpec extends T {
|
||||||
const result = searcher.data[0];
|
const result = searcher.data[0];
|
||||||
equal(result.url, "https://www.mapillary.com/map/im/bYH6FFl8LXAPapz4PNSh3Q");
|
equal(result.url, "https://www.mapillary.com/map/im/bYH6FFl8LXAPapz4PNSh3Q");
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue