diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index 204950ed9..c5598f346 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -39,20 +39,6 @@ export default class SelectedFeatureHandler { hash.addCallback(() => self.setSelectedElementFromHash()) - // IF the selected element changes, set the hash correctly - state.selectedElement.addCallback(feature => { - if (feature === undefined) { - if (!SelectedFeatureHandler._no_trigger_on.has(hash.data)) { - hash.setData("") - } - } - - const h = feature?.properties?.id; - if (h !== undefined) { - hash.setData(h) - } - }) - state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD(_ => { // New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) { diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index ca704536a..d0f03b03b 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -92,7 +92,7 @@ export default class FeaturePipeline { if (location?.zoom === undefined) { return false; } - let minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom ?? 18)); + let minzoom = Math.min(...state.filteredLayers.data.map(layer => layer.layerDef.minzoom ?? 18)); return location.zoom >= minzoom; } ); @@ -312,7 +312,7 @@ export default class FeaturePipeline { // Whenever fresh data comes in, we need to update the metatagging - self.newDataLoadedSignal.stabilized(250).addCallback(src => { + self.newDataLoadedSignal.stabilized(250).addCallback(_ => { self.updateAllMetaTagging() }) diff --git a/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts b/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts index d2e9ec95b..b56b1eb14 100644 --- a/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts @@ -3,6 +3,7 @@ import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject"; import SimpleFeatureSource from "../Sources/SimpleFeatureSource"; import FilteredLayer from "../../../Models/FilteredLayer"; +import {UIEventSource} from "../../UIEventSource"; export default class FullNodeDatabaseSource implements TileHierarchy { @@ -10,6 +11,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy void; private readonly layer: FilteredLayer private readonly nodeByIds = new Map(); + private readonly parentWays = new Map>() constructor( layer: FilteredLayer, @@ -35,7 +37,6 @@ export default class FullNodeDatabaseSource implements TileHierarchy() for (const osmObj of allObjects) { if (osmObj.type !== "way") { continue @@ -43,16 +44,20 @@ export default class FullNodeDatabaseSource implements TileHierarchyosmObj; for (const nodeId of osmWay.nodes) { - if (!parentWaysByNodeId.has(nodeId)) { - parentWaysByNodeId.set(nodeId, []) + if (!this.parentWays.has(nodeId)) { + const src = new UIEventSource([]) + this.parentWays.set(nodeId,src) + src.addCallback(parentWays => { + const tgs = nodesById.get(nodeId).tags + tgs ["parent_ways"] = JSON.stringify(parentWays.map(w => w.tags)) + tgs["parent_way_ids"] = JSON.stringify(parentWays.map(w => w.id)) + }) } - parentWaysByNodeId.get(nodeId).push(osmWay) + const src = this.parentWays.get(nodeId) + src.data.push(osmWay) + src.ping(); } } - parentWaysByNodeId.forEach((allWays, nodeId) => { - nodesById.get(nodeId).tags["parent_ways"] = JSON.stringify(allWays.map(w => w.tags)) - nodesById.get(nodeId).tags["parent_way_ids"] = JSON.stringify(allWays.map(w => w.id)) - }) const now = new Date() const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({ feature: osmNode.asGeoJson(), freshness: now @@ -71,10 +76,18 @@ export default class FullNodeDatabaseSource implements TileHierarchy { + return this.parentWays.get(nodeId) + } } diff --git a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts index 60918fbfa..98e904746 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts @@ -201,9 +201,10 @@ export interface TiledFeatureSourceOptions { readonly minZoomLevel?: number, /** * IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated. - * Setting 'dontEnforceMinZoomLevel' will still allow bigger zoom levels for those features + * Setting 'dontEnforceMinZoomLevel' will still allow bigger zoom levels for those features. + * If 'pick_first' is set, the feature will not be duplicated but set to some tile */ - readonly dontEnforceMinZoom?: boolean, + readonly dontEnforceMinZoom?: boolean | "pick_first", readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void, readonly layer?: FilteredLayer } \ No newline at end of file diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 2816621e2..e1acc4dd4 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -25,7 +25,7 @@ export class GeoOperations { } static centerpointCoordinates(feature: any): [number, number] { - return <[number, number]> turf.center(feature).geometry.coordinates; + return <[number, number]>turf.center(feature).geometry.coordinates; } /** @@ -37,10 +37,10 @@ export class GeoOperations { return turf.distance(lonlat0, lonlat1, {units: "meters"}) } - static convexHull(featureCollection, options: {concavity?: number}){ + static convexHull(featureCollection, options: { concavity?: number }) { return turf.convex(featureCollection, options) } - + /** * Calculates the overlap of 'feature' with every other specified feature. * The features with which 'feature' overlaps, are returned together with their overlap area in m² @@ -199,8 +199,8 @@ export class GeoOperations { static buffer(feature: any, bufferSizeInMeter: number) { return turf.buffer(feature, bufferSizeInMeter / 1000, { - units:'kilometers' - } ) + units: 'kilometers' + }) } static bbox(feature: any) { @@ -350,263 +350,166 @@ export class GeoOperations { } - /** - * Calculates the intersection between two features. - * Returns the length if intersecting a linestring and a (multi)polygon (in meters), returns a surface area (in m²) if intersecting two (multi)polygons - * Returns 0 if both are linestrings - * Returns null if the features are not intersecting - */ - private static calculateInstersection(feature, otherFeature, featureBBox: BBox, otherFeatureBBox?: BBox): number { - if (feature.geometry.type === "LineString") { - - - otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature); - const overlaps = featureBBox.overlapsWith(otherFeatureBBox) - if (!overlaps) { - return null; - } - - // Calculate the length of the intersection - - - let intersectionPoints = turf.lineIntersect(feature, otherFeature); - if (intersectionPoints.features.length == 0) { - // No intersections. - // If one point is inside of the polygon, all points are - - - const coors = feature.geometry.coordinates; - const startCoor = coors[0] - if (this.inside(startCoor, otherFeature)) { - return this.lengthInMeters(feature) - } - - return null; - } - let intersectionPointsArray = intersectionPoints.features.map(d => { - return d.geometry.coordinates - }); - - if (otherFeature.geometry.type === "LineString") { - if (intersectionPointsArray.length > 0) { - return 0 - } - return null; - } - if (intersectionPointsArray.length == 1) { - // We need to add the start- or endpoint of the current feature, depending on which one is embedded - const coors = feature.geometry.coordinates; - const startCoor = coors[0] - if (this.inside(startCoor, otherFeature)) { - // The startpoint is embedded - intersectionPointsArray.push(startCoor) - } else { - intersectionPointsArray.push(coors[coors.length - 1]) - } - } - - let intersection = turf.lineSlice(turf.point(intersectionPointsArray[0]), turf.point(intersectionPointsArray[1]), feature); - - if (intersection == null) { - return null; - } - const intersectionSize = turf.length(intersection); // in km - return intersectionSize * 1000 - - - } - - if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { - const otherFeatureBBox = BBox.get(otherFeature); - const overlaps = featureBBox.overlapsWith(otherFeatureBBox) - if (!overlaps) { - return null; - } - if (otherFeature.geometry.type === "LineString") { - return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox) - } - - try{ - - const intersection = turf.intersect(feature, otherFeature); - if (intersection == null) { - return null; - } - return turf.area(intersection); // in m² - }catch(e){ - if(e.message === "Each LinearRing of a Polygon must have 4 or more Positions."){ - // WORKAROUND TIME! - // See https://github.com/Turfjs/turf/pull/2238 - return null; - } - throw e; - } - - } - throw "CalculateIntersection fallthrough: can not calculate an intersection between features" - - } - /** * Calculates line intersection between two features. */ - public static LineIntersections(feature, otherFeature): [number,number][]{ - return turf.lineIntersect(feature, otherFeature).features.map(p =><[number,number]> p.geometry.coordinates) + public static LineIntersections(feature, otherFeature): [number, number][] { + return turf.lineIntersect(feature, otherFeature).features.map(p => <[number, number]>p.geometry.coordinates) } - - public static AsGpx(feature, generatedWithLayer?: LayerConfig){ - + + public static AsGpx(feature, generatedWithLayer?: LayerConfig) { + const metadata = {} const tags = feature.properties - - if(generatedWithLayer !== undefined){ - + + if (generatedWithLayer !== undefined) { + metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt - metadata["desc"] = "Generated with MapComplete layer "+generatedWithLayer.id - if(tags._backend?.contains("openstreetmap")){ - metadata["copyright"]= "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright" + metadata["desc"] = "Generated with MapComplete layer " + generatedWithLayer.id + if (tags._backend?.contains("openstreetmap")) { + metadata["copyright"] = "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright" metadata["author"] = tags["_last_edit:contributor"] - metadata["link"]= "https://www.openstreetmap.org/"+tags.id + metadata["link"] = "https://www.openstreetmap.org/" + tags.id metadata["time"] = tags["_last_edit:timestamp"] - }else{ + } else { metadata["time"] = new Date().toISOString() } } - + return togpx(feature, { - creator: "MapComplete "+Constants.vNumber, + creator: "MapComplete " + Constants.vNumber, metadata }) } - - public static IdentifieCommonSegments(coordinatess: [number,number][][] ): { + + public static IdentifieCommonSegments(coordinatess: [number, number][][]): { originalIndex: number, segmentShardWith: number[], coordinates: [] - }[]{ - + }[] { + // An edge. Note that the edge might be reversed to fix the sorting condition: start[0] < end[0] && (start[0] != end[0] || start[0] < end[1]) - type edge = {start: [number, number], end: [number, number], intermediate: [number,number][], members: {index:number, isReversed: boolean}[]} + type edge = { start: [number, number], end: [number, number], intermediate: [number, number][], members: { index: number, isReversed: boolean }[] } // The strategy: // 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them // 2. Join these edges back together - as long as their membership groups are the same // 3. Convert to results - + const allEdgesByKey = new Map() - for (let index = 0; index < coordinatess.length; index++){ + for (let index = 0; index < coordinatess.length; index++) { const coordinates = coordinatess[index]; - for (let i = 0; i < coordinates.length - 1; i++){ - + for (let i = 0; i < coordinates.length - 1; i++) { + const c0 = coordinates[i]; const c1 = coordinates[i + 1] const isReversed = (c0[0] > c1[0]) || (c0[0] == c1[0] && c0[1] > c1[1]) - - let key : string - if(isReversed){ - key = ""+c1+";"+c0 - }else{ - key = ""+c0+";"+c1 + + let key: string + if (isReversed) { + key = "" + c1 + ";" + c0 + } else { + key = "" + c0 + ";" + c1 } const member = {index, isReversed} - if(allEdgesByKey.has(key)){ + if (allEdgesByKey.has(key)) { allEdgesByKey.get(key).members.push(member) continue } - - let edge : edge; - if(!isReversed){ + + let edge: edge; + if (!isReversed) { edge = { - start : c0, + start: c0, end: c1, members: [member], intermediate: [] } - }else{ + } else { edge = { - start : c1, + start: c1, end: c0, members: [member], intermediate: [] } } allEdgesByKey.set(key, edge) - + } } // Lets merge them back together! - + let didMergeSomething = false; let allMergedEdges = Array.from(allEdgesByKey.values()) const allEdgesByStartPoint = new Map() for (const edge of allMergedEdges) { - + edge.members.sort((m0, m1) => m0.index - m1.index) - - const kstart = edge.start+"" - if(!allEdgesByStartPoint.has(kstart)){ + + const kstart = edge.start + "" + if (!allEdgesByStartPoint.has(kstart)) { allEdgesByStartPoint.set(kstart, []) } allEdgesByStartPoint.get(kstart).push(edge) } - - - function membersAreCompatible(first:edge, second:edge): boolean{ + + + function membersAreCompatible(first: edge, second: edge): boolean { // There must be an exact match between the members - if(first.members === second.members){ + if (first.members === second.members) { return true } - - if(first.members.length !== second.members.length){ + + if (first.members.length !== second.members.length) { return false } - + // Members are sorted and have the same length, so we can check quickly for (let i = 0; i < first.members.length; i++) { const m0 = first.members[i] const m1 = second.members[i] - if(m0.index !== m1.index || m0.isReversed !== m1.isReversed){ + if (m0.index !== m1.index || m0.isReversed !== m1.isReversed) { return false } } - + // Allrigth, they are the same, lets mark this permanently second.members = first.members return true - + } - - do{ + + do { didMergeSomething = false // We use 'allMergedEdges' as our running list const consumed = new Set() for (const edge of allMergedEdges) { // Can we make this edge longer at the end? - if(consumed.has(edge)){ + if (consumed.has(edge)) { continue } - + console.log("Considering edge", edge) - const matchingEndEdges = allEdgesByStartPoint.get(edge.end+"") + const matchingEndEdges = allEdgesByStartPoint.get(edge.end + "") console.log("Matchign endpoints:", matchingEndEdges) - if(matchingEndEdges === undefined){ + if (matchingEndEdges === undefined) { continue } - - - for (let i = 0; i < matchingEndEdges.length; i++){ + + + for (let i = 0; i < matchingEndEdges.length; i++) { const endEdge = matchingEndEdges[i]; - - if(consumed.has(endEdge)){ + + if (consumed.has(endEdge)) { continue } - - if(!membersAreCompatible(edge, endEdge)){ + + if (!membersAreCompatible(edge, endEdge)) { continue } - + // We can make the segment longer! didMergeSomething = true console.log("Merging ", edge, "with ", endEdge) @@ -617,13 +520,169 @@ export class GeoOperations { break; } } - + allMergedEdges = allMergedEdges.filter(edge => !consumed.has(edge)); - - }while(didMergeSomething) - + + } while (didMergeSomething) + return [] } + + /** + * Removes points that do not contribute to the geometry from linestrings and the outer ring of polygons. + * Returs a new copy of the feature + * @param feature + */ + static removeOvernoding(feature: any) { + if (feature.geometry.type !== "LineString" && feature.geometry.type !== "Polygon") { + throw "Overnode removal is only supported on linestrings and polygons" + } + + const copy = { + ...feature, + geometry: {...feature.geometry} + } + let coordinates: [number, number][] + if (feature.geometry.type === "LineString") { + coordinates = [...feature.geometry.coordinates] + copy.geometry.coordinates = coordinates + } else { + coordinates = [...feature.geometry.coordinates[0]] + copy.geometry.coordinates[0] = coordinates + } + + // inline replacement in the coordinates list + for (let i = coordinates.length - 2; i >= 1; i--) { + const coordinate = coordinates[i]; + const nextCoordinate = coordinates[i + 1] + const prevCoordinate = coordinates[i - 1] + + const distP = GeoOperations.distanceBetween(coordinate, prevCoordinate) + if(distP < 0.1){ + coordinates.splice(i, 1) + continue + } + + if(i == coordinates.length - 2){ + const distN = GeoOperations.distanceBetween(coordinate, nextCoordinate) + if(distN < 0.1){ + coordinates.splice(i, 1) + continue + } + } + + const bearingN = turf.bearing(coordinate, nextCoordinate) + const bearingP = turf.bearing(prevCoordinate, coordinate) + const diff = Math.abs(bearingN - bearingP) + if (diff < 4) { + // If the diff is low, this point is hardly relevant + coordinates.splice(i, 1) + } else if (360 - diff < 4) { + // In case that the line is going south, e.g. bearingN = 179, bearingP = -179 + coordinates.splice(i, 1) + } + + } + return copy; + + } + + /** + * Calculates the intersection between two features. + * Returns the length if intersecting a linestring and a (multi)polygon (in meters), returns a surface area (in m²) if intersecting two (multi)polygons + * Returns 0 if both are linestrings + * Returns null if the features are not intersecting + */ + private static calculateInstersection(feature, otherFeature, featureBBox: BBox, otherFeatureBBox?: BBox): number { + if (feature.geometry.type === "LineString") { + + + otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature); + const overlaps = featureBBox.overlapsWith(otherFeatureBBox) + if (!overlaps) { + return null; + } + + // Calculate the length of the intersection + + + let intersectionPoints = turf.lineIntersect(feature, otherFeature); + if (intersectionPoints.features.length == 0) { + // No intersections. + // If one point is inside of the polygon, all points are + + + const coors = feature.geometry.coordinates; + const startCoor = coors[0] + if (this.inside(startCoor, otherFeature)) { + return this.lengthInMeters(feature) + } + + return null; + } + let intersectionPointsArray = intersectionPoints.features.map(d => { + return d.geometry.coordinates + }); + + if (otherFeature.geometry.type === "LineString") { + if (intersectionPointsArray.length > 0) { + return 0 + } + return null; + } + if (intersectionPointsArray.length == 1) { + // We need to add the start- or endpoint of the current feature, depending on which one is embedded + const coors = feature.geometry.coordinates; + const startCoor = coors[0] + if (this.inside(startCoor, otherFeature)) { + // The startpoint is embedded + intersectionPointsArray.push(startCoor) + } else { + intersectionPointsArray.push(coors[coors.length - 1]) + } + } + + let intersection = turf.lineSlice(turf.point(intersectionPointsArray[0]), turf.point(intersectionPointsArray[1]), feature); + + if (intersection == null) { + return null; + } + const intersectionSize = turf.length(intersection); // in km + return intersectionSize * 1000 + + + } + + if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { + const otherFeatureBBox = BBox.get(otherFeature); + const overlaps = featureBBox.overlapsWith(otherFeatureBBox) + if (!overlaps) { + return null; + } + if (otherFeature.geometry.type === "LineString") { + return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox) + } + + try { + + const intersection = turf.intersect(feature, otherFeature); + if (intersection == null) { + return null; + } + return turf.area(intersection); // in m² + } catch (e) { + if (e.message === "Each LinearRing of a Polygon must have 4 or more Positions.") { + // WORKAROUND TIME! + // See https://github.com/Turfjs/turf/pull/2238 + return null; + } + throw e; + } + + } + throw "CalculateIntersection fallthrough: can not calculate an intersection between features" + + } } diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index d1501109c..302762110 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -56,6 +56,7 @@ export default class MetaTagging { const feature = ff.feature const freshness = ff.freshness let somethingChanged = false + let definedTags = new Set(Object.getOwnPropertyNames( feature.properties )) for (const metatag of metatagsToApply) { try { if (!metatag.keys.some(key => feature.properties[key] === undefined)) { @@ -64,8 +65,11 @@ export default class MetaTagging { } if (metatag.isLazy) { + if(!metatag.keys.some(key => !definedTags.has(key))) { + // All keys are defined - lets skip! + continue + } somethingChanged = true; - metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) } else { const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) @@ -84,12 +88,13 @@ export default class MetaTagging { } if (layerFuncs !== undefined) { + let retaggingChanged = false; try { - layerFuncs(params, feature) + retaggingChanged = layerFuncs(params, feature) } catch (e) { console.error(e) } - somethingChanged = true + somethingChanged = somethingChanged || retaggingChanged } if (somethingChanged) { @@ -99,8 +104,8 @@ export default class MetaTagging { } return atLeastOneFeatureChanged } - public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] { - const functions: ((feature: any) => void)[] = []; + public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => boolean)[] { + const functions: ((feature: any) => boolean)[] = []; for (const entry of calculatedTags) { const key = entry[0] @@ -110,10 +115,9 @@ export default class MetaTagging { continue; } - const calculateAndAssign = (feat) => { - - + const calculateAndAssign: ((feat: any) => boolean) = (feat) => { try { + let oldValue = isStrict ? feat.properties[key] : undefined let result = new Function("feat", "return " + code + ";")(feat); if (result === "") { result === undefined @@ -124,7 +128,7 @@ export default class MetaTagging { } delete feat.properties[key] feat.properties[key] = result; - return result; + return result === oldValue; }catch(e){ if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { console.warn("Could not calculate a " + (isStrict ? "strict " : "") + " calculated tag for key " + key + " defined by " + code + " (in layer" + layerId + ") due to \n" + e + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e, e.stack) @@ -133,6 +137,7 @@ export default class MetaTagging { console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now") } } + return false; } } @@ -149,9 +154,11 @@ export default class MetaTagging { configurable: true, enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this get: function () { - return calculateAndAssign(feature) + calculateAndAssign(feature) + return feature.properties[key] } }) + return true } @@ -160,17 +167,23 @@ export default class MetaTagging { return functions; } - private static retaggingFuncCache = new Map void)[]>() + private static retaggingFuncCache = new Map boolean)[]>() + /** + * Creates the function which adds all the calculated tags to a feature. Called once per layer + * @param layer + * @param state + * @private + */ private static createRetaggingFunc(layer: LayerConfig, state): - ((params: ExtraFuncParams, feature: any) => void) { + ((params: ExtraFuncParams, feature: any) => boolean) { const calculatedTags: [string, string, boolean][] = layer.calculatedTags; if (calculatedTags === undefined || calculatedTags.length === 0) { return undefined; } - let functions = MetaTagging.retaggingFuncCache.get(layer.id); + let functions :((feature: any) => boolean)[] = MetaTagging.retaggingFuncCache.get(layer.id); if (functions === undefined) { functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags) MetaTagging.retaggingFuncCache.set(layer.id, functions) @@ -192,6 +205,7 @@ export default class MetaTagging { } catch (e) { console.error("Invalid syntax in calculated tags or some other error: ", e) } + return true; // Something changed } } diff --git a/Logic/Osm/Actions/ReplaceGeometryAction.ts b/Logic/Osm/Actions/ReplaceGeometryAction.ts index 651676dc5..876ce2e96 100644 --- a/Logic/Osm/Actions/ReplaceGeometryAction.ts +++ b/Logic/Osm/Actions/ReplaceGeometryAction.ts @@ -81,8 +81,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { // noinspection JSUnusedGlobalSymbols public async getPreview(): Promise { - const {closestIds, allNodesById, detachedNodeIds} = await this.GetClosestIds(); - console.debug("Generating preview, identicals are ",) + const {closestIds, allNodesById, detachedNodes, reprojectedNodes} = await this.GetClosestIds(); const preview: GeoJSONObject[] = closestIds.map((newId, i) => { if (this.identicalTo[i] !== undefined) { return undefined @@ -93,7 +92,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { type: "Feature", properties: { "newpoint": "yes", - "id": "replace-geometry-move-" + i + "id": "replace-geometry-move-" + i, }, geometry: { type: "Point", @@ -101,49 +100,316 @@ export default class ReplaceGeometryAction extends OsmChangeAction { } }; } - const origPoint = allNodesById.get(newId).centerpoint() + + const origNode = allNodesById.get(newId); return { type: "Feature", properties: { "move": "yes", "osm-id": newId, - "id": "replace-geometry-move-" + i + "id": "replace-geometry-move-" + i, + "original-node-tags": JSON.stringify(origNode.tags) }, geometry: { type: "LineString", - coordinates: [[origPoint[1], origPoint[0]], this.targetCoordinates[i]] + coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]] } }; }) - for (const detachedNodeId of detachedNodeIds) { - const origPoint = allNodesById.get(detachedNodeId).centerpoint() + + reprojectedNodes.forEach(({newLat, newLon, nodeId}) => { + + const origNode = allNodesById.get(nodeId); + const feature = { + type: "Feature", + properties: { + "move": "yes", + "reprojection": "yes", + "osm-id": nodeId, + "id": "replace-geometry-reproject-" + nodeId, + "original-node-tags": JSON.stringify(origNode.tags) + }, + geometry: { + type: "LineString", + coordinates: [[origNode.lon, origNode.lat], [newLon, newLat]] + } + }; + preview.push(feature) + }) + + + detachedNodes.forEach(({reason}, id) => { + const origNode = allNodesById.get(id); const feature = { type: "Feature", properties: { "detach": "yes", - "id": "replace-geometry-detach-" + detachedNodeId + "id": "replace-geometry-detach-" + id, + "detach-reason": reason, + "original-node-tags": JSON.stringify(origNode.tags) }, geometry: { type: "Point", - coordinates: [origPoint[1], origPoint[0]] + coordinates: [origNode.lon, origNode.lat] } }; preview.push(feature) - } + }) return new StaticFeatureSource(Utils.NoNull(preview), false) } - protected async CreateChangeDescriptions(changes: Changes): Promise { + /** + * For 'this.feature`, gets a corresponding closest node that alreay exsists. + * + * This method contains the main logic for this module, as it decides which node gets moved where. + * + */ + public async GetClosestIds(): Promise<{ + // A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created + closestIds: number[], + allNodesById: Map, + osmWay: OsmWay, + detachedNodes: Map, + reprojectedNodes: Map + }> { + // TODO FIXME: if a new point has to be created, snap to already existing ways + + const nodeDb = this.state.featurePipeline.fullNodeDatabase; + if (nodeDb === undefined) { + throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" + } + const self = this; + let parsed: OsmObject[]; + { + // Gather the needed OsmObjects + const splitted = this.wayToReplaceId.split("/"); + const type = splitted[0]; + const idN = Number(splitted[1]); + if (idN < 0 || type !== "way") { + throw "Invalid ID to conflate: " + this.wayToReplaceId + } + const url = `${this.state.osmConnection?._oauth_config?.url ?? "https://openstreetmap.org"}/api/0.6/${this.wayToReplaceId}/full`; + const rawData = await Utils.downloadJsonCached(url, 1000) + parsed = OsmObject.ParseObjects(rawData.elements); + } + const allNodes = parsed.filter(o => o.type === "node") + const osmWay = parsed[parsed.length - 1] + if (osmWay.type !== "way") { + throw "WEIRD: expected an OSM-way as last element here!" + } + const allNodesById = new Map() + for (const node of allNodes) { + allNodesById.set(node.id, node) + } + /** + * For every already existing OSM-point, we calculate: + * + * - the distance to every target point. + * - Wether this node has (other) parent ways, which might restrict movement + * - Wether this node has tags set + * + * Having tags and/or being connected to another way indicate that there is some _relation_ with objects in the neighbourhood. + * + * The Replace-geometry action should try its best to honour these. Some 'wiggling' is allowed (e.g. moving an entrance a bit), but these relations should not be broken.l + */ + const distances = new Map distance (or undefined if a duplicate)*/ + number[]>(); + + const nodeInfo = new Map() + + for (const node of allNodes) { + + const parentWays = nodeDb.GetParentWays(node.id) + if (parentWays === undefined) { + throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?" + } + const parentWayIds = parentWays.data.map(w => w.type + "/" + w.id) + const idIndex = parentWayIds.indexOf(this.wayToReplaceId) + if (idIndex < 0) { + throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..." + } + parentWayIds.splice(idIndex, 1) + const partOfSomeWay = parentWayIds.length > 0 + const hasTags = Object.keys(node.tags).length > 1; + + const nodeDistances = this.targetCoordinates.map(_ => undefined) + for (let i = 0; i < this.targetCoordinates.length; i++) { + if (this.identicalTo[i] !== undefined) { + continue; + } + const targetCoordinate = this.targetCoordinates[i]; + const cp = node.centerpoint() + const d = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]]) + if (d > 25) { + // This is too much to move + continue + } + if (d < 3 || !(hasTags || partOfSomeWay)) { + // If there is some relation: cap the move distance to 3m + nodeDistances[i] = d; + } + + } + distances.set(node.id, nodeDistances) + nodeInfo.set(node.id, { + distances: nodeDistances, + partOfWay: partOfSomeWay, + hasTags + }) + } + + const closestIds = this.targetCoordinates.map(_ => undefined) + const unusedIds = new Map(); + { + // Search best merge candidate + /** + * Then, we search the node that has to move the least distance and add this as mapping. + * We do this until no points are left + */ + let candidate: number; + let moveDistance: number; + /** + * The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates + */ + do { + candidate = undefined; + moveDistance = Infinity; + distances.forEach((distances, nodeId) => { + const minDist = Math.min(...Utils.NoNull(distances)) + if (moveDistance > minDist) { + // We have found a candidate to move + candidate = nodeId + moveDistance = minDist + } + }) + + if (candidate !== undefined) { + // We found a candidate... Search the corresponding target id: + let targetId: number = undefined; + let lowestDistance = Number.MAX_VALUE + let nodeDistances = distances.get(candidate) + for (let i = 0; i < nodeDistances.length; i++) { + const d = nodeDistances[i] + if (d !== undefined && d < lowestDistance) { + lowestDistance = d; + targetId = i; + } + } + + // This candidates role is done, it can be removed from the distance matrix + distances.delete(candidate) + + if (targetId !== undefined) { + // At this point, we have our target coordinate index: targetId! + // Lets map it... + closestIds[targetId] = candidate + + // To indicate that this targetCoordinate is taken, we remove them from the distances matrix + distances.forEach(dists => { + dists[targetId] = undefined + }) + } else { + // Seems like all the targetCoordinates have found a source point + unusedIds.set(candidate, { + reason: "Unused by new way", + hasTags: nodeInfo.get(candidate).hasTags + }) + } + } + } while (candidate !== undefined) + } + + // If there are still unused values in 'distances', they are definitively unused + distances.forEach((_, nodeId) => { + unusedIds.set(nodeId, { + reason: "Unused by new way", + hasTags: nodeInfo.get(nodeId).hasTags + }) + }) + + const reprojectedNodes = new Map(); + { + // Lets check the unused ids: can they be detached or do they signify some relation with the object? + unusedIds.forEach(({}, id) => { + const info = nodeInfo.get(id) + if (!(info.hasTags || info.partOfWay)) { + // Nothing special here, we detach + return + } + + // The current node has tags and/or has an attached other building. + // We should project them and move them onto the building on an appropriate place + const node = allNodesById.get(id) + + // Project the node onto the target way to calculate the new coordinates + const way = { + type: "Feature", + properties: {}, + geometry: { + type: "LineString", + coordinates: self.targetCoordinates + } + }; + const projected = GeoOperations.nearestPoint( + way, [node.lon, node.lat] + ) + reprojectedNodes.set(id, { + newLon: projected.geometry.coordinates[0], + newLat: projected.geometry.coordinates[1], + projectAfterIndex: projected.properties.index, + distance: projected.properties.dist, + nodeId: id + }) + }) + + reprojectedNodes.forEach((_, nodeId) => unusedIds.delete(nodeId)) + + } + + + return {closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes}; + } + + protected async CreateChangeDescriptions(changes: Changes): Promise { + const nodeDb = this.state.featurePipeline.fullNodeDatabase; + if (nodeDb === undefined) { + throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" + } + + const {closestIds, osmWay, detachedNodes, reprojectedNodes} = await this.GetClosestIds() const allChanges: ChangeDescription[] = [] const actualIdsToUse: number[] = [] - - const {closestIds, osmWay, detachedNodeIds} = await this.GetClosestIds() - for (let i = 0; i < closestIds.length; i++) { if (this.identicalTo[i] !== undefined) { const j = this.identicalTo[i] @@ -193,13 +459,46 @@ export default class ReplaceGeometryAction extends OsmChangeAction { allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes)) } + const newCoordinates = [...this.targetCoordinates] + + { + // Add reprojected nodes to the way + + const proj = Array.from(reprojectedNodes.values()) + proj.sort((a, b) => { + // Sort descending + const diff = b.projectAfterIndex - a.projectAfterIndex; + if(diff !== 0){ + return diff + } + return b.distance - a.distance; + + + }) + + for (const reprojectedNode of proj) { + const change = { + id: reprojectedNode.nodeId, + type: "node", + meta: { + theme: this.theme, + changeType: "move" + }, + changes: {lon: reprojectedNode.newLon, lat: reprojectedNode.newLat} + } + allChanges.push(change) + actualIdsToUse.splice(reprojectedNode.projectAfterIndex + 1, 0, reprojectedNode.nodeId) + newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [reprojectedNode.newLon, reprojectedNode.newLat]) + } + } + // Actually change the nodes of the way! allChanges.push({ type: "way", id: osmWay.id, changes: { nodes: actualIdsToUse, - coordinates: this.targetCoordinates + coordinates: newCoordinates }, meta: { theme: this.theme, @@ -209,38 +508,38 @@ export default class ReplaceGeometryAction extends OsmChangeAction { // Some nodes might need to be deleted - if (detachedNodeIds.length > 0) { + if (detachedNodes.size > 0) { + detachedNodes.forEach(({hasTags, reason}, nodeId) => { + const parentWays = nodeDb.GetParentWays(nodeId) + const index = parentWays.data.map(w => w.id).indexOf(osmWay.id) + if (index < 0) { + console.error("ReplaceGeometryAction is trying to detach node " + nodeId + ", but it isn't listed as being part of way " + osmWay.id) + return; + } + // We detachted this node - so we unregister + parentWays.data.splice(index, 1) + parentWays.ping(); - const nodeDb = this.state.featurePipeline.fullNodeDatabase; - if (nodeDb === undefined) { - throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" - } - for (const nodeId of detachedNodeIds) { - const osmNode = nodeDb.GetNode(nodeId) - const parentWayIds: number[] = JSON.parse(osmNode.tags["parent_way_ids"]) - const index = parentWayIds.indexOf(osmWay.id) - if(index < 0){ - console.error("ReplaceGeometryAction is trying to detach node "+nodeId+", but it isn't listed as being part of way "+osmWay.id) - continue; + if (hasTags) { + // Has tags: we leave this node alone + return; } - parentWayIds.splice(index, 1) - osmNode.tags["parent_way_ids"] = JSON.stringify(parentWayIds) - if(parentWayIds.length == 0){ - // This point has no other ways anymore - lets clean it! - console.log("Removing node "+nodeId, "as it isn't needed anymore by any way") - - allChanges.push({ - meta: { - theme: this.theme, - changeType: "delete" - }, - doDelete: true, - type: "node", - id: nodeId, - }) - + if (parentWays.data.length != 0) { + // Still part of other ways: we leave this node alone! + return; } - } + + console.log("Removing node " + nodeId, "as it isn't needed anymore by any way") + allChanges.push({ + meta: { + theme: this.theme, + changeType: "delete" + }, + doDelete: true, + type: "node", + id: nodeId, + }) + }) } @@ -248,131 +547,5 @@ export default class ReplaceGeometryAction extends OsmChangeAction { return allChanges } - /** - * For 'this.feature`, gets a corresponding closest node that alreay exsists. - * - * This method contains the main logic for this module, as it decides which node gets moved where. - * - */ - private async GetClosestIds(): Promise<{ - - // A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created - closestIds: number[], - allNodesById: Map, - osmWay: OsmWay, - detachedNodeIds: number[] - }> { - // TODO FIXME: cap move length on points which are embedded into other ways (ev. disconnect them) - // TODO FIXME: if a new point has to be created, snap to already existing ways - - - let parsed: OsmObject[]; - { - // Gather the needed OsmObjects - const splitted = this.wayToReplaceId.split("/"); - const type = splitted[0]; - const idN = Number(splitted[1]); - if (idN < 0 || type !== "way") { - throw "Invalid ID to conflate: " + this.wayToReplaceId - } - const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`; - const rawData = await Utils.downloadJsonCached(url, 1000) - parsed = OsmObject.ParseObjects(rawData.elements); - } - const allNodes = parsed.filter(o => o.type === "node") - - /** - * For every already existing OSM-point, we calculate the distance to every target point - */ - - const distances = new Map distance (or undefined if a duplicate)*/>(); - for (const node of allNodes) { - const nodeDistances = this.targetCoordinates.map(_ => undefined) - for (let i = 0; i < this.targetCoordinates.length; i++) { - if (this.identicalTo[i] !== undefined) { - continue; - } - const targetCoordinate = this.targetCoordinates[i]; - const cp = node.centerpoint() - nodeDistances[i] = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]]) - } - distances.set(node.id, nodeDistances) - } - - /** - * Then, we search the node that has to move the least distance and add this as mapping. - * We do this until no points are left - */ - let candidate: number; - let moveDistance: number; - const closestIds = this.targetCoordinates.map(_ => undefined) - /** - * The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates - */ - const unusedIds = [] - do { - candidate = undefined; - moveDistance = Infinity; - distances.forEach((distances, nodeId) => { - const minDist = Math.min(...Utils.NoNull(distances)) - if (moveDistance > minDist) { - // We have found a candidate to move - candidate = nodeId - moveDistance = minDist - } - }) - - if (candidate !== undefined) { - // We found a candidate... Search the corresponding target id: - let targetId: number = undefined; - let lowestDistance = Number.MAX_VALUE - let nodeDistances = distances.get(candidate) - for (let i = 0; i < nodeDistances.length; i++) { - const d = nodeDistances[i] - if (d !== undefined && d < lowestDistance) { - lowestDistance = d; - targetId = i; - } - } - - // This candidates role is done, it can be removed from the distance matrix - distances.delete(candidate) - - if (targetId !== undefined) { - // At this point, we have our target coordinate index: targetId! - // Lets map it... - closestIds[targetId] = candidate - - // To indicate that this targetCoordinate is taken, we remove them from the distances matrix - distances.forEach(dists => { - dists[targetId] = undefined - }) - } else { - // Seems like all the targetCoordinates have found a source point - unusedIds.push(candidate) - } - } - } while (candidate !== undefined) - - - // If there are still unused values in 'distances', they are definitively unused - distances.forEach((_, nodeId) => { - unusedIds.push(nodeId) - }) - - { - // Some extra data is included for rendering - const osmWay = parsed[parsed.length - 1] - if (osmWay.type !== "way") { - throw "WEIRD: expected an OSM-way as last element here!" - } - const allNodesById = new Map() - for (const node of allNodes) { - allNodesById.set(node.id, node) - } - return {closestIds, allNodesById, osmWay, detachedNodeIds: unusedIds}; - } - } - } \ No newline at end of file diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index d28d8f165..4e5ccc2e3 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -391,10 +391,9 @@ export class OsmWay extends OsmObject { // This is probably part of a relation which hasn't been fully downloaded continue; } - const cp = node.centerpoint(); - this.coordinates.push(cp); - latSum += cp[0] - lonSum += cp[1] + this.coordinates.push(node.centerpoint()); + latSum += node.lat + lonSum += node.lon } let count = this.coordinates.length; this.lat = latSum / count; diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index db422827f..67be6a5af 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -272,7 +272,7 @@ export default class SimpleMetaTaggers { public static country = new CountryTagger() private static isOpen = new SimpleMetaTagger( { - keys: ["_isOpen", "_isOpen:description"], + keys: ["_isOpen"], doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", includesDates: true, isLazy: true @@ -283,7 +283,7 @@ export default class SimpleMetaTaggers { // isOpen is irrelevant return false } - + Object.defineProperty(feature.properties, "_isOpen", { enumerable: false, configurable: true, @@ -291,7 +291,8 @@ export default class SimpleMetaTaggers { delete feature.properties._isOpen feature.properties._isOpen = undefined const tagsSource = state.allElements.getEventSourceById(feature.properties.id); - tagsSource.addCallbackAndRunD(tags => { + tagsSource + .addCallbackAndRunD(tags => { if (tags.opening_hours === undefined || tags._country === undefined) { return; } @@ -341,7 +342,6 @@ export default class SimpleMetaTaggers { } } updateTags(); - return true; // Our job is done, lets unregister! } catch (e) { console.warn("Error while parsing opening hours of ", tags.id, e); delete tags._isOpen @@ -352,6 +352,7 @@ export default class SimpleMetaTaggers { return undefined } }) + return true; }) ) diff --git a/Logic/Web/IdbLocalStorage.ts b/Logic/Web/IdbLocalStorage.ts index 5b7d3db1d..7a39a0b64 100644 --- a/Logic/Web/IdbLocalStorage.ts +++ b/Logic/Web/IdbLocalStorage.ts @@ -1,5 +1,7 @@ import {UIEventSource} from "../UIEventSource"; import * as idb from "idb-keyval" +import ScriptUtils from "../../scripts/ScriptUtils"; +import {Utils} from "../../Utils"; /** * UIEventsource-wrapper around indexedDB key-value */ @@ -8,6 +10,9 @@ export class IdbLocalStorage { public static Get(key: string, options: { defaultValue?: T }): UIEventSource{ const src = new UIEventSource(options.defaultValue, "idb-local-storage:"+key) + if(Utils.runningFromConsole){ + return src; + } idb.get(key).then(v => src.setData(v ?? options.defaultValue)) src.addCallback(v => idb.set(key, v)) return src; diff --git a/Logic/Web/MangroveReviews.ts b/Logic/Web/MangroveReviews.ts index 036228248..328e6d6af 100644 --- a/Logic/Web/MangroveReviews.ts +++ b/Logic/Web/MangroveReviews.ts @@ -1,6 +1,7 @@ import * as mangrove from 'mangrove-reviews' import {UIEventSource} from "../UIEventSource"; import {Review} from "./Review"; +import {Utils} from "../../Utils"; export class MangroveIdentity { public keypair: any = undefined; @@ -23,7 +24,7 @@ export class MangroveIdentity { }) }) try { - if ((mangroveIdentity.data ?? "") === "") { + if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") { this.CreateIdentity(); } } catch (e) { diff --git a/UI/Base/Combine.ts b/UI/Base/Combine.ts index 434644b6a..3c3cb0c7f 100644 --- a/UI/Base/Combine.ts +++ b/UI/Base/Combine.ts @@ -1,7 +1,6 @@ import {FixedUiElement} from "./FixedUiElement"; import {Utils} from "../../Utils"; import BaseUIElement from "../BaseUIElement"; -import Title from "./Title"; export default class Combine extends BaseUIElement { private readonly uiElements: BaseUIElement[]; @@ -21,6 +20,13 @@ export default class Combine extends BaseUIElement { return this.uiElements.map(el => el.AsMarkdown()).join(this.HasClass("flex-col") ? "\n\n" : " "); } + Destroy() { + super.Destroy(); + for (const uiElement of this.uiElements) { + uiElement.Destroy() + } + } + protected InnerConstructElement(): HTMLElement { const el = document.createElement("span") try { diff --git a/UI/Base/MinimapImplementation.ts b/UI/Base/MinimapImplementation.ts index 4d93c4fae..e5aa7f8ef 100644 --- a/UI/Base/MinimapImplementation.ts +++ b/UI/Base/MinimapImplementation.ts @@ -48,7 +48,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini public static initialize() { Minimap.createMiniMap = options => new MinimapImplementation(options) } - + public installBounds(factor: number | BBox, showRange?: boolean) { this.leafletMap.addCallbackD(leaflet => { let bounds; @@ -105,6 +105,15 @@ export default class MinimapImplementation extends BaseUIElement implements Mini } }) } + + Destroy() { + super.Destroy(); + console.warn("Decomissioning minimap", this._id) + const mp = this.leafletMap.data + this.leafletMap.setData(null) + mp.off() + mp.remove() + } public async TakeScreenshot() { const screenshotter = new SimpleMapScreenshoter(); @@ -125,6 +134,13 @@ export default class MinimapImplementation extends BaseUIElement implements Mini const self = this; // @ts-ignore const resizeObserver = new ResizeObserver(_ => { + if(wrapper.clientHeight === 0 || wrapper.clientWidth === 0){ + return; + } + if(wrapper.offsetParent === null || window.getComputedStyle(wrapper).display === 'none'){ + // Not visible + return; + } try { self.InitMap(); self.leafletMap?.data?.invalidateSize() diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index 686649ed8..c3dd39caa 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -49,24 +49,26 @@ export default class ScrollableFullScreen extends UIElement { Hash.hash.setData(hashToShow) self.Activate(); } else { - self.clear(); + // Some cleanup... + ScrollableFullScreen.empty.AttachTo("fullscreen") + const fs = document.getElementById("fullscreen"); + ScrollableFullScreen._currentlyOpen?.isShown?.setData(false); + fs.classList.add("hidden") } }) - Hash.hash.addCallback(hash => { - if (!isShown.data) { - return; - } - if (hash === undefined || hash === "" || hash !== hashToShow) { - isShown.setData(false) - } - }) } InnerRender(): BaseUIElement { return this._component; } + Destroy() { + super.Destroy(); + this._component.Destroy() + this._fullscreencomponent.Destroy() + } + Activate(): void { this.isShown.setData(true) this._fullscreencomponent.AttachTo("fullscreen"); @@ -74,14 +76,6 @@ export default class ScrollableFullScreen extends UIElement { ScrollableFullScreen._currentlyOpen = this; fs.classList.remove("hidden") } - - private clear() { - ScrollableFullScreen.empty.AttachTo("fullscreen") - const fs = document.getElementById("fullscreen"); - ScrollableFullScreen._currentlyOpen?.isShown?.setData(false); - fs.classList.add("hidden") - } - private BuildComponent(title: BaseUIElement, content: BaseUIElement, isShown: UIEventSource) { const returnToTheMap = new Combine([ @@ -93,6 +87,7 @@ export default class ScrollableFullScreen extends UIElement { returnToTheMap.onClick(() => { isShown.setData(false) + Hash.hash.setData(undefined) }) title.SetClass("block text-l sm:text-xl md:text-2xl w-full font-bold p-0 max-h-20vh overflow-y-auto") diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index 6ee720627..6798a11f2 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -8,10 +8,19 @@ export class VariableUiElement extends BaseUIElement { super(); this._contents = contents; } + + Destroy() { + super.Destroy(); + this.isDestroyed = true; + } protected InnerConstructElement(): HTMLElement { const el = document.createElement("span"); + const self = this; this._contents.addCallbackAndRun((contents) => { + if(self.isDestroyed){ + return true; + } while (el.firstChild) { el.removeChild(el.lastChild); } diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index eaac95b4b..eb33637a8 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -11,6 +11,7 @@ export default abstract class BaseUIElement { private clss: Set = new Set(); private style: string; private _onClick: () => void; + protected isDestroyed = false; public onClick(f: (() => void)) { this._onClick = f; @@ -149,6 +150,10 @@ export default abstract class BaseUIElement { public AsMarkdown(): string { throw "AsMarkdown is not implemented by " + this.constructor.name+"; implement it in the subclass" } + + public Destroy(){ + this.isDestroyed = true; + } protected abstract InnerConstructElement(): HTMLElement; } diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index a669b856c..a996416f5 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -121,7 +121,7 @@ export default class LeftControls extends Combine { "filters", guiState.filterViewIsOpened ).SetClass("rounded-lg md:floating-element-width"), - new MapControlButton(Svg.filter_svg()) + new MapControlButton(Svg.layers_svg()) .onClick(() => guiState.filterViewIsOpened.setData(true)), guiState.filterViewIsOpened ) diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 1976f3e5a..56345766c 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -236,4 +236,5 @@ export default class FeatureInfoBox extends ScrollableFullScreen { return false; } + } diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts index 7cfa1fe7f..4800dce61 100644 --- a/UI/Popup/ImportButton.ts +++ b/UI/Popup/ImportButton.ts @@ -36,6 +36,7 @@ import {Tag} from "../../Logic/Tags/Tag"; import TagApplyButton from "./TagApplyButton"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import * as conflation_json from "../../assets/layers/conflation/conflation.json"; +import {GeoOperations} from "../../Logic/GeoOperations"; abstract class AbstractImportButton implements SpecialVisualizations { @@ -309,8 +310,6 @@ export class ConflateButton extends AbstractImportButton { args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource; targetLayer: string }, tagSource: UIEventSource, guiState: DefaultGuiState, feature: any, onCancelClicked: () => void): BaseUIElement { - return new FixedUiElement("ReplaceGeometry is currently very broken - use mapcomplete.osm.be for now").SetClass("alert") - const nodesMustMatch = args.snap_onto_layers?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) const mergeConfigs = [] @@ -326,6 +325,7 @@ export class ConflateButton extends AbstractImportButton { const key = args["way_to_conflate"] const wayToConflate = tagSource.data[key] + feature = GeoOperations.removeOvernoding(feature); const action = new ReplaceGeometryAction( state, feature, diff --git a/UI/ShowDataLayer/ShowDataLayer.ts b/UI/ShowDataLayer/ShowDataLayer.ts index d1af72001..7e372e7a7 100644 --- a/UI/ShowDataLayer/ShowDataLayer.ts +++ b/UI/ShowDataLayer/ShowDataLayer.ts @@ -29,6 +29,11 @@ export default class ShowDataLayer { // Used to generate a fresh ID when needed private _cleanCount = 0; private geoLayer = undefined; + + /** + * A collection of functions to call when the current geolayer is unregistered + */ + private unregister: (() => void)[] = []; private isDirty = false; /** * If the selected element triggers, this is used to lookup the correct layer and to open the popup @@ -56,25 +61,32 @@ export default class ShowDataLayer { const self = this; options.leafletMap.addCallback(_ => { - self.update(options) + return self.update(options) } ); this._features.features.addCallback(_ => self.update(options)); options.doShowLayer?.addCallback(doShow => { const mp = options.leafletMap.data; + if(mp === null){ + self.Destroy() + return true; + } if (mp == undefined) { return; } + if (doShow) { if (self.isDirty) { - self.update(options) + return self.update(options) } else { mp.addLayer(this.geoLayer) } } else { if (this.geoLayer !== undefined) { mp.removeLayer(this.geoLayer) + this.unregister.forEach(f => f()) + this.unregister = [] } } @@ -82,40 +94,50 @@ export default class ShowDataLayer { this._selectedElement?.addCallbackAndRunD(selected => { - if (self._leafletMap.data === undefined) { - return; - } - const v = self.leafletLayersPerId.get(selected.properties.id + selected.geometry.type) - if (v === undefined) { - return; - } - const leafletLayer = v.leafletlayer - const feature = v.feature - if (leafletLayer.getPopup().isOpen()) { - return; - } - if (selected.properties.id !== feature.properties.id) { - return; - } - - if (feature.id !== feature.properties.id) { - // Probably a feature which has renamed - // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too - console.log("Not opening the popup for", feature, "as probably renamed") - return; - } - if (selected.geometry.type === feature.geometry.type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again - ) { - console.log("Opening popup of feature", feature) - leafletLayer.openPopup() - } + self.openPopupOfSelectedElement(selected) }) this.update(options) } - private update(options: ShowDataLayerOptions) { + private Destroy() { + this.unregister.forEach(f => f()) + } + + private openPopupOfSelectedElement(selected) { + if (selected === undefined) { + return + } + if (this._leafletMap.data === undefined) { + return; + } + const v = this.leafletLayersPerId.get(selected.properties.id + selected.geometry.type) + if (v === undefined) { + return; + } + const leafletLayer = v.leafletlayer + const feature = v.feature + if (leafletLayer.getPopup().isOpen()) { + return; + } + if (selected.properties.id !== feature.properties.id) { + return; + } + + if (feature.id !== feature.properties.id) { + // Probably a feature which has renamed + // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too + console.log("Not opening the popup for", feature, "as probably renamed") + return; + } + if (selected.geometry.type === feature.geometry.type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again + ) { + leafletLayer.openPopup() + } + } + + private update(options: ShowDataLayerOptions) : boolean{ if (this._features.features.data === undefined) { return; } @@ -125,9 +147,13 @@ export default class ShowDataLayer { } const mp = options.leafletMap.data; + if(mp === null){ + return true; // Unregister as the map is destroyed + } if (mp === undefined) { return; } + console.trace("Updating... " + mp["_container"]?.id +" for layer "+this._layerToShow.id) this._cleanCount++ // clean all the old stuff away, if any if (this.geoLayer !== undefined) { @@ -145,7 +171,7 @@ export default class ShowDataLayer { pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng), onEachFeature: (feature, leafletLayer) => self.postProcessFeature(feature, leafletLayer) }); - + const selfLayer = this.geoLayer; const allFeats = this._features.features.data; for (const feat of allFeats) { @@ -176,20 +202,20 @@ export default class ShowDataLayer { offsettedLine = L.polyline(coords, lineStyle); this.postProcessFeature(feat, offsettedLine) offsettedLine.addTo(this.geoLayer) - + // If 'self.geoLayer' is not the same as the layer the feature is added to, we can safely remove this callback return self.geoLayer !== selfLayer }) } else { this.geoLayer.addData(feat); - } + } } catch (e) { console.error("Could not add ", feat, "to the geojson layer in leaflet due to", e, e.stack) } } if (options.zoomToFeatures ?? false) { - if(this.geoLayer.getLayers().length > 0){ + if (this.geoLayer.getLayers().length > 0) { try { const bounds = this.geoLayer.getBounds() mp.fitBounds(bounds, {animate: false}) @@ -203,6 +229,7 @@ export default class ShowDataLayer { mp.addLayer(this.geoLayer) } this.isDirty = false; + this.openPopupOfSelectedElement(this._selectedElement?.data) } @@ -250,7 +277,7 @@ export default class ShowDataLayer { } /** - * POst processing - basically adding the popup + * Post processing - basically adding the popup * @param feature * @param leafletLayer * @private @@ -290,6 +317,10 @@ export default class ShowDataLayer { } infobox.AttachTo(id) infobox.Activate(); + this.unregister.push(() => { + console.log("Destroying infobox") + infobox.Destroy(); + }) if (this._selectedElement?.data?.properties?.id !== feature.properties.id) { this._selectedElement?.setData(feature) } @@ -303,7 +334,6 @@ export default class ShowDataLayer { leafletlayer: leafletLayer }) - } } \ No newline at end of file diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index aa43f5891..98a889952 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -53,7 +53,7 @@ export class SubstitutedTranslation extends VariableUiElement { return viz.func.constr(state, tagsSource, proto.special.args, DefaultGuiState.state).SetStyle(proto.special.style); } catch (e) { console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) - return new FixedUiElement(`Could not generate special rendering for ${viz.func}(${viz.args.join(", ")}) ${e}`).SetStyle("alert") + return new FixedUiElement(`Could not generate special rendering for ${viz.func.funcName}(${viz.args.join(", ")}) ${e}`).SetStyle("alert") } } )) diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index 355b61424..b88cc7b37 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -7,7 +7,7 @@ export class Translation extends BaseUIElement { public static forcedLanguage = undefined; public readonly translations: object - + constructor(translations: object, context?: string) { super() if (translations === undefined) { @@ -36,6 +36,11 @@ export class Translation extends BaseUIElement { get txt(): string { return this.textFor(Translation.forcedLanguage ?? Locale.language.data) } + + Destroy() { + super.Destroy(); + this.isDestroyed = true; + } static ExtractAllTranslationsFrom(object: any, context = ""): { context: string, tr: Translation }[] { const allTranslations: { context: string, tr: Translation }[] = [] @@ -93,7 +98,11 @@ export class Translation extends BaseUIElement { InnerConstructElement(): HTMLElement { const el = document.createElement("span") + const self = this Locale.language.addCallbackAndRun(_ => { + if(self.isDestroyed){ + return true + } el.innerHTML = this.txt }) return el; diff --git a/assets/layers/address/address.json b/assets/layers/address/address.json index ae234316a..273218b05 100644 --- a/assets/layers/address/address.json +++ b/assets/layers/address/address.json @@ -108,7 +108,8 @@ "render": "Fixme description{fixme}", "question": { "en": "What should be fixed here? Please explain", - "zh_Hant": "這裡需要修什麼?請直接解釋" + "zh_Hant": "這裡需要修什麼?請直接解釋", + "de": "Was sollte hier korrigiert werden? Bitte erläutern" }, "freeform": { "key": "fixme" diff --git a/assets/layers/barrier/barrier.json b/assets/layers/barrier/barrier.json index 0d784972f..eb2ba836b 100644 --- a/assets/layers/barrier/barrier.json +++ b/assets/layers/barrier/barrier.json @@ -133,7 +133,8 @@ "then": { "en": "This is a single bollard in the road" } - },{ + }, + { "if": "barrier=cycle_barrier", "then": { "en": "This is a cycle barrier slowing down cyclists", diff --git a/assets/layers/charging_station/charging_station.json b/assets/layers/charging_station/charging_station.json index bf83ef528..96dd6f11a 100644 --- a/assets/layers/charging_station/charging_station.json +++ b/assets/layers/charging_station/charging_station.json @@ -34,7 +34,8 @@ "#": "Allowed vehicle types", "question": { "en": "Which vehicles are allowed to charge here?", - "nl": "Welke voertuigen kunnen hier opgeladen worden?" + "nl": "Welke voertuigen kunnen hier opgeladen worden?", + "de": "Welche Fahrzeuge dürfen hier laden?" }, "multiAnswer": true, "mappings": [ @@ -43,7 +44,8 @@ "ifnot": "bicycle=no", "then": { "en": "Bicycles can be charged here", - "nl": "Fietsen kunnen hier opgeladen worden" + "nl": "Fietsen kunnen hier opgeladen worden", + "de": "Fahrräder können hier geladen werden" } }, { @@ -51,7 +53,8 @@ "ifnot": "motorcar=no", "then": { "en": "Cars can be charged here", - "nl": "Elektrische auto's kunnen hier opgeladen worden" + "nl": "Elektrische auto's kunnen hier opgeladen worden", + "de": "Autos können hier geladen werden" } }, { @@ -59,7 +62,8 @@ "ifnot": "scooter=no", "then": { "en": "Scooters can be charged here", - "nl": "Electrische scooters (snorfiets of bromfiets) kunnen hier opgeladen worden" + "nl": "Electrische scooters (snorfiets of bromfiets) kunnen hier opgeladen worden", + "de": "Roller können hier geladen werden" } }, { @@ -67,7 +71,8 @@ "ifnot": "hgv=no", "then": { "en": "Heavy good vehicles (such as trucks) can be charged here", - "nl": "Vrachtwagens kunnen hier opgeladen worden" + "nl": "Vrachtwagens kunnen hier opgeladen worden", + "de": "LKW können hier geladen werden" } }, { @@ -75,7 +80,8 @@ "ifnot": "bus=no", "then": { "en": "Buses can be charged here", - "nl": "Bussen kunnen hier opgeladen worden" + "nl": "Bussen kunnen hier opgeladen worden", + "de": "Busse können hier geladen werden" } } ] @@ -152,7 +158,8 @@ "id": "Available_charging_stations (generated)", "question": { "en": "Which charging connections are available here?", - "nl": "Welke aansluitingen zijn hier beschikbaar?" + "nl": "Welke aansluitingen zijn hier beschikbaar?", + "de": "Welche Ladeanschlüsse gibt es hier?" }, "multiAnswer": true, "mappings": [ @@ -2864,14 +2871,16 @@ }, "question": { "en": "When is this charging station opened?", - "nl": "Wanneer is dit oplaadpunt beschikbaar??" + "nl": "Wanneer is dit oplaadpunt beschikbaar??", + "de": "Wann ist diese Ladestation geöffnet?" }, "mappings": [ { "if": "opening_hours=24/7", "then": { "en": "24/7 opened (including holidays)", - "nl": "24/7 open - ook tijdens vakanties" + "nl": "24/7 open - ook tijdens vakanties", + "de": "durchgehend geöffnet (einschließlich Feiertage)" } } ] @@ -3000,7 +3009,8 @@ "question": { "en": "What kind of authentication is available at the charging station?", "nl": "Hoe kan men zich aanmelden aan dit oplaadstation?", - "fr": "Quelle sorte d'authentification est disponible à cette station de charge ?" + "fr": "Quelle sorte d'authentification est disponible à cette station de charge ?", + "de": "Welche Art der Authentifizierung ist an der Ladestation möglich?" }, "multiAnswer": true, "mappings": [ @@ -3010,7 +3020,8 @@ "then": { "en": "Authentication by a membership card", "nl": "Aanmelden met een lidkaart is mogelijk", - "fr": "Authentification par carte de membre" + "fr": "Authentification par carte de membre", + "de": "Authentifizierung per Mitgliedskarte" } }, { @@ -3019,7 +3030,8 @@ "then": { "en": "Authentication by an app", "nl": "Aanmelden via een applicatie is mogelijk", - "fr": "Authentification par une app" + "fr": "Authentification par une app", + "de": "Authentifizierung per App" } }, { @@ -3028,7 +3040,8 @@ "then": { "en": "Authentication via phone call is available", "nl": "Aanmelden door te bellen naar een telefoonnummer is mogelijk", - "fr": "Authentification par appel téléphonique est disponible" + "fr": "Authentification par appel téléphonique est disponible", + "de": "Authentifizierung per Anruf ist möglich" } }, { @@ -3037,7 +3050,8 @@ "then": { "en": "Authentication via SMS is available", "nl": "Aanmelden via SMS is mogelijk", - "fr": "Authentification par SMS est disponible" + "fr": "Authentification par SMS est disponible", + "de": "Authentifizierung per SMS ist möglich" } }, { @@ -3046,7 +3060,8 @@ "then": { "en": "Authentication via NFC is available", "nl": "Aanmelden via NFC is mogelijk", - "fr": "Authentification par NFC est disponible" + "fr": "Authentification par NFC est disponible", + "de": "Authentifizierung per NFC ist möglich" } }, { @@ -3054,7 +3069,8 @@ "ifnot": "authentication:money_card=no", "then": { "en": "Authentication via Money Card is available", - "nl": "Aanmelden met Money Card is mogelijk" + "nl": "Aanmelden met Money Card is mogelijk", + "de": "Authentifizierung per Geldkarte ist möglich" } }, { @@ -3063,7 +3079,8 @@ "then": { "en": "Authentication via debit card is available", "nl": "Aanmelden met een betaalkaart is mogelijk", - "fr": "Authentification par carte de débit est disponible" + "fr": "Authentification par carte de débit est disponible", + "de": "Authentifizierung per Kreditkarte ist möglich" } }, { @@ -3072,7 +3089,8 @@ "then": { "en": "Charging here is (also) possible without authentication", "nl": "Hier opladen is (ook) mogelijk zonder aan te melden", - "fr": "Charger ici est (aussi) possible sans authentification" + "fr": "Charger ici est (aussi) possible sans authentification", + "de": "Das Laden ist hier (auch) ohne Authentifizierung möglich" } } ], @@ -3139,11 +3157,13 @@ "id": "Network", "render": { "en": "Part of the network {network}", - "nl": "Maakt deel uit van het {network}-netwerk" + "nl": "Maakt deel uit van het {network}-netwerk", + "de": "Teil des Netzwerks {network}" }, "question": { "en": "Is this charging station part of a network?", - "nl": "Is dit oplaadpunt deel van een groter netwerk?" + "nl": "Is dit oplaadpunt deel van een groter netwerk?", + "de": "Ist diese Ladestation Teil eines Netzwerks?" }, "freeform": { "key": "network" @@ -3153,14 +3173,16 @@ "if": "no:network=yes", "then": { "en": "Not part of a bigger network, e.g. because the charging station is maintained by a local business", - "nl": "Maakt geen deel uit van een groter netwerk, een lokale zaak of organisatie beheert dit oplaadpunt" + "nl": "Maakt geen deel uit van een groter netwerk, een lokale zaak of organisatie beheert dit oplaadpunt", + "de": "Nicht Teil eines größeren Netzwerks, z. B. weil die Ladestation von einem lokalen Unternehmen betrieben wird" } }, { "if": "network=none", "then": { "en": "Not part of a bigger network", - "nl": "Maakt geen deel uit van een groter netwerk" + "nl": "Maakt geen deel uit van een groter netwerk", + "de": "Nicht Teil eines größeren Netzwerks" }, "hideInAnswer": true }, @@ -3206,11 +3228,13 @@ "id": "Operator", "question": { "en": "Who is the operator of this charging station?", - "nl": "Wie beheert dit oplaadpunt?" + "nl": "Wie beheert dit oplaadpunt?", + "de": "Wer ist der Betreiber dieser Ladestation?" }, "render": { "en": "This charging station is operated by {operator}", - "nl": "Wordt beheerd door {operator}" + "nl": "Wordt beheerd door {operator}", + "de": "Diese Ladestation wird betrieben von {operator}" }, "freeform": { "key": "operator" @@ -3224,7 +3248,8 @@ }, "then": { "en": "Actually, {operator} is the network", - "nl": "Eigenlijk is {operator} het netwerk waarvan het deel uitmaakt" + "nl": "Eigenlijk is {operator} het netwerk waarvan het deel uitmaakt", + "de": "Eigentlich ist {operator} das Netzwerk" }, "addExtraTags": [ "operator=" @@ -3300,7 +3325,8 @@ "id": "Operational status", "question": { "en": "Is this charging point in use?", - "nl": "Is dit oplaadpunt operationeel?" + "nl": "Is dit oplaadpunt operationeel?", + "de": "Ist dieser Ladepunkt in Betrieb?" }, "mappings": [ { @@ -3315,7 +3341,8 @@ }, "then": { "en": "This charging station works", - "nl": "Dit oplaadpunt werkt" + "nl": "Dit oplaadpunt werkt", + "de": "Diese Ladestation ist betriebsbereit" } }, { @@ -3330,7 +3357,8 @@ }, "then": { "en": "This charging station is broken", - "nl": "Dit oplaadpunt is kapot" + "nl": "Dit oplaadpunt is kapot", + "de": "Diese Ladestation ist defekt" } }, { @@ -3345,7 +3373,8 @@ }, "then": { "en": "A charging station is planned here", - "nl": "Hier zal binnenkort een oplaadpunt gebouwd worden" + "nl": "Hier zal binnenkort een oplaadpunt gebouwd worden", + "de": "Hier ist eine Ladestation geplant" } }, { @@ -3360,7 +3389,8 @@ }, "then": { "en": "A charging station is constructed here", - "nl": "Hier wordt op dit moment een oplaadpunt gebouwd" + "nl": "Hier wordt op dit moment een oplaadpunt gebouwd", + "de": "Hier wird eine Ladestation errichtet" } }, { @@ -3375,7 +3405,8 @@ }, "then": { "en": "This charging station has beed permanently disabled and is not in use anymore but is still visible", - "nl": "Dit oplaadpunt is niet meer in gebruik maar is wel nog aanwezig" + "nl": "Dit oplaadpunt is niet meer in gebruik maar is wel nog aanwezig", + "de": "Diese Ladestation wurde dauerhaft geschlossen und wird nicht mehr benutzt, ist aber noch sichtbar" } } ] @@ -3384,21 +3415,24 @@ "id": "Parking:fee", "question": { "en": "Does one have to pay a parking fee while charging?", - "nl": "Moet men parkeergeld betalen tijdens het opladen?" + "nl": "Moet men parkeergeld betalen tijdens het opladen?", + "de": "Muss man während des Ladens eine Parkgebühr bezahlen?" }, "mappings": [ { "if": "parking:fee=no", "then": { "en": "No additional parking cost while charging", - "nl": "Geen extra parkeerkost tijdens het opladen" + "nl": "Geen extra parkeerkost tijdens het opladen", + "de": "Keine zusätzlichen Parkkosten während des Ladens" } }, { "if": "parking:fee=yes", "then": { "en": "An additional parking fee should be paid while charging", - "nl": "Tijdens het opladen moet er parkeergeld betaald worden" + "nl": "Tijdens het opladen moet er parkeergeld betaald worden", + "de": "Während des Ladens ist eine zusätzliche Parkgebühr zu entrichten" } } ], diff --git a/assets/layers/conflation/conflation.json b/assets/layers/conflation/conflation.json index 4661cbdd0..ffafc9bca 100644 --- a/assets/layers/conflation/conflation.json +++ b/assets/layers/conflation/conflation.json @@ -31,6 +31,10 @@ "icon": { "render": "circle:#0f0", "mappings": [ + { + "if": "reprojection=yes", + "then": "none:#f00" + }, { "if": "move=no", "then": "ring:#0f0" @@ -41,7 +45,15 @@ }, { "location": "start", - "icon": "square:#f00", + "icon": { + "render": "square:#f00", + "mappings": [ + { + "if": "reprojection=yes", + "then": "reload:#f00" + } + ] + }, "iconSize": { "render": "10,10,center", "mappings": [ diff --git a/assets/layers/etymology/etymology.json b/assets/layers/etymology/etymology.json index 5db0aa059..1c0f96c41 100644 --- a/assets/layers/etymology/etymology.json +++ b/assets/layers/etymology/etymology.json @@ -140,6 +140,7 @@ }, { "id": "wikipedia", + "#": "Note that this is a _read_only_ option, to prevent people entering a 'wikidata'-link instead of 'name:etymology:wikidata'", "render": { "en": "A Wikipedia article about this street exists:
{wikipedia():max-height:25rem}" }, @@ -149,7 +150,7 @@ "mapRendering": [ { "icon": { - "render": "pin:#05d7fcaa;./assets/layers/etymology/logo.svg", + "render": "pin:#05d7fcaa", "mappings": [ { "if": { @@ -158,7 +159,7 @@ "name:etymology:wikidata=" ] }, - "then": "pin:#fcca05aa;./assets/layers/etymology/logo.svg" + "then": "pin:#fcca05aa" } ] }, @@ -196,4 +197,4 @@ } } ] -} +} \ No newline at end of file diff --git a/assets/layers/food/food.json b/assets/layers/food/food.json index ae6b71081..605413cfe 100644 --- a/assets/layers/food/food.json +++ b/assets/layers/food/food.json @@ -543,7 +543,8 @@ "condition": "cuisine=friture" }, "service:electricity", - "dog-access","reviews" + "dog-access", + "reviews" ], "filter": [ { @@ -679,4 +680,4 @@ "description": { "en": "A layer showing restaurants and fast-food amenities (with a special rendering for friteries)" } -} +} \ No newline at end of file diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index b2e4b5704..c66e82696 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -817,6 +817,12 @@ "authors": [], "sources": [] }, + { + "path": "none.svg", + "license": "CC0", + "authors": [], + "sources": [] + }, { "path": "osm-logo-us.svg", "license": "Logo", diff --git a/assets/svg/none.svg b/assets/svg/none.svg new file mode 100644 index 000000000..2bcb54a9c --- /dev/null +++ b/assets/svg/none.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index f43b36ad0..43c91199b 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -20,7 +20,8 @@ "nb_NO": "Hva er respektivt Wikipedia-element?", "fr": "Quelle est l'entité Wikidata correspondante ?", "ca": "Quina és la correspondent entitat a Wikidata?", - "sv": "Vad är den motsvarande Wikidata-enheten?" + "sv": "Vad är den motsvarande Wikidata-enheten?", + "zh_Hant": "對應的維基資料項目是?" }, "mappings": [ { @@ -35,7 +36,8 @@ "nb_NO": "Ingen Wikipedia-side lenket enda", "fr": "Pas encore de lien vers une page Wikipedia", "ca": "No hi ha cap enllaça a Viquipèdia encara", - "sv": "Ingen Wikipedia-sida har länkats än" + "sv": "Ingen Wikipedia-sida har länkats än", + "zh_Hant": "還沒有連結到維基百科頁面" }, "hideInAnswer": true } @@ -81,7 +83,9 @@ "freeform": { "key": "phone", "type": "phone", - "addExtraTags": ["contact:phone="] + "addExtraTags": [ + "contact:phone=" + ] } }, "osmlink": { @@ -104,7 +108,8 @@ "it": "Qual è il corrispondente elemento su Wikipedia?", "nb_NO": "Hva er respektivt element på Wikipedia?", "ca": "Quin és l'ítem a Viquipèdia?", - "sv": "Vad är det motsvarande objektet på Wikipedia?" + "sv": "Vad är det motsvarande objektet på Wikipedia?", + "zh_Hant": "維基百科上對應的項目是什麼?" }, "mappings": [ { @@ -119,7 +124,8 @@ "nb_NO": "Ikke lenket med Wikipedia", "fr": "Non lié avec Wikipedia", "ca": "No enllaçat amb Viquipèdia", - "sv": "Inte länkad med Wikipedia" + "sv": "Inte länkad med Wikipedia", + "zh_Hant": "沒有連結到維基百科" } } ], @@ -158,7 +164,9 @@ "freeform": { "key": "email", "type": "email", - "addExtraTags": ["contact:email="] + "addExtraTags": [ + "contact:email=" + ] } }, "website": { @@ -185,7 +193,9 @@ "freeform": { "key": "website", "type": "url", - "addExtraTags": ["contact:website="] + "addExtraTags": [ + "contact:website=" + ] }, "mappings": [ { @@ -207,7 +217,8 @@ "it": "Questo luogo è accessibile con una sedia a rotelle?", "nb_NO": "Er dette stedet tilgjengelig for rullestoler?", "ca": "Aquest lloc és accessible amb cadira de rodes?", - "sv": "Är det här stället tillgängligt med en rullstol?" + "sv": "Är det här stället tillgängligt med en rullstol?", + "zh_Hant": "這個地方可以坐輪椅到達嗎?" }, "mappings": [ { @@ -227,7 +238,8 @@ "nb_NO": "Dette stedet er spesielt tilpasset rullestolsbrukere", "fr": "Cet endroit est spécialement adapté pour les usagers de fauteuils roulants", "ca": "Aquest lloc està especialment adaptat per a les cadires de rodes", - "sv": "Det här stället är speciellt anpassat för rullstolsburna användare" + "sv": "Det här stället är speciellt anpassat för rullstolsburna användare", + "zh_Hant": "這個地方有特別設計給輪椅使用者" } }, { @@ -247,7 +259,8 @@ "nb_NO": "Dette stedet kan enkelt besøkes med rullestol", "fr": "Cet endroit est facilement accessible avec un fauteuil roulant", "ca": "És facilment arribable amb cadira de rodes", - "sv": "Denna plats är lätt att nå med rullstol" + "sv": "Denna plats är lätt att nå med rullstol", + "zh_Hant": "這個地方坐輪椅很容易到達" } }, { @@ -267,7 +280,8 @@ "it": "È possibile raggiungere questo luogo con una sedia a rotella ma non è semplice", "nb_NO": "Det er mulig å besøke dette stedet i rullestol, men det er ikke lett", "ca": "És possible fer servir cadira de rodes a aquest lloc però no és fàcil", - "sv": "Det är möjligt att nå den här platsen i en rullstol, men det är inte lätt" + "sv": "Det är möjligt att nå den här platsen i en rullstol, men det är inte lätt", + "zh_Hant": "這個地方可以坐輪椅到達,但並不容易" } }, { @@ -287,7 +301,8 @@ "it": "Questo luogo non è accessibile con una sedia a rotelle", "nb_NO": "Dette stedet er ikke tilgjengelig for besøk med rullestol", "ca": "Aquest lloc no és accessible amb cadira de rodes", - "sv": "Den här platsen kan inte nås med en rullstol" + "sv": "Den här platsen kan inte nås med en rullstol", + "zh_Hant": "輪椅無法到達這個地方" } } ] @@ -303,7 +318,8 @@ "it": "I cani sono ammessi in quest’attività?", "nb_NO": "Tillates hunder i denne forretningen?", "ca": "S'accepten gossos en aquest negoci?", - "sv": "Tillåts hundar i den här affären?" + "sv": "Tillåts hundar i den här affären?", + "zh_Hant": "這間商業空間是否允許犬隻?" }, "mappings": [ { @@ -320,7 +336,8 @@ "nb_NO": "Hunder tillates", "ca": "S'accepten gossos", "ru": "Собаки разрешены", - "sv": "Hundar tillåts" + "sv": "Hundar tillåts", + "zh_Hant": "允許犬隻" } }, { @@ -336,7 +353,8 @@ "it": "I cani non sono ammessi", "nb_NO": "Hunder tillates ikke", "ca": "No s'accepten gossos", - "sv": "Hundar tillåts inte" + "sv": "Hundar tillåts inte", + "zh_Hant": "允許犬隻" } }, { @@ -351,7 +369,8 @@ "it": "Cani ammessi ma solo se tenuti al guinzaglio", "nb_NO": "Hunder tillates, men de må være i bånd", "ca": "S'accepten gossos però lligats", - "sv": "Hundar tillåts, men de måste vara kopplade" + "sv": "Hundar tillåts, men de måste vara kopplade", + "zh_Hant": "允許犬隻,但需要掛牽繩" } }, { @@ -366,7 +385,8 @@ "it": "I cani sono ammessi e possono andare in giro liberamente", "nb_NO": "Hunder tillates og kan gå fritt", "ca": "S'accepten gossos lliures", - "sv": "Hundar tillåts och får springa fritt omkring" + "sv": "Hundar tillåts och får springa fritt omkring", + "zh_Hant": "允許犬隻而且可以自由跑動" } } ] @@ -440,7 +460,8 @@ "pt": "Esta infraestrutura tem tomadas elétricas, disponíveis para os clientes quando estão no interior?", "ca": "Aquest servei té endolls elèctrics, disponibles pels clients quan hi són dins?", "de": "Gibt es an dieser Einrichtung Steckdosen, an denen Kunden ihre Geräte laden können?", - "sv": "Har den här bekvämligheten eluttag tillgängliga för kunder när de är inne?" + "sv": "Har den här bekvämligheten eluttag tillgängliga för kunder när de är inne?", + "zh_Hant": "這個便利設施有電器設備,能給客戶使用嗎?" }, "mappings": [ { @@ -450,7 +471,8 @@ "pt": "Há muitas tomadas elétricas disponíveis para clientes sentados no interior, onde estes podem carregar os seus dispositivos eletrónicos", "ca": "Està ple d'endolls pels clients de dins, on es poden carregar els aparells electrònics", "de": "Für Kunden stehen im Innenraum viele Steckdosen zur Verfügung, an denen sie ihre Geräte laden können", - "sv": "Det finns gott om hushållsuttag tillgängliga för kunder som sitter inomhus, där de kan ladda sin elektronik" + "sv": "Det finns gott om hushållsuttag tillgängliga för kunder som sitter inomhus, där de kan ladda sin elektronik", + "zh_Hant": "這邊的客戶座位有不少個室內插座,而且可以為電器充電" }, "if": "service:electricity=yes" }, @@ -461,7 +483,8 @@ "pt": "Há algumas tomadas elétricas disponíveis para clientes sentados no interior, onde estes podem carregar os seus dispositivos eletrónicos", "ca": "Hi ha aslguns endolls disponibles per als clients de dins, on es poden carregar els aparells electrònics", "de": "Für Kunden stehen im Innenraum wenig Steckdosen zur Verfügung, an denen sie ihre Geräte laden können", - "sv": "Det finns ett fåtal hushållsuttag tillgängliga för kunder som sitter inomhus, där de kan ladda sin elektronik" + "sv": "Det finns ett fåtal hushållsuttag tillgängliga för kunder som sitter inomhus, där de kan ladda sin elektronik", + "zh_Hant": "這邊客戶座位有一些室內插座,可以為電器充電" }, "if": "service:electricity=limited" }, @@ -473,7 +496,8 @@ "pt": "Não há tomadas elétricas disponíveis para clientes sentados no interior, mas pode-se pedir aos funcionários para carregar dispositivos eletrónicos", "ca": "No hi ha endolls disponibles per als clients però es pot carregar si es demana als responsables", "de": "Für Kunden stehen im Innenraum keine Steckdosen zur Verfügung, aber Laden von Geräte könnte möglich sein, wenn das Personal gefragt wird", - "sv": "Det finns inga uttag tillgängliga inomhus för kunder, men att ladda kan vara möjligt om personalen tillfrågas" + "sv": "Det finns inga uttag tillgängliga inomhus för kunder, men att ladda kan vara möjligt om personalen tillfrågas", + "zh_Hant": "這邊沒有給客戶用的插座,因此可能需要詢問員工是否能充電" }, "if": "service:electricity=ask" }, @@ -485,7 +509,8 @@ "pt": "Não há tomadas elétricas disponíveis para clientes sentados no interior", "ca": "No hi ha endolls disponibles per als clients", "de": "Für Kunden stehen im Innenraum keine Steckdosen zur Verfügung", - "sv": "Det finns inga hushållsuttag tillgängliga för kunder som sitter inomhus" + "sv": "Det finns inga hushållsuttag tillgängliga för kunder som sitter inomhus", + "zh_Hant": "這裡客戶座位沒有室內插座" }, "if": "service:electricity=no" } @@ -504,7 +529,8 @@ "it": "Quali metodi di pagamento sono accettati qui?", "nb_NO": "Hvilke betalingsmetoder godtas her?", "ca": "Quins mètodes de pagament s'accepten aquí?", - "sv": "Vilka betalningsmetoder accepteras här?" + "sv": "Vilka betalningsmetoder accepteras här?", + "zh_Hant": "這邊接受那種付款方式?" }, "multiAnswer": true, "mappings": [ @@ -523,7 +549,8 @@ "it": "I contanti sono accettati", "nb_NO": "Kontanter godtas her", "ca": "S'accepten diners", - "sv": "Pengar accepteras här" + "sv": "Pengar accepteras här", + "zh_Hant": "這邊接受現金" } }, { @@ -541,7 +568,8 @@ "it": "I pagamenti con la carta sono accettati", "nb_NO": "Betalingskort godtas her", "ca": "S'accepten targetes de crèdit", - "sv": "Betalningskort accepteras här" + "sv": "Betalningskort accepteras här", + "zh_Hant": "這邊接受現金卡" } } ] diff --git a/assets/themes/campersite/campersite.json b/assets/themes/campersite/campersite.json index 84428eaaa..61ba13a4d 100644 --- a/assets/themes/campersite/campersite.json +++ b/assets/themes/campersite/campersite.json @@ -725,7 +725,8 @@ "it": "Luogo di sversamento {name}", "fr": "Site de vidange {name}", "pt_BR": "Estação de despejo {nome}", - "de": "Entsorgungsstation {name}" + "de": "Entsorgungsstation {name}", + "zh_Hant": "{name} 垃圾站" }, "mappings": [ { @@ -741,7 +742,8 @@ "it": "Luogo di sversamento", "fr": "Site de vidange", "pt_BR": "Estação de despejo", - "de": "Entsorgungsstation" + "de": "Entsorgungsstation", + "zh_Hant": "垃圾站" } } ] @@ -847,7 +849,8 @@ "it": "Questo luogo ha un punto per l'approvvigionamento di acqua?", "fr": "Ce site dispose-t’il d’un point d’eau ?", "pt_BR": "Este lugar tem um ponto de água?", - "de": "Hat dieser Ort eine Wasserstelle?" + "de": "Hat dieser Ort eine Wasserstelle?", + "zh_Hant": "這個地方有取水點嗎?" }, "mappings": [ { @@ -863,7 +866,8 @@ "it": "Questo luogo ha un punto per l'approvvigionamento di acqua", "fr": "Ce site a un point d’eau", "pt_BR": "Este lugar tem um ponto de água", - "de": "Dieser Ort hat eine Wasserstelle" + "de": "Dieser Ort hat eine Wasserstelle", + "zh_Hant": "這個地方有取水點" } }, { @@ -879,7 +883,8 @@ "it": "Questo luogo non ha un punto per l'approvvigionamento di acqua", "fr": "Ce site n’a pas de point d’eau", "pt_BR": "Este lugar não tem um ponto de água", - "de": "Dieser Ort hat keine Wasserstelle" + "de": "Dieser Ort hat keine Wasserstelle", + "zh_Hant": "這個地方沒有取水點" } } ] @@ -892,7 +897,8 @@ "ja": "汚水(雑排水)はこちらで処分できますか?", "it": "Si possono smaltire le acque grigie qui?", "fr": "Est-il possible d’y faire sa vidange des eaux usées ?", - "de": "Können Sie hier Brauch-/Grauwasser entsorgen?" + "de": "Können Sie hier Brauch-/Grauwasser entsorgen?", + "zh_Hant": "你能在這裡排放洗滌水嗎?" }, "mappings": [ { @@ -907,7 +913,8 @@ "ja": "ここで汚水(雑排水)を捨てることができます", "it": "Si possono smaltire le acque grigie qui", "fr": "Il est possible d’y vidanger ses eaux usées", - "de": "Hier können Sie Brauch-/Grauwasser entsorgen" + "de": "Hier können Sie Brauch-/Grauwasser entsorgen", + "zh_Hant": "你可以在這裡排放洗滌水" } }, { @@ -922,7 +929,8 @@ "ja": "ここでは汚水(雑排水)を捨てることはできない", "it": "Non si possono smaltire le acque grigie qui", "fr": "Il n’est pas possible d’y vidanger ses eaux usées", - "de": "Hier können Sie kein Brauch-/Grauwasser entsorgen" + "de": "Hier können Sie kein Brauch-/Grauwasser entsorgen", + "zh_Hant": "你無法在這裡排放洗滌水" } } ] @@ -1057,7 +1065,8 @@ "it": "Questo luogo è parte della rete {network}", "ru": "Эта станция - часть сети {network}", "fr": "Cette station fait parte d’un réseau {network}", - "de": "Diese Station gehört zum Verbund/Netzwerk {network}" + "de": "Diese Station gehört zum Verbund/Netzwerk {network}", + "zh_Hant": "這車站是屬於 {network} 網路的一部分" }, "question": { "en": "What network is this place a part of? (skip if none)", @@ -1065,7 +1074,8 @@ "it": "Di quale rete fa parte questo luogo? (se non fa parte di nessuna rete, salta)", "ru": "К какой сети относится эта станция? (пропустите, если неприменимо)", "fr": "De quel réseau fait-elle partie ? (Passer si aucun)", - "de": "Zu welchem Verbund/Netzwerk gehört dieser Ort? (Überspringen, wenn nicht zutreffend)" + "de": "Zu welchem Verbund/Netzwerk gehört dieser Ort? (Überspringen, wenn nicht zutreffend)", + "zh_Hant": "這裡是屬於那個網路的? (沒有則跳過)" }, "freeform": { "key": "network" @@ -1128,14 +1138,16 @@ "ja": "この場所は{operator}によって運営されます", "it": "Questo luogo è gestito da {operator}", "fr": "Ce site est exploité par {operator}", - "de": "Dieser Ort wird betrieben von {operator}" + "de": "Dieser Ort wird betrieben von {operator}", + "zh_Hant": "這個地方由 {operator} 營運的" }, "question": { "en": "Who operates this place?", "ja": "この店は誰が経営しているんですか?", "it": "Chi gestisce questo luogo?", "fr": "Qui est l’exploitant du site ?", - "de": "Wer betreibt diesen Ort?" + "de": "Wer betreibt diesen Ort?", + "zh_Hant": "這個地方是誰營運的?" }, "freeform": { "key": "operator" diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index a070e0d0b..609081b77 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -1142,7 +1142,8 @@ "en": "The containing feature states that this is publicly accessible
{_embedding_feature:access:description}", "nl": "Een omvattend element geeft aan dat dit publiek toegangkelijk is
{_embedding_feature:access:description}", "fr": "L’élément englobant indique un accès libre
{_embedding_feature:access:description}", - "it": "L’ elemento in cui è contenuto indica che è pubblicamente accessibile
{_embedding_feature:access:description}" + "it": "L’ elemento in cui è contenuto indica che è pubblicamente accessibile
{_embedding_feature:access:description}", + "de": "Das enthaltende Objekt gibt an, dass es öffentlich zugänglich ist
{_embedding_feature:access:description}" } }, { @@ -1151,7 +1152,8 @@ "en": "The containing feature states that a permit is needed to access
{_embedding_feature:access:description}", "nl": "Een omvattend element geeft aan dat een toelating nodig is om hier te klimmen
{_embedding_feature:access:description}", "fr": "L’élément englobant indique qu’ une autorisation d’accès est nécessaire
{_embedding_feature:access:description}", - "it": "L’elemento che lo contiene indica che è richiesto un’autorizzazione per accedervi
{_embedding_feature:access:description}" + "it": "L’elemento che lo contiene indica che è richiesto un’autorizzazione per accedervi
{_embedding_feature:access:description}", + "de": "Das enthaltende Objekt besagt, dass eine Genehmigung erforderlich ist für den Zugang zu
{_embedding_feature:access:description}" } }, { @@ -1159,7 +1161,8 @@ "then": { "en": "The containing feature states that this is only accessible to customers
{_embedding_feature:access:description}", "fr": "L’élément englobant indique que l’accès est réservés aux clients
{_embedding_feature:access:description}", - "it": "L’ elemento che lo contiene indica che è accessibile solo ai clienti
{_embedding_feature:access:description}" + "it": "L’ elemento che lo contiene indica che è accessibile solo ai clienti
{_embedding_feature:access:description}", + "de": "Das enthaltende Objekt besagt, dass es nur für Kunden zugänglich ist
{_embedding_feature:access:description}" } }, { @@ -1167,7 +1170,8 @@ "then": { "en": "The containing feature states that this is only accessible to club members
{_embedding_feature:access:description}", "fr": "L’élément englobant indique que l’accès est réservé aux membres
{_embedding_feature:access:description}", - "it": "L’ elemento che lo contiene indica che è accessibile solamente ai membri del club
{_embedding_feature:access:description}" + "it": "L’ elemento che lo contiene indica che è accessibile solamente ai membri del club
{_embedding_feature:access:description}", + "de": "Das enthaltende Objekt besagt, dass es nur für Mitglieder zugänglich ist
{_embedding_feature:access:description}" } }, { diff --git a/assets/themes/cycle_infra/cycle_infra.json b/assets/themes/cycle_infra/cycle_infra.json index 1397494a7..68330f8e9 100644 --- a/assets/themes/cycle_infra/cycle_infra.json +++ b/assets/themes/cycle_infra/cycle_infra.json @@ -6,20 +6,23 @@ "de": "Fahrradinfrastruktur", "it": "Infrastruttura dei velocipedi", "nb_NO": "Sykkelinfrastruktur", - "ru": "Велосипедная дорожка" + "ru": "Велосипедная дорожка", + "zh_Hant": "單車設施" }, "shortDescription": { "en": "A map where you can view and edit things related to the bicycle infrastructure.", "nl": "Een kaart waar je info over de fietsinfrastructuur kan bekijken en bewerken.", "de": "Eine Karte zum Ansehen und Bearbeiten verschiedener Elementen der Fahrradinfrastruktur.", "it": "Una cartina dove vedere e modificare gli elementi riguardanti l’infrastruttura dei velocipedi.", - "nb_NO": "Alt relatert til sykkelinfrastruktur." + "nb_NO": "Alt relatert til sykkelinfrastruktur.", + "zh_Hant": "檢視與編輯單車相關設施的地圖。" }, "description": { "en": "A map where you can view and edit things related to the bicycle infrastructure. Made during #osoc21.", "nl": "Een kaart waar je info over de fietsinfrastructuur kan bekijken en bewerken. Gemaakt tijdens #osoc21.", "de": "Eine Karte zum Ansehen und Bearbeiten verschiedener Elementen der Fahrradinfrastruktur. Erstellt während #osoc21.", - "it": "Una cartina dove vedere e modificare gli elementi riguardanti l’infrastruttura dei velocipedi. Realizzata durante #osoc21." + "it": "Una cartina dove vedere e modificare gli elementi riguardanti l’infrastruttura dei velocipedi. Realizzata durante #osoc21.", + "zh_Hant": "可以檢視與編輯單車相關設施的地圖,在 #os0c21時製作。" }, "language": [ "en", @@ -27,7 +30,8 @@ "de", "it", "nb_NO", - "ru" + "ru", + "zh_Hant" ], "maintainer": "MapComplete", "hideFromOverview": false, diff --git a/assets/themes/cyclenodes/cyclenodes.json b/assets/themes/cyclenodes/cyclenodes.json index f08c74612..d1bc138a1 100644 --- a/assets/themes/cyclenodes/cyclenodes.json +++ b/assets/themes/cyclenodes/cyclenodes.json @@ -28,7 +28,8 @@ { "id": "node2node", "name": { - "en": "node to node links" + "en": "node to node links", + "de": "Knotenpunktverbindungen" }, "source": { "osmTags": { @@ -42,13 +43,15 @@ "minzoom": 12, "title": { "render": { - "en": "node to node link" + "en": "node to node link", + "de": "Knotenpunktverbindung" }, "mappings": [ { "if": "ref~*", "then": { - "en": "node to node link {ref}" + "en": "node to node link {ref}", + "de": "Knotenpunktverbindung {ref}" } } ] @@ -66,10 +69,12 @@ "tagRenderings": [ { "question": { - "en": "When was this node to node link last surveyed?" + "en": "When was this node to node link last surveyed?", + "de": "Wann wurde diese Knotenpunktverbindung zuletzt überprüft?" }, "render": { - "en": "This node to node link was last surveyed on {survey:date}" + "en": "This node to node link was last surveyed on {survey:date}", + "de": "Diese Knotenpunktverbindung wurde zuletzt am {survey:date} überprüft" }, "freeform": { "key": "survey:date", @@ -89,7 +94,8 @@ { "id": "node", "name": { - "en": "nodes" + "en": "nodes", + "de": "Knotenpunkte" }, "source": { "osmTags": { @@ -147,10 +153,12 @@ "tagRenderings": [ { "question": { - "en": "When was this cycle node last surveyed?" + "en": "When was this cycle node last surveyed?", + "de": "Wann wurde dieser Fahrradknotenpunkt zuletzt überprüft?" }, "render": { - "en": "This cycle node was last surveyed on {survey:date}" + "en": "This cycle node was last surveyed on {survey:date}", + "de": "Dieser Fahrradknoten wurde zuletzt überprüft am {survey:date}" }, "freeform": { "key": "survey:date", @@ -166,10 +174,12 @@ }, { "question": { - "en": "How many other cycle nodes does this node link to?" + "en": "How many other cycle nodes does this node link to?", + "de": "Mit wie vielen anderen Knoten des Fahrradknotenpunktnetzwerkes ist dieser Knoten verbunden?" }, "render": { - "en": "This node links to {expected_rcn_route_relations} other cycle nodes." + "en": "This node links to {expected_rcn_route_relations} other cycle nodes.", + "de": "Dieser Knoten ist mit {expected_rcn_route_relations} anderen Knoten des Fahrradknotenpunktnetzwerkes verbunden." }, "freeform": { "key": "expected_rcn_route_relations", diff --git a/assets/themes/etymology.json b/assets/themes/etymology.json index edb16bd76..dc828d6af 100644 --- a/assets/themes/etymology.json +++ b/assets/themes/etymology.json @@ -5,26 +5,30 @@ "nl": "Open Etymology-kaart", "de": "Open Etymology Map", "it": "Apri Carta Etimologica", - "ru": "Открытая этимологическая карта" + "ru": "Открытая этимологическая карта", + "zh_Hant": "開放詞源地圖" }, "shortDescription": { "en": "What is the origin of a toponym?", "nl": "Wat is de oorsprong van een plaatsnaam?", "de": "Was ist der Ursprung eines Ortsnamens?", - "it": "Qual è l’origine di un toponimo?" + "it": "Qual è l’origine di un toponimo?", + "zh_Hant": "地名的由來是?" }, "description": { "en": "On this map, you can see what an object is named after. The streets, buildings, ... come from OpenStreetMap which got linked with Wikidata. In the popup, you'll see the Wikipedia article (if it exists) or a wikidata box of what the object is named after. If the object itself has a wikipedia page, that'll be shown too.

You can help contribute too!Zoom in enough and all streets will show up. You can click one and a Wikidata-search box will popup. With a few clicks, you can add an etymology link. Note that you need a free OpenStreetMap account to do this.", "nl": "Op deze kaart zie je waar een plaats naar is vernoemd. De straten, gebouwen, ... komen uit OpenStreetMap, waar een link naar Wikidata werd gelegd. In de popup zie je het Wikipedia-artikel van hetgeen naarwaar het vernoemd is of de Wikidata-box.

Je kan zelf ook meehelpen!Als je ver inzoomt, krijg je alle straten te zien. Klik je een straat aan, dan krijg je een zoekfunctie waarmee je snel een nieuwe link kan leggen. Je hebt hiervoor een gratis OpenStreetMap account nodig.", "de": "Auf dieser Karte können Sie sehen, wonach ein Objekt benannt ist. Die Straßen, Gebäude, ... stammen von OpenStreetMap, das mit Wikidata verknüpft wurde. In dem Popup sehen Sie den Wikipedia-Artikel (falls vorhanden) oder ein Wikidata-Feld, nach dem das Objekt benannt ist. Wenn das Objekt selbst eine Wikipedia-Seite hat, wird auch diese angezeigt.

Sie können auch einen Beitrag leisten!Zoomen Sie genug hinein und alle Straßen werden angezeigt. Wenn Sie auf eine Straße klicken, öffnet sich ein Wikidata-Suchfeld. Mit ein paar Klicks können Sie einen Etymologie-Link hinzufügen. Beachten Sie, dass Sie dazu ein kostenloses OpenStreetMap-Konto benötigen.", - "it": "Su questa cartina sono visibili i nomi a cui sono riferiti gli oggetti. Le strade, gli edifici, etc. provengono da OpenStreetMap che è a sua volta collegata a Wikidata. Nel popup, se esiste, verrà mostrato l’articolo Wikipedia o l'elemento Wikidata a cui si riferisce il nome di quell’oggetto. Se l’oggetto stesso ha una pagina Wikpedia, anch’essa verrà mostrata.

Anche tu puoi contribuire!Ingrandisci abbastanza e tutte le strade appariranno. Puoi cliccare su una e apparirà un popup con la ricerca Wikidata. Con pochi clic puoi aggiungere un collegamento etimologico. Tieni presente che per farlo, hai bisogno di un account gratuito su OpenStreetMap." + "it": "Su questa cartina sono visibili i nomi a cui sono riferiti gli oggetti. Le strade, gli edifici, etc. provengono da OpenStreetMap che è a sua volta collegata a Wikidata. Nel popup, se esiste, verrà mostrato l’articolo Wikipedia o l'elemento Wikidata a cui si riferisce il nome di quell’oggetto. Se l’oggetto stesso ha una pagina Wikpedia, anch’essa verrà mostrata.

Anche tu puoi contribuire!Ingrandisci abbastanza e tutte le strade appariranno. Puoi cliccare su una e apparirà un popup con la ricerca Wikidata. Con pochi clic puoi aggiungere un collegamento etimologico. Tieni presente che per farlo, hai bisogno di un account gratuito su OpenStreetMap.", + "zh_Hant": "在這份地圖,你可以看到物件是以何命名,道路、 建築等的命名由來連到 Wikidata。在跳出選單,你可以看到物件命名由來的維基條目 (如果有的話),或是 Wikidata 框。如果物件本身有維基頁面,也會顯示。

你也可以貢獻!放大到夠大的層級,然後所有道路都會顯示。你可以點選一個之後 Wikidata 搜尋框會跳出來。只要點幾下,你可以新增詞源連結。注意你要有開放街圖帳號才能這麼做。" }, "language": [ "en", "nl", "de", "it", - "ru" + "ru", + "zh_Hant" ], "maintainer": "", "icon": "./assets/layers/etymology/logo.svg", @@ -48,7 +52,8 @@ "en": "Streets without etymology information", "nl": "Straten zonder etymologische informatie", "de": "Straßen ohne Informationen zur Namensherkunft", - "it": "Strade senza informazioni etimologiche" + "it": "Strade senza informazioni etimologiche", + "zh_Hant": "道路沒有詞源資訊" }, "minzoom": 18, "source": { @@ -70,7 +75,8 @@ "en": "Parks and forests without etymology information", "nl": "Parken en bossen zonder etymologische informatie", "de": "Parks und Waldflächen ohne Informationen zur Namensherkunft", - "it": "Parchi e foreste senza informazioni etimologiche" + "it": "Parchi e foreste senza informazioni etimologiche", + "zh_Hant": "公園與森哥沒有詞源資訊" }, "minzoom": 18, "source": { diff --git a/assets/themes/facadegardens/facadegardens.json b/assets/themes/facadegardens/facadegardens.json index 2ec4c03bf..00bf0839c 100644 --- a/assets/themes/facadegardens/facadegardens.json +++ b/assets/themes/facadegardens/facadegardens.json @@ -180,7 +180,8 @@ "ja": "庭に水桶が設置されているのですか?", "fr": "Des réserves d’eau ont-elles été installées pour le jardin ?", "de": "Gibt es ein Wasserfass für den Garten?", - "it": "È stata installata una riserva d’acqua per il giardino?" + "it": "È stata installata una riserva d’acqua per il giardino?", + "zh_Hant": "花園當中有設置雨筒嗎?" }, "mappings": [ { @@ -196,7 +197,8 @@ "it": "C'è un contenitore per raccogliere la pioggia", "ru": "Есть бочка с дождевой водой", "fr": "Il y a des réserves", - "de": "Es gibt eine Regentonne" + "de": "Es gibt eine Regentonne", + "zh_Hant": "這裡有個雨筒" } }, { @@ -212,7 +214,8 @@ "it": "Non c'è un contenitore per raccogliere la pioggia", "ru": "Нет бочки с дождевой водой", "fr": "Il n’y a pas de réserves", - "de": "Es gibt keine Regentonne" + "de": "Es gibt keine Regentonne", + "zh_Hant": "這裡沒有雨筒" } } ] diff --git a/assets/themes/food/food.json b/assets/themes/food/food.json index f8008e5ac..269135161 100644 --- a/assets/themes/food/food.json +++ b/assets/themes/food/food.json @@ -5,7 +5,8 @@ "en": "Restaurants and fast food", "de": "Restaurants und Schnellimbisse", "it": "Ristoranti e fast food", - "nb_NO": "Restauranter og søppelmat" + "nb_NO": "Restauranter og søppelmat", + "zh_Hant": "餐廳與快餐店" }, "description": { "nl": "Restaurants en fast food" @@ -15,7 +16,8 @@ "en", "de", "it", - "nb_NO" + "nb_NO", + "zh_Hant" ], "maintainer": "", "icon": "./assets/layers/food/restaurant.svg", diff --git a/assets/themes/grb_import/grb.json b/assets/themes/grb_import/grb.json index f3cc1d2fd..77bda9e99 100644 --- a/assets/themes/grb_import/grb.json +++ b/assets/themes/grb_import/grb.json @@ -12,10 +12,11 @@ }, "language": [ "nl", - "en" + "en", + "de" ], "maintainer": "", - "icon": "./assets/svg/bug.svg", + "icon": "./assets/themes/grb_import/grb.svg", "version": "0", "startLat": 51.0249, "startLon": 4.026489, @@ -43,28 +44,10 @@ "iconSize": "15,15,center" } ], - "calculatedTags": [ - "_embedded_crab_addresses= Number(feat.properties.zoom) >= 18 ? feat.overlapWith('crab_address').length : undefined" - ], - "minZoom": 18, "tagRenderings": [ { "id": "hw", - "render": "There are {_embedded_crab_addresses} adresses in view", - "mappings": [ - { - "if": "zoom<18", - "then": "Zoom in more..." - }, - { - "if": "_embedded_crab_addresses=", - "then": "Loading..." - }, - { - "if": "_embedded_crab_addresses=0", - "then": "No CRAB addresses in view. Zoom in more to see them" - } - ] + "render": "Beep boop! I'm a bot!" } ] } @@ -99,7 +82,9 @@ "osmTags": "building~*", "maxCacheAge": 0 }, - "calculatedTags": [], + "calculatedTags": [ + "_surface:strict:=feat.get('_surface')" + ], "mapRendering": [ { "width": { @@ -152,7 +137,8 @@ }, "render": "The building type is {building}", "question": { - "en": "What kind of building is this?" + "en": "What kind of building is this?", + "de": "Was ist das für ein Gebäude?" }, "mappings": [ { @@ -488,14 +474,16 @@ "calculatedTags": [ "_overlaps_with_buildings=feat.overlapWith('osm-buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)", "_overlaps_with=feat.get('_overlaps_with_buildings').filter(f => f.overlap > 1 /* square meter */ )[0] ?? ''", - "_overlap_absolute=feat.get('_overlaps_with')?.overlap", - "_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface')) ", "_osm_obj:source:ref=feat.get('_overlaps_with')?.feat?.properties['source:geometry:ref']", + "_osm_obj:id=feat.get('_overlaps_with')?.feat?.properties?.id", "_osm_obj:source:date=feat.get('_overlaps_with')?.feat?.properties['source:geometry:date'].replace(/\\//g, '-')", "_osm_obj:building=feat.get('_overlaps_with')?.feat?.properties?.building", - "_osm_obj:id=feat.get('_overlaps_with')?.feat?.properties?.id", "_osm_obj:addr:street=(feat.get('_overlaps_with')?.feat?.properties ?? {})['addr:street']", "_osm_obj:addr:housenumber=(feat.get('_overlaps_with')?.feat?.properties ?? {})['addr:housenumber']", + "_osm_obj:surface=(feat.get('_overlaps_with')?.feat?.properties ?? {})['_surface:strict']", + "_overlap_absolute=feat.get('_overlaps_with')?.overlap", + "_reverse_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface'))", + "_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_osm_obj:surface'))", "_grb_ref=feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']", "_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties._grb_ref", "_grb_date=feat.properties['source:geometry:date'].replace(/\\//g,'-')", @@ -510,24 +498,30 @@ "render": "{import_way_button(osm-buildings,building=$building;man_made=$man_made; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref; addr:street=$addr:street; addr:housenumber=$addr:housenumber; building:min_level=$_building:min_level, Upload this building to OpenStreetMap,,_is_part_of_building=true,1,_moveable=true)}", "mappings": [ { - "#": "Hide import button if intersection with other objects are detected", - "if": "_intersects_with_other_features~*", - "then": "This GRB building intersects with the following features: {_intersects_with_other_features}.
Fix the overlap and try again" - }, - { + "#": "Actually the same as below, except that the text shows 'add the address' too", "if": { "and": [ + "_overlap_percentage>50", + "_reverse_overlap_percentage>50", "_overlaps_with!=", "_osm_obj:addr:street=", "_osm_obj:addr:housenumber=", "addr:street~*", - "addr:housenumber~*" + "addr:housenumber~*", + "addr:street!:={_osm_obj:addr:street}", + "addr:housenumber!:={_osm_obj:addr:housenumber}" ] }, "then": "{conflate_button(osm-buildings,building=$_target_building_type; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref; addr:street=$addr:street; addr:housenumber=$addr:housenumber, Replace the geometry in OpenStreetMap and add the address,,_osm_obj:id)}" }, { - "if": "_overlaps_with!=", + "if": { + "and": [ + "_overlap_percentage>50", + "_reverse_overlap_percentage>50", + "_overlaps_with!=" + ] + }, "then": "{conflate_button(osm-buildings,building=$_target_building_type; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Replace the geometry in OpenStreetMap,,_osm_obj:id)}" } ] @@ -588,7 +582,7 @@ }, { "id": "overlapping building type", - "render": "
The overlapping openstreetmap-building is a {_osm_obj:building} and covers {_overlap_percentage}% of the GRB building

GRB geometry:

{minimap(21, id):height:10rem;border-radius:1rem;overflow:hidden}

OSM geometry:

{minimap(21,_osm_obj:id):height:10rem;border-radius:1rem;overflow:hidden}", + "render": "
The overlapping openstreetmap-building is a {_osm_obj:building} and covers {_overlap_percentage}% of the GRB building.
The GRB-building covers {_reverse_overlap_percentage}% of the OSM building

GRB geometry:

{minimap(21, id):height:10rem;border-radius:1rem;overflow:hidden}

OSM geometry:

{minimap(21,_osm_obj:id):height:10rem;border-radius:1rem;overflow:hidden}", "condition": "_overlaps_with!=" }, { diff --git a/assets/themes/grb_import/grb_fixme.json b/assets/themes/grb_import/grb_fixme.json index e1687783f..f9e7f71dc 100644 --- a/assets/themes/grb_import/grb_fixme.json +++ b/assets/themes/grb_import/grb_fixme.json @@ -11,7 +11,8 @@ }, "language": [ "nl", - "en" + "en", + "de" ], "maintainer": "", "icon": "./assets/svg/bug.svg", @@ -114,7 +115,8 @@ }, "render": "The building type is {building}", "question": { - "en": "What kind of building is this?" + "en": "What kind of building is this?", + "de": "Was ist das für ein Gebäude?" }, "mappings": [ { diff --git a/assets/themes/hackerspaces/hackerspaces.json b/assets/themes/hackerspaces/hackerspaces.json index 660224bb1..57b4bef37 100644 --- a/assets/themes/hackerspaces/hackerspaces.json +++ b/assets/themes/hackerspaces/hackerspaces.json @@ -4,12 +4,14 @@ "en": "Hackerspaces", "de": "Hackerspaces", "it": "Hackerspace", - "ru": "Хакерспейсы" + "ru": "Хакерспейсы", + "zh_Hant": "駭客空間" }, "shortDescription": { "en": "A map of hackerspaces", "de": "Eine Karte von Hackerspaces", - "it": "Una cartina degli hackerspace" + "it": "Una cartina degli hackerspace", + "zh_Hant": "駭客空間的地圖" }, "description": { "en": "On this map you can see hackerspaces, add a new hackerspace or update data directly", @@ -46,7 +48,8 @@ "render": { "en": "Hackerspace", "de": "Hackerspace", - "ru": "Хакерспейс" + "ru": "Хакерспейс", + "zh_Hant": "駭客空間" }, "mappings": [ { @@ -58,7 +61,8 @@ "then": { "en": " {name}", "de": " {name}", - "ru": " {name}" + "ru": " {name}", + "zh_Hant": " {name}" } } ] @@ -74,21 +78,24 @@ "id": "is_makerspace", "question": { "en": "Is this a hackerspace or a makerspace?", - "de": "Ist dies ein Hackerspace oder ein Makerspace?" + "de": "Ist dies ein Hackerspace oder ein Makerspace?", + "zh_Hant": "這邊是駭客空間還是創客空間?" }, "mappings": [ { "if": "hackerspace=makerspace", "then": { "en": "This is a makerspace", - "de": "Dies ist ein Makerspace" + "de": "Dies ist ein Makerspace", + "zh_Hant": "這是創客空間" } }, { "if": "hackerspace=", "then": { "en": "This is a traditional (software oriented) hackerspace", - "de": "Dies ist ein traditioneller (softwareorientierter) Hackerspace" + "de": "Dies ist ein traditioneller (softwareorientierter) Hackerspace", + "zh_Hant": "這是傳統的 (軟體導向) 駭客空間" } } ] @@ -96,11 +103,13 @@ { "question": { "en": "What is the name of this hackerspace?", - "de": "Wie lautet der Name dieses Hackerspace?" + "de": "Wie lautet der Name dieses Hackerspace?", + "zh_Hant": "這個駭客空間的名稱是?" }, "render": { "en": "This hackerspace is named {name}", - "de": "Dieser Hackerspace heißt {name}" + "de": "Dieser Hackerspace heißt {name}", + "zh_Hant": "這個駭客空間叫 {name}" }, "freeform": { "key": "name" @@ -113,7 +122,8 @@ { "question": { "en": "When is this hackerspace opened?", - "de": "Wann hat dieser Hackerspace geöffnet?" + "de": "Wann hat dieser Hackerspace geöffnet?", + "zh_Hant": "這個駭客空間的營業時間?" }, "freeform": { "key": "opening_hours", @@ -122,7 +132,8 @@ "render": { "en": "{opening_hours_table()}", "de": "{opening_hours_table()}", - "ru": "{opening_hours_table()}" + "ru": "{opening_hours_table()}", + "zh_Hant": "{opening_hours_table()}" }, "mappings": [ { @@ -134,7 +145,8 @@ "then": { "en": "Opened 24/7", "de": "durchgehend geöffnet", - "ru": "Открыто 24/7" + "ru": "Открыто 24/7", + "zh_Hant": "24/7 營業" } } ], @@ -145,7 +157,8 @@ "id": "hs-club-mate", "question": { "en": "Does this hackerspace serve Club Mate?", - "de": "Gibt es in diesem Hackerspace Club Mate?" + "de": "Gibt es in diesem Hackerspace Club Mate?", + "zh_Hant": "這個駭客空間是否服務俱樂部伙伴?" }, "mappings": [ { @@ -156,7 +169,8 @@ }, "then": { "en": "This hackerspace serves club mate", - "de": "In diesem Hackerspace gibt es Club Mate" + "de": "In diesem Hackerspace gibt es Club Mate", + "zh_Hant": "這個駭客空間服務俱樂部伙伴" } }, { @@ -167,7 +181,8 @@ }, "then": { "en": "This hackerspace does not serve club mate", - "de": "In diesem Hackerspace gibt es kein Club Mate" + "de": "In diesem Hackerspace gibt es kein Club Mate", + "zh_Hant": "這個駭客空間沒有服務俱樂部伙伴" } } ] @@ -175,11 +190,13 @@ { "render": { "en": "This hackerspace was founded at {start_date}", - "de": "Dieser Hackerspace wurde gegründet am {start_date}" + "de": "Dieser Hackerspace wurde gegründet am {start_date}", + "zh_Hant": "這駭客空間是 {start_date} 成立的" }, "question": { "en": "When was this hackerspace founded?", - "de": "Wann wurde dieser Hackerspace gegründet?" + "de": "Wann wurde dieser Hackerspace gegründet?", + "zh_Hant": "這個駭客空間何時成立的?" }, "freeform": { "key": "start_date", @@ -212,11 +229,13 @@ ], "title": { "en": "Makerspace", - "de": "Makerspace" + "de": "Makerspace", + "zh_Hant": "創客空間" }, "description": { "en": "A makerspace is a place where DIY-enthusiasts gather to experiment with electronics such as arduino, LEDstrips, ...", - "de": "Ein Makerspace ist ein Ort, an dem Heimwerker-Enthusiasten zusammenkommen, um mit Elektronik zu experimentieren, wie Arduino, LED-Strips, ..." + "de": "Ein Makerspace ist ein Ort, an dem Heimwerker-Enthusiasten zusammenkommen, um mit Elektronik zu experimentieren, wie Arduino, LED-Strips, ...", + "zh_Hant": "創客空間是 DIY 愛好者聚集在一起弄電子零件實驗,例如用 arduino、LEDstrips 等..." } } ], diff --git a/assets/themes/hailhydrant/hailhydrant.json b/assets/themes/hailhydrant/hailhydrant.json index 548b5ad13..e81e72da7 100644 --- a/assets/themes/hailhydrant/hailhydrant.json +++ b/assets/themes/hailhydrant/hailhydrant.json @@ -7,7 +7,8 @@ "ru": "Пожарные гидранты, огнетушители, пожарные станции и станции скорой помощи.", "fr": "Bornes incendies, extincteurs, casernes de pompiers et ambulanciers.", "it": "Idranti, estintori, caserme dei vigili del fuoco e stazioni delle ambulanze.", - "nb_NO": "Hydranter, brannslukkere, brannstasjoner, og ambulansestasjoner." + "nb_NO": "Hydranter, brannslukkere, brannstasjoner, og ambulansestasjoner.", + "de": "Hydranten, Feuerlöscher, Feuerwachen und Rettungswachen." }, "shortDescription": { "en": "Map to show hydrants, extinguishers, fire stations, and ambulance stations.", diff --git a/langs/de.json b/langs/de.json index 6f094830a..a8c3e585a 100644 --- a/langs/de.json +++ b/langs/de.json @@ -214,7 +214,7 @@ "write_a_comment": "Schreibe einen Kommentar…", "no_rating": "Keine Bewertung vorhanden", "posting_as": "Angemeldet als", - "i_am_affiliated": "Ich bin angehörig
Überprüfe, ob du Eigentümer, Ersteller, Angestellter etc. bist", + "i_am_affiliated": "Ich bin mit diesem Objekt vertraut
Überprüfe, ob du Eigentümer, Ersteller, Angestellter etc. bist", "saving_review": "Speichern…", "saved": "Bewertung gespeichert. Danke fürs Teilen!", "tos": "Mit deiner Bewertung stimmst du den AGB und den Datenschutzrichtlinien von Mangrove.reviews zu", diff --git a/langs/layers/de.json b/langs/layers/de.json index dc35e9bcb..bffb1cd71 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -3,6 +3,9 @@ "description": "Adressen", "name": "Bekannte Adressen in OSM", "tagRenderings": { + "fixme": { + "question": "Was sollte hier korrigiert werden? Bitte erläutern" + }, "housenumber": { "mappings": { "0": { @@ -1090,6 +1093,120 @@ "1": { "title": "Ladestation für e-bikes" } + }, + "tagRenderings": { + "Authentication": { + "mappings": { + "0": { + "then": "Authentifizierung per Mitgliedskarte" + }, + "1": { + "then": "Authentifizierung per App" + }, + "2": { + "then": "Authentifizierung per Anruf ist möglich" + }, + "3": { + "then": "Authentifizierung per SMS ist möglich" + }, + "4": { + "then": "Authentifizierung per NFC ist möglich" + }, + "5": { + "then": "Authentifizierung per Geldkarte ist möglich" + }, + "6": { + "then": "Authentifizierung per Kreditkarte ist möglich" + }, + "7": { + "then": "Das Laden ist hier (auch) ohne Authentifizierung möglich" + } + }, + "question": "Welche Art der Authentifizierung ist an der Ladestation möglich?" + }, + "Available_charging_stations (generated)": { + "question": "Welche Ladeanschlüsse gibt es hier?" + }, + "Network": { + "mappings": { + "0": { + "then": "Nicht Teil eines größeren Netzwerks, z. B. weil die Ladestation von einem lokalen Unternehmen betrieben wird" + }, + "1": { + "then": "Nicht Teil eines größeren Netzwerks" + } + }, + "question": "Ist diese Ladestation Teil eines Netzwerks?", + "render": "Teil des Netzwerks {network}" + }, + "OH": { + "mappings": { + "0": { + "then": "durchgehend geöffnet (einschließlich Feiertage)" + } + }, + "question": "Wann ist diese Ladestation geöffnet?" + }, + "Operational status": { + "mappings": { + "0": { + "then": "Diese Ladestation ist betriebsbereit" + }, + "1": { + "then": "Diese Ladestation ist defekt" + }, + "2": { + "then": "Hier ist eine Ladestation geplant" + }, + "3": { + "then": "Hier wird eine Ladestation errichtet" + }, + "4": { + "then": "Diese Ladestation wurde dauerhaft geschlossen und wird nicht mehr benutzt, ist aber noch sichtbar" + } + }, + "question": "Ist dieser Ladepunkt in Betrieb?" + }, + "Operator": { + "mappings": { + "0": { + "then": "Eigentlich ist {operator} das Netzwerk" + } + }, + "question": "Wer ist der Betreiber dieser Ladestation?", + "render": "Diese Ladestation wird betrieben von {operator}" + }, + "Parking:fee": { + "mappings": { + "0": { + "then": "Keine zusätzlichen Parkkosten während des Ladens" + }, + "1": { + "then": "Während des Ladens ist eine zusätzliche Parkgebühr zu entrichten" + } + }, + "question": "Muss man während des Ladens eine Parkgebühr bezahlen?" + }, + "Type": { + "mappings": { + "0": { + "then": "Fahrräder können hier geladen werden" + }, + "1": { + "then": "Autos können hier geladen werden" + }, + "2": { + "then": "Roller können hier geladen werden" + }, + "3": { + "then": "LKW können hier geladen werden" + }, + "4": { + "then": "Busse können hier geladen werden" + } + }, + "question": "Welche Fahrzeuge dürfen hier laden?" + } } }, "crossings": { diff --git a/langs/layers/en.json b/langs/layers/en.json index 59d75cc71..9bf448acf 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -213,6 +213,16 @@ "question": "How wide is the smallest opening next to the barriers?", "render": "Width of opening: {width:opening} m" }, + "barrier_type": { + "mappings": { + "0": { + "then": "This is a single bollard in the road" + }, + "1": { + "then": "This is a cycle barrier slowing down cyclists" + } + } + }, "bicycle=yes/no": { "mappings": { "0": { diff --git a/langs/layers/nl.json b/langs/layers/nl.json index 03a8e7a83..0b49ddb23 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -151,6 +151,13 @@ "question": "Hoe breed is de smalste opening naast de barrières?", "render": "Breedte van de opening: {width:opening} m" }, + "barrier_type": { + "mappings": { + "1": { + "then": "Dit zijn fietshekjes die fietsers afremmen" + } + } + }, "bicycle=yes/no": { "mappings": { "0": { diff --git a/langs/shared-questions/zh_Hant.json b/langs/shared-questions/zh_Hant.json index eb0b0c8c8..f18875b11 100644 --- a/langs/shared-questions/zh_Hant.json +++ b/langs/shared-questions/zh_Hant.json @@ -3,6 +3,23 @@ "description": { "question": "有什麼相關的資訊你無法在先前的問題回應的嗎?請加在這邊吧。
不要重覆答覆已經知道的事情" }, + "dog-access": { + "mappings": { + "0": { + "then": "允許犬隻" + }, + "1": { + "then": "允許犬隻" + }, + "2": { + "then": "允許犬隻,但需要掛牽繩" + }, + "3": { + "then": "允許犬隻而且可以自由跑動" + } + }, + "question": "這間商業空間是否允許犬隻?" + }, "email": { "question": "{name} 的電子郵件地址是什麼?" }, @@ -28,11 +45,72 @@ "question": "{name} 的開放時間是什麼?", "render": "

開放時間

{opening_hours_table(opening_hours)}" }, + "payment-options": { + "mappings": { + "0": { + "then": "這邊接受現金" + }, + "1": { + "then": "這邊接受現金卡" + } + }, + "question": "這邊接受那種付款方式?" + }, "phone": { "question": "{name} 的電話號碼是什麼?" }, + "service:electricity": { + "mappings": { + "0": { + "then": "這邊的客戶座位有不少個室內插座,而且可以為電器充電" + }, + "1": { + "then": "這邊客戶座位有一些室內插座,可以為電器充電" + }, + "2": { + "then": "這邊沒有給客戶用的插座,因此可能需要詢問員工是否能充電" + }, + "3": { + "then": "這裡客戶座位沒有室內插座" + } + }, + "question": "這個便利設施有電器設備,能給客戶使用嗎?" + }, "website": { "question": "{name} 網址是什麼?" + }, + "wheelchair-access": { + "mappings": { + "0": { + "then": "這個地方有特別設計給輪椅使用者" + }, + "1": { + "then": "這個地方坐輪椅很容易到達" + }, + "2": { + "then": "這個地方可以坐輪椅到達,但並不容易" + }, + "3": { + "then": "輪椅無法到達這個地方" + } + }, + "question": "這個地方可以坐輪椅到達嗎?" + }, + "wikipedia": { + "mappings": { + "0": { + "then": "還沒有連結到維基百科頁面" + } + }, + "question": "對應的維基資料項目是?" + }, + "wikipedialink": { + "mappings": { + "0": { + "then": "沒有連結到維基百科" + } + }, + "question": "維基百科上對應的項目是什麼?" } } } \ No newline at end of file diff --git a/langs/themes/de.json b/langs/themes/de.json index 658598de6..47f261290 100644 --- a/langs/themes/de.json +++ b/langs/themes/de.json @@ -458,6 +458,22 @@ "0": { "question": "Gibt es eine (inoffizielle) Website mit mehr Informationen (z.B. Topos)?" }, + "1": { + "mappings": { + "0": { + "then": "Das enthaltende Objekt gibt an, dass es öffentlich zugänglich ist
{_embedding_feature:access:description}" + }, + "1": { + "then": "Das enthaltende Objekt besagt, dass eine Genehmigung erforderlich ist für den Zugang zu
{_embedding_feature:access:description}" + }, + "2": { + "then": "Das enthaltende Objekt besagt, dass es nur für Kunden zugänglich ist
{_embedding_feature:access:description}" + }, + "3": { + "then": "Das enthaltende Objekt besagt, dass es nur für Mitglieder zugänglich ist
{_embedding_feature:access:description}" + } + } + }, "2": { "mappings": { "0": { @@ -596,7 +612,25 @@ "cyclenodes": { "description": "Diese Karte zeigt Knotenpunktnetzwerke für Radfahrer und erlaubt auch neue Knoten zu mappen", "layers": { + "0": { + "name": "Knotenpunktverbindungen", + "tagRenderings": { + "node2node-survey:date": { + "question": "Wann wurde diese Knotenpunktverbindung zuletzt überprüft?", + "render": "Diese Knotenpunktverbindung wurde zuletzt am {survey:date} überprüft" + } + }, + "title": { + "mappings": { + "0": { + "then": "Knotenpunktverbindung {ref}" + } + }, + "render": "Knotenpunktverbindung" + } + }, "1": { + "name": "Knotenpunkte", "presets": { "0": { "title": "Knotenpunkt" @@ -605,6 +639,16 @@ "title": "Knotenpunkt im Netzwerk Spree-Neiße" } }, + "tagRenderings": { + "node-expected_rcn_route_relations": { + "question": "Mit wie vielen anderen Knoten des Fahrradknotenpunktnetzwerkes ist dieser Knoten verbunden?", + "render": "Dieser Knoten ist mit {expected_rcn_route_relations} anderen Knoten des Fahrradknotenpunktnetzwerkes verbunden." + }, + "node-survey:date": { + "question": "Wann wurde dieser Fahrradknotenpunkt zuletzt überprüft?", + "render": "Dieser Fahrradknoten wurde zuletzt überprüft am {survey:date}" + } + }, "title": { "render": "Knotenpunkt {rcn_ref}" } @@ -795,6 +839,28 @@ "description": "Ein Geisterrad ist ein weißes Fahrrad, dass zum Gedenken eines tödlich verunglückten Radfahrers vor Ort aufgestellt wurde.

Auf dieser Karte kann man alle Geisterräder sehen, die OpenStreetMap eingetragen sind. Fehlt ein Geisterrad? Jeder kann hier Informationen hinzufügen oder aktualisieren - Sie benötigen lediglich einen (kostenlosen) OpenStreetMap-Account.", "title": "Geisterräder" }, + "grb": { + "layers": { + "2": { + "tagRenderings": { + "building type": { + "question": "Was ist das für ein Gebäude?" + } + } + } + } + }, + "grb_fixme": { + "layers": { + "0": { + "tagRenderings": { + "building type": { + "question": "Was ist das für ein Gebäude?" + } + } + } + } + }, "hackerspaces": { "description": "Auf dieser Karte können Sie Hackerspaces sehen, einen neuen Hackerspace hinzufügen oder Daten direkt aktualisieren", "layers": { @@ -867,7 +933,8 @@ }, "hailhydrant": { "description": "Auf dieser Karte können Sie Hydranten, Feuerwachen, Krankenwagen und Feuerlöscher in Ihren bevorzugten Stadtvierteln finden und aktualisieren.\n\nSie können Ihren genauen Standort verfolgen (nur mobil) und in der unteren linken Ecke die für Sie relevanten Ebenen auswählen. Sie können mit diesem Tool auch Pins (Points of Interest) zur Karte hinzufügen oder bearbeiten und durch die Beantwortung verfügbarer Fragen zusätzliche Angaben machen.\n\nAlle von Ihnen vorgenommenen Änderungen werden automatisch in der globalen Datenbank von OpenStreetMap gespeichert und können von anderen frei weiterverwendet werden.", - "shortDescription": "Hydranten, Feuerlöscher, Feuerwachen und Rettungswachen." + "shortDescription": "Hydranten, Feuerlöscher, Feuerwachen und Rettungswachen.", + "title": "Hydranten, Feuerlöscher, Feuerwachen und Rettungswachen." }, "maps": { "description": "Auf dieser Karte findest du alle Karten, die OpenStreetMap kennt - typischerweise eine große Karte auf einer Informationstafel, die das Gebiet, die Stadt oder die Region zeigt, z.B. eine touristische Karte auf der Rückseite einer Plakatwand, eine Karte eines Naturschutzgebietes, eine Karte der Radwegenetze in der Region, ...)

Wenn eine Karte fehlt, können Sie diese leicht auf OpenStreetMap kartieren.", diff --git a/langs/themes/zh_Hant.json b/langs/themes/zh_Hant.json index 5f6e1e0a3..db3f47313 100644 --- a/langs/themes/zh_Hant.json +++ b/langs/themes/zh_Hant.json @@ -195,7 +195,49 @@ } }, "question": "這個地方需要付費嗎?" + }, + "dumpstations-grey-water": { + "mappings": { + "0": { + "then": "你可以在這裡排放洗滌水" + }, + "1": { + "then": "你無法在這裡排放洗滌水" + } + }, + "question": "你能在這裡排放洗滌水嗎?" + }, + "dumpstations-network": { + "question": "這裡是屬於那個網路的? (沒有則跳過)", + "render": "這車站是屬於 {network} 網路的一部分" + }, + "dumpstations-waterpoint": { + "mappings": { + "0": { + "then": "這個地方有取水點" + }, + "1": { + "then": "這個地方沒有取水點" + } + }, + "question": "這個地方有取水點嗎?" } + }, + "title": { + "mappings": { + "0": { + "then": "垃圾站" + } + }, + "render": "{name} 垃圾站" + } + } + }, + "overrideAll": { + "tagRenderings+": { + "0": { + "question": "這個地方是誰營運的?", + "render": "這個地方由 {operator} 營運的" } } }, @@ -231,6 +273,11 @@ }, "title": "開放攀爬地圖" }, + "cycle_infra": { + "description": "可以檢視與編輯單車相關設施的地圖,在 #os0c21時製作。", + "shortDescription": "檢視與編輯單車相關設施的地圖。", + "title": "單車設施" + }, "cyclestreets": { "description": "單車街道是機動車輛受限制,只允許單車通行的道路。通常會有路標顯示特別的交通指標。單車街道通常在荷蘭、比利時看到,但德國與法國也有。 ", "layers": { @@ -252,11 +299,41 @@ "description": "在這份地圖上,公共可及的飲水點可以顯示出來,也能輕易的增加", "title": "飲用水" }, + "etymology": { + "description": "在這份地圖,你可以看到物件是以何命名,道路、 建築等的命名由來連到 Wikidata。在跳出選單,你可以看到物件命名由來的維基條目 (如果有的話),或是 Wikidata 框。如果物件本身有維基頁面,也會顯示。

你也可以貢獻!放大到夠大的層級,然後所有道路都會顯示。你可以點選一個之後 Wikidata 搜尋框會跳出來。只要點幾下,你可以新增詞源連結。注意你要有開放街圖帳號才能這麼做。", + "layers": { + "1": { + "override": { + "name": "道路沒有詞源資訊" + } + }, + "2": { + "override": { + "name": "公園與森哥沒有詞源資訊" + } + } + }, + "shortDescription": "地名的由來是?", + "title": "開放詞源地圖" + }, "facadegardens": { "layers": { "0": { "description": "立面花園", "name": "立面花園", + "tagRenderings": { + "facadegardens-rainbarrel": { + "mappings": { + "0": { + "then": "這裡有個雨筒" + }, + "1": { + "then": "這裡沒有雨筒" + } + }, + "question": "花園當中有設置雨筒嗎?" + } + }, "title": { "render": "立面花園" } @@ -265,6 +342,9 @@ "shortDescription": "這地圖顯示立面花園的照片以及其他像是方向、日照以及植栽種類等實用訊息。", "title": "立面花園" }, + "food": { + "title": "餐廳與快餐店" + }, "ghostbikes": { "description": "幽靈單車是用來紀念死於交通事故的單車騎士,在事發地點附近放置白色單車。

在這份地圖上面,你可以看到所有在開放街圖已知的幽靈單車。有缺漏的幽靈單車嗎?所有人都可以在這邊新增或是更新資訊-只有你有(免費)開放街圖帳號。", "title": "幽靈單車" @@ -279,10 +359,65 @@ "0": { "description": "駭客空間是對軟體有興趣的人聚集的地方", "title": "駭客空間" + }, + "1": { + "description": "創客空間是 DIY 愛好者聚集在一起弄電子零件實驗,例如用 arduino、LEDstrips 等...", + "title": "創客空間" } + }, + "tagRenderings": { + "hackerspaces-name": { + "question": "這個駭客空間的名稱是?", + "render": "這個駭客空間叫 {name}" + }, + "hackerspaces-opening_hours": { + "mappings": { + "0": { + "then": "24/7 營業" + } + }, + "question": "這個駭客空間的營業時間?", + "render": "{opening_hours_table()}" + }, + "hackerspaces-start_date": { + "question": "這個駭客空間何時成立的?", + "render": "這駭客空間是 {start_date} 成立的" + }, + "hs-club-mate": { + "mappings": { + "0": { + "then": "這個駭客空間服務俱樂部伙伴" + }, + "1": { + "then": "這個駭客空間沒有服務俱樂部伙伴" + } + }, + "question": "這個駭客空間是否服務俱樂部伙伴?" + }, + "is_makerspace": { + "mappings": { + "0": { + "then": "這是創客空間" + }, + "1": { + "then": "這是傳統的 (軟體導向) 駭客空間" + } + }, + "question": "這邊是駭客空間還是創客空間?" + } + }, + "title": { + "mappings": { + "0": { + "then": " {name}" + } + }, + "render": "駭客空間" } } - } + }, + "shortDescription": "駭客空間的地圖", + "title": "駭客空間" }, "hailhydrant": { "description": "在這份地圖上面你可以在你喜愛的社區尋找與更新消防栓、消防隊、急救站與滅火器。\n\n你可以追蹤確切位置 (只有行動版) 以及在左下角選擇與你相關的圖層。你也可以使用這工具新增或編輯地圖上的釘子 (興趣點),以及透過回答一些問題提供額外的資訊。\n\n所有你做出的變動都會自動存到開放街圖這個全球資料庫,而且能自由讓其他人取用。", diff --git a/langs/zh_Hant.json b/langs/zh_Hant.json index 4fcec2206..d78897f31 100644 --- a/langs/zh_Hant.json +++ b/langs/zh_Hant.json @@ -177,7 +177,7 @@ "loginWithOpenStreetMap": "用開放街圖帳號登入" }, "backgroundMap": "背景地圖", - "aboutMapcomplete": "

關於 MapComplete

使用 MapComplete 你可以藉由單一主題豐富開放街圖的圖資。回答幾個問題,然後幾分鐘之內你的貢獻立刻就傳遍全球!主題維護者定議主題的元素、問題與語言。

發現更多

MapComplete 總是提供學習更多開放街圖下一步的知識

  • 當你內嵌網站,網頁內嵌會連結到全螢幕的 MapComplete
  • 全螢幕的版本提供關於開放街圖的資訊
  • 不登入檢視成果,但是要編輯則需登入 OSM。
  • 如果你沒有登入,你會被要求先登入
  • 當你回答單一問題時,你可以在地圖新增新的節點
  • 過了一陣子,實際的 OSM-標籤會顯示,之後會連結到 wiki


你有注意到問題嗎?你想請求功能嗎?想要幫忙翻譯嗎?來到原始碼或是問題追蹤器。

想要看到你的進度嗎?到OsmCha追蹤編輯數。

", + "aboutMapcomplete": "

關於 MapComplete

使用 MapComplete 你可以藉由單一主題新增開放街圖的圖資。回答幾個問題,然後幾分鐘之內你的貢獻立刻就傳遍全球!主題維護者定議主題的元素、問題與語言。

發現更多

MapComplete 總是提供學習更多開放街圖下一步的知識

  • 當你內嵌網站,網頁內嵌會連結到全螢幕的 MapComplete
  • 全螢幕的版本提供關於開放街圖的資訊
  • 不登入檢視成果,但是要編輯則需要 OSM 帳號。
  • 如果你沒有登入,你會被要求先登入
  • 當你回答單一問題時,你可以在地圖新增新的節點
  • 過了一陣子,實際的 OSM-標籤會顯示,之後會連結到 wiki


你有注意到問題嗎?你想請求功能嗎?想要幫忙翻譯嗎?來到原始碼或是問題追蹤器。

想要看到你的進度嗎?到OsmCha追蹤編輯數。

", "customThemeIntro": "

客製化主題

觀看這些先前使用者創造的主題。", "noTagsSelected": "沒有選取標籤", "getStartedNewAccount": " 或是 註冊新帳號", @@ -188,8 +188,10 @@ "morescreen": { "createYourOwnTheme": "從零開始建立你的 MapComplete 主題", "streetcomplete": "行動裝置另有類似的應用程式 StreetComplete。", - "requestATheme": "如果你有客製化要求,請到問題追踪器那邊提出要求", - "intro": "

看更多主題地圖?

您喜歡蒐集地理資料嗎?
還有更多主題。" + "requestATheme": "如果你有客製化主題,請到問題追踪器那邊提出要求", + "intro": "

看更多主題地圖?

您喜歡蒐集地理資料嗎?
還有更多主題。", + "previouslyHiddenTitle": "先前看過隱藏的主題", + "hiddenExplanation": "這些主題只能透過連結來打開,你已經發現 {total_hidden} 當中 {hidden_discovered} 的隱藏主題。" }, "sharescreen": { "fsIncludeCurrentLocation": "包含目前位置", @@ -202,7 +204,7 @@ "fsWelcomeMessage": "顯示歡迎訊息以及相關頁籤", "fsSearch": "啟用搜尋列", "fsUserbadge": "啟用登入按鈕", - "editThemeDescription": "新增或改變這個地圖的問題", + "editThemeDescription": "新增或改變這個地圖主題的問題", "editThisTheme": "編輯這個主題", "thanksForSharing": "感謝分享!", "copiedToClipboard": "複製連結到簡貼簿", @@ -221,7 +223,7 @@ "attributionContent": "

所有資料由開放街圖提供,在開放資料庫授權條款之下自由再利用。

", "attributionTitle": "署名通知" }, - "openStreetMapIntro": "

開放的地圖

如果有一份地圖,任何人都能自由使用與編輯,單一的地圖能夠儲存所有地理相關資訊?這樣不就很酷嗎?接著,所有的網站使用不同的、範圍小的,不相容的地圖 (通常也都過時了),也就不再需要了。

開放街圖就是這樣的地圖,人人都能免費這些圖資 (只要署名與公開變動這資料)。只要遵循這些,任何人都能自由新增新資料與修正錯誤,這些網站也都使用開放街圖,資料也都來自開放街圖,你的答案與修正也會加到開放街圖上面。

許多人與應用程式已經採用開放街圖了:Organic MapsOsmAnd,還有 Facebook、Instagram,蘋果地圖、Bing 地圖(部分)採用開放街圖。如果你在開放街圖上變動資料,也會同時影響這些應用 - 在他們下次更新資料之後!

", + "openStreetMapIntro": "

開放的地圖

如果有一份地圖,任何人都能使用與自由編輯,單一的地圖能夠儲存所有地理相關資訊。不同的、範圍小的,不相容甚至過時不再被需要的地圖。

開放街圖不是敵人的地圖,人人都能自由使用這些圖資, (只要署名與公開變動這資料)。任何人都能新增新資料與修正錯誤,這些網站也用開放街圖,資料也都來自開放街圖,你的答案與修正也會加被用到/p>

許多人與應用程式已經採用開放街圖了:Organic MapsOsmAnd,還有 Facebook、Instagram,蘋果地圖、Bing 地圖(部分)採用開放街圖。

", "questions": { "emailIs": "{category} 的電子郵件地址是{email}", "emailOf": "{category} 的電子郵件地址是?", @@ -244,9 +246,16 @@ "pleaseLogin": "請先登入來新增節點", "intro": "您點擊處目前未有已知的資料。
", "title": "新增新的節點?", - "addNew": "在這裡新增新的 {category}" + "addNew": "在這裡新增新的 {category}", + "hasBeenImported": "這個點已經被匯入了", + "disableFilters": "關閉所有篩選器", + "disableFiltersExplanation": "有些圖徵可能被篩選器隱藏", + "presetInfo": "新的興趣點有 {tags}", + "addNewMapLabel": "新增新項目", + "warnVisibleForEveryone": "你新增的東西將會被所有人看到", + "zoomInMore": "再放大來匯入這一圖徵" }, - "osmLinkTooltip": "在開放街圖歷史和更多編輯選項下面來檢視這物件", + "osmLinkTooltip": "在開放街圖歷史和更多編輯選項下面來瀏覽這物件", "number": "號碼", "skippedQuestions": "有些問題已經跳過了", "oneSkippedQuestion": "跳過一個問題", @@ -262,7 +271,32 @@ }, "loginToStart": "登入之後來回答這問題", "welcomeBack": "你已經登入了,歡迎回來!", - "loginWithOpenStreetMap": "用開放街圖帳號登入" + "loginWithOpenStreetMap": "用開放街圖帳號登入", + "loginOnlyNeededToEdit": "如果你想要編輯地圖", + "layerSelection": { + "zoomInToSeeThisLayer": "放大來看看這一圖層", + "title": "選擇圖層" + }, + "pdf": { + "generatedWith": "用 MapComplete.osm.be 產生的", + "attr": "地圖資料 @ 開放街圖貢獻者,採用 ODbL 授權可再利用", + "versionInfo": "v{version} - {date} 產生的", + "attrBackground": "背景圖層:{background}" + }, + "download": { + "downloadCSV": "下載可視資料為 CSV", + "downloadGeojson": "下載可視資料為 GeoJSON", + "exporting": "匯出…", + "includeMetaData": "包括 metadata (上次編輯者、計算數值等)", + "title": "下載可視的資料", + "downloadAsPdf": "下載目前地圖的 PDF 檔", + "downloadAsPdfHelper": "列印當前地圖相當理想", + "downloadGeoJsonHelper": "與 QGIS、ArcGIS、ESRI 等相容", + "downloadCSVHelper": "與 LibreOffice Calc、Excel 等相容" + }, + "testing": "測試 - 改變還沒有儲存", + "openTheMap": "開啟地圖", + "loading": "載入中..." }, "index": { "pickTheme": "請挑選主題來開始。", @@ -301,7 +335,26 @@ "cannotBeDeleted": "這圖徵無法刪除", "loginToDelete": "你必須登入才能刪除點", "isDeleted": "這圖徵已經刪除", - "cancel": "取消" + "cancel": "取消", + "useSomethingElse": "請使用其他的開放街圖編輯器來刪除", + "explanations": { + "softDelete": "這個圖徵已經被更新,然後從程式被隱藏了。{reason}", + "selectReason": "請選擇為什麼這個圖徵該被刪除?", + "hardDelete": "這個點已經在開放街圖被刪除了,可以被實驗性的貢獻者恢復" + }, + "loading": "調查屬性來確定是否能刪除這一圖徵。", + "reasons": { + "disused": "這個圖徵已經不使用或是被移除了", + "test": "這是測試點 - 並真的不存在那邊的圖徵", + "notFound": "找不到這個圖徵了", + "duplicate": "這個點與其他圖徵重覆了" + }, + "readMessages": "你有未讀的訊息,請先閱讀再來刪除點 - 也許有人有回饋意見", + "isntAPoint": "只有點可以被刪,選取的圖徵是路徑、區域或是關聯。", + "onlyEditedByLoggedInUser": "這個點只有被你編輯,所以你可以安全地刪除。", + "whyDelete": "為什麼這個點要被刪除?", + "notEnoughExperience": "這個點是由其他人做的。", + "partOfOthers": "這個點屬於一些路徑或關聯的一部分,因此不能直接刪除。" }, "split": { "loginToSplit": "你必須登入才能分割道路", diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index 9137ca84a..ccb7dffa1 100644 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -36,7 +36,7 @@ class TranslationPart { } const v = translations[translationsKey] if (typeof (v) != "string") { - console.error("Non-string object in translation while trying to add more translations to '", translationsKey, "': ", v) + console.error(`Non-string object at ${context} in translation while trying to add more translations to '` + translationsKey + "': ", v) throw "Error in an object depicting a translation: a non-string object was found. (" + context + ")\n You probably put some other section accidentally in the translation" } this.contents.set(translationsKey, v) diff --git a/test.ts b/test.ts index 7e9e298ad..2b0b84df9 100644 --- a/test.ts +++ b/test.ts @@ -1,24 +1,152 @@ -import BackgroundMapSwitch from "./UI/BigComponents/BackgroundMapSwitch"; -import {UIEventSource} from "./Logic/UIEventSource"; -import Loc from "./Models/Loc"; +import State from "./State"; +import AllKnownLayers from "./Customizations/AllKnownLayers"; import AvailableBaseLayersImplementation from "./Logic/Actors/AvailableBaseLayersImplementation"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; -import BaseLayer from "./Models/BaseLayer"; -import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import MinimapImplementation from "./UI/Base/MinimapImplementation"; +import {Utils} from "./Utils"; +import * as grb from "./assets/themes/grb_import/grb.json"; +import ReplaceGeometryAction from "./Logic/Osm/Actions/ReplaceGeometryAction"; +import Minimap from "./UI/Base/Minimap"; +import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer"; +import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; +import {BBox} from "./Logic/BBox"; AvailableBaseLayers.implement(new AvailableBaseLayersImplementation()) -const state = { - currentBackground: new UIEventSource(AvailableBaseLayers.osmCarto), - locationControl: new UIEventSource({ - zoom: 19, - lat: 51.2, - lon: 3.2 - }) -} -const actualBackground = new UIEventSource(AvailableBaseLayers.osmCarto) -new BackgroundMapSwitch(state, - { - currentBackground: actualBackground - }).AttachTo("maindiv") +MinimapImplementation.initialize() -new VariableUiElement(actualBackground.map(bg => bg.id)).AttachTo("extradiv") \ No newline at end of file +async function test() { + + const wayId = "way/323230330"; + const targetFeature = { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 4.483118100000016, + 51.028366499999706 + ], + [ + 4.483135099999986, + 51.028325800000005 + ], + [ + 4.483137700000021, + 51.02831960000019 + ], + [ + 4.4831429000000025, + 51.0283205 + ], + [ + 4.483262199999987, + 51.02834059999982 + ], + [ + 4.483276700000019, + 51.028299999999746 + ], + [ + 4.483342100000037, + 51.02830730000009 + ], + [ + 4.483340700000012, + 51.028331299999934 + ], + [ + 4.483346499999953, + 51.02833189999984 + ], + [ + 4.483290600000001, + 51.028500699999846 + ], + [ + 4.4833335999999635, + 51.02851150000015 + ], + [ + 4.4833433000000475, + 51.028513999999944 + ], + [ + 4.483312899999958, + 51.02857759999998 + ], + [ + 4.483141100000033, + 51.02851780000015 + ], + [ + 4.483193100000022, + 51.028409999999894 + ], + [ + 4.483206100000019, + 51.02838310000014 + ], + [ + 4.483118100000016, + 51.028366499999706 + ] + ] + ] + }, + "id": "https://betadata.grbosm.site/grb?bbox=498980.9206456306,6626173.107985358,499133.7947022009,6626325.98204193/30", + "bbox": { + "maxLat": 51.02857759999998, + "maxLon": 4.483346499999953, + "minLat": 51.028299999999746, + "minLon": 4.483118100000016 + }, + "_lon": 4.483232299999985, + "_lat": 51.02843879999986 + } + + + const layout = AllKnownLayouts.allKnownLayouts.get("grb") + const state = new State(layout) + State.state = state; + const bbox = new BBox( + [[ + 4.482952281832695, + 51.02828527958197 + ], + [ + 4.483400881290436, + 51.028578384406984 + ] + + ]) + const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` + const data = await Utils.downloadJson(url) + + state.featurePipeline.fullNodeDatabase.handleOsmJson(data, 0) + + + const action = new ReplaceGeometryAction(state, targetFeature, wayId, { + theme: "test" + } + ) + + console.log(">>>>> ", action.GetClosestIds()) + + const map = Minimap.createMiniMap({ + attribution: false, + }) + const preview = await action.getPreview() + new ShowDataLayer({ + layerToShow: AllKnownLayers.sharedLayers.get("conflation"), + features: preview, + leafletMap: map.leafletMap, + zoomToFeatures: true + }) + map + .SetStyle("height: 75vh;") + .AttachTo("maindiv") +} + +test() \ No newline at end of file diff --git a/test/GeoOperations.spec.ts b/test/GeoOperations.spec.ts index 10c595b92..a8df76883 100644 --- a/test/GeoOperations.spec.ts +++ b/test/GeoOperations.spec.ts @@ -362,6 +362,85 @@ export default class GeoOperationsSpec extends T { const overlapsRev = GeoOperations.calculateOverlap(polyHouse, [polyGrb]) Assert.equal(overlapsRev.length, 0) + }], + ["Overnode removal test", () => { + + const feature = { "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 4.477944199999975, + 51.02783550000022 + ], + [ + 4.477987899999996, + 51.027818800000034 + ], + [ + 4.478004500000021, + 51.02783399999988 + ], + [ + 4.478025499999962, + 51.02782489999994 + ], + [ + 4.478079099999993, + 51.027873899999896 + ], + [ + 4.47801040000006, + 51.027903799999955 + ], + [ + 4.477964799999972, + 51.02785709999982 + ], + [ + 4.477964699999964, + 51.02785690000006 + ], + [ + 4.477944199999975, + 51.02783550000022 + ] + ] + ] + }} + + const copy = GeoOperations.removeOvernoding(feature) + Assert.equal(copy.geometry.coordinates[0].length, 7) + T.listIdentical([ + [ + 4.477944199999975, + 51.02783550000022 + ], + [ + 4.477987899999996, + 51.027818800000034 + ], + [ + 4.478004500000021, + 51.02783399999988 + ], + [ + 4.478025499999962, + 51.02782489999994 + ], + [ + 4.478079099999993, + 51.027873899999896 + ], + [ + 4.47801040000006, + 51.027903799999955 + ], + [ + 4.477944199999975, + 51.02783550000022 + ] + ], copy.geometry.coordinates[0]) }] ] ) diff --git a/test/ReplaceGeometry.spec.ts b/test/ReplaceGeometry.spec.ts index 4919914cc..acc87d216 100644 --- a/test/ReplaceGeometry.spec.ts +++ b/test/ReplaceGeometry.spec.ts @@ -1,14 +1,340 @@ import T from "./TestHelper"; import {Utils} from "../Utils"; +import ReplaceGeometryAction from "../Logic/Osm/Actions/ReplaceGeometryAction"; +import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; +import {Tag} from "../Logic/Tags/Tag"; +import MapState from "../Logic/State/MapState"; +import * as grb from "../assets/themes/grb_import/grb.json" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; +import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; +import State from "../State"; +import {BBox} from "../Logic/BBox"; +import Minimap from "../UI/Base/Minimap"; export default class ReplaceGeometrySpec extends T { + + private static readonly grbStripped = { + "id": "grb", + "title": { + "nl": "GRB import helper" + }, + "description":"Smaller version of the GRB theme", + "language": [ + "nl", + "en" + ], + "version": "0", + "startLat": 51.0249, + "startLon": 4.026489, + "startZoom": 9, + "clustering":false, + "overrideAll": { + "minzoom": 19 + }, + "layers": [ + { + "builtin": "type_node", + "override": { + "calculatedTags": [ + "_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false", + "_is_part_of_grb_building=feat.get('parent_ways')?.some(p => p['source:geometry:ref'] !== undefined) ?? false", + "_is_part_of_building_passage=feat.get('parent_ways')?.some(p => p.tunnel === 'building_passage') ?? false", + "_is_part_of_highway=!feat.get('is_part_of_building_passage') && (feat.get('parent_ways')?.some(p => p.highway !== undefined && p.highway !== '') ?? false)", + "_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false", + "_moveable=feat.get('_is_part_of_building') && !feat.get('_is_part_of_grb_building')" + ], + "mapRendering": [ + { + "icon": "square:#cc0", + "iconSize": "5,5,center", + "location": [ + "point" + ] + } + ], + "passAllFeatures": true + } + }, + { + "id": "osm-buildings", + "name": "All OSM-buildings", + "source": { + "osmTags": "building~*", + "maxCacheAge": 0 + }, + "calculatedTags": [ + "_surface:strict:=feat.get('_surface')" + ], + "mapRendering": [ + { + "width": { + "render": "2", + "mappings": [ + { + "if": "fixme~*", + "then": "5" + } + ] + }, + "color": { + "render": "#00c", + "mappings": [ + { + "if": "fixme~*", + "then": "#ff00ff" + }, + { + "if": "building=house", + "then": "#a00" + }, + { + "if": "building=shed", + "then": "#563e02" + }, + { + "if": { + "or": [ + "building=garage", + "building=garages" + ] + }, + "then": "#f9bfbb" + }, + { + "if": "building=yes", + "then": "#0774f2" + } + ] + } + } + ], + "title": "OSM-gebouw", + "tagRenderings": [ + { + "id": "building type", + "freeform": { + "key": "building" + }, + "render": "The building type is {building}", + "question": { + "en": "What kind of building is this?" + }, + "mappings": [ + { + "if": "building=house", + "then": "A normal house" + }, + { + "if": "building=detached", + "then": "A house detached from other building" + }, + { + "if": "building=semidetached_house", + "then": "A house sharing only one wall with another house" + }, + { + "if": "building=apartments", + "then": "An apartment building - highrise for living" + }, + { + "if": "building=office", + "then": "An office building - highrise for work" + }, + { + "if": "building=apartments", + "then": "An apartment building" + }, + { + "if": "building=shed", + "then": "A small shed, e.g. in a garden" + }, + { + "if": "building=garage", + "then": "A single garage to park a car" + }, + { + "if": "building=garages", + "then": "A building containing only garages; typically they are all identical" + }, + { + "if": "building=yes", + "then": "A building - no specification" + } + ] + }, + { + "id": "grb-housenumber", + "render": { + "nl": "Het huisnummer is {addr:housenumber}" + }, + "question": { + "nl": "Wat is het huisnummer?" + }, + "freeform": { + "key": "addr:housenumber" + }, + "mappings": [ + { + "if": { + "and": [ + "not:addr:housenumber=yes", + "addr:housenumber=" + ] + }, + "then": { + "nl": "Geen huisnummer" + } + } + ] + }, + { + "id": "grb-unit", + "question": "Wat is de wooneenheid-aanduiding?", + "render": { + "nl": "De wooneenheid-aanduiding is {addr:unit} " + }, + "freeform": { + "key": "addr:unit" + }, + "mappings": [ + { + "if": "addr:unit=", + "then": "Geen wooneenheid-nummer" + } + ] + }, + { + "id": "grb-street", + "render": { + "nl": "De straat is {addr:street}" + }, + "freeform": { + "key": "addr:street" + }, + "question": { + "nl": "Wat is de straat?" + } + }, + { + "id": "grb-fixme", + "render": { + "nl": "De fixme is {fixme}" + }, + "question": { + "nl": "Wat zegt de fixme?" + }, + "freeform": { + "key": "fixme" + }, + "mappings": [ + { + "if": { + "and": [ + "fixme=" + ] + }, + "then": { + "nl": "Geen fixme" + } + } + ] + }, + { + "id": "grb-min-level", + "render": { + "nl": "Dit gebouw begint maar op de {building:min_level} verdieping" + }, + "question": { + "nl": "Hoeveel verdiepingen ontbreken?" + }, + "freeform": { + "key": "building:min_level", + "type": "pnat" + } + }, + "all_tags" + ], + "filter": [ + { + "id": "has-fixme", + "options": [ + { + "osmTags": "fixme~*", + "question": "Heeft een FIXME" + } + ] + } + ] + }, + "address", + { + "id": "grb", + "description": "Geometry which comes from GRB with tools to import them", + "source": { + "osmTags": { + "and": [ + "HUISNR~*", + "man_made!=mast" + ] + }, + "geoJson": "https://betadata.grbosm.site/grb?bbox={x_min},{y_min},{x_max},{y_max}", + "geoJsonZoomLevel": 18, + "mercatorCrs": true, + "maxCacheAge": 0 + }, + "name": "GRB geometries", + "title": "GRB outline", + "calculatedTags": [ + "_overlaps_with_buildings=feat.overlapWith('osm-buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)", + "_overlaps_with=feat.get('_overlaps_with_buildings').filter(f => f.overlap > 1 /* square meter */ )[0] ?? ''", + "_osm_obj:source:ref=feat.get('_overlaps_with')?.feat?.properties['source:geometry:ref']", + "_osm_obj:id=feat.get('_overlaps_with')?.feat?.properties?.id", + "_osm_obj:source:date=feat.get('_overlaps_with')?.feat?.properties['source:geometry:date'].replace(/\\//g, '-')", + "_osm_obj:building=feat.get('_overlaps_with')?.feat?.properties?.building", + "_osm_obj:addr:street=(feat.get('_overlaps_with')?.feat?.properties ?? {})['addr:street']", + "_osm_obj:addr:housenumber=(feat.get('_overlaps_with')?.feat?.properties ?? {})['addr:housenumber']", + "_osm_obj:surface=(feat.get('_overlaps_with')?.feat?.properties ?? {})['_surface:strict']", + + "_overlap_absolute=feat.get('_overlaps_with')?.overlap", + "_reverse_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface'))", + "_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_osm_obj:surface'))", + "_grb_ref=feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']", + "_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties._grb_ref", + "_grb_date=feat.properties['source:geometry:date'].replace(/\\//g,'-')", + "_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date", + "_target_building_type=feat.properties['_osm_obj:building'] === 'yes' ? feat.properties.building : (feat.properties['_osm_obj:building'] ?? feat.properties.building)", + "_building:min_level= feat.properties['fixme']?.startsWith('verdieping, correct the building tag, add building:level and building:min_level before upload in JOSM!') ? '1' : ''", + "_intersects_with_other_features=feat.intersectionsWith('generic_osm_object').map(f => \"\" + f.feat.properties.id + \"\").join(', ')" + ], + "tagRenderings": [], + "mapRendering": [ + { + "iconSize": "50,50,center", + "icon": "./assets/themes/grb_import/housenumber_blank.svg", + "location": [ + "point", + "centroid" + ] + } + ] + } + ] + } + + + + constructor() { super("ReplaceGeometry", [ - ["Simple house replacement", async () => { - const coordinates = <[number, number][]>[[ - 3.216690793633461, - 51.21474084112525 - ], + ["House replacement with connected node", async () => { + + Minimap.createMiniMap = () => undefined; + + const coordinates = <[number, number][]>[ + [ + 3.216690793633461, + 51.21474084112525 + ], [ 3.2167256623506546, 51.214696737309964 @@ -51,132 +377,82 @@ export default class ReplaceGeometrySpec extends T { ] ] + const targetFeature = { + type: "Feature", + properties: {}, + geometry: { + type: "Polygon", + coordinates: [coordinates] + } + } + + const wayId = "way/160909312" + Utils.injectJsonDownloadForTests( - "https://www.openstreetmap.org/api/0.6/way/160909312/full", - { - "version": "0.6", - "generator": "CGImap 0.8.5 (920083 spike-06.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 1728823481, - "lat": 51.2146969, - "lon": 3.2167247, - "timestamp": "2017-07-18T22:52:45Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728823483, - "lat": 51.2147409, - "lon": 3.216693, - "timestamp": "2017-07-18T22:52:45Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728823514, - "lat": 51.2147863, - "lon": 3.2168551, - "timestamp": "2017-07-18T22:52:45Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728823549, - "lat": 51.2147399, - "lon": 3.2168871, - "timestamp": "2017-07-18T22:52:46Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 4978288381, - "lat": 51.2147638, - "lon": 3.2168856, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 4978289383, - "lat": 51.2147676, - "lon": 3.2169973, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 4978289384, - "lat": 51.2147683, - "lon": 3.2168674, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 4978289386, - "lat": 51.2147718, - "lon": 3.2168815, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 4978289388, - "lat": 51.2147884, - "lon": 3.2169829, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "way", - "id": 160909312, - "timestamp": "2017-07-18T22:52:30Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209, - "nodes": [1728823483, 1728823514, 4978289384, 4978289386, 4978288381, 4978289388, 4978289383, 1728823549, 1728823481, 1728823483], - "tags": { - "addr:city": "Brugge", - "addr:country": "BE", - "addr:housenumber": "108", - "addr:postcode": "8000", - "addr:street": "Ezelstraat", - "building": "yes" - } - }] + "https://www.openstreetmap.org/api/0.6/map.json?bbox=3.2166673243045807,51.21467321525788,3.217007964849472,51.21482442824023" , + {"version":"0.6","generator":"CGImap 0.8.6 (1549677 spike-06.openstreetmap.org)","copyright":"OpenStreetMap and contributors","attribution":"http://www.openstreetmap.org/copyright","license":"http://opendatacommons.org/licenses/odbl/1-0/","bounds":{"minlat":51.2146732,"minlon":3.2166673,"maxlat":51.2148244,"maxlon":3.217008},"elements":[{"type":"node","id":1612385157,"lat":51.2148016,"lon":3.2168453,"timestamp":"2018-04-30T12:26:00Z","version":3,"changeset":58553478,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":1728816256,"lat":51.2147111,"lon":3.2170233,"timestamp":"2017-07-18T22:52:44Z","version":2,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":1728816287,"lat":51.2146408,"lon":3.2167601,"timestamp":"2021-10-29T16:24:43Z","version":3,"changeset":113131915,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":1728823481,"lat":51.2146968,"lon":3.2167242,"timestamp":"2021-11-02T23:37:11Z","version":5,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":1728823499,"lat":51.2147127,"lon":3.2170302,"timestamp":"2017-07-18T22:52:45Z","version":2,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":1728823501,"lat":51.2148696,"lon":3.2168941,"timestamp":"2017-07-18T22:52:45Z","version":2,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":1728823514,"lat":51.2147863,"lon":3.2168551,"timestamp":"2021-11-02T23:37:11Z","version":5,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":1728823522,"lat":51.2148489,"lon":3.2169012,"timestamp":"2017-07-18T22:52:45Z","version":2,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":1728823523,"lat":51.2147578,"lon":3.2169995,"timestamp":"2017-07-18T22:52:45Z","version":2,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":1728823543,"lat":51.2148075,"lon":3.2166445,"timestamp":"2017-07-18T22:52:46Z","version":3,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":1728823544,"lat":51.2148553,"lon":3.2169315,"timestamp":"2017-07-18T22:52:46Z","version":2,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":1728823549,"lat":51.2147401,"lon":3.2168877,"timestamp":"2021-11-02T23:37:11Z","version":5,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978288376,"lat":51.2147306,"lon":3.2168928,"timestamp":"2017-07-18T22:52:21Z","version":1,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":4978288381,"lat":51.2147638,"lon":3.2168856,"timestamp":"2021-11-02T23:37:11Z","version":4,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978288382,"lat":51.2148189,"lon":3.216912,"timestamp":"2017-07-18T22:52:21Z","version":1,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":4978288385,"lat":51.2148835,"lon":3.2170623,"timestamp":"2017-07-18T22:52:21Z","version":1,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":4978288387,"lat":51.2148904,"lon":3.2171037,"timestamp":"2017-07-18T22:52:21Z","version":1,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":4978289383,"lat":51.2147678,"lon":3.2169969,"timestamp":"2021-11-02T23:37:11Z","version":4,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978289384,"lat":51.2147684,"lon":3.2168674,"timestamp":"2021-11-02T23:37:11Z","version":4,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978289386,"lat":51.2147716,"lon":3.2168811,"timestamp":"2021-11-02T23:37:11Z","version":4,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978289388,"lat":51.2148115,"lon":3.216966,"timestamp":"2021-11-02T23:38:13Z","version":7,"changeset":113306325,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978289391,"lat":51.2148019,"lon":3.2169194,"timestamp":"2017-07-18T22:52:21Z","version":1,"changeset":50391526,"user":"catweazle67","uid":1976209},{"type":"node","id":9219974337,"lat":51.2148449,"lon":3.2171278,"timestamp":"2021-11-02T23:40:52Z","version":1,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":9219979643,"lat":51.2147405,"lon":3.216693,"timestamp":"2021-11-02T23:37:11Z","version":1,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":9219979646,"lat":51.2148043,"lon":3.2169312,"timestamp":"2021-11-02T23:38:13Z","version":2,"changeset":113306325,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":9219979647,"lat":51.2147792,"lon":3.2169466,"timestamp":"2021-11-02T23:37:11Z","version":1,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"way","id":160909311,"timestamp":"2021-12-23T12:03:37Z","version":6,"changeset":115295690,"user":"s8evq","uid":3710738,"nodes":[1728823481,1728823549,4978288376,1728823523,1728823499,1728816256,1728816287,1728823481],"tags":{"addr:city":"Brugge","addr:country":"BE","addr:housenumber":"106","addr:postcode":"8000","addr:street":"Ezelstraat","building":"house","source:geometry:date":"2015-07-09","source:geometry:ref":"Gbg/2391617"}},{"type":"way","id":160909312,"timestamp":"2021-11-02T23:38:13Z","version":4,"changeset":113306325,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[9219979643,1728823481,1728823549,4978289383,4978289388,9219979646,9219979647,4978288381,4978289386,4978289384,1728823514,9219979643],"tags":{"addr:city":"Brugge","addr:country":"BE","addr:housenumber":"108","addr:postcode":"8000","addr:street":"Ezelstraat","building":"house","source:geometry:date":"2018-10-02","source:geometry:ref":"Gbg/5926383"}},{"type":"way","id":160909315,"timestamp":"2021-12-23T12:03:37Z","version":8,"changeset":115295690,"user":"s8evq","uid":3710738,"nodes":[1728823543,1728823501,1728823522,4978288382,1612385157,1728823514,9219979643,1728823543],"tags":{"addr:city":"Brugge","addr:country":"BE","addr:housenumber":"110","addr:postcode":"8000","addr:street":"Ezelstraat","building":"house","name":"La Style","shop":"hairdresser","source:geometry:date":"2015-07-09","source:geometry:ref":"Gbg/5260837"}},{"type":"way","id":508533816,"timestamp":"2021-12-23T12:03:37Z","version":7,"changeset":115295690,"user":"s8evq","uid":3710738,"nodes":[4978288387,4978288385,1728823544,1728823522,4978288382,4978289391,9219979646,4978289388,9219974337,4978288387],"tags":{"building":"yes","source:geometry:date":"2015-07-09","source:geometry:ref":"Gbg/5260790"}}]} + ) + + Utils.injectJsonDownloadForTests( + "https://www.openstreetmap.org/api/0.6/way/160909312/full" , + {"version":"0.6","generator":"CGImap 0.8.6 (2407324 spike-06.openstreetmap.org)","copyright":"OpenStreetMap and contributors","attribution":"http://www.openstreetmap.org/copyright","license":"http://opendatacommons.org/licenses/odbl/1-0/","elements":[{"type":"node","id":1728823481,"lat":51.2146968,"lon":3.2167242,"timestamp":"2021-11-02T23:37:11Z","version":5,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":1728823514,"lat":51.2147863,"lon":3.2168551,"timestamp":"2021-11-02T23:37:11Z","version":5,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":1728823549,"lat":51.2147401,"lon":3.2168877,"timestamp":"2021-11-02T23:37:11Z","version":5,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978288381,"lat":51.2147638,"lon":3.2168856,"timestamp":"2021-11-02T23:37:11Z","version":4,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978289383,"lat":51.2147678,"lon":3.2169969,"timestamp":"2021-11-02T23:37:11Z","version":4,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978289384,"lat":51.2147684,"lon":3.2168674,"timestamp":"2021-11-02T23:37:11Z","version":4,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978289386,"lat":51.2147716,"lon":3.2168811,"timestamp":"2021-11-02T23:37:11Z","version":4,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":4978289388,"lat":51.2148115,"lon":3.216966,"timestamp":"2021-11-02T23:38:13Z","version":7,"changeset":113306325,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":9219979643,"lat":51.2147405,"lon":3.216693,"timestamp":"2021-11-02T23:37:11Z","version":1,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":9219979646,"lat":51.2148043,"lon":3.2169312,"timestamp":"2021-11-02T23:38:13Z","version":2,"changeset":113306325,"user":"Pieter Vander Vennet","uid":3818858},{"type":"node","id":9219979647,"lat":51.2147792,"lon":3.2169466,"timestamp":"2021-11-02T23:37:11Z","version":1,"changeset":113305401,"user":"Pieter Vander Vennet","uid":3818858},{"type":"way","id":160909312,"timestamp":"2021-11-02T23:38:13Z","version":4,"changeset":113306325,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[9219979643,1728823481,1728823549,4978289383,4978289388,9219979646,9219979647,4978288381,4978289386,4978289384,1728823514,9219979643],"tags":{"addr:city":"Brugge","addr:country":"BE","addr:housenumber":"108","addr:postcode":"8000","addr:street":"Ezelstraat","building":"house","source:geometry:date":"2018-10-02","source:geometry:ref":"Gbg/5926383"}}]} + ) + Utils.injectJsonDownloadForTests("https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country/0.0.0.json","be") + + const layout = new LayoutConfig( ReplaceGeometrySpec.grbStripped) + + + + const state = new State(layout) + State.state = state; + const bbox = new BBox( + [[ + 3.2166673243045807, + 51.21467321525788 + ], + [ + 3.217007964849472, + 51.21482442824023 + ] + ]) + const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` + const data = await Utils.downloadJson(url) + + state.featurePipeline.fullNodeDatabase.handleOsmJson(data, 0) + + + const action = new ReplaceGeometryAction(state, targetFeature, wayId, { + theme: "test" } ) - - const wayId = "way/160909312" - const url = `https://www.openstreetmap.org/api/0.6/${wayId}/full`; - const rawData = await Utils.downloadJsonCached(url, 1000) - - - }] + const closestIds = await action.GetClosestIds() + T.listIdentical( + [9219979643, + 1728823481, + 4978289383, + 4978289388, + 9219979646, + 9219979647, + 4978288381, + 4978289386, + 4978289384, + 1728823514, + undefined], + closestIds.closestIds + ) ; + + T.equals( 1 , closestIds.reprojectedNodes.size, "Expected only a single reprojected node"); + const reproj = closestIds.reprojectedNodes.get(1728823549) + T.equals(1, reproj.projectAfterIndex) + T.equals( 3.2168880864669203, reproj.newLon); + T.equals( 51.214739524104694, reproj.newLat); + T.equals(0, closestIds.detachedNodes.size) + const changes = await action.Perform(state.changes) + T.listIdentical([[3.216690793633461,51.21474084112525],[3.2167256623506546,51.214696737309964],[3.2168880864669203,51.214739524104694],[3.2169999182224274,51.214768983537674],[3.2169650495052338,51.21480720678671],[3.2169368863105774,51.21480090625335],[3.2169489562511444,51.21478074454077],[3.216886594891548,51.214765203214625],[3.2168812304735184,51.21477192378873],[3.2168644666671753,51.214768983537674],[3.2168537378311157,51.21478746511261],[3.216690793633461,51.21474084112525]], + changes[11].changes["coordinates"]) + + }], ]); } } \ No newline at end of file diff --git a/test/TestAll.ts b/test/TestAll.ts index f53e1beb4..dabfc8e68 100644 --- a/test/TestAll.ts +++ b/test/TestAll.ts @@ -17,59 +17,66 @@ import ReplaceGeometrySpec from "./ReplaceGeometry.spec"; import LegacyThemeLoaderSpec from "./LegacyThemeLoader.spec"; -ScriptUtils.fixUtils() -const allTests = [ - new OsmObjectSpec(), - new TagSpec(), - new ImageAttributionSpec(), - new GeoOperationsSpec(), - new ThemeSpec(), - new UtilsSpec(), - new UnitsSpec(), - new RelationSplitHandlerSpec(), - new SplitActionSpec(), - new TileFreshnessCalculatorSpec(), - new WikidataSpecTest(), - new ImageProviderSpec(), - new ActorsSpec(), - new ReplaceGeometrySpec(), - new LegacyThemeLoaderSpec() -] +async function main() { -Utils.externalDownloadFunction = async (url) => { - console.error("Fetching ", url, "blocked in tests, use Utils.injectJsonDownloadForTests") - const data = await ScriptUtils.DownloadJSON(url) - console.log("\n\n ----------- \nBLOCKED DATA\n Utils.injectJsonDownloadForTests(\n" + - " ", JSON.stringify(url), ", \n", - " ", JSON.stringify(data), "\n )\n------------------\n\n") - throw "Detected internet access for URL " + url + ", please inject it with Utils.injectJsonDownloadForTests" -} + ScriptUtils.fixUtils() + const allTests = [ + new OsmObjectSpec(), + new TagSpec(), + new ImageAttributionSpec(), + new GeoOperationsSpec(), + new ThemeSpec(), + new UtilsSpec(), + new UnitsSpec(), + new RelationSplitHandlerSpec(), + new SplitActionSpec(), + new TileFreshnessCalculatorSpec(), + new WikidataSpecTest(), + new ImageProviderSpec(), + new ActorsSpec(), + new ReplaceGeometrySpec(), + new LegacyThemeLoaderSpec() + ] -let args = [...process.argv] -args.splice(0, 2) -args = args.map(a => a.toLowerCase()) - -const allFailures: { testsuite: string, name: string, msg: string } [] = [] -let testsToRun = allTests -if (args.length > 0) { - args = args.map(a => a.toLowerCase()) - testsToRun = allTests.filter(t => args.indexOf(t.name.toLowerCase()) >= 0) -} - -if (testsToRun.length == 0) { - throw "No tests found. Try one of " + allTests.map(t => t.name).join(", ") -} - -for (let i = 0; i < testsToRun.length; i++) { - const test = testsToRun[i]; - console.log(" Running test", i, "/", testsToRun.length, test.name) - allFailures.push(...(test.Run() ?? [])) - console.log("OK!") -} -if (allFailures.length > 0) { - for (const failure of allFailures) { - console.error(" !! " + failure.testsuite + "." + failure.name + " failed due to: " + failure.msg) + Utils.externalDownloadFunction = async (url) => { + console.error("Fetching ", url, "blocked in tests, use Utils.injectJsonDownloadForTests") + const data = await ScriptUtils.DownloadJSON(url) + console.log("\n\n ----------- \nBLOCKED DATA\n Utils.injectJsonDownloadForTests(\n" + + " ", JSON.stringify(url), ", \n", + " ", JSON.stringify(data), "\n )\n------------------\n\n") + throw "Detected internet access for URL " + url + ", please inject it with Utils.injectJsonDownloadForTests" } - throw "Some test failed" + + let args = [...process.argv] + args.splice(0, 2) + args = args.map(a => a.toLowerCase()) + + const allFailures: { testsuite: string, name: string, msg: string } [] = [] + let testsToRun = allTests + if (args.length > 0) { + args = args.map(a => a.toLowerCase()) + testsToRun = allTests.filter(t => args.indexOf(t.name.toLowerCase()) >= 0) + } + + if (testsToRun.length == 0) { + throw "No tests found. Try one of " + allTests.map(t => t.name).join(", ") + } + + for (let i = 0; i < testsToRun.length; i++) { + const test = testsToRun[i]; + console.log(" Running test", i, "/", testsToRun.length, test.name) + + allFailures.push(...(await test.Run() ?? [])) + console.log("OK!") + } + if (allFailures.length > 0) { + for (const failure of allFailures) { + console.error(" !! " + failure.testsuite + "." + failure.name + " failed due to: " + failure.msg) + } + throw "Some test failed" + } + console.log("All tests successful: ", testsToRun.map(t => t.name).join(", ")) + } -console.log("All tests successful: ", testsToRun.map(t => t.name).join(", ")) + +main() \ No newline at end of file diff --git a/test/TestHelper.ts b/test/TestHelper.ts index f8a6714cb..924c00cd1 100644 --- a/test/TestHelper.ts +++ b/test/TestHelper.ts @@ -45,7 +45,9 @@ export default class T { throw `ListIdentical failed: expected a list of length ${expected.length} but got a list of length ${actual.length}` } for (let i = 0; i < expected.length; i++) { - if (expected[i] !== actual[i]) { + if(expected[i] !== undefined && expected[i]["length"] !== undefined ){ + T.listIdentical( expected[i], actual[i]) + }else if (expected[i] !== actual[i]) { throw `ListIdentical failed at index ${i}: expected ${expected[i]} but got ${actual[i]}` } } @@ -56,16 +58,18 @@ export default class T { * Returns an empty list if successful * @constructor */ - public Run(): { testsuite: string, name: string, msg: string } [] { + public async Run(): Promise<{ testsuite: string, name: string, msg: string } []> { const failures: { testsuite: string, name: string, msg: string } [] = [] for (const [name, test] of this._tests) { try { const r = test() if (r instanceof Promise) { - r.catch(e => { + try { + await r + } catch (e) { console.log("ASYNC ERROR: ", e, e.stack) failures.push({testsuite: this.name, name: name, msg: "" + e}); - }); + } } } catch (e) {