Add the possibility to snap onto another layer with imports, add location confirm on input, add metalayer exporting all nodes, various fixes

This commit is contained in:
Pieter Vander Vennet 2021-10-31 02:08:39 +01:00
parent f5d6441b70
commit 23ae9d39c8
24 changed files with 807 additions and 390 deletions

View file

@ -26,6 +26,7 @@ import {OsmConnection} from "../Osm/OsmConnection";
import {Tiles} from "../../Models/TileRange"; import {Tiles} from "../../Models/TileRange";
import TileFreshnessCalculator from "./TileFreshnessCalculator"; import TileFreshnessCalculator from "./TileFreshnessCalculator";
import {ElementStorage} from "../ElementStorage"; import {ElementStorage} from "../ElementStorage";
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource";
/** /**
@ -146,6 +147,11 @@ export default class FeaturePipeline {
this.freshnesses.set(id, new TileFreshnessCalculator()) this.freshnesses.set(id, new TileFreshnessCalculator())
if(id === "type_node"){
// Handles by the 'FullNodeDatabaseSource'
continue;
}
if (source.geojsonSource === undefined) { if (source.geojsonSource === undefined) {
// This is an OSM layer // This is an OSM layer
// We load the cached values and register them // We load the cached values and register them
@ -221,6 +227,14 @@ export default class FeaturePipeline {
}) })
}) })
if(state.layoutToUse.trackAllNodes){
new FullNodeDatabaseSource(state, osmFeatureSource, tile => {
new RegisteringAllFromFeatureSourceActor(tile)
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
})
}
const updater = this.initOverpassUpdater(state, useOsmApi) const updater = this.initOverpassUpdater(state, useOsmApi)
this.overpassUpdater = updater; this.overpassUpdater = updater;

View file

@ -1,8 +1,6 @@
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {Utils} from "../../../Utils";
import {Tiles} from "../../../Models/TileRange";
import {BBox} from "../../BBox"; import {BBox} from "../../BBox";
export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled {

View file

@ -0,0 +1,70 @@
import TileHierarchy from "./TileHierarchy";
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject";
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
import {UIEventSource} from "../../UIEventSource";
import FilteredLayer from "../../../Models/FilteredLayer";
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> {
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
private readonly layer : FilteredLayer
constructor(
state: {
readonly filteredLayers: UIEventSource<FilteredLayer[]>},
osmFeatureSource: { rawDataHandlers: ((data: any, tileId: number) => void)[] },
onTileLoaded: ((tile: Tiled & FeatureSourceForLayer) => void)) {
this.onTileLoaded = onTileLoaded
this.layer = state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0]
if(this.layer === undefined){
throw "Weird: tracking all nodes, but layer 'type_node' is not defined"
}
const self = this
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => self.handleOsmXml(osmJson, tileId))
}
private handleOsmXml(osmJson: any, tileId: number) {
const allObjects = OsmObject.ParseObjects(osmJson.elements)
const nodesById = new Map<number, OsmNode>()
for (const osmObj of allObjects) {
if (osmObj.type !== "node") {
continue
}
const osmNode = <OsmNode>osmObj;
nodesById.set(osmNode.id, osmNode)
}
const parentWaysByNodeId = new Map<number, OsmWay[]>()
for (const osmObj of allObjects) {
if (osmObj.type !== "way") {
continue
}
const osmWay = <OsmWay>osmObj;
for (const nodeId of osmWay.nodes) {
if (!parentWaysByNodeId.has(nodeId)) {
parentWaysByNodeId.set(nodeId, [])
}
parentWaysByNodeId.get(nodeId).push(osmWay)
}
}
parentWaysByNodeId.forEach((allWays, nodeId) => {
nodesById.get(nodeId).tags["parent_ways"] = JSON.stringify(allWays.map(w => w.tags))
})
const now = new Date()
const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({
feature: osmNode.asGeoJson(),freshness: now
}))
const featureSource = new SimpleFeatureSource(this.layer, tileId)
featureSource.features.setData(asGeojsonFeatures)
this.loadedTiles.set(tileId, featureSource)
this.onTileLoaded(featureSource)
}
}

View file

@ -31,6 +31,8 @@ export default class OsmFeatureSource {
public readonly downloadedTiles = new Set<number>() public readonly downloadedTiles = new Set<number>()
private readonly allowedTags: TagsFilter; private readonly allowedTags: TagsFilter;
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
constructor(options: { constructor(options: {
handleTile: (tile: FeatureSourceForLayer & Tiled) => void; handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
isActive: UIEventSource<boolean>, isActive: UIEventSource<boolean>,
@ -94,11 +96,11 @@ export default class OsmFeatureSource {
try { try {
console.log("Attempting to get tile", z, x, y, "from the osm api") console.log("Attempting to get tile", z, x, y, "from the osm api")
const osmXml = await Utils.download(url, {"accept": "application/xml"}) const osmJson = await Utils.downloadJson(url)
try { try {
const parsed = new DOMParser().parseFromString(osmXml, "text/xml");
console.log("Got tile", z, x, y, "from the osm api") console.log("Got tile", z, x, y, "from the osm api")
const geojson = OsmToGeoJson.default(parsed, this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y)))
const geojson = OsmToGeoJson.default(osmJson,
// @ts-ignore // @ts-ignore
{ {
flatProperties: true flatProperties: true

View file

@ -235,6 +235,13 @@ export class GeoOperations {
* @param point Point defined as [lon, lat] * @param point Point defined as [lon, lat]
*/ */
public static nearestPoint(way, point: [number, number]) { public static nearestPoint(way, point: [number, number]) {
if(way.geometry.type === "Polygon"){
way = {...way}
way.geometry = {...way.geometry}
way.geometry.type = "LineString"
way.geometry.coordinates = way.geometry.coordinates[0]
}
return turf.nearestPointOnLine(way, point, {units: "kilometers"}); return turf.nearestPointOnLine(way, point, {units: "kilometers"});
} }

View file

@ -4,12 +4,19 @@ import {Changes} from "../Changes";
import {Tag} from "../../Tags/Tag"; import {Tag} from "../../Tags/Tag";
import CreateNewNodeAction from "./CreateNewNodeAction"; import CreateNewNodeAction from "./CreateNewNodeAction";
import {And} from "../../Tags/And"; import {And} from "../../Tags/And";
import {TagsFilter} from "../../Tags/TagsFilter";
export default class CreateNewWayAction extends OsmChangeAction { export default class CreateNewWayAction extends OsmChangeAction {
public newElementId: string = undefined public newElementId: string = undefined
private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[]; private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[];
private readonly tags: Tag[]; private readonly tags: Tag[];
private readonly _options: { theme: string }; private readonly _options: {
theme: string, existingPointHandling?: {
withinRangeOfM: number,
ifMatches?: TagsFilter,
mode: "reuse_osm_point" | "move_osm_point"
} []
};
/*** /***
@ -18,8 +25,18 @@ export default class CreateNewWayAction extends OsmChangeAction {
* @param coordinates: the coordinates. Might have a nodeId, in this case, this node will be used * @param coordinates: the coordinates. Might have a nodeId, in this case, this node will be used
* @param options * @param options
*/ */
constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[], options: { constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[],
theme: string options: {
theme: string,
/**
* IF specified, an existing OSM-point within this range and satisfying the condition 'ifMatches' will be used instead of a new coordinate.
* If multiple points are possible, only the closest point is considered
*/
existingPointHandling?: {
withinRangeOfM: number,
ifMatches?: TagsFilter,
mode: "reuse_osm_point" | "move_osm_point"
} []
}) { }) {
super() super()
this.coordinates = coordinates; this.coordinates = coordinates;
@ -53,10 +70,10 @@ export default class CreateNewWayAction extends OsmChangeAction {
const id = changes.getNewID() const id = changes.getNewID()
const newWay = <ChangeDescription> { const newWay = <ChangeDescription>{
id, id,
type: "way", type: "way",
meta:{ meta: {
theme: this._options.theme, theme: this._options.theme,
changeType: "import" changeType: "import"
}, },
@ -67,7 +84,7 @@ export default class CreateNewWayAction extends OsmChangeAction {
} }
} }
newElements.push(newWay) newElements.push(newWay)
this.newElementId = "way/"+id this.newElementId = "way/" + id
return newElements return newElements
} }

View file

@ -206,7 +206,7 @@ export abstract class OsmObject {
return result; return result;
} }
private static ParseObjects(elements: any[]): OsmObject[] { public static ParseObjects(elements: any[]): OsmObject[] {
const objects: OsmObject[] = []; const objects: OsmObject[] = [];
const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>() const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>()

View file

@ -15,6 +15,8 @@ import LineRenderingConfig from "./LineRenderingConfig";
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"; import PointRenderingConfigJson from "./Json/PointRenderingConfigJson";
import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"; import LineRenderingConfigJson from "./Json/LineRenderingConfigJson";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../../UI/BaseUIElement";
export default class LayerConfig extends WithContextLoader { export default class LayerConfig extends WithContextLoader {
@ -59,11 +61,11 @@ export default class LayerConfig extends WithContextLoader {
this.id = json.id; this.id = json.id;
if (json.source === undefined) { if (json.source === undefined) {
throw "Layer " + this.id + " does not define a source section ("+context+")" throw "Layer " + this.id + " does not define a source section (" + context + ")"
} }
if (json.source.osmTags === undefined) { if (json.source.osmTags === undefined) {
throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers ("+context+")" throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + context + ")"
} }
@ -262,6 +264,15 @@ export default class LayerConfig extends WithContextLoader {
} }
} }
public defaultIcon() : BaseUIElement | undefined{
const mapRendering = this.mapRendering.filter(r => r.location.has("point"))[0]
if (mapRendering === undefined) {
return undefined
}
const defaultTags = new UIEventSource(TagUtils.changeAsProperties(this.source.osmTags.asChange({id: "node/-1"})))
return mapRendering.GenerateLeafletStyle(defaultTags, false, {noSize: true}).html
}
public ExtractLayerTagRenderings(json: LayerConfigJson): TagRenderingConfig[] { public ExtractLayerTagRenderings(json: LayerConfigJson): TagRenderingConfig[] {
if (json.tagRenderings === undefined) { if (json.tagRenderings === undefined) {
@ -358,7 +369,6 @@ export default class LayerConfig extends WithContextLoader {
} }
public CustomCodeSnippets(): string[] { public CustomCodeSnippets(): string[] {
if (this.calculatedTags === undefined) { if (this.calculatedTags === undefined) {
return []; return [];
@ -366,7 +376,6 @@ export default class LayerConfig extends WithContextLoader {
return this.calculatedTags.map((code) => code[1]); return this.calculatedTags.map((code) => code[1]);
} }
public ExtractImages(): Set<string> { public ExtractImages(): Set<string> {
const parts: Set<string>[] = []; const parts: Set<string>[] = [];
parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false))); parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false)));

View file

@ -1,7 +1,5 @@
import {Translation} from "../../UI/i18n/Translation"; import {Translation} from "../../UI/i18n/Translation";
import TagRenderingConfig from "./TagRenderingConfig";
import {LayoutConfigJson} from "./Json/LayoutConfigJson"; import {LayoutConfigJson} from "./Json/LayoutConfigJson";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import AllKnownLayers from "../../Customizations/AllKnownLayers"; import AllKnownLayers from "../../Customizations/AllKnownLayers";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import LayerConfig from "./LayerConfig"; import LayerConfig from "./LayerConfig";
@ -54,6 +52,7 @@ export default class LayoutConfig {
public readonly overpassMaxZoom: number public readonly overpassMaxZoom: number
public readonly osmApiTileSize: number public readonly osmApiTileSize: number
public readonly official: boolean; public readonly official: boolean;
public readonly trackAllNodes : boolean;
constructor(json: LayoutConfigJson, official = true, context?: string) { constructor(json: LayoutConfigJson, official = true, context?: string) {
this.official = official; this.official = official;
@ -63,6 +62,8 @@ export default class LayoutConfig {
this.credits = json.credits; this.credits = json.credits;
this.version = json.version; this.version = json.version;
this.language = []; this.language = [];
this.trackAllNodes = false
if (typeof json.language === "string") { if (typeof json.language === "string") {
this.language = [json.language]; this.language = [json.language];
} else { } else {
@ -92,11 +93,15 @@ export default class LayoutConfig {
if(json.widenFactor > 20){ if(json.widenFactor > 20){
throw "Widenfactor is very big, use a value between 1 and 5 (current value is "+json.widenFactor+") at "+context throw "Widenfactor is very big, use a value between 1 and 5 (current value is "+json.widenFactor+") at "+context
} }
this.widenFactor = json.widenFactor ?? 1.5; this.widenFactor = json.widenFactor ?? 1.5;
this.defaultBackgroundId = json.defaultBackgroundId; this.defaultBackgroundId = json.defaultBackgroundId;
this.tileLayerSources = (json.tileLayerSources??[]).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`)) this.tileLayerSources = (json.tileLayerSources??[]).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`))
this.layers = LayoutConfig.ExtractLayers(json, official, context); const layerInfo = LayoutConfig.ExtractLayers(json, official, context);
this.layers = layerInfo.layers
this.trackAllNodes = layerInfo.extractAllNodes
this.clustering = { this.clustering = {
maxZoom: 16, maxZoom: 16,
@ -147,10 +152,11 @@ export default class LayoutConfig {
} }
private static ExtractLayers(json: LayoutConfigJson, official: boolean, context: string): LayerConfig[] { private static ExtractLayers(json: LayoutConfigJson, official: boolean, context: string): {layers: LayerConfig[], extractAllNodes: boolean} {
const result: LayerConfig[] = [] const result: LayerConfig[] = []
let exportAllNodes = false
json.layers.forEach((layer, i) => { json.layers.forEach((layer, i) => {
if (typeof layer === "string") { if (typeof layer === "string") {
if (AllKnownLayers.sharedLayersJson.get(layer) !== undefined) { if (AllKnownLayers.sharedLayersJson.get(layer) !== undefined) {
if (json.overrideAll !== undefined) { if (json.overrideAll !== undefined) {
@ -177,12 +183,19 @@ export default class LayoutConfig {
result.push(newLayer) result.push(newLayer)
return return
} }
// @ts-ignore // @ts-ignore
let names = layer.builtin; let names = layer.builtin;
if (typeof names === "string") { if (typeof names === "string") {
names = [names] names = [names]
} }
names.forEach(name => { names.forEach(name => {
if(name === "type_node"){
// This is a very special layer which triggers special behaviour
exportAllNodes = true;
}
const shared = AllKnownLayers.sharedLayersJson.get(name); const shared = AllKnownLayers.sharedLayersJson.get(name);
if (shared === undefined) { if (shared === undefined) {
throw `Unknown shared/builtin layer ${name} at ${context}.layers[${i}]. Available layers are ${Array.from(AllKnownLayers.sharedLayersJson.keys()).join(", ")}`; throw `Unknown shared/builtin layer ${name} at ${context}.layers[${i}]. Available layers are ${Array.from(AllKnownLayers.sharedLayersJson.keys()).join(", ")}`;
@ -199,7 +212,7 @@ export default class LayoutConfig {
}); });
return result return {layers: result, extractAllNodes: exportAllNodes}
} }
public CustomCodeSnippets(): string[] { public CustomCodeSnippets(): string[] {

View file

@ -14,7 +14,6 @@ import FilteredLayer from "../../Models/FilteredLayer";
import BackgroundSelector from "./BackgroundSelector"; import BackgroundSelector from "./BackgroundSelector";
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
import {TagUtils} from "../../Logic/Tags/TagUtils";
export default class FilterView extends VariableUiElement { export default class FilterView extends VariableUiElement {
constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) { constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) {
@ -113,18 +112,8 @@ export default class FilterView extends VariableUiElement {
const style = const style =
"display:flex;align-items:center;padding:0.5rem 0;"; "display:flex;align-items:center;padding:0.5rem 0;";
const mapRendering = layer.mapRendering.filter(r => r.location.has("point"))[0] const layerIcon = layer.defaultIcon()?.SetClass("w-8 h-8 ml-2")
let layerIcon = undefined const layerIconUnchecked = layer.defaultIcon()?.SetClass("opacity-50 w-8 h-8 ml-2")
let layerIconUnchecked = undefined
try {
if (mapRendering !== undefined) {
const defaultTags = new UIEventSource( TagUtils.changeAsProperties(layer.source.osmTags.asChange({id: "node/-1"})))
layerIcon = mapRendering.GenerateLeafletStyle(defaultTags, false, {noSize: true}).html.SetClass("w-8 h-8 ml-2")
layerIconUnchecked = mapRendering.GenerateLeafletStyle(defaultTags, false, {noSize: true}).html.SetClass("opacity-50 w-8 h-8 ml-2")
}
} catch (e) {
console.error(e)
}
const layerChecked = new Combine([icon, layerIcon, styledNameChecked, zoomStatus]) const layerChecked = new Combine([icon, layerIcon, styledNameChecked, zoomStatus])
.SetStyle(style) .SetStyle(style)

View file

@ -16,12 +16,25 @@ import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {Changes} from "../../Logic/Osm/Changes"; import {Changes} from "../../Logic/Osm/Changes";
import {ElementStorage} from "../../Logic/ElementStorage"; import {ElementStorage} from "../../Logic/ElementStorage";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import Lazy from "../Base/Lazy";
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
import {PresetInfo} from "./SimpleAddUI";
import Img from "../Base/Img";
import {Translation} from "../i18n/Translation";
import FilteredLayer from "../../Models/FilteredLayer";
import SpecialVisualizations, {SpecialVisualization} from "../SpecialVisualizations";
import {FixedUiElement} from "../Base/FixedUiElement";
import Svg from "../../Svg";
import {Utils} from "../../Utils";
export default class ImportButton extends Toggle {
constructor(imageUrl: string | BaseUIElement, export interface ImportButtonState {
description?: Translation;
image: () => BaseUIElement,
message: string | BaseUIElement, message: string | BaseUIElement,
originalTags: UIEventSource<any>, originalTags: UIEventSource<any>,
newTags: UIEventSource<Tag[]>, newTags: UIEventSource<Tag[]>,
targetLayer: FilteredLayer,
feature: any, feature: any,
minZoom: number, minZoom: number,
state: { state: {
@ -33,12 +46,135 @@ export default class ImportButton extends Toggle {
osmConnection: OsmConnection, osmConnection: OsmConnection,
changes: Changes, changes: Changes,
locationControl: UIEventSource<{ zoom: number }> locationControl: UIEventSource<{ zoom: number }>
}) { },
guiState: { filterViewIsOpened: UIEventSource<boolean> },
snapToLayers?: string[],
snapToLayersMaxDist?: number
}
export class ImportButtonSpecialViz implements SpecialVisualization {
funcName = "import_button"
docs = `This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes.
#### Importing a dataset into OpenStreetMap: requirements
If you want to import a dataset, make sure that:
1. The dataset to import has a suitable license
2. The community has been informed of the import
3. All other requirements of the [import guidelines](https://wiki.openstreetmap.org/wiki/Import/Guidelines) have been followed
There are also some technicalities in your theme to keep in mind:
1. The new feature will be added and will flow through the program as any other new point as if it came from OSM.
This means that there should be a layer which will match the new tags and which will display it.
2. The original feature from your geojson layer will gain the tag '_imported=yes'.
This should be used to change the appearance or even to hide it (eg by changing the icon size to zero)
3. There should be a way for the theme to detect previously imported points, even after reloading.
A reference number to the original dataset is an excellent way to do this
4. When importing ways, the theme creator is also responsible of avoiding overlapping ways.
#### Disabled in unofficial themes
The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md).
The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console.
In the case that MapComplete is pointed to the testing grounds, the edit will be made on ${OsmConnection.oauth_configs["osm-test"].url}
#### Specifying which tags to copy or add
The first argument of the import button takes a \`;\`-seperated list of tags to add.
${Utils.Special_visualizations_tagsToApplyHelpText}
`
args = [
{
name: "targetLayer",
doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements"
},
{
name: "tags",
doc: "The tags to add onto the new object - see specification above"
},
{
name: "text",
doc: "The text to show on the button",
defaultValue: "Import this data into OpenStreetMap"
},
{
name: "icon",
doc: "A nice icon to show in the button",
defaultValue: "./assets/svg/addSmall.svg"
},
{
name: "minzoom",
doc: "How far the contributor must zoom in before being able to import the point",
defaultValue: "18"
}, {
name: "Snap onto layer(s)",
doc: "If a way of the given layer is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list",
}, {
name: "snap max distance",
doc: "The maximum distance that this point will move to snap onto a layer (in meters)",
defaultValue: "5"
}]
constr(state, tagSource, args, guiState) {
if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) {
return new Combine([new FixedUiElement("The import button is disabled for unofficial themes to prevent accidents.").SetClass("alert"),
new FixedUiElement("To test, add <b>test=true</b> or <b>backend=osm-test</b> to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")])
}
const newTags = SpecialVisualizations.generateTagsToApply(args[1], tagSource)
const id = tagSource.data.id;
const feature = state.allElements.ContainingFeatures.get(id)
let minZoom = args[4] == "" ? 18 : Number(args[4])
if(isNaN(minZoom)){
console.warn("Invalid minzoom:", minZoom)
minZoom = 18
}
const message = args[2]
const imageUrl = args[3]
let img: () => BaseUIElement
const targetLayer: FilteredLayer = state.filteredLayers.data.filter(fl => fl.layerDef.id === args[0])[0]
if (imageUrl !== undefined && imageUrl !== "") {
img = () => new Img(imageUrl)
} else {
img = () => Svg.add_ui()
}
const snapToLayers = args[5]?.split(";").filter(s => s !== "")
const snapToLayersMaxDist = Number(args[6] ?? 6)
if (targetLayer === undefined) {
const e = "Target layer not defined: error in import button for theme: " + state.layoutToUse.id + ": layer " + args[0] + " not found"
console.error(e)
return new FixedUiElement(e).SetClass("alert")
}
return new ImportButton(
{
state, guiState, image: img,
feature, newTags, message, minZoom,
originalTags: tagSource,
targetLayer,
snapToLayers,
snapToLayersMaxDist
}
);
}
}
export default class ImportButton extends Toggle {
constructor(o: ImportButtonState) {
const t = Translations.t.general.add; const t = Translations.t.general.add;
const isImported = originalTags.map(tags => tags._imported === "yes") const isImported = o.originalTags.map(tags => tags._imported === "yes")
const appliedTags = new Toggle( const appliedTags = new Toggle(
new VariableUiElement( new VariableUiElement(
newTags.map(tgs => { o.newTags.map(tgs => {
const parts = [] const parts = []
for (const tag of tgs) { for (const tag of tgs) {
parts.push(tag.key + "=" + tag.value) parts.push(tag.key + "=" + tag.value)
@ -46,63 +182,106 @@ export default class ImportButton extends Toggle {
const txt = parts.join(" & ") const txt = parts.join(" & ")
return t.presetInfo.Subs({tags: txt}).SetClass("subtle") return t.presetInfo.Subs({tags: txt}).SetClass("subtle")
})), undefined, })), undefined,
state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt) o.state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt)
) )
const button = new SubtleButton(imageUrl, message) const button = new SubtleButton(o.image(), o.message)
minZoom = Math.max(16, minZoom ?? 19) o.minZoom = Math.max(16, o.minZoom ?? 19)
button.onClick(async () => {
if (isImported.data) {
return
}
originalTags.data["_imported"] = "yes"
originalTags.ping() // will set isImported as per its definition
const newElementAction = ImportButton.createAddActionForFeature(newTags.data, feature, state.layoutToUse.id)
await state.changes.applyAction(newElementAction)
state.selectedElement.setData(state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
console.log("Did set selected element to", state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
})
const withLoadingCheck = new Toggle(new Toggle( const withLoadingCheck = new Toggle(new Toggle(
new Loading(t.stillLoading.Clone()), new Loading(t.stillLoading.Clone()),
new Combine([button, appliedTags]).SetClass("flex flex-col"), new Combine([button, appliedTags]).SetClass("flex flex-col"),
state.featurePipeline.runningQuery o.state.featurePipeline.runningQuery
), t.zoomInFurther.Clone(), ), t.zoomInFurther.Clone(),
state.locationControl.map(l => l.zoom >= minZoom) o.state.locationControl.map(l => l.zoom >= o.minZoom)
) )
const importButton = new Toggle(t.hasBeenImported, withLoadingCheck, isImported) const importButton = new Toggle(t.hasBeenImported, withLoadingCheck, isImported)
const importClicked = new UIEventSource(false);
const importFlow = new Toggle(
new Lazy(() => ImportButton.createConfirmPanel(o, isImported, importClicked)),
importButton,
importClicked
)
button.onClick(() => {
importClicked.setData(true);
})
const pleaseLoginButton = const pleaseLoginButton =
new Toggle(t.pleaseLogin.Clone() new Toggle(t.pleaseLogin.Clone()
.onClick(() => state.osmConnection.AttemptLogin()) .onClick(() => o.state.osmConnection.AttemptLogin())
.SetClass("login-button-friendly"), .SetClass("login-button-friendly"),
undefined, undefined,
state.featureSwitchUserbadge) o.state.featureSwitchUserbadge)
super(new Toggle(importButton, super(new Toggle(importFlow,
pleaseLoginButton, pleaseLoginButton,
state.osmConnection.isLoggedIn o.state.osmConnection.isLoggedIn
), ),
t.wrongType, t.wrongType,
new UIEventSource(ImportButton.canBeImported(feature)) new UIEventSource(ImportButton.canBeImported(o.feature))
) )
} }
public static createConfirmPanel(
o: ImportButtonState,
isImported: UIEventSource<boolean>,
importClicked: UIEventSource<boolean>): BaseUIElement {
async function confirm() {
if (isImported.data) {
return
}
o.originalTags.data["_imported"] = "yes"
o.originalTags.ping() // will set isImported as per its definition
const newElementAction = ImportButton.createAddActionForFeature(o.newTags.data, o.feature, o.state.layoutToUse.id)
await o.state.changes.applyAction(newElementAction)
o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
console.log("Did set selected element to", o.state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
}
function cancel() {
importClicked.setData(false)
}
if (o.feature.geometry.type === "Point") {
const presetInfo = <PresetInfo>{
tags: o.newTags.data,
icon: o.image,
description: o.description,
layerToAddTo: o.targetLayer,
name: o.message,
title: o.message,
preciseInput: { snapToLayers: o.snapToLayers,
maxSnapDistance: o.snapToLayersMaxDist}
}
const [lon, lat] = o.feature.geometry.coordinates
console.log("Creating an import dialog at location", lon, lat)
return new ConfirmLocationOfPoint(o.state, o.guiState.filterViewIsOpened, presetInfo, Translations.W(o.message), {
lon,
lat
}, confirm, cancel)
}
}
private static canBeImported(feature: any) { private static canBeImported(feature: any) {
const type = feature.geometry.type const type = feature.geometry.type
return type === "Point" || type === "LineString" || (type === "Polygon" && feature.geometry.coordinates.length === 1) return type === "Point" || type === "LineString" || (type === "Polygon" && feature.geometry.coordinates.length === 1)
} }
private static createAddActionForFeature(newTags: Tag[], feature: any, theme: string): OsmChangeAction & { newElementId: string } { private static createAddActionForFeature(newTags: Tag[], feature: any, theme: string):
OsmChangeAction & { newElementId: string } {
const geometry = feature.geometry const geometry = feature.geometry
const type = geometry.type const type = geometry.type
if (type === "Point") { if (type === "Point") {

View file

@ -12,18 +12,16 @@ import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement"; import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle"; import Toggle from "../Input/Toggle";
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
import LocationInput from "../Input/LocationInput";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
import PresetConfig from "../../Models/ThemeConfig/PresetConfig"; import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
import FilteredLayer from "../../Models/FilteredLayer"; import FilteredLayer from "../../Models/FilteredLayer";
import {BBox} from "../../Logic/BBox";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../Logic/Osm/Changes"; import {Changes} from "../../Logic/Osm/Changes";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {ElementStorage} from "../../Logic/ElementStorage"; import {ElementStorage} from "../../Logic/ElementStorage";
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
/* /*
* The SimpleAddUI is a single panel, which can have multiple states: * The SimpleAddUI is a single panel, which can have multiple states:
@ -33,8 +31,7 @@ import {ElementStorage} from "../../Logic/ElementStorage";
* - A 'read your unread messages before adding a point' * - A 'read your unread messages before adding a point'
*/ */
/*private*/ export interface PresetInfo extends PresetConfig {
interface PresetInfo extends PresetConfig {
name: string | BaseUIElement, name: string | BaseUIElement,
icon: () => BaseUIElement, icon: () => BaseUIElement,
layerToAddTo: FilteredLayer layerToAddTo: FilteredLayer
@ -91,8 +88,9 @@ export default class SimpleAddUI extends Toggle {
if (preset === undefined) { if (preset === undefined) {
return presetsOverview return presetsOverview
} }
return SimpleAddUI.CreateConfirmButton(state, filterViewIsOpened, preset,
(tags, location, snapOntoWayId?: string) => {
function confirm(tags, location, snapOntoWayId?: string) {
if (snapOntoWayId === undefined) { if (snapOntoWayId === undefined) {
createNewPoint(tags, location, undefined) createNewPoint(tags, location, undefined)
} else { } else {
@ -101,10 +99,18 @@ export default class SimpleAddUI extends Toggle {
return true; return true;
}) })
} }
}, }
() => {
function cancel() {
selectedPreset.setData(undefined) selectedPreset.setData(undefined)
}) }
const message =Translations.t.general.add.addNew.Subs({category: preset.name});
return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset,
message,
state.LastClickLocation.data,
confirm,
cancel)
} }
)) ))
@ -134,170 +140,7 @@ export default class SimpleAddUI extends Toggle {
} }
private static CreateConfirmButton( public static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) {
state: {
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
osmConnection: OsmConnection,
featurePipeline: FeaturePipeline
},
filterViewIsOpened: UIEventSource<boolean>,
preset: PresetInfo,
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
cancel: () => void): BaseUIElement {
let location = state.LastClickLocation;
let preciseInput: LocationInput = undefined
if (preset.preciseInput !== undefined) {
// We uncouple the event source
const locationSrc = new UIEventSource({
lat: location.data.lat,
lon: location.data.lon,
zoom: 19
});
let backgroundLayer = undefined;
if (preset.preciseInput.preferredBackground) {
backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
}
let snapToFeatures: UIEventSource<{ feature: any }[]> = undefined
let mapBounds: UIEventSource<BBox> = undefined
if (preset.preciseInput.snapToLayers) {
snapToFeatures = new UIEventSource<{ feature: any }[]>([])
mapBounds = new UIEventSource<BBox>(undefined)
}
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
preciseInput = new LocationInput({
mapBackground: backgroundLayer,
centerLocation: locationSrc,
snapTo: snapToFeatures,
snappedPointTags: tags,
maxSnapDistance: preset.preciseInput.maxSnapDistance,
bounds: mapBounds
})
preciseInput.installBounds(0.15, true)
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
if (preset.preciseInput.snapToLayers) {
// We have to snap to certain layers.
// Lets fetch them
let loadedBbox: BBox = undefined
mapBounds?.addCallbackAndRunD(bbox => {
if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) {
// All is already there
// return;
}
bbox = bbox.pad(2);
loadedBbox = bbox;
const allFeatures: { feature: any }[] = []
preset.preciseInput.snapToLayers.forEach(layerId => {
state.featurePipeline.GetFeaturesWithin(layerId, bbox).forEach(feats => allFeatures.push(...feats.map(f => ({feature: f}))))
})
snapToFeatures.setData(allFeatures)
})
}
}
let confirmButton: BaseUIElement = new SubtleButton(preset.icon(),
new Combine([
Translations.t.general.add.addNew.Subs({category: preset.name}),
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
]).SetClass("flex flex-col")
).SetClass("font-bold break-words")
.onClick(() => {
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data, preciseInput?.snappedOnto?.data?.properties?.id);
});
if (preciseInput !== undefined) {
confirmButton = new Combine([preciseInput, confirmButton])
}
const openLayerControl =
new SubtleButton(
Svg.layers_ui(),
new Combine([
Translations.t.general.add.layerNotEnabled
.Subs({layer: preset.layerToAddTo.layerDef.name})
.SetClass("alert"),
Translations.t.general.add.openLayerControl
])
)
.onClick(() => filterViewIsOpened.setData(true))
const openLayerOrConfirm = new Toggle(
confirmButton,
openLayerControl,
preset.layerToAddTo.isDisplayed
)
const disableFilter = new SubtleButton(
new Combine([
Svg.filter_ui().SetClass("absolute w-full"),
Svg.cross_bottom_right_svg().SetClass("absolute red-svg")
]).SetClass("relative"),
new Combine(
[
Translations.t.general.add.disableFiltersExplanation.Clone(),
Translations.t.general.add.disableFilters.Clone().SetClass("text-xl")
]
).SetClass("flex flex-col")
).onClick(() => {
preset.layerToAddTo.appliedFilters.setData([])
cancel()
})
const disableFiltersOrConfirm = new Toggle(
openLayerOrConfirm,
disableFilter,
preset.layerToAddTo.appliedFilters.map(filters => {
if (filters === undefined || filters.length === 0) {
return true;
}
for (const filter of filters) {
if (filter.selected === 0 && filter.filter.options.length === 1) {
return false;
}
if (filter.selected !== undefined) {
const tags = filter.filter.options[filter.selected].osmTags
if (tags !== undefined && tags["and"]?.length !== 0) {
// This actually doesn't filter anything at all
return false;
}
}
}
return true
})
)
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection);
const cancelButton = new SubtleButton(Svg.close_ui(),
Translations.t.general.cancel
).onClick(cancel)
return new Combine([
state.osmConnection.userDetails.data.dryRun ?
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
disableFiltersOrConfirm,
cancelButton,
preset.description,
tagInfo
]).SetClass("flex flex-col")
}
private static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) {
const csCount = osmConnection.userDetails.data.csCount; const csCount = osmConnection.userDetails.data.csCount;
return new Toggle( return new Toggle(
Translations.t.general.add.presetInfo.Subs({ Translations.t.general.add.presetInfo.Subs({
@ -329,7 +172,7 @@ export default class SimpleAddUI extends Toggle {
private static CreatePresetSelectButton(preset: PresetInfo, osmConnection: OsmConnection) { private static CreatePresetSelectButton(preset: PresetInfo, osmConnection: OsmConnection) {
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, osmConnection ,false); const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, osmConnection, false);
return new SubtleButton( return new SubtleButton(
preset.icon(), preset.icon(),
new Combine([ new Combine([
@ -368,7 +211,7 @@ export default class SimpleAddUI extends Toggle {
for (const preset of presets) { for (const preset of presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? []); const tags = TagUtils.KVtoProperties(preset.tags ?? []);
let icon: () => BaseUIElement = () => layer.layerDef.mapRendering[0]. GenerateLeafletStyle(new UIEventSource<any>(tags), false).html let icon: () => BaseUIElement = () => layer.layerDef.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html
.SetClass("w-12 h-12 block relative"); .SetClass("w-12 h-12 block relative");
const presetInfo: PresetInfo = { const presetInfo: PresetInfo = {
tags: preset.tags, tags: preset.tags,

View file

@ -32,6 +32,7 @@ export class DefaultGuiState {
public readonly copyrightViewIsOpened: UIEventSource<boolean>; public readonly copyrightViewIsOpened: UIEventSource<boolean>;
public readonly welcomeMessageOpenedTab: UIEventSource<number> public readonly welcomeMessageOpenedTab: UIEventSource<number>
public readonly allFullScreenStates: UIEventSource<boolean>[] = [] public readonly allFullScreenStates: UIEventSource<boolean>[] = []
static state: DefaultGuiState;
constructor() { constructor() {

View file

@ -96,6 +96,8 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
let min = undefined; let min = undefined;
let matchedWay = undefined; let matchedWay = undefined;
for (const feature of self._snapTo.data ?? []) { for (const feature of self._snapTo.data ?? []) {
try{
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat]) const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat])
if (min === undefined) { if (min === undefined) {
min = nearestPointOnLine min = nearestPointOnLine
@ -108,6 +110,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
matchedWay = feature.feature; matchedWay = feature.feature;
} }
}catch(e){
console.log("Snapping to a nearest point failed for ", feature.feature,"due to ", e)
}
} }
if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) { if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) {

View file

@ -0,0 +1,184 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import BaseUIElement from "../BaseUIElement";
import LocationInput from "../Input/LocationInput";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {BBox} from "../../Logic/BBox";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import {SubtleButton} from "../Base/SubtleButton";
import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import Svg from "../../Svg";
import Toggle from "../Input/Toggle";
import SimpleAddUI, {PresetInfo} from "../BigComponents/SimpleAddUI";
export default class ConfirmLocationOfPoint extends Combine {
constructor(
state: {
osmConnection: OsmConnection,
featurePipeline: FeaturePipeline
},
filterViewIsOpened: UIEventSource<boolean>,
preset: PresetInfo,
confirmText: BaseUIElement,
loc: { lon: number, lat: number },
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
cancel: () => void,
) {
let preciseInput: LocationInput = undefined
if (preset.preciseInput !== undefined) {
// We uncouple the event source
const zloc = {...loc, zoom: 19}
const locationSrc = new UIEventSource(zloc);
let backgroundLayer = undefined;
if (preset.preciseInput.preferredBackground) {
backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
}
let snapToFeatures: UIEventSource<{ feature: any }[]> = undefined
let mapBounds: UIEventSource<BBox> = undefined
if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) {
snapToFeatures = new UIEventSource<{ feature: any }[]>([])
mapBounds = new UIEventSource<BBox>(undefined)
}
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
preciseInput = new LocationInput({
mapBackground: backgroundLayer,
centerLocation: locationSrc,
snapTo: snapToFeatures,
snappedPointTags: tags,
maxSnapDistance: preset.preciseInput.maxSnapDistance,
bounds: mapBounds
})
preciseInput.installBounds(0.15, true)
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) {
// We have to snap to certain layers.
// Lets fetch them
let loadedBbox: BBox = undefined
mapBounds?.addCallbackAndRunD(bbox => {
if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) {
// All is already there
// return;
}
bbox = bbox.pad(2);
loadedBbox = bbox;
const allFeatures: { feature: any }[] = []
preset.preciseInput.snapToLayers.forEach(layerId => {
console.log("Snapping to", layerId)
state.featurePipeline.GetFeaturesWithin(layerId, bbox)?.forEach(feats => allFeatures.push(...feats.map(f => ({feature: f}))))
})
console.log("Snapping to", allFeatures)
snapToFeatures.setData(allFeatures)
})
}
}
let confirmButton: BaseUIElement = new SubtleButton(preset.icon(),
new Combine([
confirmText,
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
]).SetClass("flex flex-col")
).SetClass("font-bold break-words")
.onClick(() => {
confirm(preset.tags, (preciseInput?.GetValue()?.data ?? loc), preciseInput?.snappedOnto?.data?.properties?.id);
});
if (preciseInput !== undefined) {
confirmButton = new Combine([preciseInput, confirmButton])
}
const openLayerControl =
new SubtleButton(
Svg.layers_ui(),
new Combine([
Translations.t.general.add.layerNotEnabled
.Subs({layer: preset.layerToAddTo.layerDef.name})
.SetClass("alert"),
Translations.t.general.add.openLayerControl
])
)
.onClick(() => filterViewIsOpened.setData(true))
const openLayerOrConfirm = new Toggle(
confirmButton,
openLayerControl,
preset.layerToAddTo.isDisplayed
)
const disableFilter = new SubtleButton(
new Combine([
Svg.filter_ui().SetClass("absolute w-full"),
Svg.cross_bottom_right_svg().SetClass("absolute red-svg")
]).SetClass("relative"),
new Combine(
[
Translations.t.general.add.disableFiltersExplanation.Clone(),
Translations.t.general.add.disableFilters.Clone().SetClass("text-xl")
]
).SetClass("flex flex-col")
).onClick(() => {
preset.layerToAddTo.appliedFilters.setData([])
cancel()
})
const disableFiltersOrConfirm = new Toggle(
openLayerOrConfirm,
disableFilter,
preset.layerToAddTo.appliedFilters.map(filters => {
if (filters === undefined || filters.length === 0) {
return true;
}
for (const filter of filters) {
if (filter.selected === 0 && filter.filter.options.length === 1) {
return false;
}
if (filter.selected !== undefined) {
const tags = filter.filter.options[filter.selected].osmTags
if (tags !== undefined && tags["and"]?.length !== 0) {
// This actually doesn't filter anything at all
return false;
}
}
}
return true
})
)
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection);
const cancelButton = new SubtleButton(Svg.close_ui(),
Translations.t.general.cancel
).onClick(cancel)
super([
state.osmConnection.userDetails.data.dryRun ?
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
disableFiltersOrConfirm,
cancelButton,
preset.description,
tagInfo
])
this.SetClass("flex flex-col")
}
}

View file

@ -20,7 +20,7 @@ import Histogram from "./BigComponents/Histogram";
import Loc from "../Models/Loc"; import Loc from "../Models/Loc";
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import ImportButton from "./BigComponents/ImportButton"; import ImportButton, {ImportButtonSpecialViz} from "./BigComponents/ImportButton";
import {Tag} from "../Logic/Tags/Tag"; import {Tag} from "../Logic/Tags/Tag";
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
@ -38,10 +38,13 @@ import {SubtleButton} from "./Base/SubtleButton";
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"; import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction";
import {And} from "../Logic/Tags/And"; import {And} from "../Logic/Tags/And";
import Toggle from "./Input/Toggle"; import Toggle from "./Input/Toggle";
import {DefaultGuiState} from "./DefaultGUI";
import Img from "./Base/Img";
import FilteredLayer from "../Models/FilteredLayer";
export interface SpecialVisualization { export interface SpecialVisualization {
funcName: string, funcName: string,
constr: ((state: State, tagSource: UIEventSource<any>, argument: string[]) => BaseUIElement), constr: ((state: State, tagSource: UIEventSource<any>, argument: string[], guistate: DefaultGuiState) => BaseUIElement),
docs: string, docs: string,
example?: string, example?: string,
args: { name: string, defaultValue?: string, doc: string }[] args: { name: string, defaultValue?: string, doc: string }[]
@ -49,17 +52,7 @@ export interface SpecialVisualization {
export default class SpecialVisualizations { export default class SpecialVisualizations {
private static tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`. static tagsToApplyHelpText = Utils.Special_visualizations_tagsToApplyHelpText
This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature.
If a value to substitute is undefined, empty string will be used instead.
This supports multiple values, e.g. \`ref=$source:geometry:type/$source:geometry:ref\`
Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with \`[a-zA-Z0-9_:]*\`). Sadly, delimiting with \`{}\` as these already mark the boundaries of the special rendering...
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
`
public static specialVisualizations: SpecialVisualization[] = public static specialVisualizations: SpecialVisualization[] =
[ [
{ {
@ -490,79 +483,7 @@ Note that these values can be prepare with javascript in the theme by using a [c
) )
} }
}, },
{ new ImportButtonSpecialViz(),
funcName: "import_button",
args: [
{
name: "tags",
doc: "The tags to add onto the new object - see specification above"
},
{
name: "text",
doc: "The text to show on the button",
defaultValue: "Import this data into OpenStreetMap"
},
{
name: "icon",
doc: "A nice icon to show in the button",
defaultValue: "./assets/svg/addSmall.svg"
},
{
name: "minzoom",
doc: "How far the contributor must zoom in before being able to import the point",
defaultValue: "18"
}],
docs: `This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes.
#### Importing a dataset into OpenStreetMap: requirements
If you want to import a dataset, make sure that:
1. The dataset to import has a suitable license
2. The community has been informed of the import
3. All other requirements of the [import guidelines](https://wiki.openstreetmap.org/wiki/Import/Guidelines) have been followed
There are also some technicalities in your theme to keep in mind:
1. The new feature will be added and will flow through the program as any other new point as if it came from OSM.
This means that there should be a layer which will match the new tags and which will display it.
2. The original feature from your geojson layer will gain the tag '_imported=yes'.
This should be used to change the appearance or even to hide it (eg by changing the icon size to zero)
3. There should be a way for the theme to detect previously imported points, even after reloading.
A reference number to the original dataset is an excellent way to do this
4. When importing ways, the theme creator is also responsible of avoiding overlapping ways.
#### Disabled in unofficial themes
The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md).
The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console.
In the case that MapComplete is pointed to the testing grounds, the edit will be made on ${OsmConnection.oauth_configs["osm-test"].url}
#### Specifying which tags to copy or add
The first argument of the import button takes a \`;\`-seperated list of tags to add.
${SpecialVisualizations.tagsToApplyHelpText}
`,
constr: (state, tagSource, args) => {
if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) {
return new Combine([new FixedUiElement("The import button is disabled for unofficial themes to prevent accidents.").SetClass("alert"),
new FixedUiElement("To test, add <b>test=true</b> or <b>backend=osm-test</b> to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")])
}
const rewrittenTags = SpecialVisualizations.generateTagsToApply(args[0], tagSource)
const id = tagSource.data.id;
const feature = state.allElements.ContainingFeatures.get(id)
const minzoom = Number(args[3])
const message = args[1]
const image = args[2]
return new ImportButton(
image, message, tagSource, rewrittenTags, feature, minzoom, state
)
}
},
{ {
funcName: "multi_apply", funcName: "multi_apply",
docs: "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags", docs: "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags",
@ -687,7 +608,7 @@ ${SpecialVisualizations.tagsToApplyHelpText}
} }
] ]
private static generateTagsToApply(spec: string, tagSource: UIEventSource<any>): UIEventSource<Tag[]> { static generateTagsToApply(spec: string, tagSource: UIEventSource<any>): UIEventSource<Tag[]> {
const tgsSpec = spec.split(";").map(spec => { const tgsSpec = spec.split(";").map(spec => {
const kv = spec.split("=").map(s => s.trim()); const kv = spec.split("=").map(s => s.trim());

View file

@ -8,6 +8,7 @@ import {Utils} from "../Utils";
import {VariableUiElement} from "./Base/VariableUIElement"; import {VariableUiElement} from "./Base/VariableUIElement";
import Combine from "./Base/Combine"; import Combine from "./Base/Combine";
import BaseUIElement from "./BaseUIElement"; import BaseUIElement from "./BaseUIElement";
import {DefaultGuiState} from "./DefaultGUI";
export class SubstitutedTranslation extends VariableUiElement { export class SubstitutedTranslation extends VariableUiElement {
@ -49,7 +50,7 @@ export class SubstitutedTranslation extends VariableUiElement {
} }
const viz = proto.special; const viz = proto.special;
try { try {
return viz.func.constr(State.state, tagsSource, proto.special.args).SetStyle(proto.special.style); return viz.func.constr(State.state, tagsSource, proto.special.args, DefaultGuiState.state).SetStyle(proto.special.style);
} catch (e) { } catch (e) {
console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, 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}(${viz.args.join(", ")}) ${e}`).SetStyle("alert")

View file

@ -15,6 +15,18 @@ export class Utils {
private static injectedDownloads = {} private static injectedDownloads = {}
private static _download_cache = new Map<string, { promise: Promise<any>, timestamp: number }>() private static _download_cache = new Map<string, { promise: Promise<any>, timestamp: number }>()
public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`.
This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature.
If a value to substitute is undefined, empty string will be used instead.
This supports multiple values, e.g. \`ref=$source:geometry:type/$source:geometry:ref\`
Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with \`[a-zA-Z0-9_:]*\`). Sadly, delimiting with \`{}\` as these already mark the boundaries of the special rendering...
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
`
static EncodeXmlValue(str) { static EncodeXmlValue(str) {
if (typeof str !== "string") { if (typeof str !== "string") {
str = "" + str str = "" + str

View file

@ -0,0 +1,12 @@
{
"id": "type_node",
"description": "This is a special meta_layer which exports _every_ point in OSM. This only works if zoomed below the point that the full tile is loaded (and not loaded via Overpass). Note that this point will also contain a property `parent_ways` which contains all the ways this node is part of as a list",
"minzoom": 18,
"source": {
"osmTags": "id~node/.*"
},
"mapRendering": [],
"name": "All OSM Nodes",
"title": "OSM node {id}",
"tagRendering": [ ]
}

View file

@ -28,7 +28,20 @@
"overrideAll": { "overrideAll": {
"minzoom": 18 "minzoom": 18
}, },
"trackAllNodes": true,
"layers": [ "layers": [
{
"builtin": "type_node",
"isShown": {
"render": "no"
},
"override": {
"calculatedTags": [
"_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false",
"_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false"
]
}
},
{ {
"id": "OSM-buildings", "id": "OSM-buildings",
"name": "All OSM-buildings", "name": "All OSM-buildings",
@ -413,7 +426,7 @@
"all_tags", "all_tags",
{ {
"id": "import-button", "id": "import-button",
"render": "{import_button(addr:street=$STRAATNM; addr:housenumber=$HUISNR)}" "render": "{import_button(OSM-buildings, addr:street=$STRAATNM; addr:housenumber=$HUISNR,Import this address,,,OSM-buildings,5)}"
} }
] ]
}, },
@ -657,7 +670,7 @@
}, },
{ {
"id": "Import-button", "id": "Import-button",
"render": "{import_button(building=$building; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Upload this building to OpenStreetMap)}", "render": "{import_button(OSM-buildings,building=$building; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Upload this building to OpenStreetMap)}",
"mappings": [ "mappings": [
{ {
"if": "_overlaps_with!=null", "if": "_overlaps_with!=null",

View file

@ -93,7 +93,7 @@
}, },
{ {
"id": "uk_addresses_import_button", "id": "uk_addresses_import_button",
"render": "{import_button(ref:inspireid=$inspireid, Add this address, ./assets/themes/uk_addresses/housenumber_add.svg)}" "render": "{import_button(addresses, ref:inspireid=$inspireid, Add this address, ./assets/themes/uk_addresses/housenumber_add.svg)}"
} }
], ],
"calculatedTags": [ "calculatedTags": [

View file

@ -64,9 +64,11 @@ class Init {
const guiState = new DefaultGuiState() const guiState = new DefaultGuiState()
State.state = new State(layoutToUse); State.state = new State(layoutToUse);
DefaultGuiState.state = guiState;
// This 'leaks' the global state via the window object, useful for debugging // This 'leaks' the global state via the window object, useful for debugging
// @ts-ignore // @ts-ignore
window.mapcomplete_state = State.state; window.mapcomplete_state = State.state;
new DefaultGUI(State.state, guiState) new DefaultGUI(State.state, guiState)
if (encoded !== undefined && encoded.length > 10) { if (encoded !== undefined && encoded.length > 10) {

View file

@ -12,8 +12,8 @@ import {Utils} from "../Utils";
// It spits out an overview of those to be used to load them // It spits out an overview of those to be used to load them
interface LayersAndThemes { interface LayersAndThemes {
themes: any[], themes: LayoutConfigJson[],
layers: { parsed: any, path: string }[] layers: { parsed: LayerConfigJson, path: string }[]
} }
@ -35,7 +35,6 @@ class LayerOverviewUtils {
} }
} }
writeFiles(lt: LayersAndThemes) { writeFiles(lt: LayersAndThemes) {
writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({ writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({
"layers": lt.layers.map(l => l.parsed), "layers": lt.layers.map(l => l.parsed),
@ -43,7 +42,6 @@ class LayerOverviewUtils {
})) }))
} }
validateLayer(layerJson: LayerConfigJson, path: string, knownPaths: Set<string>, context?: string): string[] { validateLayer(layerJson: LayerConfigJson, path: string, knownPaths: Set<string>, context?: string): string[] {
let errorCount = []; let errorCount = [];
if (layerJson["overpassTags"] !== undefined) { if (layerJson["overpassTags"] !== undefined) {
@ -109,6 +107,8 @@ class LayerOverviewUtils {
} }
let themeErrorCount = [] let themeErrorCount = []
// used only for the reports
let themeConfigs: LayoutConfig[] = []
for (const themeInfo of themeFiles) { for (const themeInfo of themeFiles) {
const themeFile = themeInfo.parsed const themeFile = themeInfo.parsed
const themePath = themeInfo.path const themePath = themeInfo.path
@ -146,15 +146,15 @@ class LayerOverviewUtils {
} }
const referencedLayers = Utils.NoNull([].concat(...themeFile.layers.map(layer => { const referencedLayers = Utils.NoNull([].concat(...themeFile.layers.map(layer => {
if(typeof layer === "string"){ if (typeof layer === "string") {
return layer return layer
} }
if(layer["builtin"] !== undefined){ if (layer["builtin"] !== undefined) {
return layer["builtin"] return layer["builtin"]
} }
return undefined return undefined
}).map(layerName => { }).map(layerName => {
if(typeof layerName === "string"){ if (typeof layerName === "string") {
return [layerName] return [layerName]
} }
return layerName return layerName
@ -176,9 +176,9 @@ class LayerOverviewUtils {
} }
const neededLanguages = themeFile["mustHaveLanguage"] const neededLanguages = themeFile["mustHaveLanguage"]
if (neededLanguages !== undefined) { if (neededLanguages !== undefined) {
console.log("Checking language requerements for ", theme.id, "as it must have", neededLanguages.join(", ")) console.log("Checking language requirements for ", theme.id, "as it must have", neededLanguages.join(", "))
const allTranslations = [].concat(Translation.ExtractAllTranslationsFrom(theme, theme.id), const allTranslations = [].concat(Translation.ExtractAllTranslationsFrom(theme, theme.id),
...referencedLayers.map(layerId => Translation.ExtractAllTranslationsFrom(knownLayerIds.get(layerId), theme.id+"->"+layerId))) ...referencedLayers.map(layerId => Translation.ExtractAllTranslationsFrom(knownLayerIds.get(layerId), theme.id + "->" + layerId)))
for (const neededLanguage of neededLanguages) { for (const neededLanguage of neededLanguages) {
allTranslations allTranslations
.filter(t => t.tr.translations[neededLanguage] === undefined && t.tr.translations["*"] === undefined) .filter(t => t.tr.translations[neededLanguage] === undefined && t.tr.translations["*"] === undefined)
@ -189,7 +189,7 @@ class LayerOverviewUtils {
} }
themeConfigs.push(theme)
} catch (e) { } catch (e) {
themeErrorCount.push("Could not parse theme " + themeFile["id"] + "due to", e) themeErrorCount.push("Could not parse theme " + themeFile["id"] + "due to", e)
} }
@ -210,12 +210,11 @@ class LayerOverviewUtils {
console.log(msg) console.log(msg)
console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
if (process.argv.indexOf("--report") >= 0) { if (args.indexOf("--report") >= 0) {
console.log("Writing report!") console.log("Writing report!")
writeFileSync("layer_report.txt", errors) writeFileSync("layer_report.txt", errors)
} }
if (args.indexOf("--no-fail") < 0) {
if (process.argv.indexOf("--no-fail") < 0) {
throw msg; throw msg;
} }
} }

146
test.ts
View file

@ -1,13 +1,139 @@
import * as wd from "wikidata-sdk"
import * as wds from "wikibase-sdk"
import {Utils} from "./Utils"; import {Utils} from "./Utils";
import FullNodeDatabaseSource from "./Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
const url = wd.getEntities(["Q42"])
console.log(url) const data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
Utils.downloadJson(url).then(async (entities) => { "<osm version=\"0.6\" generator=\"CGImap 0.8.5 (2610109 spike-08.openstreetmap.org)\" copyright=\"OpenStreetMap and contributors\" attribution=\"http://www.openstreetmap.org/copyright\" license=\"http://opendatacommons.org/licenses/odbl/1-0/\">\n" +
//const parsed = wd.parse.wb.entities(entities)["Q42"] " <bounds minlat=\"51.2154864\" minlon=\"3.2176208\" maxlat=\"51.2163466\" maxlon=\"3.2189941\"/>\n" +
console.log(entities) " <node id=\"315739208\" visible=\"true\" version=\"6\" changeset=\"11063832\" timestamp=\"2012-03-22T15:12:47Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2149470\" lon=\"3.2183010\"/>\n" +
console.log(wds.simplify.entity(entities.entities["Q42"], { " <node id=\"315739215\" visible=\"true\" version=\"8\" changeset=\"7350988\" timestamp=\"2011-02-21T09:50:46Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2160281\" lon=\"3.2174966\"/>\n" +
timeConverter: 'simple-day' " <node id=\"315739216\" visible=\"true\" version=\"7\" changeset=\"11037951\" timestamp=\"2012-03-20T06:38:28Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2152977\" lon=\"3.2195995\"/>\n" +
})) " <node id=\"315739242\" visible=\"true\" version=\"2\" changeset=\"11037951\" timestamp=\"2012-03-20T06:38:29Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2164491\" lon=\"3.2187218\"/>\n" +
" <node id=\"315739243\" visible=\"true\" version=\"9\" changeset=\"11037951\" timestamp=\"2012-03-20T06:38:29Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2166162\" lon=\"3.2182807\"/>\n" +
" <node id=\"1682824800\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154405\" lon=\"3.2193489\"/>\n" +
" <node id=\"1682824805\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154354\" lon=\"3.2193255\"/>\n" +
" <node id=\"1682824813\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155117\" lon=\"3.2192071\"/>\n" +
" <node id=\"1682824815\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154693\" lon=\"3.2193015\"/>\n" +
" <node id=\"1682824817\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153821\" lon=\"3.2193628\"/>\n" +
" <node id=\"1682832761\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159858\" lon=\"3.2190646\"/>\n" +
" <node id=\"1682832762\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157564\" lon=\"3.2191917\"/>\n" +
" <node id=\"1682832764\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157971\" lon=\"3.2187018\"/>\n" +
" <node id=\"1682832775\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153841\" lon=\"3.2186735\"/>\n" +
" <node id=\"1682832777\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156194\" lon=\"3.2188368\"/>\n" +
" <node id=\"1682832780\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153013\" lon=\"3.2188337\"/>\n" +
" <node id=\"1682832782\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157716\" lon=\"3.2190346\"/>\n" +
" <node id=\"1682832784\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153225\" lon=\"3.2189135\"/>\n" +
" <node id=\"1682832786\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154508\" lon=\"3.2187894\"/>\n" +
" <node id=\"1682832789\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159305\" lon=\"3.2188530\"/>\n" +
" <node id=\"1682832791\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157323\" lon=\"3.2180624\"/>\n" +
" <node id=\"1682832793\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159622\" lon=\"3.2188025\"/>\n" +
" <node id=\"1682832795\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159076\" lon=\"3.2189725\"/>\n" +
" <node id=\"1682832804\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157453\" lon=\"3.2186489\"/>\n" +
" <node id=\"1682832808\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157935\" lon=\"3.2186116\"/>\n" +
" <node id=\"1682832813\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2152199\" lon=\"3.2186903\"/>\n" +
" <node id=\"1682832815\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160796\" lon=\"3.2189893\"/>\n" +
" <node id=\"1682832817\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154484\" lon=\"3.2188760\"/>\n" +
" <node id=\"1682832818\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2152395\" lon=\"3.2187686\"/>\n" +
" <node id=\"1682832819\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153621\" lon=\"3.2185962\"/>\n" +
" <node id=\"1682832820\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155109\" lon=\"3.2184903\"/>\n" +
" <node id=\"1682832821\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155922\" lon=\"3.2187693\"/>\n" +
" <node id=\"1682832825\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154945\" lon=\"3.2191530\"/>\n" +
" <node id=\"1682832826\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157122\" lon=\"3.2192275\"/>\n" +
" <node id=\"1682832827\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154505\" lon=\"3.2187683\"/>\n" +
" <node id=\"1682832828\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160312\" lon=\"3.2188379\"/>\n" +
" <node id=\"1682832829\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160071\" lon=\"3.2188563\"/>\n" +
" <node id=\"1682832830\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158432\" lon=\"3.2189346\"/>\n" +
" <node id=\"1682832831\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157027\" lon=\"3.2184476\"/>\n" +
" <node id=\"1682832832\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159424\" lon=\"3.2189018\"/>\n" +
" <node id=\"1682832833\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156684\" lon=\"3.2191183\"/>\n" +
" <node id=\"1682832834\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158295\" lon=\"3.2185829\"/>\n" +
" <node id=\"1682832835\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156820\" lon=\"3.2180867\"/>\n" +
" <node id=\"1682832836\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155219\" lon=\"3.2191313\"/>\n" +
" <node id=\"1682832837\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158056\" lon=\"3.2190810\"/>\n" +
" <node id=\"1682832838\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157585\" lon=\"3.2190453\"/>\n" +
" <node id=\"1682832839\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153634\" lon=\"3.2186009\"/>\n" +
" <node id=\"1682832840\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157257\" lon=\"3.2185567\"/>\n" +
" <node id=\"1682832841\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159207\" lon=\"3.2190210\"/>\n" +
" <node id=\"1682832842\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160295\" lon=\"3.2190298\"/>\n" +
" <node id=\"1682832843\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156835\" lon=\"3.2183577\"/>\n" +
" <node id=\"1682832846\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158946\" lon=\"3.2189083\"/>\n" +
" <node id=\"1682832848\" visible=\"true\" version=\"1\" changeset=\"11037951\" timestamp=\"2012-03-20T06:38:23Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2162257\" lon=\"3.2189152\"/>\n" +
" <node id=\"1682832850\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156455\" lon=\"3.2189033\"/>\n" +
" <node id=\"1682832851\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157814\" lon=\"3.2191010\"/>\n" +
" <node id=\"1682832852\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158297\" lon=\"3.2191463\"/>\n" +
" <node id=\"1682832853\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153884\" lon=\"3.2186848\"/>\n" +
" <node id=\"1682832854\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155190\" lon=\"3.2191206\"/>\n" +
" <node id=\"1682832856\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156298\" lon=\"3.2190100\"/>\n" +
" <node id=\"1682832858\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156874\" lon=\"3.2190086\"/>\n" +
" <node id=\"1682832859\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159818\" lon=\"3.2188688\"/>\n" +
" <node id=\"1682832860\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159413\" lon=\"3.2190997\"/>\n" +
" <node id=\"1682832861\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159831\" lon=\"3.2187874\"/>\n" +
" <node id=\"1682832862\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157432\" lon=\"3.2189602\"/>\n" +
" <node id=\"1682837112\" visible=\"true\" version=\"3\" changeset=\"113131738\" timestamp=\"2021-10-29T16:20:51Z\" user=\"Pieter Vander Vennet\" uid=\"3818858\" lat=\"51.2160077\" lon=\"3.2187651\"/>\n" +
" <node id=\"1682837113\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161667\" lon=\"3.2187271\"/>\n" +
" <node id=\"1682837114\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162728\" lon=\"3.2185965\"/>\n" +
" <node id=\"1682837115\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161349\" lon=\"3.2185984\"/>\n" +
" <node id=\"1682837120\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163153\" lon=\"3.2187451\"/>\n" +
" <node id=\"1682837122\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2164091\" lon=\"3.2186697\"/>\n" +
" <node id=\"1682837124\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161535\" lon=\"3.2183166\"/>\n" +
" <node id=\"1682837126\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162789\" lon=\"3.2187745\"/>\n" +
" <node id=\"1682837128\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161929\" lon=\"3.2185504\"/>\n" +
" <node id=\"1682837130\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162296\" lon=\"3.2186760\"/>\n" +
" <node id=\"1682837132\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2165327\" lon=\"3.2183323\"/>\n" +
" <node id=\"1682837133\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163805\" lon=\"3.2186936\"/>\n" +
" <node id=\"1682837134\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162456\" lon=\"3.2186644\"/>\n" +
" <node id=\"1682837135\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162404\" lon=\"3.2188053\"/>\n" +
" <node id=\"1682837136\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162428\" lon=\"3.2184412\"/>\n" +
" <node id=\"1682837138\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162938\" lon=\"3.2185770\"/>\n" +
" <node id=\"1682837142\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161321\" lon=\"3.2183620\"/>\n" +
" <node id=\"1682837143\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161991\" lon=\"3.2188378\"/>\n" +
" <node id=\"1682837145\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160656\" lon=\"3.2189444\"/>\n" +
" <node id=\"1682837146\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163975\" lon=\"3.2181513\"/>\n" +
" <node id=\"1682837147\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161668\" lon=\"3.2185723\"/>\n" +
" <node id=\"1682837148\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161842\" lon=\"3.2187118\"/>\n" +
" <node id=\"1682837149\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160893\" lon=\"3.2186362\"/>\n" +
" <node id=\"1682837150\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161619\" lon=\"3.2188670\"/>\n" +
" <node id=\"1682874140\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163477\" lon=\"3.2191243\"/>\n" +
" <node id=\"1682874141\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160579\" lon=\"3.2190935\"/>\n" +
" <node id=\"1682874142\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161979\" lon=\"3.2193991\"/>\n" +
" <node id=\"1682874143\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162760\" lon=\"3.2189217\"/>\n" +
" <node id=\"1682874144\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163468\" lon=\"3.2188607\"/>\n" +
" <node id=\"1682874150\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163835\" lon=\"3.2190937\"/>\n" +
" <node id=\"1682874151\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2164206\" lon=\"3.2190633\"/>\n" +
" <node id=\"1682874161\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:33Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161339\" lon=\"3.2194350\"/>\n" +
" <node id=\"1682874164\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:33Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163293\" lon=\"3.2191391\"/>\n" +
" <node id=\"1682874165\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:33Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160741\" lon=\"3.2191659\"/>\n" +
" <node id=\"1682874166\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:33Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163108\" lon=\"3.2188917\"/>\n" +
" <node id=\"1682875368\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159045\" lon=\"3.2178572\"/>\n" +
" <node id=\"1682875370\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158354\" lon=\"3.2179572\"/>\n" +
" <node id=\"1682875372\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158939\" lon=\"3.2176815\"/>\n" +
" <node id=\"1682875373\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158483\" lon=\"3.2179326\"/>\n" +
" <node id=\"1682875374\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157153\" lon=\"3.2177946\"/>\n" +
" <node id=\"1682875381\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158748\" lon=\"3.2178159\"/>\n" +
" <node id=\"1682875385\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158683\" lon=\"3.2179618\"/>\n" +
" <node id=\"1682875387\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158782\" lon=\"3.2179443\"/>\n" +
" <node id=\"1682875389\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158141\" lon=\"3.2177312\"/>\n" +
" <node id=\"1682875395\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157649\" lon=\"3.2177632\"/>\n" +
" <node id=\"1682876085\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155829\" lon=\"3.2178849\"/>\n" +
" <node id=\"1682876086\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156324\" lon=\"3.2178513\"/>\n" +
" <node id=\"1682876088\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156714\" lon=\"3.2180361\"/>\n" +
" <node id=\"1682876091\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156926\" lon=\"3.2178103\"/>\n" +
" <node id=\"1682876092\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156297\" lon=\"3.2178592\"/>\n" +
" <node id=\"1682876099\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157351\" lon=\"3.2180027\"/>\n" +
" <node id=\"1682876100\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156190\" lon=\"3.2180623\"/>\n" +
" <node id=\"1682876102\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157232\" lon=\"3.2179435\"/>\n" +
" <node id=\"1682877254\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154254\" lon=\"3.2179953\"/>\n" +
" <node id=\"1682877255\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155716\" lon=\"3.2180819\"/>\n" +
" <node id=\"1682877256\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155250\" lon=\"3.2179243\"/>\n" +
" <node id=\"1682877257\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154821\" lon=\"3.2181863\"/>\n" +
" <node id=\"1682879309\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154292\" lon=\"3.2182116\"/>\n" +
" </osm>"
const url = "https://www.openstreetmap.org/api/0.6/map?bbox=3.217620849609375,51.21548639922819,3.218994140625,51.21634661126673"
Utils.downloadJson(url).then(data =>{
const osmSource = {
rawDataHandlers : []
}
new FullNodeDatabaseSource(osmSource)
osmSource.rawDataHandlers[0]( data, 0)
}) })