Merge develop

This commit is contained in:
pietervdvn 2022-01-06 20:10:57 +01:00
commit 94f66eafc1
56 changed files with 2336 additions and 832 deletions

View file

@ -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)) {

View file

@ -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()
})

View file

@ -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<FeatureSource & Tiled> {
@ -10,6 +11,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
private readonly layer: FilteredLayer
private readonly nodeByIds = new Map<number, OsmNode>();
private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>()
constructor(
layer: FilteredLayer,
@ -35,7 +37,6 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
this.nodeByIds.set(osmNode.id, osmNode)
}
const parentWaysByNodeId = new Map<number, OsmWay[]>()
for (const osmObj of allObjects) {
if (osmObj.type !== "way") {
continue
@ -43,16 +44,20 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
const osmWay = <OsmWay>osmObj;
for (const nodeId of osmWay.nodes) {
if (!parentWaysByNodeId.has(nodeId)) {
parentWaysByNodeId.set(nodeId, [])
if (!this.parentWays.has(nodeId)) {
const src = new UIEventSource<OsmWay[]>([])
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<FeatureSour
* @param id
* @constructor
*/
public GetNode(id: number) : OsmNode {
public GetNode(id: number): OsmNode {
return this.nodeByIds.get(id)
}
/**
* Gets the parent way list
* @param nodeId
* @constructor
*/
public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> {
return this.parentWays.get(nodeId)
}
}

View file

@ -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
}

View file

@ -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<string, edge>()
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<string, edge[]>()
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<edge>()
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"
}
}

View file

@ -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<string, ((feature: any) => void)[]>()
private static retaggingFuncCache = new Map<string, ((feature: any) => 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
}
}

View file

@ -81,8 +81,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
// noinspection JSUnusedGlobalSymbols
public async getPreview(): Promise<FeatureSource> {
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<ChangeDescription[]> {
/**
* 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<number, OsmNode>,
osmWay: OsmWay,
detachedNodes: Map<number, {
reason: string,
hasTags: boolean
}>,
reprojectedNodes: Map<number, {
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
projectAfterIndex: number,
distance: number,
newLat: number,
newLon: number,
nodeId: number
}>
}> {
// 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 = <OsmWay>parsed[parsed.length - 1]
if (osmWay.type !== "way") {
throw "WEIRD: expected an OSM-way as last element here!"
}
const allNodesById = new Map<number, OsmNode>()
for (const node of allNodes) {
allNodesById.set(node.id, <OsmNode>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<number /* osmId*/,
/** target coordinate index --> distance (or undefined if a duplicate)*/
number[]>();
const nodeInfo = new Map<number /* osmId*/, {
distances: number[],
// Part of some other way then the one that should be replaced
partOfWay: boolean,
hasTags: boolean
}>()
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<number, {
reason: string,
hasTags: boolean
}>();
{
// 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<number, {
/*Move the node with this ID into the way as extra node, as it has some relation with the original object*/
projectAfterIndex: number,
distance: number,
newLat: number,
newLon: number,
nodeId: number
}>();
{
// 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<ChangeDescription[]> {
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 = <ChangeDescription>{
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<number, OsmNode>,
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<number /* osmId*/, number[] /* target coordinate index --> 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 = <OsmWay>parsed[parsed.length - 1]
if (osmWay.type !== "way") {
throw "WEIRD: expected an OSM-way as last element here!"
}
const allNodesById = new Map<number, OsmNode>()
for (const node of allNodes) {
allNodesById.set(node.id, <OsmNode>node)
}
return {closestIds, allNodesById, osmWay, detachedNodeIds: unusedIds};
}
}
}

View file

@ -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;

View file

@ -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;
})
)

View file

@ -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<T>(key: string, options: { defaultValue?: T }): UIEventSource<T>{
const src = new UIEventSource<T>(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;

View file

@ -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) {

View file

@ -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 {

View file

@ -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()

View file

@ -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<boolean>) {
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")

View file

@ -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);
}

View file

@ -11,6 +11,7 @@ export default abstract class BaseUIElement {
private clss: Set<string> = new Set<string>();
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;
}

View file

@ -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
)

View file

@ -236,4 +236,5 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
return false;
}
}

View file

@ -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<Tag[]>; targetLayer: string },
tagSource: UIEventSource<any>, 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,

View file

@ -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
})
}
}

View file

@ -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")
}
}
))

View file

@ -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;

View file

@ -108,7 +108,8 @@
"render": "<b>Fixme description</b>{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"

View file

@ -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",

View file

@ -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": "<b>Bicycles</b> can be charged here",
"nl": "<b>Fietsen</b> kunnen hier opgeladen worden"
"nl": "<b>Fietsen</b> kunnen hier opgeladen worden",
"de": "<b>Fahrräder</b> können hier geladen werden"
}
},
{
@ -51,7 +53,8 @@
"ifnot": "motorcar=no",
"then": {
"en": "<b>Cars</b> can be charged here",
"nl": "<b>Elektrische auto's</b> kunnen hier opgeladen worden"
"nl": "<b>Elektrische auto's</b> kunnen hier opgeladen worden",
"de": "<b>Autos</b> können hier geladen werden"
}
},
{
@ -59,7 +62,8 @@
"ifnot": "scooter=no",
"then": {
"en": "<b>Scooters</b> can be charged here",
"nl": "<b>Electrische scooters</b> (snorfiets of bromfiets) kunnen hier opgeladen worden"
"nl": "<b>Electrische scooters</b> (snorfiets of bromfiets) kunnen hier opgeladen worden",
"de": "<b>Roller</b> können hier geladen werden"
}
},
{
@ -67,7 +71,8 @@
"ifnot": "hgv=no",
"then": {
"en": "<b>Heavy good vehicles</b> (such as trucks) can be charged here",
"nl": "<b>Vrachtwagens</b> kunnen hier opgeladen worden"
"nl": "<b>Vrachtwagens</b> kunnen hier opgeladen worden",
"de": "<b>LKW</b> können hier geladen werden"
}
},
{
@ -75,7 +80,8 @@
"ifnot": "bus=no",
"then": {
"en": "<b>Buses</b> can be charged here",
"nl": "<b>Bussen</b> kunnen hier opgeladen worden"
"nl": "<b>Bussen</b> kunnen hier opgeladen worden",
"de": "<b>Busse</b> 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 <b>{network}</b>",
"nl": "Maakt deel uit van het <b>{network}</b>-netwerk"
"nl": "Maakt deel uit van het <b>{network}</b>-netwerk",
"de": "Teil des Netzwerks <b>{network}</b>"
},
"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"
}
}
],

View file

@ -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": [

View file

@ -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 <b>street</b> exists:<br/>{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 @@
}
}
]
}
}

View file

@ -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)"
}
}
}

View file

@ -817,6 +817,12 @@
"authors": [],
"sources": []
},
{
"path": "none.svg",
"license": "CC0",
"authors": [],
"sources": []
},
{
"path": "osm-logo-us.svg",
"license": "Logo",

79
assets/svg/none.svg Normal file
View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="487.23px"
height="487.23px"
viewBox="0 0 487.23 487.23"
style="enable-background:new 0 0 487.23 487.23;"
xml:space="preserve"
sodipodi:docname="none.svg"
inkscape:version="1.1.1 (1:1.1+202109281949+c3084ef5ed)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs43" /><sodipodi:namedview
id="namedview41"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="1.6624592"
inkscape:cx="243.61501"
inkscape:cy="243.91576"
inkscape:current-layer="Capa_1" />
<g
id="g10">
</g>
<g
id="g12">
</g>
<g
id="g14">
</g>
<g
id="g16">
</g>
<g
id="g18">
</g>
<g
id="g20">
</g>
<g
id="g22">
</g>
<g
id="g24">
</g>
<g
id="g26">
</g>
<g
id="g28">
</g>
<g
id="g30">
</g>
<g
id="g32">
</g>
<g
id="g34">
</g>
<g
id="g36">
</g>
<g
id="g38">
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -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 questattività?",
"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 <b>non</b> sono ammessi",
"nb_NO": "Hunder tillates <b>ikke</b>",
"ca": "<b>No</b> s'accepten gossos",
"sv": "Hundar tillåts <b>inte</b>"
"sv": "Hundar tillåts <b>inte</b>",
"zh_Hant": "<b>不</b>允許犬隻"
}
},
{
@ -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": "這邊接受現金卡"
}
}
]

View file

@ -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-til dun point deau ?",
"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 deau",
"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 na pas de point deau",
"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 dy 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 dy 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 nest pas possible dy 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 dun 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 lexploitant du site ?",
"de": "Wer betreibt diesen Ort?"
"de": "Wer betreibt diesen Ort?",
"zh_Hant": "這個地方是誰營運的?"
},
"freeform": {
"key": "operator"

View file

@ -1142,7 +1142,8 @@
"en": "<span class='subtle'>The <a href='#{_embedding_feature:id}'>containing feature</a> states that this is</span> publicly accessible<br/>{_embedding_feature:access:description}",
"nl": "<span class='subtle'>Een <a href='#{_embedding_feature:id}'>omvattend element</a> geeft aan dat dit <span>publiek toegangkelijk is</span><br/>{_embedding_feature:access:description}",
"fr": "<span class='subtle'>L<a href='#{_embedding_feature:id}'>élément englobant</a> indique un </span> accès libre<br/>{_embedding_feature:access:description}",
"it": "<span class='subtle'>L <a href='#{_embedding_feature:id}'>elemento in cui è contenuto</a> indica che è</span> pubblicamente accessibile<br/>{_embedding_feature:access:description}"
"it": "<span class='subtle'>L <a href='#{_embedding_feature:id}'>elemento in cui è contenuto</a> indica che è</span> pubblicamente accessibile<br/>{_embedding_feature:access:description}",
"de": "<span class='subtle'>Das <a href='#{_embedding_feature:id}'>enthaltende Objekt</a> gibt an, dass es </span>öffentlich zugänglich ist<br/>{_embedding_feature:access:description}"
}
},
{
@ -1151,7 +1152,8 @@
"en": "<span class='subtle'>The <a href='#{_embedding_feature:id}'>containing feature</a> states that </span> a permit is needed to access<br/>{_embedding_feature:access:description}",
"nl": "<span class='subtle'>Een <a href='#{_embedding_feature:id}'>omvattend element</a> geeft aan dat</span> een toelating nodig is om hier te klimmen<br/>{_embedding_feature:access:description}",
"fr": "<span class='subtle'>L<a href='#{_embedding_feature:id}'>élément englobant</a> indique qu</span> une autorisation daccès est nécessaire<br/>{_embedding_feature:access:description}",
"it": "<span class='subtle'>L<a href='#{_embedding_feature:id}'>elemento che lo contiene</a> indica che </span> è richiesto unautorizzazione per accedervi<br/>{_embedding_feature:access:description}"
"it": "<span class='subtle'>L<a href='#{_embedding_feature:id}'>elemento che lo contiene</a> indica che </span> è richiesto unautorizzazione per accedervi<br/>{_embedding_feature:access:description}",
"de": "<span class='subtle'>Das <a href='#{_embedding_feature:id}'>enthaltende Objekt</a> besagt, dass </span> eine Genehmigung erforderlich ist für den Zugang zu<br/>{_embedding_feature:access:description}"
}
},
{
@ -1159,7 +1161,8 @@
"then": {
"en": "<span class='subtle'>The <a href='#{_embedding_feature:id}'>containing feature</a> states that this is</span> only accessible to customers<br/>{_embedding_feature:access:description}",
"fr": "<span class='subtle'>L<a href='#{_embedding_feature:id}'>élément englobant</a> indique que </span> laccès est réservés aux clients<br/>{_embedding_feature:access:description}",
"it": "<span class='subtle'>L <a href='#{_embedding_feature:id}'>elemento che lo contiene</a> indica che è</span> accessibile solo ai clienti<br/>{_embedding_feature:access:description}"
"it": "<span class='subtle'>L <a href='#{_embedding_feature:id}'>elemento che lo contiene</a> indica che è</span> accessibile solo ai clienti<br/>{_embedding_feature:access:description}",
"de": "<span class='subtle'>Das <a href='#{_embedding_feature:id}'>enthaltende Objekt</a> besagt, dass es nur für Kunden</span> zugänglich ist<br/>{_embedding_feature:access:description}"
}
},
{
@ -1167,7 +1170,8 @@
"then": {
"en": "<span class='subtle'>The <a href='#{_embedding_feature:id}'>containing feature</a> states that this is</span> only accessible to club members<br/>{_embedding_feature:access:description}",
"fr": "<span class='subtle'>L<a href='#{_embedding_feature:id}'>élément englobant</a> indique que </span> laccès est réservé aux membres<br/>{_embedding_feature:access:description}",
"it": "<span class='subtle'>L <a href='#{_embedding_feature:id}'>elemento che lo contiene</a> indica che è </span> accessibile solamente ai membri del club<br/>{_embedding_feature:access:description}"
"it": "<span class='subtle'>L <a href='#{_embedding_feature:id}'>elemento che lo contiene</a> indica che è </span> accessibile solamente ai membri del club<br/>{_embedding_feature:access:description}",
"de": "<span class='subtle'>Das <a href='#{_embedding_feature:id}'>enthaltende Objekt</a> besagt, dass es </span>nur für Mitglieder zugänglich ist<br/>{_embedding_feature:access:description}"
}
},
{

View file

@ -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 linfrastruttura 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 linfrastruttura dei velocipedi. Realizzata durante #osoc21."
"it": "Una cartina dove vedere e modificare gli elementi riguardanti linfrastruttura 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,

View file

@ -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 <strong>{ref}</strong>"
"en": "node to node link <strong>{ref}</strong>",
"de": "Knotenpunktverbindung <strong>{ref}</strong>"
}
}
]
@ -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",

View file

@ -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 è lorigine di un toponimo?"
"it": "Qual è lorigine 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.<br/><br/><b>You can help contribute too!</b>Zoom in enough and <i>all</i> 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.<br/><br/><b>Je kan zelf ook meehelpen!</b>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.<br/><br/><b>Sie können auch einen Beitrag leisten!</b>Zoomen Sie genug hinein und <i>alle</i> 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 larticolo Wikipedia o l'elemento Wikidata a cui si riferisce il nome di quelloggetto. Se loggetto stesso ha una pagina Wikpedia, anchessa verrà mostrata.<br/><br/><b>Anche tu puoi contribuire!</b>Ingrandisci abbastanza e <i>tutte</i> 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 larticolo Wikipedia o l'elemento Wikidata a cui si riferisce il nome di quelloggetto. Se loggetto stesso ha una pagina Wikpedia, anchessa verrà mostrata.<br/><br/><b>Anche tu puoi contribuire!</b>Ingrandisci abbastanza e <i>tutte</i> 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 框。如果物件本身有維基頁面,也會顯示。<br/><br/><b>你也可以貢獻!</b>放大到夠大的層級,然後<i>所有</i>道路都會顯示。你可以點選一個之後 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": {

View file

@ -180,7 +180,8 @@
"ja": "庭に水桶が設置されているのですか?",
"fr": "Des réserves deau ont-elles été installées pour le jardin ?",
"de": "Gibt es ein Wasserfass für den Garten?",
"it": "È stata installata una riserva dacqua per il giardino?"
"it": "È stata installata una riserva dacqua 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 ny a pas de réserves",
"de": "Es gibt keine Regentonne"
"de": "Es gibt keine Regentonne",
"zh_Hant": "這裡沒有雨筒"
}
}
]

View file

@ -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",

View file

@ -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 <b>{building}</b>",
"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}.<br/>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": "<div>The overlapping <a href='https://osm.org/{_osm_obj:id}' target='_blank'>openstreetmap-building</a> is a <b>{_osm_obj:building}</b> and covers <b>{_overlap_percentage}%</b> of the GRB building<div><h3>GRB geometry:</h3>{minimap(21, id):height:10rem;border-radius:1rem;overflow:hidden}<h3>OSM geometry:</h3>{minimap(21,_osm_obj:id):height:10rem;border-radius:1rem;overflow:hidden}",
"render": "<div>The overlapping <a href='https://osm.org/{_osm_obj:id}' target='_blank'>openstreetmap-building</a> is a <b>{_osm_obj:building}</b> and covers <b>{_overlap_percentage}%</b> of the GRB building.<br/>The GRB-building covers <b>{_reverse_overlap_percentage}%</b> of the OSM building<div><h3>GRB geometry:</h3>{minimap(21, id):height:10rem;border-radius:1rem;overflow:hidden}<h3>OSM geometry:</h3>{minimap(21,_osm_obj:id):height:10rem;border-radius:1rem;overflow:hidden}",
"condition": "_overlaps_with!="
},
{

View file

@ -11,7 +11,8 @@
},
"language": [
"nl",
"en"
"en",
"de"
],
"maintainer": "",
"icon": "./assets/svg/bug.svg",
@ -114,7 +115,8 @@
},
"render": "The building type is <b>{building}</b>",
"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": [
{

View file

@ -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 <b>{name}</b>",
"de": "Dieser Hackerspace heißt <b>{name}</b>"
"de": "Dieser Hackerspace heißt <b>{name}</b>",
"zh_Hant": "這個駭客空間叫 <b>{name}</b>"
},
"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 等..."
}
}
],

View file

@ -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.",

View file

@ -214,7 +214,7 @@
"write_a_comment": "Schreibe einen Kommentar…",
"no_rating": "Keine Bewertung vorhanden",
"posting_as": "Angemeldet als",
"i_am_affiliated": "<span>Ich bin angehörig</span><br/><span class='subtle'>Überprüfe, ob du Eigentümer, Ersteller, Angestellter etc. bist</span>",
"i_am_affiliated": "<span>Ich bin mit diesem Objekt vertraut</span><br><span class=\"subtle\">Überprüfe, ob du Eigentümer, Ersteller, Angestellter etc. bist</span>",
"saving_review": "Speichern…",
"saved": "<span class=\"thanks\">Bewertung gespeichert. Danke fürs Teilen!</span>",
"tos": "Mit deiner Bewertung stimmst du den <a href=\"https://mangrove.reviews/terms\" target=\"_blank\">AGB und den Datenschutzrichtlinien von Mangrove.reviews zu</a>",

View file

@ -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 <b>{network}</b>"
},
"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": "<b>Fahrräder</b> können hier geladen werden"
},
"1": {
"then": "<b>Autos</b> können hier geladen werden"
},
"2": {
"then": "<b>Roller</b> können hier geladen werden"
},
"3": {
"then": "<b>LKW</b> können hier geladen werden"
},
"4": {
"then": "<b>Busse</b> können hier geladen werden"
}
},
"question": "Welche Fahrzeuge dürfen hier laden?"
}
}
},
"crossings": {

View file

@ -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": {

View file

@ -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": {

View file

@ -3,6 +3,23 @@
"description": {
"question": "有什麼相關的資訊你無法在先前的問題回應的嗎?請加在這邊吧。<br/><span style='font-size: small'>不要重覆答覆已經知道的事情</span>"
},
"dog-access": {
"mappings": {
"0": {
"then": "允許犬隻"
},
"1": {
"then": "<b>不</b>允許犬隻"
},
"2": {
"then": "允許犬隻,但需要掛牽繩"
},
"3": {
"then": "允許犬隻而且可以自由跑動"
}
},
"question": "這間商業空間是否允許犬隻?"
},
"email": {
"question": "{name} 的電子郵件地址是什麼?"
},
@ -28,11 +45,72 @@
"question": "{name} 的開放時間是什麼?",
"render": "<h3>開放時間</h3>{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": "維基百科上對應的項目是什麼?"
}
}
}

View file

@ -458,6 +458,22 @@
"0": {
"question": "Gibt es eine (inoffizielle) Website mit mehr Informationen (z.B. Topos)?"
},
"1": {
"mappings": {
"0": {
"then": "<span class='subtle'>Das <a href='#{_embedding_feature:id}'>enthaltende Objekt</a> gibt an, dass es </span>öffentlich zugänglich ist<br/>{_embedding_feature:access:description}"
},
"1": {
"then": "<span class='subtle'>Das <a href='#{_embedding_feature:id}'>enthaltende Objekt</a> besagt, dass </span> eine Genehmigung erforderlich ist für den Zugang zu<br/>{_embedding_feature:access:description}"
},
"2": {
"then": "<span class='subtle'>Das <a href='#{_embedding_feature:id}'>enthaltende Objekt</a> besagt, dass es nur für Kunden</span> zugänglich ist<br/>{_embedding_feature:access:description}"
},
"3": {
"then": "<span class='subtle'>Das <a href='#{_embedding_feature:id}'>enthaltende Objekt</a> besagt, dass es </span>nur für Mitglieder zugänglich ist<br/>{_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 <strong>{ref}</strong>"
}
},
"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 <strong>{rcn_ref}</strong>"
}
@ -795,6 +839,28 @@
"description": "Ein <b>Geisterrad</b> ist ein weißes Fahrrad, dass zum Gedenken eines tödlich verunglückten Radfahrers vor Ort aufgestellt wurde.<br/><br/> 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, ...) <br/><br/>Wenn eine Karte fehlt, können Sie diese leicht auf OpenStreetMap kartieren.",

View file

@ -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": "單車街道是<b>機動車輛受限制,只允許單車通行</b>的道路。通常會有路標顯示特別的交通指標。單車街道通常在荷蘭、比利時看到,但德國與法國也有。 ",
"layers": {
@ -252,11 +299,41 @@
"description": "在這份地圖上,公共可及的飲水點可以顯示出來,也能輕易的增加",
"title": "飲用水"
},
"etymology": {
"description": "在這份地圖,你可以看到物件是以何命名,道路、 建築等的命名由來連到 Wikidata。在跳出選單你可以看到物件命名由來的維基條目 (如果有的話),或是 Wikidata 框。如果物件本身有維基頁面,也會顯示。<br/><br/><b>你也可以貢獻!</b>放大到夠大的層級,然後<i>所有</i>道路都會顯示。你可以點選一個之後 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": "<b>幽靈單車</b>是用來紀念死於交通事故的單車騎士,在事發地點附近放置白色單車。<br/><br/>在這份地圖上面,你可以看到所有在開放街圖已知的幽靈單車。有缺漏的幽靈單車嗎?所有人都可以在這邊新增或是更新資訊-只有你有(免費)開放街圖帳號。",
"title": "幽靈單車"
@ -279,10 +359,65 @@
"0": {
"description": "駭客空間是對軟體有興趣的人聚集的地方",
"title": "駭客空間"
},
"1": {
"description": "創客空間是 DIY 愛好者聚集在一起弄電子零件實驗,例如用 arduino、LEDstrips 等...",
"title": "創客空間"
}
},
"tagRenderings": {
"hackerspaces-name": {
"question": "這個駭客空間的名稱是?",
"render": "這個駭客空間叫 <b>{name}</b>"
},
"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所有你做出的變動都會自動存到開放街圖這個全球資料庫而且能自由讓其他人取用。",

View file

@ -177,7 +177,7 @@
"loginWithOpenStreetMap": "用開放街圖帳號登入"
},
"backgroundMap": "背景地圖",
"aboutMapcomplete": "<h3>關於 MapComplete</h3><p>使用 MapComplete 你可以藉由<b>單一主題</b>豐富開放街圖的圖資。回答幾個問題,然後幾分鐘之內你的貢獻立刻就傳遍全球!<b>主題維護者</b>定議主題的元素、問題與語言。</p><h3>發現更多</h3><p>MapComplete 總是提供學習更多開放街圖<b>下一步的知識</b>。</p><ul><li>當你內嵌網站,網頁內嵌會連結到全螢幕的 MapComplete</li><li>全螢幕的版本提供關於開放街圖的資訊</li><li>不登入檢視成果,但是要編輯則需登入 OSM。</li><li>如果你沒有登入,你會被要求先登入</li><li>當你回答單一問題時,你可以在地圖新增新的節點</li><li>過了一陣子,實際的 OSM-標籤會顯示,之後會連結到 wiki</li></ul><p></p><br><p>你有注意到<b>問題</b>嗎?你想請求<b>功能</b>嗎?想要<b>幫忙翻譯</b>嗎?來到<a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">原始碼</a>或是<a href=\"https://github.com/pietervdvn/MapComplete/issues\" target=\"_blank\">問題追蹤器。</a></p><p>想要看到<b>你的進度</b>嗎?到<a href=\"https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D\" target=\"_blank\">OsmCha</a>追蹤編輯數。</p>",
"aboutMapcomplete": "<h3>關於 MapComplete</h3><p>使用 MapComplete 你可以藉由<b>單一主題</b>新增開放街圖的圖資。回答幾個問題,然後幾分鐘之內你的貢獻立刻就傳遍全球!<b>主題維護者</b>定議主題的元素、問題與語言。</p><h3>發現更多</h3><p>MapComplete 總是提供學習更多開放街圖<b>下一步的知識</b>。</p><ul><li>當你內嵌網站,網頁內嵌會連結到全螢幕的 MapComplete</li><li>全螢幕的版本提供關於開放街圖的資訊</li><li>不登入檢視成果,但是要編輯則需要 OSM 帳號。</li><li>如果你沒有登入,你會被要求先登入</li><li>當你回答單一問題時,你可以在地圖新增新的節點</li><li>過了一陣子,實際的 OSM-標籤會顯示,之後會連結到 wiki</li></ul><p></p><br><p>你有注意到<b>問題</b>嗎?你想請求<b>功能</b>嗎?想要<b>幫忙翻譯</b>嗎?來到<a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">原始碼</a>或是<a href=\"https://github.com/pietervdvn/MapComplete/issues\" target=\"_blank\">問題追蹤器。</a></p><p>想要看到<b>你的進度</b>嗎?到<a href=\"{osmcha_link}\" target=\"_blank\">OsmCha</a>追蹤編輯數。</p>",
"customThemeIntro": "<h3>客製化主題</h3>觀看這些先前使用者創造的主題。",
"noTagsSelected": "沒有選取標籤",
"getStartedNewAccount": " 或是 <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">註冊新帳號</a>",
@ -188,8 +188,10 @@
"morescreen": {
"createYourOwnTheme": "從零開始建立你的 MapComplete 主題",
"streetcomplete": "行動裝置另有類似的應用程式 <a class=\"underline hover:text-blue-800\" href=\"https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete\" target=\"_blank\">StreetComplete</a>。",
"requestATheme": "如果你有客製化要求,請到問題追踪器那邊提出要求",
"intro": "<h3>看更多主題地圖?</h3>您喜歡蒐集地理資料嗎?<br>還有更多主題。"
"requestATheme": "如果你有客製化主題,請到問題追踪器那邊提出要求",
"intro": "<h3>看更多主題地圖?</h3>您喜歡蒐集地理資料嗎?<br>還有更多主題。",
"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": "<p>所有資料由<a href=\"https://osm.org\" target=\"_blank\">開放街圖</a>提供,在<a href=\"https://osm.org/copyright\" target=\"_blank\">開放資料庫授權條款</a>之下自由再利用。</p>",
"attributionTitle": "署名通知"
},
"openStreetMapIntro": "<h3>開放的地圖</h3><p>如果有一份地圖,任何人都能自由使用與編輯,單一的地圖能夠儲存所有地理相關資訊?這樣不就很酷嗎?接著,所有的網站使用不同的、範圍小的,不相容的地圖 (通常也都過時了),也就不再需要了。</p><p><b><a href=\"https://OpenStreetMap.org\" target=\"_blank\">開放街圖</a></b>就是這樣的地圖,人人都能免費這些圖資 (只要<a href=\"https://osm.org/copyright\" target=\"_blank\">署名與公開變動這資料</a>)。只要遵循這些,任何人都能自由新增新資料與修正錯誤,這些網站也都使用開放街圖,資料也都來自開放街圖,你的答案與修正也會加到開放街圖上面。</p><p>許多人與應用程式已經採用開放街圖了:<a href=\"https://organicmaps.app//\" target=\"_blank\">Organic Maps</a>、<a href=\"https://osmAnd.net\" target=\"_blank\">OsmAnd</a>,還有 Facebook、Instagram蘋果地圖、Bing 地圖(部分)採用開放街圖。如果你在開放街圖上變動資料,也會同時影響這些應用 - 在他們下次更新資料之後!</p>",
"openStreetMapIntro": "<h3>開放的地圖</h3><p>如果有一份地圖,任何人都能使用與自由編輯,單一的地圖能夠儲存所有地理相關資訊。不同的、範圍小的,不相容甚至過時不再被需要的地圖。</p><p><b><a href=\"https://OpenStreetMap.org\" target=\"_blank\">開放街圖</a></b>不是敵人的地圖,人人都能自由使用這些圖資, (只要<a href=\"https://osm.org/copyright\" target=\"_blank\">署名與公開變動這資料</a>)。任何人都能新增新資料與修正錯誤,這些網站也用開放街圖,資料也都來自開放街圖,你的答案與修正也會加被用到/p&gt;</p><p>許多人與應用程式已經採用開放街圖了:<a href=\"https://organicmaps.app//\" target=\"_blank\">Organic Maps</a>、<a href=\"https://osmAnd.net\" target=\"_blank\">OsmAnd</a>,還有 Facebook、Instagram蘋果地圖、Bing 地圖(部分)採用開放街圖。</p>",
"questions": {
"emailIs": "{category} 的電子郵件地址是<a href=\"mailto:{email}\" target=\"_blank\">{email}</a>",
"emailOf": "{category} 的電子郵件地址是?",
@ -244,9 +246,16 @@
"pleaseLogin": "<a class=\"activate-osm-authentication\">請先登入來新增節點</a>",
"intro": "您點擊處目前未有已知的資料。<br>",
"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": "這個圖徵已經被更新,然後從程式被隱藏了。<span class=\"subtle\">{reason}</span>",
"selectReason": "請選擇為什麼這個圖徵該被刪除?",
"hardDelete": "這個點已經在開放街圖被刪除了,可以被實驗性的貢獻者恢復"
},
"loading": "調查屬性來確定是否能刪除這一圖徵。",
"reasons": {
"disused": "這個圖徵已經不使用或是被移除了",
"test": "這是測試點 - 並真的不存在那邊的圖徵",
"notFound": "找不到這個圖徵了",
"duplicate": "這個點與其他圖徵重覆了"
},
"readMessages": "你有未讀的訊息,請先閱讀再來刪除點 - 也許有人有回饋意見",
"isntAPoint": "只有點可以被刪,選取的圖徵是路徑、區域或是關聯。",
"onlyEditedByLoggedInUser": "這個點只有被你編輯,所以你可以安全地刪除。",
"whyDelete": "為什麼這個點要被刪除?",
"notEnoughExperience": "這個點是由其他人做的。",
"partOfOthers": "這個點屬於一些路徑或關聯的一部分,因此不能直接刪除。"
},
"split": {
"loginToSplit": "你必須登入才能分割道路",

View file

@ -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)

166
test.ts
View file

@ -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<BaseLayer>(AvailableBaseLayers.osmCarto),
locationControl: new UIEventSource<Loc>({
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")
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()

View file

@ -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])
}]
]
)

File diff suppressed because one or more lines are too long

View file

@ -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()

View file

@ -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(<any> expected[i], <any> 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) {