Merge branch 'master' into develop

This commit is contained in:
Pieter Vander Vennet 2025-05-12 12:47:53 +02:00
commit 25880ed7d3
16 changed files with 111 additions and 63 deletions

View file

@ -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. 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) ### [0.51.6](https://source.mapcomplete.org/MapComplete/MapComplete/compare/v0.51.5...v0.51.6) (2025-05-08)

View file

@ -28,7 +28,7 @@
"_first_user_id:=get(feat)('comments')[0].uid", "_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] })()" "_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, "minzoom": 7,
"title": { "title": {
"render": { "render": {

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.51.6", "version": "0.51.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.51.6", "version": "0.51.7",
"hasInstallScript": true, "hasInstallScript": true,
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.51.6", "version": "0.51.7",
"repository": "https://source.mapcomplete.org/MapComplete/MapComplete", "repository": "https://source.mapcomplete.org/MapComplete/MapComplete",
"description": "A small website to edit OSM easily", "description": "A small website to edit OSM easily",
"bugs": "hhttps://source.mapcomplete.org/MapComplete/MapComplete/issues", "bugs": "hhttps://source.mapcomplete.org/MapComplete/MapComplete/issues",

View file

@ -8,7 +8,7 @@ export class LicenseInfo {
copyrighted?: boolean = false copyrighted?: boolean = false
credit?: string = "" credit?: string = ""
description?: string = "" description?: string = ""
informationLocation?: URL = undefined informationLocation?: URL | string = undefined
date?: Date date?: Date
views?: number views?: number
} }

View file

@ -192,7 +192,12 @@ export class Mapillary extends ImageProvider {
license.license = "CC BY-SA 4.0" license.license = "CC BY-SA 4.0"
// license.license = "Creative Commons Attribution-ShareAlike 4.0 International License"; // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
license.attributionRequired = true 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 return license
} }

View file

@ -54,7 +54,7 @@ export class Changes {
featureSwitchIsTesting?: Store<boolean> featureSwitchIsTesting?: Store<boolean>
} }
osmConnection: OsmConnection osmConnection: OsmConnection
reportError?: (error: string) => void reportError?: ((message: string | Error | XMLHttpRequest, extramessage?: string) => void),
featureProperties?: FeaturePropertiesStore featureProperties?: FeaturePropertiesStore
historicalUserLocations?: FeatureSource<Feature<Point, GeoLocationPointProperties>> historicalUserLocations?: FeatureSource<Feature<Point, GeoLocationPointProperties>>
allElements?: IndexedFeatureSource allElements?: IndexedFeatureSource
@ -75,7 +75,7 @@ export class Changes {
} }
this.state = state this.state = state
this.backend = state.osmConnection.Backend() this.backend = state.osmConnection.Backend()
this._reportError = state.reportError this._reportError = (msg, err) => state.reportError(msg, err)
this._changesetHandler = new ChangesetHandler( this._changesetHandler = new ChangesetHandler(
state.featureSwitches?.featureSwitchIsTesting ?? new ImmutableStore(false), state.featureSwitches?.featureSwitchIsTesting ?? new ImmutableStore(false),
state.osmConnection, state.osmConnection,
@ -669,19 +669,24 @@ export class Changes {
const createdIds = new Set( const createdIds = new Set(
pending.filter((cd) => cd.changes !== undefined).map((cd) => cd.id) pending.filter((cd) => cd.changes !== undefined).map((cd) => cd.id)
) )
pending.forEach((c) => { for (const c of pending) {
if (c.id < 0) { let id = c.id
if (createdIds.has(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) toUpload.push(c)
} else { } else {
this._reportError( 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) 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) { if (matchFound) {
toUpload.push(c) toUpload.push(c)
} else { } else {
@ -689,12 +694,12 @@ export class Changes {
"Refusing change about " + "Refusing change about " +
c.type + c.type +
"/" + "/" +
c.id + id +
" as not in the objects. No internet?" " as not in the objects. No internet?"
) )
refused.push(c) refused.push(c)
} }
}) }
return { refused, toUpload } return { refused, toUpload }
} }

View file

@ -40,7 +40,8 @@ export class ChangesetHandler {
private readonly backend: string private readonly backend: string
/** /**
* Contains previously rewritten IDs * Contains previously rewritten IDs, e.g. {"node/-1" --> "node/123456"}
*
* @private * @private
*/ */
public readonly _remappings = new Map<string, string>() public readonly _remappings = new Map<string, string>()

View file

@ -9,6 +9,7 @@ import { Utils } from "../../Utils"
import { Point } from "geojson" import { Point } from "geojson"
import { Imgur } from "../ImageProviders/Imgur" import { Imgur } from "../ImageProviders/Imgur"
import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist" import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist"
import { Mapillary } from "../ImageProviders/Mapillary"
interface ImageFetcher { interface ImageFetcher {
/** /**
@ -222,6 +223,11 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher {
const promises: Promise<ImageData[]>[] = [] const promises: Promise<ImageData[]>[] = []
const maxRadius = this._radius const maxRadius = this._radius
let prevRadius = 0 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) { for (const radiusSetting of radiusSettings) {
const promise = this._panoramax.search({ const promise = this._panoramax.search({
place: [lon, lat], place: [lon, lat],
@ -265,7 +271,7 @@ class MapillaryFetcher implements ImageFetcher {
async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> { async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
const boundingBox = new BBox([[lon, lat]]).padAbsolute(0.003) const boundingBox = new BBox([[lon, lat]]).padAbsolute(0.003)
let url = 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.getWest(),
boundingBox.getSouth(), boundingBox.getSouth(),
@ -293,13 +299,14 @@ class MapillaryFetcher implements ImageFetcher {
const response = await Utils.downloadJson<{ const response = await Utils.downloadJson<{
data: { data: {
id: string id: string
creator: string creator: { username: string }
geometry: Point geometry: Point
computed_geometry: Point computed_geometry: Point
is_pano: boolean is_pano: boolean
thumb_256_url: string thumb_256_url: string
thumb_original_url: string thumb_original_url: string
compass_angle: number compass_angle: number
captured_at: number
}[] }[]
}>(url) }>(url)
const pics: P4CPicture[] = [] const pics: P4CPicture[] = []
@ -308,6 +315,7 @@ class MapillaryFetcher implements ImageFetcher {
if (img.thumb_original_url === undefined) { if (img.thumb_original_url === undefined) {
continue continue
} }
const [lon, lat] = img.computed_geometry.coordinates
pics.push({ pics.push({
pictureUrl: img.thumb_original_url, pictureUrl: img.thumb_original_url,
provider: "Mapillary", provider: "Mapillary",
@ -319,6 +327,12 @@ class MapillaryFetcher implements ImageFetcher {
details: { details: {
isSpherical: this._panoramas === "only", 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 return pics
@ -367,7 +381,6 @@ export class CombinedFetcher {
): Promise<void> { ): Promise<void> {
try { try {
const pics = await source.fetchImages(lat, lon) const pics = await source.fetchImages(lat, lon)
console.log(source.name, "==>>", pics)
state.data[source.name] = "done" state.data[source.name] = "done"
state.ping() state.ping()

View file

@ -246,5 +246,5 @@ export interface TagRenderingConfigJson {
* Note: if the theme already has a layer with this ID, the value is ignored * Note: if the theme already has a layer with this ID, the value is ignored
* group: hidden * group: hidden
*/ */
requiredLayers: { id: string; minzoom?: number }[] requiredLayers?: { id: string; minzoom?: number }[]
} }

View file

@ -1,5 +1,7 @@
import { Changes } from "../../Logic/Osm/Changes" 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 { WithLayoutSourceState } from "./WithLayoutSourceState"
import ThemeConfig from "../ThemeConfig/ThemeConfig" import ThemeConfig from "../ThemeConfig/ThemeConfig"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
@ -18,9 +20,7 @@ import { Map as MlMap } from "maplibre-gl"
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource" import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource"
import ShowDataLayer from "../../UI/Map/ShowDataLayer" import ShowDataLayer from "../../UI/Map/ShowDataLayer"
import SelectedElementTagsUpdater from "../../Logic/Actors/SelectedElementTagsUpdater" import SelectedElementTagsUpdater from "../../Logic/Actors/SelectedElementTagsUpdater"
import NoElementsInViewDetector, { import NoElementsInViewDetector, { FeatureViewState } from "../../Logic/Actors/NoElementsInViewDetector"
FeatureViewState,
} from "../../Logic/Actors/NoElementsInViewDetector"
export class WithChangesState extends WithLayoutSourceState { export class WithChangesState extends WithLayoutSourceState {
readonly changes: Changes readonly changes: Changes
@ -43,7 +43,7 @@ export class WithChangesState extends WithLayoutSourceState {
osmConnection: this.osmConnection, osmConnection: this.osmConnection,
featureProperties: this.featureProperties, featureProperties: this.featureProperties,
historicalUserLocations: this.historicalUserLocations, historicalUserLocations: this.historicalUserLocations,
reportError: this.reportError, reportError: (err, msg) => this.reportError(err, msg)
}, },
theme?.isLeftRightSensitive() ?? false theme?.isLeftRightSensitive() ?? false
) )
@ -104,16 +104,6 @@ export class WithChangesState extends WithLayoutSourceState {
return return
} }
const isTesting = this.featureSwitchIsTesting?.data 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]") { if ("" + message === "[object XMLHttpRequest]") {
const req = <XMLHttpRequest>message const req = <XMLHttpRequest>message
@ -137,25 +127,35 @@ export class WithChangesState extends WithLayoutSourceState {
} }
const stacktrace: string = new Error().stack const stacktrace: string = new Error().stack
try { 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, { await fetch(Constants.ErrorReportServer, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify(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),
}),
}) })
} catch (e) { } catch (e) {
console.error("Could not upload an error report") console.error("Could not upload an error report", e)
} }
} }

View file

@ -29,7 +29,7 @@ export class WithImageState extends WithGuiState implements SpecialVisualization
this.osmConnection, this.osmConnection,
this.changes, this.changes,
this.geolocation.geolocationState.currentGPSLocation, this.geolocation.geolocationState.currentGPSLocation,
this.reportError (err, msg) => this.reportError(err, msg)
) )
const longAgo = new Date() const longAgo = new Date()
longAgo.setTime(new Date().getTime() - 5 * 365 * 24 * 60 * 60 * 1000) longAgo.setTime(new Date().getTime() - 5 * 365 * 24 * 60 * 60 * 1000)

View file

@ -41,7 +41,10 @@ export class WithSelectedElementState extends UserMapFeatureswitchState {
const [osm_type, osm_id] = selected.properties.id.split("/") const [osm_type, osm_id] = selected.properties.id.split("/")
const [lon, lat] = GeoOperations.centerpointCoordinates(selected) const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
const layer = this.theme.getMatchingLayer(selected.properties) const layer = this.theme.getMatchingLayer(selected.properties)
if (!layer.isNormal()) { if (!layer) {
return
}
if (!layer?.isNormal()) {
return return
} }

View file

@ -37,18 +37,29 @@
} }
}) })
const t = Translations.t.image.nearby 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 = { let license: LicenseInfo = {
artist: image.author, artist: image.author,
license: image.license, license: image.license,
date: new Date(image.date), informationLocation: (image.detailsUrl ?? image.pictureUrl ?? image.thumbUrl),
informationLocation: image.detailsUrl, date
} }
console.log(">>> trying to create license info based on", image, license)
let providedImage: ProvidedImage = { let providedImage: ProvidedImage = {
url: image.thumbUrl ?? image.pictureUrl, url: image.thumbUrl ?? image.pictureUrl,
url_hd: image.pictureUrl, url_hd: image.pictureUrl,
key: undefined, key: undefined,
provider: AllImageProviders.byName(image.provider), provider: AllImageProviders.byName(image.provider),
date: new Date(image.date), date,
id: Object.values(image.osmTags)[0], id: Object.values(image.osmTags)[0],
isSpherical: image.details.isSpherical, isSpherical: image.details.isSpherical,
license, license,

View file

@ -7,7 +7,7 @@
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch" import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch"
import LinkableImage from "./LinkableImage.svelte" 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 LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
@ -25,7 +25,7 @@
import { BBox } from "../../Logic/BBox" import { BBox } from "../../Logic/BBox"
import PanoramaxLink from "../BigComponents/PanoramaxLink.svelte" import PanoramaxLink from "../BigComponents/PanoramaxLink.svelte"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import type { PanoramaView } from "../../Logic/ImageProviders/ImageProvider" import type { HotspotProperties, PanoramaView } from "../../Logic/ImageProviders/ImageProvider"
export let tags: UIEventSource<OsmTags> export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
@ -52,6 +52,7 @@
[loadedImages] [loadedImages]
) )
// Panorama-views get a geojson feature to browse around
let asFeatures = result.map((p4cs) => let asFeatures = result.map((p4cs) =>
p4cs.map( p4cs.map(
(p4c) => (p4c) =>
@ -147,25 +148,27 @@
highlighted.set(feature.properties.id) highlighted.set(feature.properties.id)
}, },
}) })
let nearbyFeatures: Store<Feature[]> = asFeatures.map((nearbyPoints) => {
let nearbyFeatures: Store<Feature<Geometry, HotspotProperties>[]> = asFeatures.map((nearbyPoints) => {
return [ return [
{ {
type: "Feature", type: "Feature",
geometry: { type: "Point", coordinates: GeoOperations.centerpointCoordinates(feature) }, geometry: { type: "Point", coordinates: GeoOperations.centerpointCoordinates(feature) },
properties: { properties: <HotspotProperties>{
name: layer.title?.GetRenderValue(feature.properties).Subs(feature.properties).txt, name: layer.title?.GetRenderValue(feature.properties).Subs(feature.properties).txt,
focus: true, focus: true,
}, },
}, },
...nearbyPoints ...nearbyPoints
.filter((p) => p.properties.spherical === "yes") .filter((p) => p.properties["spherical"] === "yes")
.map((f) => ({ .map((f) => ({
...f, ...f,
properties: { properties: <HotspotProperties>{
name: "Nearby panorama", name: "Nearby panorama",
pitch: "auto", pitch: "auto",
type: "scene", type: "scene",
gotoPanorama: f, gotoPanorama: f,
focus: false
}, },
})), })),
] ]

View file

@ -3,7 +3,7 @@ import {
DataDrivenPropertyValueSpecification, DataDrivenPropertyValueSpecification,
LayerSpecification, LayerSpecification,
Map as MlMap, Map as MlMap,
SymbolLayerSpecification, SymbolLayerSpecification
} from "maplibre-gl" } from "maplibre-gl"
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
@ -1752,7 +1752,6 @@ export class ProtomapsLanguageSupport {
} }
const newExpressionF = ProtomapsLanguageSupport.expressions[layer.id] const newExpressionF = ProtomapsLanguageSupport.expressions[layer.id]
if (!newExpressionF) { if (!newExpressionF) {
console.log(">>> No function found for", layer.id)
return return
} }
const newExpression = newExpressionF(language) const newExpression = newExpressionF(language)