From 7f5544c1e52aac146fa7313f7b9b7335649f55d2 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 12 May 2025 11:37:26 +0200 Subject: [PATCH 1/4] Fix: license info in 'nearby images' now works for mapillary, add bbox search for panoramax --- src/Logic/ImageProviders/LicenseInfo.ts | 2 +- src/Logic/ImageProviders/Mapillary.ts | 7 ++++++- src/Logic/Web/NearbyImagesSearch.ts | 19 ++++++++++++++++--- .../Json/TagRenderingConfigJson.ts | 2 +- src/UI/Image/LinkableImage.svelte | 17 ++++++++++++++--- src/UI/Image/NearbyImages.svelte | 15 +++++++++------ 6 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/Logic/ImageProviders/LicenseInfo.ts b/src/Logic/ImageProviders/LicenseInfo.ts index da82a9b37b..74f4f0be44 100644 --- a/src/Logic/ImageProviders/LicenseInfo.ts +++ b/src/Logic/ImageProviders/LicenseInfo.ts @@ -8,7 +8,7 @@ export class LicenseInfo { copyrighted?: boolean = false credit?: string = "" description?: string = "" - informationLocation?: URL = undefined + informationLocation?: URL | string = undefined date?: Date views?: number } diff --git a/src/Logic/ImageProviders/Mapillary.ts b/src/Logic/ImageProviders/Mapillary.ts index 86e9ea6297..43cdb7ae70 100644 --- a/src/Logic/ImageProviders/Mapillary.ts +++ b/src/Logic/ImageProviders/Mapillary.ts @@ -192,7 +192,12 @@ export class Mapillary extends ImageProvider { license.license = "CC BY-SA 4.0" // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License"; license.attributionRequired = true - license.date = new Date(response["captured_at"]) + const date = response["captured_at"] + try { + license.date = new Date(date) + } catch (e) { + console.warn("Could not parse captured_at date from mapillary image. The date is:", date) + } return license } diff --git a/src/Logic/Web/NearbyImagesSearch.ts b/src/Logic/Web/NearbyImagesSearch.ts index 4783ee643a..066bb10cf0 100644 --- a/src/Logic/Web/NearbyImagesSearch.ts +++ b/src/Logic/Web/NearbyImagesSearch.ts @@ -9,6 +9,7 @@ import { Utils } from "../../Utils" import { Point } from "geojson" import { Imgur } from "../ImageProviders/Imgur" import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist" +import { Mapillary } from "../ImageProviders/Mapillary" interface ImageFetcher { /** @@ -222,6 +223,11 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher { const promises: Promise[] = [] const maxRadius = this._radius let prevRadius = 0 + + const nearby = this._panoramax.search({ + bbox: new BBox([[lon, lat]]).pad(0.001).toLngLatFlat() + }) + promises.push(nearby) // We do a nearby search with bbox, see https://source.mapcomplete.org/MapComplete/MapComplete/issues/2384 for (const radiusSetting of radiusSettings) { const promise = this._panoramax.search({ place: [lon, lat], @@ -265,7 +271,7 @@ class MapillaryFetcher implements ImageFetcher { async fetchImages(lat: number, lon: number): Promise { const boundingBox = new BBox([[lon, lat]]).padAbsolute(0.003) let url = - "https://graph.mapillary.com/images?fields=geometry,computed_geometry,creator,id,thumb_256_url,thumb_original_url,compass_angle&bbox=" + + "https://graph.mapillary.com/images?fields=geometry,computed_geometry,creator,id,captured_at,thumb_256_url,thumb_original_url,compass_angle&bbox=" + [ boundingBox.getWest(), boundingBox.getSouth(), @@ -293,13 +299,14 @@ class MapillaryFetcher implements ImageFetcher { const response = await Utils.downloadJson<{ data: { id: string - creator: string + creator: { username: string } geometry: Point computed_geometry: Point is_pano: boolean thumb_256_url: string thumb_original_url: string compass_angle: number + captured_at: number }[] }>(url) const pics: P4CPicture[] = [] @@ -308,6 +315,7 @@ class MapillaryFetcher implements ImageFetcher { if (img.thumb_original_url === undefined) { continue } + const [lon, lat] = img.computed_geometry.coordinates pics.push({ pictureUrl: img.thumb_original_url, provider: "Mapillary", @@ -319,6 +327,12 @@ class MapillaryFetcher implements ImageFetcher { details: { isSpherical: this._panoramas === "only", }, + + detailsUrl: Mapillary.singleton.visitUrl(img, { lon, lat }), + date: img.captured_at, + license: "CC-BY-SA", + author: img.creator.username, + direction: img.compass_angle }) } return pics @@ -367,7 +381,6 @@ export class CombinedFetcher { ): Promise { try { const pics = await source.fetchImages(lat, lon) - console.log(source.name, "==>>", pics) state.data[source.name] = "done" state.ping() diff --git a/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts index 31ffba1dd7..a6dbc12865 100644 --- a/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/TagRenderingConfigJson.ts @@ -246,5 +246,5 @@ export interface TagRenderingConfigJson { * Note: if the theme already has a layer with this ID, the value is ignored * group: hidden */ - requiredLayers: { id: string; minzoom?: number }[] + requiredLayers?: { id: string; minzoom?: number }[] } diff --git a/src/UI/Image/LinkableImage.svelte b/src/UI/Image/LinkableImage.svelte index 18aed32692..69f3aff679 100644 --- a/src/UI/Image/LinkableImage.svelte +++ b/src/UI/Image/LinkableImage.svelte @@ -37,18 +37,29 @@ } }) const t = Translations.t.image.nearby + + let date: Date + if (image.date) { + try { + date = new Date(image.date) + } catch (e) { + console.warn("Could not parse image date", image.date, "for", image.detailsUrl) + } + } + let license: LicenseInfo = { artist: image.author, license: image.license, - date: new Date(image.date), - informationLocation: image.detailsUrl, + informationLocation: (image.detailsUrl ?? image.pictureUrl ?? image.thumbUrl), + date } + console.log(">>> trying to create license info based on", image, license) let providedImage: ProvidedImage = { url: image.thumbUrl ?? image.pictureUrl, url_hd: image.pictureUrl, key: undefined, provider: AllImageProviders.byName(image.provider), - date: new Date(image.date), + date, id: Object.values(image.osmTags)[0], isSpherical: image.details.isSpherical, license, diff --git a/src/UI/Image/NearbyImages.svelte b/src/UI/Image/NearbyImages.svelte index 5cfff37420..29bb15c268 100644 --- a/src/UI/Image/NearbyImages.svelte +++ b/src/UI/Image/NearbyImages.svelte @@ -7,7 +7,7 @@ import type { SpecialVisualizationState } from "../SpecialVisualization" import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch" import LinkableImage from "./LinkableImage.svelte" - import type { Feature, Point } from "geojson" + import type { Feature, Geometry, Point } from "geojson" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import Loading from "../Base/Loading.svelte" import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" @@ -25,7 +25,7 @@ import { BBox } from "../../Logic/BBox" import PanoramaxLink from "../BigComponents/PanoramaxLink.svelte" import { GeoOperations } from "../../Logic/GeoOperations" - import type { PanoramaView } from "../../Logic/ImageProviders/ImageProvider" + import type { HotspotProperties, PanoramaView } from "../../Logic/ImageProviders/ImageProvider" export let tags: UIEventSource export let state: SpecialVisualizationState @@ -52,6 +52,7 @@ [loadedImages] ) + // Panorama-views get a geojson feature to browse around let asFeatures = result.map((p4cs) => p4cs.map( (p4c) => @@ -147,25 +148,27 @@ highlighted.set(feature.properties.id) }, }) - let nearbyFeatures: Store = asFeatures.map((nearbyPoints) => { + + let nearbyFeatures: Store[]> = asFeatures.map((nearbyPoints) => { return [ { type: "Feature", geometry: { type: "Point", coordinates: GeoOperations.centerpointCoordinates(feature) }, - properties: { + properties: { name: layer.title?.GetRenderValue(feature.properties).Subs(feature.properties).txt, focus: true, }, }, ...nearbyPoints - .filter((p) => p.properties.spherical === "yes") + .filter((p) => p.properties["spherical"] === "yes") .map((f) => ({ ...f, - properties: { + properties: { name: "Nearby panorama", pitch: "auto", type: "scene", gotoPanorama: f, + focus: false }, })), ] From 913add4295216f45098cf7f8a98bcc263f7a7d93 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 12 May 2025 12:19:14 +0200 Subject: [PATCH 2/4] Fix: fix error reporting --- src/Logic/Osm/Changes.ts | 25 ++++---- src/Logic/Osm/ChangesetHandler.ts | 3 +- src/Models/ThemeViewState/WithChangesState.ts | 58 +++++++++---------- src/Models/ThemeViewState/WithImageState.ts | 2 +- src/UI/Map/ProtomapsLanguageSupport.ts | 3 +- 5 files changed, 48 insertions(+), 43 deletions(-) diff --git a/src/Logic/Osm/Changes.ts b/src/Logic/Osm/Changes.ts index ed2b8c1f47..a547b0f52b 100644 --- a/src/Logic/Osm/Changes.ts +++ b/src/Logic/Osm/Changes.ts @@ -54,7 +54,7 @@ export class Changes { featureSwitchIsTesting?: Store } osmConnection: OsmConnection - reportError?: (error: string) => void + reportError?: ((message: string | Error | XMLHttpRequest, extramessage?: string) => void), featureProperties?: FeaturePropertiesStore historicalUserLocations?: FeatureSource> allElements?: IndexedFeatureSource @@ -75,7 +75,7 @@ export class Changes { } this.state = state this.backend = state.osmConnection.Backend() - this._reportError = state.reportError + this._reportError = (msg, err) => state.reportError(msg, err) this._changesetHandler = new ChangesetHandler( state.featureSwitches?.featureSwitchIsTesting ?? new ImmutableStore(false), state.osmConnection, @@ -669,19 +669,24 @@ export class Changes { const createdIds = new Set( pending.filter((cd) => cd.changes !== undefined).map((cd) => cd.id) ) - pending.forEach((c) => { - if (c.id < 0) { - if (createdIds.has(c.id)) { + for (const c of pending) { + let id = c.id + const newId = this._changesetHandler._remappings.get(c.type + "/" + c.id) + if (newId) { + id = Number(newId.split("/")[1]) + } + if (id < 0) { + if (createdIds.has(id)) { toUpload.push(c) } else { this._reportError( - `Got an orphaned change. The 'creation'-change description for ${c.type}/${c.id} got lost. Permanently dropping this change:` + + `Got an orphaned change. The 'creation'-change description for ${c.type}/${id} got lost. Permanently dropping this change:` + JSON.stringify(c) ) } - return + continue } - const matchFound = !!objects.find((o) => o.id === c.id && o.type === c.type) + const matchFound = !!objects.find((o) => o.id === id && o.type === c.type) if (matchFound) { toUpload.push(c) } else { @@ -689,12 +694,12 @@ export class Changes { "Refusing change about " + c.type + "/" + - c.id + + id + " as not in the objects. No internet?" ) refused.push(c) } - }) + } return { refused, toUpload } } diff --git a/src/Logic/Osm/ChangesetHandler.ts b/src/Logic/Osm/ChangesetHandler.ts index 7e8a2fb7a1..72c3df8854 100644 --- a/src/Logic/Osm/ChangesetHandler.ts +++ b/src/Logic/Osm/ChangesetHandler.ts @@ -40,7 +40,8 @@ export class ChangesetHandler { private readonly backend: string /** - * Contains previously rewritten IDs + * Contains previously rewritten IDs, e.g. {"node/-1" --> "node/123456"} + * * @private */ public readonly _remappings = new Map() diff --git a/src/Models/ThemeViewState/WithChangesState.ts b/src/Models/ThemeViewState/WithChangesState.ts index 0f5499b725..27640295d4 100644 --- a/src/Models/ThemeViewState/WithChangesState.ts +++ b/src/Models/ThemeViewState/WithChangesState.ts @@ -1,5 +1,7 @@ import { Changes } from "../../Logic/Osm/Changes" -import { NewGeometryFromChangesFeatureSource } from "../../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" +import { + NewGeometryFromChangesFeatureSource +} from "../../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" import { WithLayoutSourceState } from "./WithLayoutSourceState" import ThemeConfig from "../ThemeConfig/ThemeConfig" import { Utils } from "../../Utils" @@ -18,9 +20,7 @@ import { Map as MlMap } from "maplibre-gl" import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource" import ShowDataLayer from "../../UI/Map/ShowDataLayer" import SelectedElementTagsUpdater from "../../Logic/Actors/SelectedElementTagsUpdater" -import NoElementsInViewDetector, { - FeatureViewState, -} from "../../Logic/Actors/NoElementsInViewDetector" +import NoElementsInViewDetector, { FeatureViewState } from "../../Logic/Actors/NoElementsInViewDetector" export class WithChangesState extends WithLayoutSourceState { readonly changes: Changes @@ -43,7 +43,7 @@ export class WithChangesState extends WithLayoutSourceState { osmConnection: this.osmConnection, featureProperties: this.featureProperties, historicalUserLocations: this.historicalUserLocations, - reportError: this.reportError, + reportError: (err, msg) => this.reportError(err, msg) }, theme?.isLeftRightSensitive() ?? false ) @@ -104,16 +104,6 @@ export class WithChangesState extends WithLayoutSourceState { return } const isTesting = this.featureSwitchIsTesting?.data - console.log( - isTesting - ? ">>> _Not_ reporting error to report server as testmode is on" - : ">>> Reporting error to", - Constants.ErrorReportServer, - message - ) - if (isTesting) { - return - } if ("" + message === "[object XMLHttpRequest]") { const req = message @@ -137,25 +127,35 @@ export class WithChangesState extends WithLayoutSourceState { } const stacktrace: string = new Error().stack - try { + const err = { + stacktrace, + message: "" + message, + theme: this.theme?.id, + version: Constants.vNumber, + language: this.userRelatedState.language.data, + username: this.osmConnection.userDetails.data?.name, + userid: this.osmConnection.userDetails.data?.uid, + pendingChanges: this.changes.pendingChanges.data, + previousChanges: this.changes.allChanges.data, + changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings) + } + console.trace( + isTesting + ? ">>> _Not_ reporting error to report server as testmode is on" + : ">>> Reporting error to", + Constants.ErrorReportServer, + message, err + ) + if (isTesting) { + return + } await fetch(Constants.ErrorReportServer, { method: "POST", - body: JSON.stringify({ - stacktrace, - message: "" + message, - theme: this.theme.id, - version: Constants.vNumber, - language: this.userRelatedState.language.data, - username: this.osmConnection.userDetails.data?.name, - userid: this.osmConnection.userDetails.data?.uid, - pendingChanges: this.changes.pendingChanges.data, - previousChanges: this.changes.allChanges.data, - changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings), - }), + body: JSON.stringify(err) }) } catch (e) { - console.error("Could not upload an error report") + console.error("Could not upload an error report", e) } } diff --git a/src/Models/ThemeViewState/WithImageState.ts b/src/Models/ThemeViewState/WithImageState.ts index da1d564ebe..79a55aa8bd 100644 --- a/src/Models/ThemeViewState/WithImageState.ts +++ b/src/Models/ThemeViewState/WithImageState.ts @@ -29,7 +29,7 @@ export class WithImageState extends WithGuiState implements SpecialVisualization this.osmConnection, this.changes, this.geolocation.geolocationState.currentGPSLocation, - this.reportError + (err, msg) => this.reportError(err, msg) ) const longAgo = new Date() longAgo.setTime(new Date().getTime() - 5 * 365 * 24 * 60 * 60 * 1000) diff --git a/src/UI/Map/ProtomapsLanguageSupport.ts b/src/UI/Map/ProtomapsLanguageSupport.ts index 7df4ab144a..3f5a75b7aa 100644 --- a/src/UI/Map/ProtomapsLanguageSupport.ts +++ b/src/UI/Map/ProtomapsLanguageSupport.ts @@ -3,7 +3,7 @@ import { DataDrivenPropertyValueSpecification, LayerSpecification, Map as MlMap, - SymbolLayerSpecification, + SymbolLayerSpecification } from "maplibre-gl" import Locale from "../i18n/Locale" import { Utils } from "../../Utils" @@ -1752,7 +1752,6 @@ export class ProtomapsLanguageSupport { } const newExpressionF = ProtomapsLanguageSupport.expressions[layer.id] if (!newExpressionF) { - console.log(">>> No function found for", layer.id) return } const newExpression = newExpressionF(language) From b22dcd58c58aebb488eb1d5b6f41426894fa8fa5 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 12 May 2025 12:19:29 +0200 Subject: [PATCH 3/4] chore(release): 0.51.7 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf688dbc29..e93528b6f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.51.7](https://source.mapcomplete.org/MapComplete/MapComplete/compare/v0.51.6...v0.51.7) (2025-05-12) + + +### Bug Fixes + +* fix error reporting ([913add4](https://source.mapcomplete.org/MapComplete/MapComplete/commits/913add4295216f45098cf7f8a98bcc263f7a7d93)) +* license info in 'nearby images' now works for mapillary, add bbox search for panoramax ([7f5544c](https://source.mapcomplete.org/MapComplete/MapComplete/commits/7f5544c1e52aac146fa7313f7b9b7335649f55d2)) + ### [0.51.6](https://source.mapcomplete.org/MapComplete/MapComplete/compare/v0.51.5...v0.51.6) (2025-05-08) diff --git a/package-lock.json b/package-lock.json index b4e343f7ca..b6cd2e2e3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapcomplete", - "version": "0.51.6", + "version": "0.51.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mapcomplete", - "version": "0.51.6", + "version": "0.51.7", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 0f95c7e86b..7f60784299 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.51.6", + "version": "0.51.7", "repository": "https://source.mapcomplete.org/MapComplete/MapComplete", "description": "A small website to edit OSM easily", "bugs": "hhttps://source.mapcomplete.org/MapComplete/MapComplete/issues", From cde7bf6d9aa7c521b9b4a22fd52509d8e725cae5 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 12 May 2025 12:47:39 +0200 Subject: [PATCH 4/4] Fix: fix loading notes based on hash --- assets/layers/note/note.json | 2 +- src/Models/ThemeViewState/WithSelectedElementState.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/assets/layers/note/note.json b/assets/layers/note/note.json index 4cb47e53e9..fba8654ced 100644 --- a/assets/layers/note/note.json +++ b/assets/layers/note/note.json @@ -28,7 +28,7 @@ "_first_user_id:=get(feat)('comments')[0].uid", "_is_import_note:=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.\\(osm.be|org\\)/\\([a-zA-Z_-]+\\)\\(.html\\).*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()" ], - "isShown": "_total_comments>0", + "isShown": "comments!=[]", "minzoom": 7, "title": { "render": { diff --git a/src/Models/ThemeViewState/WithSelectedElementState.ts b/src/Models/ThemeViewState/WithSelectedElementState.ts index f755c05906..2e1e6d45d2 100644 --- a/src/Models/ThemeViewState/WithSelectedElementState.ts +++ b/src/Models/ThemeViewState/WithSelectedElementState.ts @@ -41,7 +41,10 @@ export class WithSelectedElementState extends UserMapFeatureswitchState { const [osm_type, osm_id] = selected.properties.id.split("/") const [lon, lat] = GeoOperations.centerpointCoordinates(selected) const layer = this.theme.getMatchingLayer(selected.properties) - if (!layer.isNormal()) { + if (!layer) { + return + } + if (!layer?.isNormal()) { return }