From 5fd1d75efb0fbd49cac3eb5f7a37104887f92f10 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 30 Sep 2024 01:08:07 +0200 Subject: [PATCH] Feat: more integrations of panoramax: add panoramax to nearby images view, add to drawer, add icon, see #1451 --- assets/svg/license_info.json | 20 ++ assets/svg/panoramax.svg | 187 ++++++++++++++++ assets/svg/panoramax.svg.license | 2 + assets/svg/panoramax_bw.svg | 217 +++++++++++++++++++ assets/svg/panoramax_bw.svg.license | 2 + langs/en.json | 2 + langs/layers/en.json | 3 + package-lock.json | 28 +-- package.json | 4 +- src/Logic/BBox.ts | 7 + src/Logic/FeatureSource/Sources/MvtSource.ts | 23 +- src/Logic/GeoOperations.ts | 7 + src/Logic/ImageProviders/ImageProvider.ts | 5 +- src/Logic/ImageProviders/Mapillary.ts | 3 +- src/Logic/ImageProviders/Panoramax.ts | 21 +- src/Logic/Web/NearbyImagesSearch.ts | 62 +++++- src/UI/BigComponents/MenuDrawer.svelte | 2 + src/UI/BigComponents/PanoramaxLink.svelte | 40 ++++ src/UI/Image/ImageAttribution.svelte | 2 +- src/UI/Image/NearbyImages.svelte | 2 +- src/Utils.ts | 1 - src/assets/svg/Circle.svelte | 2 +- src/assets/svg/Panoramax.svelte | 4 + src/assets/svg/Panoramax_bw.svelte | 4 + 24 files changed, 617 insertions(+), 33 deletions(-) create mode 100644 assets/svg/panoramax.svg create mode 100644 assets/svg/panoramax.svg.license create mode 100644 assets/svg/panoramax_bw.svg create mode 100644 assets/svg/panoramax_bw.svg.license create mode 100644 src/UI/BigComponents/PanoramaxLink.svelte create mode 100644 src/assets/svg/Panoramax.svelte create mode 100644 src/assets/svg/Panoramax_bw.svelte diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index 9a6714190..8543720f2 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -877,6 +877,26 @@ "https://www.OpenStreetMap.org" ] }, + { + "path": "panoramax.svg", + "license": "LOGO", + "authors": [ + "Panoramax" + ], + "sources": [ + "https://commons.wikimedia.org/wiki/File:Panoramax.svg" + ] + }, + { + "path": "panoramax_bw.svg", + "license": "LOGO", + "authors": [ + "Panoramax" + ], + "sources": [ + "https://commons.wikimedia.org/wiki/File:Panoramax.svg" + ] + }, { "path": "party.svg", "license": "CC-BY-4.0", diff --git a/assets/svg/panoramax.svg b/assets/svg/panoramax.svg new file mode 100644 index 000000000..ed1746c88 --- /dev/null +++ b/assets/svg/panoramax.svg @@ -0,0 +1,187 @@ + + + +image/svg+xml diff --git a/assets/svg/panoramax.svg.license b/assets/svg/panoramax.svg.license new file mode 100644 index 000000000..f0263b4cb --- /dev/null +++ b/assets/svg/panoramax.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Panoramax +SPDX-License-Identifier: LicenseRef-LOGO \ No newline at end of file diff --git a/assets/svg/panoramax_bw.svg b/assets/svg/panoramax_bw.svg new file mode 100644 index 000000000..e50264ff3 --- /dev/null +++ b/assets/svg/panoramax_bw.svg @@ -0,0 +1,217 @@ + + + +image/svg+xml + + + + + + diff --git a/assets/svg/panoramax_bw.svg.license b/assets/svg/panoramax_bw.svg.license new file mode 100644 index 000000000..f0263b4cb --- /dev/null +++ b/assets/svg/panoramax_bw.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Panoramax +SPDX-License-Identifier: LicenseRef-LOGO \ No newline at end of file diff --git a/langs/en.json b/langs/en.json index 300dca8a2..7e5907479 100644 --- a/langs/en.json +++ b/langs/en.json @@ -202,7 +202,9 @@ "openMapillary": "Open Mapillary here", "openOsmcha": "See latest edits made with {theme}", "openOsmchaLastWeek": "See edits from the last 7 days", + "openPanoramax": "Open Panoramax here", "openThemeDocumentation": "Open the documentation for thematic map {name}", + "panoramaxHelp": "Panoramax is an online service which gathers street-level pictures and offers them under a free license. Contributors are allowed to use these pictures to improve OpenStreetMap", "seeOnMapillary": "See this image on Mapillary", "themeBy": "Theme maintained by {author}", "title": "Copyright and attribution", diff --git a/langs/layers/en.json b/langs/layers/en.json index 9158b0c40..60daa8113 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -4142,6 +4142,9 @@ }, "2": { "then": "This drinking water is closed" + }, + "3": { + "then": "This drinking water is permanently closed" } }, "question": "Is this drinking water spot still operational?", diff --git a/package-lock.json b/package-lock.json index cea1831a3..ef5e1ba5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,13 +58,13 @@ "maplibre-gl": "^4.1.1", "marked": "^12.0.2", "monaco-editor": "^0.46.0", - "mvt-to-geojson": "^0.0.2", + "mvt-to-geojson": "^0.0.5", "name-suggestion-index": "^6.0.20240422", "npm": "^10.7.0", "opening_hours": "^3.6.0", "osm-auth": "^2.5.0", "osmtogeojson": "^3.0.0-beta.5", - "panoramax-js": "^0.1.7", + "panoramax-js": "^0.3.6", "panzoom": "^9.4.3", "papaparse": "^5.3.1", "pg": "^8.11.3", @@ -13146,9 +13146,9 @@ "license": "MIT" }, "node_modules/mvt-to-geojson": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/mvt-to-geojson/-/mvt-to-geojson-0.0.2.tgz", - "integrity": "sha512-DLi1Hgowqy6ygOUIuOhtI5RLdph9VpQSP6Ct3qbOY/y0dSTBjTvpIXgbK3J0XSO9feBRrhjcQAMkzOVD/KkuDA==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mvt-to-geojson/-/mvt-to-geojson-0.0.5.tgz", + "integrity": "sha512-IG4bJJP9nxY/9LenWBY0yi5SEv9a1G06trP5b4Tg01LCrzB32US/FO9bygJr7c+ZVbt175AzBwJiiawaxfLTbA==", "dependencies": { "@types/geojson": "^7946.0.14", "pbf": "^3.2.1" @@ -16003,9 +16003,9 @@ "license": "MIT" }, "node_modules/panoramax-js": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.7.tgz", - "integrity": "sha512-G5m0cs+kODGyxmuLI06m+BcQecey2d6zhk+bajUMTkeXqvIIxP+CUS6NKnmN0WyIntd4q91gdGJeOR0Q2I/jeg==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.3.6.tgz", + "integrity": "sha512-CRdXwh91H6chd1PYptG070ukx+S6IkVaUeQVG91ySevoJoCYOuBT65qkMhRo49X2um1nGcs9UqolW90R57875g==", "dependencies": { "@ogcapi-js/features": "^1.1.1", "@ogcapi-js/shared": "^1.1.1", @@ -30241,9 +30241,9 @@ "version": "1.0.0" }, "mvt-to-geojson": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/mvt-to-geojson/-/mvt-to-geojson-0.0.2.tgz", - "integrity": "sha512-DLi1Hgowqy6ygOUIuOhtI5RLdph9VpQSP6Ct3qbOY/y0dSTBjTvpIXgbK3J0XSO9feBRrhjcQAMkzOVD/KkuDA==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mvt-to-geojson/-/mvt-to-geojson-0.0.5.tgz", + "integrity": "sha512-IG4bJJP9nxY/9LenWBY0yi5SEv9a1G06trP5b4Tg01LCrzB32US/FO9bygJr7c+ZVbt175AzBwJiiawaxfLTbA==", "requires": { "@types/geojson": "^7946.0.14", "pbf": "^3.2.1" @@ -32074,9 +32074,9 @@ "version": "1.0.0" }, "panoramax-js": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.7.tgz", - "integrity": "sha512-G5m0cs+kODGyxmuLI06m+BcQecey2d6zhk+bajUMTkeXqvIIxP+CUS6NKnmN0WyIntd4q91gdGJeOR0Q2I/jeg==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.3.6.tgz", + "integrity": "sha512-CRdXwh91H6chd1PYptG070ukx+S6IkVaUeQVG91ySevoJoCYOuBT65qkMhRo49X2um1nGcs9UqolW90R57875g==", "requires": { "@ogcapi-js/features": "^1.1.1", "@ogcapi-js/shared": "^1.1.1", diff --git a/package.json b/package.json index 2f161e592..7137590a0 100644 --- a/package.json +++ b/package.json @@ -200,13 +200,13 @@ "maplibre-gl": "^4.1.1", "marked": "^12.0.2", "monaco-editor": "^0.46.0", - "mvt-to-geojson": "^0.0.2", + "mvt-to-geojson": "^0.0.5", "name-suggestion-index": "^6.0.20240422", "npm": "^10.7.0", "opening_hours": "^3.6.0", "osm-auth": "^2.5.0", "osmtogeojson": "^3.0.0-beta.5", - "panoramax-js": "^0.1.7", + "panoramax-js": "^0.3.6", "panzoom": "^9.4.3", "papaparse": "^5.3.1", "pg": "^8.11.3", diff --git a/src/Logic/BBox.ts b/src/Logic/BBox.ts index b54928418..5f98cb726 100644 --- a/src/Logic/BBox.ts +++ b/src/Logic/BBox.ts @@ -249,6 +249,13 @@ export class BBox { ] } + toLngLatFlat(): [number, number, number, number] { + return [ + this.minLon, this.minLat, + this.maxLon, this.maxLat, + ] + } + public asGeojsonCached() { if (this["geojsonCache"] === undefined) { this["geojsonCache"] = this.asGeoJson({}) diff --git a/src/Logic/FeatureSource/Sources/MvtSource.ts b/src/Logic/FeatureSource/Sources/MvtSource.ts index b65595016..6342fea90 100644 --- a/src/Logic/FeatureSource/Sources/MvtSource.ts +++ b/src/Logic/FeatureSource/Sources/MvtSource.ts @@ -63,7 +63,28 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature return } const buffer = await result.arrayBuffer() - const features = await MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z) + const features = MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z) + for (const feature of features) { + const properties = feature.properties + if(!properties["osm_type"]){ + continue + } + let type: string = "node" + switch (properties["osm_type"]) { + case "N": + type = "node" + break + case "W": + type = "way" + break + case "R": + type = "relation" + break + } + properties["id"] = type + "/" + properties["osm_id"] + delete properties["osm_id"] + delete properties["osm_type"] + } this._features.setData(features) } catch (e) { console.error("Could not download MVT " + this._url + " tile due to", e) diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index 7c3dabb6c..ab3795182 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -92,6 +92,13 @@ export class GeoOperations { return turf.distance(lonlat0, lonlat1, { units: "meters" }) } + /** + * Starting on `from`, travels `distance` meters in the direction of the `bearing` (default: 90) + */ + static destination(from: Coord | [number,number],distance: number, bearing: number = 90): [number,number]{ + return <[number,number]> turf.destination(from, distance, bearing, {units: "meters"}).geometry.coordinates + } + static convexHull(featureCollection, options: { concavity?: number }) { return turf.convex(featureCollection, options) } diff --git a/src/Logic/ImageProviders/ImageProvider.ts b/src/Logic/ImageProviders/ImageProvider.ts index 50878a94a..4588297c8 100644 --- a/src/Logic/ImageProviders/ImageProvider.ts +++ b/src/Logic/ImageProviders/ImageProvider.ts @@ -17,7 +17,8 @@ export interface ProvidedImage { */ rotation?: number lat?: number, - lon?: number + lon?: number, + host?: string } export default abstract class ImageProvider { @@ -25,7 +26,7 @@ export default abstract class ImageProvider { public abstract readonly name: string - public abstract SourceIcon(id?: string, location?: { lon: number; lat: number }): BaseUIElement + public abstract SourceIcon(img?: {id: string, url: string, host?: string}, location?: { lon: number; lat: number }): BaseUIElement /** diff --git a/src/Logic/ImageProviders/Mapillary.ts b/src/Logic/ImageProviders/Mapillary.ts index 5a26e31c2..2cc370f32 100644 --- a/src/Logic/ImageProviders/Mapillary.ts +++ b/src/Logic/ImageProviders/Mapillary.ts @@ -118,13 +118,14 @@ export class Mapillary extends ImageProvider { } SourceIcon( - id: string, + img: {id: string, url: string}, location?: { lon: number lat: number } ): BaseUIElement { let url: string = undefined + const id = img.id if (id) { url = Mapillary.createLink(location, 16, "" + id) } diff --git a/src/Logic/ImageProviders/Panoramax.ts b/src/Logic/ImageProviders/Panoramax.ts index 9536facb6..0fc4da425 100644 --- a/src/Logic/ImageProviders/Panoramax.ts +++ b/src/Logic/ImageProviders/Panoramax.ts @@ -7,6 +7,9 @@ import { LicenseInfo } from "./LicenseInfo" import { GeoOperations } from "../GeoOperations" import Constants from "../../Models/Constants" import { Store, Stores, UIEventSource } from "../UIEventSource" +import SvelteUIElement from "../../UI/Base/SvelteUIElement" +import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte" +import Link from "../../UI/Base/Link" export default class PanoramaxImageProvider extends ImageProvider { @@ -14,13 +17,18 @@ export default class PanoramaxImageProvider extends ImageProvider { public static readonly singleton = new PanoramaxImageProvider() private static readonly xyz = new PanoramaxXYZ() private static defaultPanoramax = new AuthorizedPanoramax(Constants.panoramax.url, Constants.panoramax.token) + public defaultKeyPrefixes: string[] = ["panoramax"] public readonly name: string = "panoramax" private static knownMeta: Record = {} - public SourceIcon(id?: string, location?: { lon: number; lat: number; }): BaseUIElement { - return undefined + public SourceIcon(img?: { id: string, url: string, host?: string }, location?: { lon: number; lat: number; }): BaseUIElement { + const p = new Panoramax(img.host) + return new Link(new SvelteUIElement(Panoramax_bw), p.createViewLink({ + imageId: img?.id, + location + }), true) } public addKnownMeta(meta: ImageData) { @@ -35,7 +43,7 @@ export default class PanoramaxImageProvider extends ImageProvider { private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData, url: string }> { const sequence = "6e702976-580b-419c-8fb3-cf7bd364e6f8" // We always reuse this sequence const url = `https://panoramax.mapcomplete.org/` - const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(sequence, id) + const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(id, sequence) return { url, data } } @@ -67,10 +75,14 @@ export default class PanoramaxImageProvider extends ImageProvider { } const [lon, lat] = GeoOperations.centerpointCoordinates(meta) + const hd = meta.properties + console.log(">>>",meta) + // const hdUrl = new URL(hd) return { id: meta.id, url: makeAbsolute(meta.assets.sd.href), url_hd: makeAbsolute(meta.assets.hd.href), + host: meta["links"].find(l => l.rel === "root")?.href, lon, lat, key: "panoramax", provider: this, @@ -103,8 +115,9 @@ export default class PanoramaxImageProvider extends ImageProvider { } return undefined } + public async ExtractUrls(key: string, value: string): Promise { - if(!Panoramax.isId(value)){ + if (!Panoramax.isId(value)) { return undefined } return [await this.getInfoFor(value).then(r => this.featureToImage(r))] diff --git a/src/Logic/Web/NearbyImagesSearch.ts b/src/Logic/Web/NearbyImagesSearch.ts index 49853485d..bf9f571f4 100644 --- a/src/Logic/Web/NearbyImagesSearch.ts +++ b/src/Logic/Web/NearbyImagesSearch.ts @@ -10,6 +10,7 @@ import { Point } from "geojson" import MvtSource from "../FeatureSource/Sources/MvtSource" import AllImageProviders from "../ImageProviders/AllImageProviders" import { Imgur } from "../ImageProviders/Imgur" +import { Panoramax, PanoramaxXYZ } from "panoramax-js/dist" interface ImageFetcher { /** @@ -102,7 +103,7 @@ class P4CImageFetcher implements ImageFetcher { { mindate: new Date().getTime() - maxAgeSeconds, towardscenter: false, - } + }, ) } catch (e) { console.log("P4C image fetcher failed with", e) @@ -163,6 +164,55 @@ class ImagesInLoadedDataFetcher implements ImageFetcher { } } +class ImagesFromPanoramaxFetcher implements ImageFetcher { + private readonly _radius: number + private readonly _panoramax: Panoramax + name: string = "panoramax" + + constructor(url?: string, radius: number = 100) { + this._radius = radius + if (url) { + + this._panoramax = new Panoramax(url) + } else { + this._panoramax = new PanoramaxXYZ() + } + } + + + public async fetchImages(lat: number, lon: number): Promise { + + const bboxObj = new BBox([ + GeoOperations.destination([lon, lat], this._radius * Math.sqrt(2), -45), + GeoOperations.destination([lon, lat], this._radius * Math.sqrt(2), 135), + ]) + const bbox: [number, number, number, number] = bboxObj.toLngLatFlat() + const images = await this._panoramax.search({ bbox, limit: 1000 }) + + return images.map(i => { + const [lng, lat] = i.geometry.coordinates + return ({ + pictureUrl: i.assets.sd.href, + coordinates: { lng, lat }, + + provider: "panoramax", + direction: i.properties["view:azimuth"], + osmTags: { + "panoramax": i.id, + }, + thumbUrl: i.assets.thumb.href, + date: new Date(i.properties.datetime).getTime(), + license: i.properties["geovisio:license"], + author: i.providers.at(-1).name, + detailsUrl: i.id, + details: { + isSpherical: i.properties["exif"]["Xmp.GPano.ProjectionType"] === "equirectangular", + }, + }) + }) + } +} + class ImagesFromCacheServerFetcher implements ImageFetcher { private readonly _searchRadius: number public readonly name = "fromCacheServer" @@ -186,7 +236,7 @@ class ImagesFromCacheServerFetcher implements ImageFetcher { async fetchImagesForType( targetlat: number, targetlon: number, - type: "lines" | "pois" | "polygons" + type: "lines" | "pois" | "polygons", ): Promise { const { x, y, z } = Tiles.embedded_tile(targetlat, targetlon, 14) @@ -203,7 +253,7 @@ class ImagesFromCacheServerFetcher implements ImageFetcher { }), x, y, - z + z, ) await src.updateAsync() return src.features.data @@ -360,6 +410,8 @@ export class CombinedFetcher { this.sources = [ new ImagesInLoadedDataFetcher(indexedFeatures, radius), new ImagesFromCacheServerFetcher(radius), + new ImagesFromPanoramaxFetcher(), + new ImagesFromPanoramaxFetcher(Constants.panoramax.url), new MapillaryFetcher({ panoramas: "no", max_images: 25, @@ -375,7 +427,7 @@ export class CombinedFetcher { lat: number, lon: number, state: UIEventSource>, - sink: UIEventSource + sink: UIEventSource, ): Promise { try { const pics = await source.fetchImages(lat, lon) @@ -408,7 +460,7 @@ export class CombinedFetcher { public getImagesAround( lon: number, - lat: number + lat: number, ): { images: Store state: Store> diff --git a/src/UI/BigComponents/MenuDrawer.svelte b/src/UI/BigComponents/MenuDrawer.svelte index 989e19ebe..8e95253db 100644 --- a/src/UI/BigComponents/MenuDrawer.svelte +++ b/src/UI/BigComponents/MenuDrawer.svelte @@ -48,6 +48,7 @@ import Copyright from "../../assets/svg/Copyright.svelte" import Pencil from "../../assets/svg/Pencil.svelte" import Squares2x2 from "@babeard/svelte-heroicons/mini/Squares2x2" + import PanoramaxLink from "./PanoramaxLink.svelte" export let state: ThemeViewState let userdetails = state.osmConnection.userDetails @@ -229,6 +230,7 @@ + diff --git a/src/UI/BigComponents/PanoramaxLink.svelte b/src/UI/BigComponents/PanoramaxLink.svelte new file mode 100644 index 000000000..058674c06 --- /dev/null +++ b/src/UI/BigComponents/PanoramaxLink.svelte @@ -0,0 +1,40 @@ + + + + + {#if large} +
+ + +
+ {:else} + + {/if} +
diff --git a/src/UI/Image/ImageAttribution.svelte b/src/UI/Image/ImageAttribution.svelte index 9594c87d8..46bb525fc 100644 --- a/src/UI/Image/ImageAttribution.svelte +++ b/src/UI/Image/ImageAttribution.svelte @@ -16,7 +16,7 @@ let license: Store = UIEventSource.FromPromise( image.provider?.DownloadAttribution(image) ) - let icon = image.provider?.SourceIcon(image.id) + let icon = image.provider?.SourceIcon(image) {#if $license !== undefined} diff --git a/src/UI/Image/NearbyImages.svelte b/src/UI/Image/NearbyImages.svelte index d70dc2ba5..ca9f3ceb8 100644 --- a/src/UI/Image/NearbyImages.svelte +++ b/src/UI/Image/NearbyImages.svelte @@ -130,7 +130,7 @@ for (const f of features) { bbox = bbox.unionWith(BBox.get(f)) } - mapProperties.maxbounds.set(bbox.pad(1.1)) + mapProperties.maxbounds.set(bbox.pad(4)) }) ) diff --git a/src/Utils.ts b/src/Utils.ts index b2b7f879f..421c6c5cf 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1488,7 +1488,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be if (!element) { return } - console.log("Scrolling into view:", element) // Is the element completely in the view? const parentRect = Utils.findParentWithScrolling(element)?.getBoundingClientRect() if (!parentRect) { diff --git a/src/assets/svg/Circle.svelte b/src/assets/svg/Circle.svelte index f1926471a..510ddce25 100644 --- a/src/assets/svg/Circle.svelte +++ b/src/assets/svg/Circle.svelte @@ -1,4 +1,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/svg/Panoramax.svelte b/src/assets/svg/Panoramax.svelte new file mode 100644 index 000000000..6a20baf15 --- /dev/null +++ b/src/assets/svg/Panoramax.svelte @@ -0,0 +1,4 @@ + + image/svg+xml \ No newline at end of file diff --git a/src/assets/svg/Panoramax_bw.svelte b/src/assets/svg/Panoramax_bw.svelte new file mode 100644 index 000000000..71900d7f0 --- /dev/null +++ b/src/assets/svg/Panoramax_bw.svelte @@ -0,0 +1,4 @@ + + image/svg+xml \ No newline at end of file