forked from MapComplete/MapComplete
More work on refactoring the changes handling
This commit is contained in:
parent
42391b4ff1
commit
b55f9a25c6
19 changed files with 181 additions and 105 deletions
|
@ -7,7 +7,7 @@ export default class ChangeToElementsActor {
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
const id = change.type + "/" + change.id;
|
const id = change.type + "/" + change.id;
|
||||||
if (!allElements.has(id)) {
|
if (!allElements.has(id)) {
|
||||||
continue; // Will be picked up later on
|
continue; // Ignored as the geometryFixer will introduce this
|
||||||
}
|
}
|
||||||
const src = allElements.getEventSourceById(id)
|
const src = allElements.getEventSourceById(id)
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default class SelectedFeatureHandler {
|
||||||
private readonly _hash: UIEventSource<string>;
|
private readonly _hash: UIEventSource<string>;
|
||||||
private readonly _selectedFeature: UIEventSource<any>;
|
private readonly _selectedFeature: UIEventSource<any>;
|
||||||
|
|
||||||
private static readonly _no_trigger_on = ["welcome","copyright","layers"]
|
private static readonly _no_trigger_on = ["welcome","copyright","layers","new"]
|
||||||
private readonly _osmApiSource: OsmApiFeatureSource;
|
private readonly _osmApiSource: OsmApiFeatureSource;
|
||||||
|
|
||||||
constructor(hash: UIEventSource<string>,
|
constructor(hash: UIEventSource<string>,
|
||||||
|
@ -60,7 +60,9 @@ export default class SelectedFeatureHandler {
|
||||||
if(hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){
|
if(hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){
|
||||||
return; // No valid feature selected
|
return; // No valid feature selected
|
||||||
}
|
}
|
||||||
// We should have a valid osm-ID and zoom to it
|
// We should have a valid osm-ID and zoom to it... But we wrap it in try-catch to be sure
|
||||||
|
try{
|
||||||
|
|
||||||
OsmObject.DownloadObject(hash).addCallbackAndRunD(element => {
|
OsmObject.DownloadObject(hash).addCallbackAndRunD(element => {
|
||||||
const centerpoint = element.centerpoint();
|
const centerpoint = element.centerpoint();
|
||||||
console.log("Zooming to location for select point: ", centerpoint)
|
console.log("Zooming to location for select point: ", centerpoint)
|
||||||
|
@ -68,6 +70,9 @@ export default class SelectedFeatureHandler {
|
||||||
location.data.lon = centerpoint[1]
|
location.data.lon = centerpoint[1]
|
||||||
location.ping();
|
location.ping();
|
||||||
})
|
})
|
||||||
|
}catch(e){
|
||||||
|
console.error("Could not download OSM-object with id", hash, " - probably a weird hash")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private downloadFeature(hash: string){
|
private downloadFeature(hash: string){
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {ChangeDescription} from "../Osm/Actions/ChangeDescription";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
|
import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies changes from 'Changes' onto a featureSource
|
* Applies changes from 'Changes' onto a featureSource
|
||||||
*/
|
*/
|
||||||
|
@ -16,25 +17,41 @@ export default class ChangeApplicator implements FeatureSource {
|
||||||
|
|
||||||
this.name = "ChangesApplied(" + source.name + ")"
|
this.name = "ChangesApplied(" + source.name + ")"
|
||||||
this.features = source.features
|
this.features = source.features
|
||||||
|
const seenChanges = new Set<ChangeDescription>();
|
||||||
|
const self = this;
|
||||||
|
let runningUpdate = false;
|
||||||
source.features.addCallbackAndRunD(features => {
|
source.features.addCallbackAndRunD(features => {
|
||||||
|
if(runningUpdate){
|
||||||
|
return; // No need to ping again
|
||||||
|
}
|
||||||
ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data)
|
ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data)
|
||||||
|
seenChanges.clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
changes.pendingChanges.addCallbackAndRunD(changes => {
|
changes.pendingChanges.addCallbackAndRunD(changes => {
|
||||||
ChangeApplicator.ApplyChanges(source.features.data, changes)
|
runningUpdate = true;
|
||||||
|
changes = changes.filter(ch => !seenChanges.has(ch))
|
||||||
|
changes.forEach(c => seenChanges.add(c))
|
||||||
|
console.log("Called back", changes)
|
||||||
|
ChangeApplicator.ApplyChanges(self.features.data, changes)
|
||||||
source.features.ping()
|
source.features.ping()
|
||||||
|
runningUpdate = false;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static ApplyChanges(features: { feature: any, freshness: Date }[], cs: ChangeDescription[]) {
|
/**
|
||||||
if (cs.length === 0 || features === undefined) {
|
* Returns true if the geometry is changed and the source should be pinged
|
||||||
return features;
|
*/
|
||||||
|
private static ApplyChanges(features: {feature: any, freshness: Date}[], cs: ChangeDescription[]): boolean {
|
||||||
|
if (cs.length === 0 || features === undefined ) {
|
||||||
|
return ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Applying changes ", this.name, cs)
|
||||||
|
let geometryChanged = false;
|
||||||
const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>()
|
const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>()
|
||||||
for (const c of cs) {
|
for (const c of cs) {
|
||||||
const id = c.type + "/" + c.id
|
const id = c.type + "/" + c.id
|
||||||
|
@ -52,6 +69,8 @@ export default class ChangeApplicator implements FeatureSource {
|
||||||
feature: feature,
|
feature: feature,
|
||||||
freshness: now
|
freshness: now
|
||||||
})
|
})
|
||||||
|
console.log("Added a new feature: ", feature)
|
||||||
|
geometryChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, create the new features - they have a negative ID
|
// First, create the new features - they have a negative ID
|
||||||
|
@ -61,7 +80,11 @@ export default class ChangeApplicator implements FeatureSource {
|
||||||
if (change.id >= 0) {
|
if (change.id >= 0) {
|
||||||
return; // Nothing to do here, already created
|
return; // Nothing to do here, already created
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(change.changes === undefined){
|
||||||
|
// An update to the object - not the actual created
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
@ -93,8 +116,8 @@ export default class ChangeApplicator implements FeatureSource {
|
||||||
|
|
||||||
|
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
const id = feature.feature.properties.id;
|
|
||||||
const f = feature.feature;
|
const f = feature.feature;
|
||||||
|
const id = f.properties.id;
|
||||||
if (!changesPerId.has(id)) {
|
if (!changesPerId.has(id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -118,11 +141,12 @@ export default class ChangeApplicator implements FeatureSource {
|
||||||
|
|
||||||
// Apply other changes to the object
|
// Apply other changes to the object
|
||||||
if (change.changes !== undefined) {
|
if (change.changes !== undefined) {
|
||||||
|
geometryChanged = true;
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
case "node":
|
case "node":
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const coor: { lat, lon } = change.changes;
|
const coor: { lat, lon } = change.changes;
|
||||||
f.geometry.coordinates = [[coor.lon, coor.lat]]
|
f.geometry.coordinates = [coor.lon, coor.lat]
|
||||||
break;
|
break;
|
||||||
case "way":
|
case "way":
|
||||||
f.geometry.coordinates = change.changes["locations"]
|
f.geometry.coordinates = change.changes["locations"]
|
||||||
|
@ -134,5 +158,6 @@ export default class ChangeApplicator implements FeatureSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return geometryChanged
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,15 +15,19 @@ export default class OsmApiFeatureSource implements FeatureSource {
|
||||||
|
|
||||||
|
|
||||||
public load(id: string) {
|
public load(id: string) {
|
||||||
if(id.indexOf("-") >= 0){
|
if (id.indexOf("-") >= 0) {
|
||||||
// Newly added point - not yet in OSM
|
// Newly added point - not yet in OSM
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.debug("Downloading", id, "from the OSM-API")
|
console.debug("Downloading", id, "from the OSM-API")
|
||||||
OsmObject.DownloadObject(id).addCallbackAndRunD(element => {
|
OsmObject.DownloadObject(id).addCallbackAndRunD(element => {
|
||||||
const geojson = element.asGeoJson();
|
try {
|
||||||
geojson.id = geojson.properties.id;
|
const geojson = element.asGeoJson();
|
||||||
this.features.setData([{feature: geojson, freshness: element.timestamp}])
|
geojson.id = geojson.properties.id;
|
||||||
|
this.features.setData([{feature: geojson, freshness: element.timestamp}])
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +62,7 @@ export default class OsmApiFeatureSource implements FeatureSource {
|
||||||
const bounds = Utils.tile_bounds(z, x, y);
|
const bounds = Utils.tile_bounds(z, x, y);
|
||||||
console.log("Loading OSM data tile", z, x, y, " with bounds", bounds)
|
console.log("Loading OSM data tile", z, x, y, " with bounds", bounds)
|
||||||
OsmObject.LoadArea(bounds, objects => {
|
OsmObject.LoadArea(bounds, objects => {
|
||||||
const keptGeoJson: {feature:any, freshness: Date}[] = []
|
const keptGeoJson: { feature: any, freshness: Date }[] = []
|
||||||
// Which layer does the object match?
|
// Which layer does the object match?
|
||||||
for (const object of objects) {
|
for (const object of objects) {
|
||||||
|
|
||||||
|
@ -69,7 +73,7 @@ export default class OsmApiFeatureSource implements FeatureSource {
|
||||||
if (doesMatch) {
|
if (doesMatch) {
|
||||||
const geoJson = object.asGeoJson();
|
const geoJson = object.asGeoJson();
|
||||||
geoJson._matching_layer_id = layer.id
|
geoJson._matching_layer_id = layer.id
|
||||||
keptGeoJson.push({feature: geoJson, freshness: object.timestamp})
|
keptGeoJson.push({feature: geoJson, freshness: object.timestamp})
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default class ChangeTagAction extends OsmChangeAction {
|
||||||
return {k: key.trim(), v: value.trim()};
|
return {k: key.trim(), v: value.trim()};
|
||||||
}
|
}
|
||||||
|
|
||||||
Perform(changes: Changes): ChangeDescription [] {
|
CreateChangeDescriptions(changes: Changes): ChangeDescription [] {
|
||||||
const changedTags: { k: string, v: string }[] = this._tagsFilter.asChange(this._currentTags).map(ChangeTagAction.checkChange)
|
const changedTags: { k: string, v: string }[] = this._tagsFilter.asChange(this._currentTags).map(ChangeTagAction.checkChange)
|
||||||
const typeId = this._elementId.split("/")
|
const typeId = this._elementId.split("/")
|
||||||
const type = typeId[0]
|
const type = typeId[0]
|
||||||
|
|
|
@ -4,23 +4,27 @@ import {Changes} from "../Changes";
|
||||||
import {ChangeDescription} from "./ChangeDescription";
|
import {ChangeDescription} from "./ChangeDescription";
|
||||||
import {And} from "../../Tags/And";
|
import {And} from "../../Tags/And";
|
||||||
|
|
||||||
export default class CreateNewNodeAction implements OsmChangeAction {
|
export default class CreateNewNodeAction extends OsmChangeAction {
|
||||||
|
|
||||||
private readonly _basicTags: Tag[];
|
private readonly _basicTags: Tag[];
|
||||||
private readonly _lat: number;
|
private readonly _lat: number;
|
||||||
private readonly _lon: number;
|
private readonly _lon: number;
|
||||||
|
|
||||||
|
public newElementId : string = undefined
|
||||||
|
|
||||||
constructor(basicTags: Tag[], lat: number, lon: number) {
|
constructor(basicTags: Tag[], lat: number, lon: number) {
|
||||||
|
super()
|
||||||
this._basicTags = basicTags;
|
this._basicTags = basicTags;
|
||||||
this._lat = lat;
|
this._lat = lat;
|
||||||
this._lon = lon;
|
this._lon = lon;
|
||||||
}
|
}
|
||||||
|
|
||||||
Perform(changes: Changes): ChangeDescription[] {
|
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||||
const id = changes.getNewID()
|
const id = changes.getNewID()
|
||||||
const properties = {
|
const properties = {
|
||||||
id: "node/" + id
|
id: "node/" + id
|
||||||
}
|
}
|
||||||
|
this.newElementId = "node/"+id
|
||||||
for (const kv of this._basicTags) {
|
for (const kv of this._basicTags) {
|
||||||
if (typeof kv.value !== "string") {
|
if (typeof kv.value !== "string") {
|
||||||
throw "Invalid value: don't use a regex in a preset"
|
throw "Invalid value: don't use a regex in a preset"
|
||||||
|
|
|
@ -7,10 +7,17 @@ import {ChangeDescription} from "./ChangeDescription";
|
||||||
|
|
||||||
export default abstract class OsmChangeAction {
|
export default abstract class OsmChangeAction {
|
||||||
|
|
||||||
|
private isUsed = false
|
||||||
|
|
||||||
|
public Perform(changes: Changes) {
|
||||||
|
if (this.isUsed) {
|
||||||
|
throw "This ChangeAction is already used: " + this.constructor.name
|
||||||
|
}
|
||||||
|
this.isUsed = true;
|
||||||
|
return this.CreateChangeDescriptions(changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract CreateChangeDescriptions(changes: Changes): ChangeDescription[]
|
||||||
|
|
||||||
|
|
||||||
public abstract Perform(changes: Changes): ChangeDescription[]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -12,7 +12,7 @@ export default class RelationSplitlHandler extends OsmChangeAction{
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
Perform(changes: Changes): ChangeDescription[] {
|
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default class SplitAction extends OsmChangeAction {
|
||||||
return wayParts.filter(wp => wp.length > 0)
|
return wayParts.filter(wp => wp.length > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
Perform(changes: Changes): ChangeDescription[] {
|
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||||
const splitPoints = this._splitPoints
|
const splitPoints = this._splitPoints
|
||||||
// We mark the new split points with a new id
|
// We mark the new split points with a new id
|
||||||
console.log(splitPoints)
|
console.log(splitPoints)
|
||||||
|
@ -72,7 +72,6 @@ export default class SplitAction extends OsmChangeAction {
|
||||||
|
|
||||||
// Next up is creating actual parts from this
|
// Next up is creating actual parts from this
|
||||||
const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo);
|
const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo);
|
||||||
console.log("WayParts", wayParts, "by", splitInfo)
|
|
||||||
// Allright! At this point, we have our new ways!
|
// Allright! At this point, we have our new ways!
|
||||||
// Which one is the longest of them (and can keep the id)?
|
// Which one is the longest of them (and can keep the id)?
|
||||||
|
|
||||||
|
@ -144,7 +143,7 @@ console.log("WayParts", wayParts, "by", splitInfo)
|
||||||
|
|
||||||
// At last, we still have to check that we aren't part of a relation...
|
// At last, we still have to check that we aren't part of a relation...
|
||||||
// At least, the order of the ways is identical, so we can keep the same roles
|
// At least, the order of the ways is identical, so we can keep the same roles
|
||||||
changeDescription.push(...new RelationSplitlHandler(partOf, newWayIds, originalNodes).Perform(changes))
|
changeDescription.push(...new RelationSplitlHandler(partOf, newWayIds, originalNodes).CreateChangeDescriptions(changes))
|
||||||
|
|
||||||
// And we have our objects!
|
// And we have our objects!
|
||||||
// Time to upload
|
// Time to upload
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {UIEventSource} from "../UIEventSource";
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import OsmChangeAction from "./Actions/OsmChangeAction";
|
import OsmChangeAction from "./Actions/OsmChangeAction";
|
||||||
import {ChangeDescription} from "./Actions/ChangeDescription";
|
import {ChangeDescription} from "./Actions/ChangeDescription";
|
||||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,25 +22,23 @@ export class Changes {
|
||||||
|
|
||||||
public readonly pendingChanges = new UIEventSource<ChangeDescription[]>([]) // LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
public readonly pendingChanges = new UIEventSource<ChangeDescription[]>([]) // LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||||
private readonly isUploading = new UIEventSource(false);
|
private readonly isUploading = new UIEventSource(false);
|
||||||
|
|
||||||
|
private readonly previouslyCreated : OsmObject[] = []
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.isUploading.addCallbackAndRun(u => {
|
|
||||||
if (u) {
|
|
||||||
console.trace("Uploading set!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createChangesetFor(csId: string,
|
private static createChangesetFor(csId: string,
|
||||||
allChanges: {
|
allChanges: {
|
||||||
modifiedObjects?: OsmObject[],
|
modifiedObjects: OsmObject[],
|
||||||
newElements?: OsmObject[],
|
newObjects: OsmObject[],
|
||||||
deletedElements?: OsmObject[]
|
deletedObjects: OsmObject[]
|
||||||
}): string {
|
}): string {
|
||||||
|
|
||||||
const changedElements = allChanges.modifiedObjects ?? []
|
const changedElements = allChanges.modifiedObjects ?? []
|
||||||
const newElements = allChanges.newElements ?? []
|
const newElements = allChanges.newObjects ?? []
|
||||||
const deletedElements = allChanges.deletedElements ?? []
|
const deletedElements = allChanges.deletedObjects ?? []
|
||||||
|
|
||||||
let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
|
let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
|
||||||
if (newElements.length > 0) {
|
if (newElements.length > 0) {
|
||||||
|
@ -73,7 +70,7 @@ export class Changes {
|
||||||
.map(c => c.type + "/" + c.id))
|
.map(c => c.type + "/" + c.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
|
private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
|
||||||
newObjects: OsmObject[],
|
newObjects: OsmObject[],
|
||||||
modifiedObjects: OsmObject[]
|
modifiedObjects: OsmObject[]
|
||||||
deletedObjects: OsmObject[]
|
deletedObjects: OsmObject[]
|
||||||
|
@ -87,12 +84,21 @@ export class Changes {
|
||||||
states.set(o.type + "/" + o.id, "unchanged")
|
states.set(o.type + "/" + o.id, "unchanged")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const o of this.previouslyCreated) {
|
||||||
|
objects.set(o.type + "/" + o.id, o)
|
||||||
|
states.set(o.type + "/" + o.id, "unchanged")
|
||||||
|
}
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
const id = change.type + "/" + change.id
|
const id = change.type + "/" + change.id
|
||||||
if (!objects.has(id)) {
|
if (!objects.has(id)) {
|
||||||
|
if(change.id >= 0){
|
||||||
|
throw "Did not get an object that should be known: "+id
|
||||||
|
}
|
||||||
// This is a new object that should be created
|
// This is a new object that should be created
|
||||||
states.set(id, "created")
|
states.set(id, "created")
|
||||||
|
console.log("Creating object for changeDescription", change)
|
||||||
let osmObj: OsmObject = undefined;
|
let osmObj: OsmObject = undefined;
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
case "node":
|
case "node":
|
||||||
|
@ -116,6 +122,7 @@ export class Changes {
|
||||||
throw "Hmm? This is a bug"
|
throw "Hmm? This is a bug"
|
||||||
}
|
}
|
||||||
objects.set(id, osmObj)
|
objects.set(id, osmObj)
|
||||||
|
this.previouslyCreated.push(osmObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = states.get(id)
|
const state = states.get(id)
|
||||||
|
@ -195,8 +202,8 @@ export class Changes {
|
||||||
newObjects: [],
|
newObjects: [],
|
||||||
modifiedObjects: [],
|
modifiedObjects: [],
|
||||||
deletedObjects: []
|
deletedObjects: []
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
objects.forEach((v, id) => {
|
objects.forEach((v, id) => {
|
||||||
|
|
||||||
const state = states.get(id)
|
const state = states.get(id)
|
||||||
|
@ -228,20 +235,18 @@ export class Changes {
|
||||||
*/
|
*/
|
||||||
public flushChanges(flushreason: string = undefined) {
|
public flushChanges(flushreason: string = undefined) {
|
||||||
if (this.pendingChanges.data.length === 0) {
|
if (this.pendingChanges.data.length === 0) {
|
||||||
console.log("No pending changes")
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (flushreason !== undefined) {
|
|
||||||
console.log(flushreason)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isUploading.data) {
|
if (this.isUploading.data) {
|
||||||
console.log("Is uploading... Abort")
|
console.log("Is already uploading... Abort")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.isUploading.setData(true)
|
this.isUploading.setData(true)
|
||||||
|
|
||||||
console.log("Beginning upload...");
|
console.log("Beginning upload... "+flushreason ?? "");
|
||||||
// At last, we build the changeset and upload
|
// At last, we build the changeset and upload
|
||||||
const self = this;
|
const self = this;
|
||||||
const pending = self.pendingChanges.data;
|
const pending = self.pendingChanges.data;
|
||||||
|
@ -249,8 +254,12 @@ export class Changes {
|
||||||
console.log("Needed ids", neededIds)
|
console.log("Needed ids", neededIds)
|
||||||
OsmObject.DownloadAll(neededIds, true).addCallbackAndRunD(osmObjects => {
|
OsmObject.DownloadAll(neededIds, true).addCallbackAndRunD(osmObjects => {
|
||||||
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
|
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
|
||||||
const changes = Changes.CreateChangesetObjects(pending, osmObjects)
|
const changes: {
|
||||||
console.log("Changes", changes)
|
newObjects: OsmObject[],
|
||||||
|
modifiedObjects: OsmObject[]
|
||||||
|
deletedObjects: OsmObject[]
|
||||||
|
|
||||||
|
} = self.CreateChangesetObjects(pending, osmObjects)
|
||||||
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
|
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
|
||||||
console.log("No changes to be made")
|
console.log("No changes to be made")
|
||||||
this.pendingChanges.setData([])
|
this.pendingChanges.setData([])
|
||||||
|
@ -262,11 +271,8 @@ export class Changes {
|
||||||
State.state.osmConnection.UploadChangeset(
|
State.state.osmConnection.UploadChangeset(
|
||||||
State.state.layoutToUse.data,
|
State.state.layoutToUse.data,
|
||||||
State.state.allElements,
|
State.state.allElements,
|
||||||
(csId) => {
|
(csId) => Changes.createChangesetFor(csId, changes),
|
||||||
return Changes.createChangesetFor(csId, changes);
|
|
||||||
},
|
|
||||||
() => {
|
() => {
|
||||||
// When done
|
|
||||||
console.log("Upload successfull!")
|
console.log("Upload successfull!")
|
||||||
self.pendingChanges.setData([]);
|
self.pendingChanges.setData([]);
|
||||||
self.isUploading.setData(false)
|
self.isUploading.setData(false)
|
||||||
|
|
|
@ -23,7 +23,7 @@ export abstract class OsmObject {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.tags = {
|
this.tags = {
|
||||||
id: id
|
id: `${this.type}/${id}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +52,9 @@ export abstract class OsmObject {
|
||||||
const splitted = id.split("/");
|
const splitted = id.split("/");
|
||||||
const type = splitted[0];
|
const type = splitted[0];
|
||||||
const idN = Number(splitted[1]);
|
const idN = Number(splitted[1]);
|
||||||
|
if(idN <0){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
OsmObject.objectCache.set(id, src);
|
OsmObject.objectCache.set(id, src);
|
||||||
const newContinuation = (element: OsmObject) => {
|
const newContinuation = (element: OsmObject) => {
|
||||||
|
@ -69,7 +72,7 @@ export abstract class OsmObject {
|
||||||
new OsmRelation(idN).Download(newContinuation);
|
new OsmRelation(idN).Download(newContinuation);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw "Invalid road type:" + type;
|
throw "Invalid object type:" + type + id;
|
||||||
|
|
||||||
}
|
}
|
||||||
return src;
|
return src;
|
||||||
|
@ -105,7 +108,7 @@ export abstract class OsmObject {
|
||||||
if (OsmObject.referencingRelationsCache.has(id)) {
|
if (OsmObject.referencingRelationsCache.has(id)) {
|
||||||
return OsmObject.referencingRelationsCache.get(id);
|
return OsmObject.referencingRelationsCache.get(id);
|
||||||
}
|
}
|
||||||
const relsSrc = new UIEventSource<OsmRelation[]>([])
|
const relsSrc = new UIEventSource<OsmRelation[]>(undefined)
|
||||||
OsmObject.referencingRelationsCache.set(id, relsSrc);
|
OsmObject.referencingRelationsCache.set(id, relsSrc);
|
||||||
Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`)
|
Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
|
@ -6,7 +6,7 @@ export class UIEventSource<T> {
|
||||||
public data: T;
|
public data: T;
|
||||||
public trace: boolean;
|
public trace: boolean;
|
||||||
private readonly tag: string;
|
private readonly tag: string;
|
||||||
private _callbacks = [];
|
private _callbacks: ((t: T) => (boolean | void | any)) [] = [];
|
||||||
|
|
||||||
constructor(data: T, tag: string = "") {
|
constructor(data: T, tag: string = "") {
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
|
@ -31,7 +31,7 @@ export class UIEventSource<T> {
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> {
|
public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> {
|
||||||
const sink = new UIEventSource<X>(source.data?.data);
|
const sink = new UIEventSource<X>(source.data?.data);
|
||||||
|
|
||||||
|
@ -63,7 +63,13 @@ export class UIEventSource<T> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addCallback(callback: ((latestData: T) => void)): UIEventSource<T> {
|
/**
|
||||||
|
* Adds a callback
|
||||||
|
*
|
||||||
|
* If the result of the callback is 'true', the callback is considered finished and will be removed again
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
public addCallback(callback: ((latestData: T) => (boolean | void | any))): UIEventSource<T> {
|
||||||
if (callback === console.log) {
|
if (callback === console.log) {
|
||||||
// This ^^^ actually works!
|
// This ^^^ actually works!
|
||||||
throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead."
|
throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead."
|
||||||
|
@ -90,8 +96,21 @@ export class UIEventSource<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ping(): void {
|
public ping(): void {
|
||||||
|
let toDelete = undefined
|
||||||
for (const callback of this._callbacks) {
|
for (const callback of this._callbacks) {
|
||||||
callback(this.data);
|
if (callback(this.data) === true) {
|
||||||
|
// This callback wants to be deleted
|
||||||
|
if (toDelete === undefined) {
|
||||||
|
toDelete = [callback]
|
||||||
|
} else {
|
||||||
|
toDelete.push(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toDelete !== undefined) {
|
||||||
|
for (const toDeleteElement of toDelete) {
|
||||||
|
this._callbacks.splice(this._callbacks.indexOf(toDeleteElement), 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
State.ts
18
State.ts
|
@ -20,7 +20,7 @@ import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
|
||||||
import {Relation} from "./Logic/Osm/ExtractRelations";
|
import {Relation} from "./Logic/Osm/ExtractRelations";
|
||||||
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
||||||
import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor";
|
import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor";
|
||||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains the global state: a bunch of UI-event sources
|
* Contains the global state: a bunch of UI-event sources
|
||||||
|
@ -32,7 +32,7 @@ export default class State {
|
||||||
public static state: State;
|
public static state: State;
|
||||||
|
|
||||||
|
|
||||||
public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined);
|
public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined, "layoutToUse");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The mapping from id -> UIEventSource<properties>
|
The mapping from id -> UIEventSource<properties>
|
||||||
|
@ -45,7 +45,7 @@ export default class State {
|
||||||
/**
|
/**
|
||||||
The leaflet instance of the big basemap
|
The leaflet instance of the big basemap
|
||||||
*/
|
*/
|
||||||
public leafletMap = new UIEventSource<L.Map>(undefined);
|
public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap");
|
||||||
/**
|
/**
|
||||||
* Background layer id
|
* Background layer id
|
||||||
*/
|
*/
|
||||||
|
@ -70,7 +70,7 @@ export default class State {
|
||||||
}[]> = new UIEventSource<{
|
}[]> = new UIEventSource<{
|
||||||
readonly isDisplayed: UIEventSource<boolean>,
|
readonly isDisplayed: UIEventSource<boolean>,
|
||||||
readonly layerDef: LayerConfig;
|
readonly layerDef: LayerConfig;
|
||||||
}[]>([])
|
}[]>([], "filteredLayers")
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,13 +101,13 @@ export default class State {
|
||||||
public readonly featureSwitchFakeUser: UIEventSource<boolean>;
|
public readonly featureSwitchFakeUser: UIEventSource<boolean>;
|
||||||
|
|
||||||
|
|
||||||
public readonly featurePipeline: FeaturePipeline;
|
public featurePipeline: FeatureSource;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The map location: currently centered lat, lon and zoom
|
* The map location: currently centered lat, lon and zoom
|
||||||
*/
|
*/
|
||||||
public readonly locationControl = new UIEventSource<Loc>(undefined);
|
public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl");
|
||||||
public backgroundLayer;
|
public backgroundLayer;
|
||||||
public readonly backgroundLayerId: UIEventSource<string>;
|
public readonly backgroundLayerId: UIEventSource<string>;
|
||||||
|
|
||||||
|
@ -149,11 +149,13 @@ export default class State {
|
||||||
.syncWith(LocalStorageSource.Get("lon")));
|
.syncWith(LocalStorageSource.Get("lon")));
|
||||||
|
|
||||||
|
|
||||||
this.locationControl = new UIEventSource<Loc>({
|
this.locationControl.setData({
|
||||||
zoom: Utils.asFloat(zoom.data),
|
zoom: Utils.asFloat(zoom.data),
|
||||||
lat: Utils.asFloat(lat.data),
|
lat: Utils.asFloat(lat.data),
|
||||||
lon: Utils.asFloat(lon.data),
|
lon: Utils.asFloat(lon.data),
|
||||||
}).addCallback((latlonz) => {
|
})
|
||||||
|
this.locationControl.addCallback((latlonz) => {
|
||||||
|
// Sync th location controls
|
||||||
zoom.setData(latlonz.zoom);
|
zoom.setData(latlonz.zoom);
|
||||||
lat.setData(latlonz.lat);
|
lat.setData(latlonz.lat);
|
||||||
lon.setData(latlonz.lon);
|
lon.setData(latlonz.lon);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {InputElement} from "../Input/InputElement";
|
||||||
import Loc from "../../Models/Loc";
|
import Loc from "../../Models/Loc";
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||||
|
import Hash from "../../Logic/Web/Hash";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||||
|
@ -71,10 +72,11 @@ export default class SimpleAddUI extends Toggle {
|
||||||
}
|
}
|
||||||
return SimpleAddUI.CreateConfirmButton(preset,
|
return SimpleAddUI.CreateConfirmButton(preset,
|
||||||
(tags, location) => {
|
(tags, location) => {
|
||||||
let changes =
|
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon)
|
||||||
State.state.changes.applyAction(new CreateNewNodeAction(tags, location.lat, location.lon))
|
State.state.changes.applyAction(newElementAction)
|
||||||
State.state.selectedElement.setData(changes.newFeatures[0]);
|
|
||||||
selectedPreset.setData(undefined)
|
selectedPreset.setData(undefined)
|
||||||
|
isShown.setData(false)
|
||||||
|
Hash.hash.setData(newElementAction.newElementId)
|
||||||
}, () => {
|
}, () => {
|
||||||
selectedPreset.setData(undefined)
|
selectedPreset.setData(undefined)
|
||||||
})
|
})
|
||||||
|
@ -119,16 +121,16 @@ export default class SimpleAddUI extends Toggle {
|
||||||
lon: location.data.lon,
|
lon: location.data.lon,
|
||||||
zoom: 19
|
zoom: 19
|
||||||
});
|
});
|
||||||
|
|
||||||
let backgroundLayer = undefined;
|
let backgroundLayer = undefined;
|
||||||
if(preset.preciseInput.preferredBackground){
|
if (preset.preciseInput.preferredBackground) {
|
||||||
backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
|
backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
|
||||||
}
|
}
|
||||||
|
|
||||||
preciseInput = new LocationInput({
|
preciseInput = new LocationInput({
|
||||||
mapBackground: backgroundLayer,
|
mapBackground: backgroundLayer,
|
||||||
centerLocation:locationSrc
|
centerLocation: locationSrc
|
||||||
|
|
||||||
})
|
})
|
||||||
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
|
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
|
||||||
}
|
}
|
||||||
|
@ -143,7 +145,7 @@ export default class SimpleAddUI extends Toggle {
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data);
|
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (preciseInput !== undefined) {
|
if (preciseInput !== undefined) {
|
||||||
confirmButton = new Combine([preciseInput, confirmButton])
|
confirmButton = new Combine([preciseInput, confirmButton])
|
||||||
}
|
}
|
||||||
|
@ -239,7 +241,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.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
|
let icon: () => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.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,
|
||||||
|
|
|
@ -92,18 +92,24 @@ export default class SplitRoadWizard extends Toggle {
|
||||||
// Save button
|
// Save button
|
||||||
const saveButton = new Button(t.split.Clone(), () => {
|
const saveButton = new Button(t.split.Clone(), () => {
|
||||||
hasBeenSplit.setData(true)
|
hasBeenSplit.setData(true)
|
||||||
OsmObject.DownloadObject(id).addCallbackAndRunD(way => {
|
const way = OsmObject.DownloadObject(id)
|
||||||
OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD(
|
const partOfSrc = OsmObject.DownloadReferencingRelations(id);
|
||||||
partOf => {
|
let hasRun = false
|
||||||
const splitAction = new SplitAction(
|
way.map(way => {
|
||||||
<OsmWay>way, way.asGeoJson(), partOf, splitPoints.data.map(ff => ff.feature)
|
const partOf = partOfSrc.data
|
||||||
)
|
if(way === undefined || partOf === undefined){
|
||||||
State.state.changes.applyAction(splitAction)
|
return;
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
if(hasRun){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hasRun = true
|
||||||
|
const splitAction = new SplitAction(
|
||||||
|
<OsmWay>way, way.asGeoJson(), partOf, splitPoints.data.map(ff => ff.feature)
|
||||||
|
)
|
||||||
|
State.state.changes.applyAction(splitAction)
|
||||||
|
|
||||||
|
}, [partOfSrc])
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -123,7 +129,7 @@ export default class SplitRoadWizard extends Toggle {
|
||||||
|
|
||||||
const splitTitle = new Title(t.splitTitle);
|
const splitTitle = new Title(t.splitTitle);
|
||||||
|
|
||||||
const mapView = new Combine([splitTitle, miniMap, new Combine([cancelButton, saveToggle])]);
|
const mapView = new Combine([splitTitle, miniMap, new Combine([cancelButton, saveToggle]).SetClass("flex flex-row")]);
|
||||||
mapView.SetClass("question")
|
mapView.SetClass("question")
|
||||||
const confirm = new Toggle(mapView, splitToggle, splitClicked);
|
const confirm = new Toggle(mapView, splitToggle, splitClicked);
|
||||||
super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit)
|
super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit)
|
||||||
|
|
|
@ -61,7 +61,6 @@ export default class ShowDataLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
const allFeats = features.data.map(ff => ff.feature);
|
const allFeats = features.data.map(ff => ff.feature);
|
||||||
console.log("Rendering ",allFeats, "features at layer ", name)
|
|
||||||
geoLayer = self.CreateGeojsonLayer();
|
geoLayer = self.CreateGeojsonLayer();
|
||||||
for (const feat of allFeats) {
|
for (const feat of allFeats) {
|
||||||
if (feat === undefined) {
|
if (feat === undefined) {
|
||||||
|
@ -87,6 +86,7 @@ export default class ShowDataLayer {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
State.state.selectedElement.ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
features.addCallback(() => update());
|
features.addCallback(() => update());
|
||||||
|
|
|
@ -19,7 +19,6 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
const extraMappings: SpecialVisualization[] = [];
|
const extraMappings: SpecialVisualization[] = [];
|
||||||
|
|
||||||
mapping?.forEach((value, key) => {
|
mapping?.forEach((value, key) => {
|
||||||
console.log("KV:", key, value)
|
|
||||||
extraMappings.push(
|
extraMappings.push(
|
||||||
{
|
{
|
||||||
funcName: key,
|
funcName: key,
|
||||||
|
@ -73,11 +72,6 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
}
|
}
|
||||||
}[] {
|
}[] {
|
||||||
|
|
||||||
if (extraMappings.length > 0) {
|
|
||||||
|
|
||||||
console.log("Extra mappings are", extraMappings)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) {
|
for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) {
|
||||||
|
|
||||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||||
|
|
|
@ -109,9 +109,9 @@ export class Translation extends BaseUIElement {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const date: Date = el;
|
const date: Date = el;
|
||||||
rtext = date.toLocaleString();
|
rtext = date.toLocaleString();
|
||||||
} else if (el.ConstructElement() === undefined) {
|
} else if (el.ConstructElement === undefined) {
|
||||||
console.error("InnerREnder is not defined", el);
|
console.error("ConstructElement is not defined", el);
|
||||||
throw "Hmmm, el.InnerRender is not defined?"
|
throw "ConstructElement is not defined, you are working with a "+(typeof el)+":"+(el.constructor.name)
|
||||||
} else {
|
} else {
|
||||||
Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day
|
Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day
|
||||||
rtext = el.ConstructElement().innerHTML;
|
rtext = el.ConstructElement().innerHTML;
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
"startZoom": 14,
|
"startZoom": 14,
|
||||||
"startLon": 3.2228,
|
"startLon": 3.2228,
|
||||||
"maintainer": "MapComplete",
|
"maintainer": "MapComplete",
|
||||||
"widenfactor": 0.05,
|
"widenfactor": 0.01,
|
||||||
"roamingRenderings": [
|
"roamingRenderings": [
|
||||||
{
|
{
|
||||||
"question": {
|
"question": {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue