From c8eacaa409242070f859b07c5085b3f2219cf042 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 15 Sep 2021 01:33:52 +0200 Subject: [PATCH] Add support for mapillary api v4, fixes #364 --- Logic/Actors/ImageSearcher.ts | 2 +- .../ImageProviders/ImageAttributionSource.ts | 2 +- Logic/ImageProviders/Mapillary.ts | 73 ++++++++++++++++--- Models/Constants.ts | 2 +- UI/Base/Img.ts | 17 ++++- UI/Image/AttributedImage.ts | 20 +++-- UI/Image/ImageCarousel.ts | 7 +- test.ts | 53 ++++---------- test/ImageSearcher.spec.ts | 4 +- 9 files changed, 117 insertions(+), 63 deletions(-) diff --git a/Logic/Actors/ImageSearcher.ts b/Logic/Actors/ImageSearcher.ts index 13fb0aebf9..c2d089e513 100644 --- a/Logic/Actors/ImageSearcher.ts +++ b/Logic/Actors/ImageSearcher.ts @@ -100,7 +100,7 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]> public static construct(tags: UIEventSource, imagePrefix = "image", loadSpecial = true): ImageSearcher { 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) } diff --git a/Logic/ImageProviders/ImageAttributionSource.ts b/Logic/ImageProviders/ImageAttributionSource.ts index daf889a6ab..7841c899b1 100644 --- a/Logic/ImageProviders/ImageAttributionSource.ts +++ b/Logic/ImageProviders/ImageAttributionSource.ts @@ -21,7 +21,7 @@ export default abstract class ImageAttributionSource { public abstract SourceIcon(backlinkSource?: string): BaseUIElement; /*Converts a value to a URL. Can return null if not applicable*/ - public PrepareUrl(value: string): string { + public PrepareUrl(value: string): string | UIEventSource{ return value; } diff --git a/Logic/ImageProviders/Mapillary.ts b/Logic/ImageProviders/Mapillary.ts index 51983d0c5c..72a593d299 100644 --- a/Logic/ImageProviders/Mapillary.ts +++ b/Logic/ImageProviders/Mapillary.ts @@ -8,39 +8,92 @@ import {Utils} from "../../Utils"; export class Mapillary extends ImageAttributionSource { public static readonly singleton = new Mapillary(); + + private static readonly v4_cached_urls = new Map>(); + + private static readonly client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2' + private static readonly client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" private constructor() { super(); } - private static ExtractKeyFromURL(value: string) { + private static ExtractKeyFromURL(value: string): { + key: string, + isApiv4?: boolean + } { 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\/([^/]*)/) - if (matchApi !== null) { - return matchApi[1]; + const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/) + if (newApiFormat !== null) { + 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/")) { // Extract the key of the image 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 { return Svg.mapillary_svg(); } - PrepareUrl(value: string): string { - const key = Mapillary.ExtractKeyFromURL(value) - return `https://images.mapillary.com/${key}/thumb-640.jpg?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` + PrepareUrl(value: string): string | UIEventSource { + const keyV = Mapillary.ExtractKeyFromURL(value) + 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(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 { - 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(license) + + } + const key = keyV.key + const metadataURL = `https://a.mapillary.com/v3/images/${key}?client_id=TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2` const source = new UIEventSource(undefined) Utils.downloadJson(metadataURL).then(data => { diff --git a/Models/Constants.ts b/Models/Constants.ts index 76075a740e..539ab8c6fb 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import {Utils} from "../Utils"; 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 public static userJourney = { diff --git a/UI/Base/Img.ts b/UI/Base/Img.ts index f2628bdb9f..947536b05e 100644 --- a/UI/Base/Img.ts +++ b/UI/Base/Img.ts @@ -4,11 +4,15 @@ import BaseUIElement from "../BaseUIElement"; export default class Img extends BaseUIElement { private _src: string; private readonly _rawSvg: boolean; + private _options: { fallbackImage?: string }; - constructor(src: string, rawSvg = false) { + constructor(src: string, rawSvg = false, options?: { + fallbackImage?: string + }) { super(); this._src = src; this._rawSvg = rawSvg; + this._options = options; } static AsData(source: string) { @@ -23,7 +27,7 @@ export default class Img extends BaseUIElement { } protected InnerConstructElement(): HTMLElement { - + const self = this; if (this._rawSvg) { const e = document.createElement("div") e.innerHTML = this._src @@ -35,6 +39,15 @@ export default class Img extends BaseUIElement { el.onload = () => { 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; } } diff --git a/UI/Image/AttributedImage.ts b/UI/Image/AttributedImage.ts index 51e7c4ae5d..69835e105d 100644 --- a/UI/Image/AttributedImage.ts +++ b/UI/Image/AttributedImage.ts @@ -2,16 +2,26 @@ import Combine from "../Base/Combine"; import Attribution from "./Attribution"; import Img from "../Base/Img"; import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource"; +import BaseUIElement from "../BaseUIElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; export class AttributedImage extends Combine { constructor(urlSource: string, imgSource: ImageAttributionSource) { - urlSource = imgSource.PrepareUrl(urlSource) - super([ - new Img(urlSource), - new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon()) - ]); + const preparedUrl = imgSource.PrepareUrl(urlSource) + let img: BaseUIElement; + let attr: BaseUIElement + 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'); } diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 39afb6f53f..4e7d032d28 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -59,7 +59,12 @@ export class ImageCarousel extends Toggle { 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; + } } } \ No newline at end of file diff --git a/test.ts b/test.ts index f06cf5a67a..c34a6c3033 100644 --- a/test.ts +++ b/test.ts @@ -1,41 +1,16 @@ -import {UIEventSource} from "./Logic/UIEventSource"; -import DirectionInput from "./UI/Input/DirectionInput"; -import Loc from "./Models/Loc"; -import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; -import Minimap from "./UI/Base/Minimap"; +const client_token = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" - -const location = new UIEventSource({ - zoom: 18, - lat: 51.2, - lon: 4.3 -}) -DirectionInput.constructMinimap = options => new Minimap(options) - -new DirectionInput( - AvailableBaseLayers.SelectBestLayerAccordingTo(location, new UIEventSource("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") +const image_id = '196804715753265'; +const api_url = 'https://graph.mapillary.com/' + image_id + '?fields=thumb_1024_url&&access_token=' + client_token; +fetch(api_url, + { + headers: {'Authorization': 'OAuth ' + client_token} } -)) - .AttachTo("maindiv")*/ \ No newline at end of file +).then(response => { + return response.json() +}).then( + json => { + const thumbnail_url = json["thumb_1024"] + console.log(thumbnail_url) + } +) \ No newline at end of file diff --git a/test/ImageSearcher.spec.ts b/test/ImageSearcher.spec.ts index 58ea282171..49254fd62b 100644 --- a/test/ImageSearcher.spec.ts +++ b/test/ImageSearcher.spec.ts @@ -19,9 +19,7 @@ export default class ImageSearcherSpec extends T { const result = searcher.data[0]; equal(result.url, "https://www.mapillary.com/map/im/bYH6FFl8LXAPapz4PNSh3Q"); } - ] - - + ], ]); }