forked from MapComplete/MapComplete
Better handling of metatags, more robust error handling when calculating tags
This commit is contained in:
parent
69363fbf0f
commit
6ac8a5373c
10 changed files with 134 additions and 89 deletions
|
@ -7,6 +7,7 @@ import Bounds from "../../Models/Bounds";
|
||||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import {TagsFilter} from "../Tags/TagsFilter";
|
import {TagsFilter} from "../Tags/TagsFilter";
|
||||||
|
import SimpleMetaTagger from "../SimpleMetaTagger";
|
||||||
|
|
||||||
|
|
||||||
export default class UpdateFromOverpass implements FeatureSource {
|
export default class UpdateFromOverpass implements FeatureSource {
|
||||||
|
@ -158,14 +159,17 @@ export default class UpdateFromOverpass implements FeatureSource {
|
||||||
function (data, date) {
|
function (data, date) {
|
||||||
self._previousBounds.get(z).push(queryBounds);
|
self._previousBounds.get(z).push(queryBounds);
|
||||||
self.retries.setData(0);
|
self.retries.setData(0);
|
||||||
self.features.setData(data.features.map(f => ({feature: f, freshness: date})));
|
const features = data.features.map(f => ({feature: f, freshness: date}));
|
||||||
|
SimpleMetaTagger.objectMetaInfo.addMetaTags(features)
|
||||||
|
|
||||||
|
self.features.setData(features);
|
||||||
self.runningQuery.setData(false);
|
self.runningQuery.setData(false);
|
||||||
},
|
},
|
||||||
function (reason) {
|
function (reason) {
|
||||||
self.retries.data++;
|
self.retries.data++;
|
||||||
self.ForceRefresh();
|
self.ForceRefresh();
|
||||||
self.timeout.setData(self.retries.data * 5);
|
self.timeout.setData(self.retries.data * 5);
|
||||||
console.log(`QUERY FAILED (retrying in ${5 * self.retries.data} sec)`);
|
console.log(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to ${reason}`);
|
||||||
self.retries.ping();
|
self.retries.ping();
|
||||||
self.runningQuery.setData(false);
|
self.runningQuery.setData(false);
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,19 @@ export class ElementStorage {
|
||||||
return es;
|
return es;
|
||||||
}
|
}
|
||||||
|
|
||||||
addOrGetById(elementId: string, newProperties: any): UIEventSource<any> {
|
getEventSourceById(elementId): UIEventSource<any> {
|
||||||
|
if (this._elements.has(elementId)) {
|
||||||
|
return this._elements.get(elementId);
|
||||||
|
}
|
||||||
|
console.error("Can not find eventsource with id ", elementId);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(id) {
|
||||||
|
return this._elements.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addOrGetById(elementId: string, newProperties: any): UIEventSource<any> {
|
||||||
if (!this._elements.has(elementId)) {
|
if (!this._elements.has(elementId)) {
|
||||||
const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId);
|
const eventSource = new UIEventSource<any>(newProperties, "tags of " + elementId);
|
||||||
this._elements.set(elementId, eventSource);
|
this._elements.set(elementId, eventSource);
|
||||||
|
@ -48,30 +60,33 @@ export class ElementStorage {
|
||||||
const keptKeys = es.data;
|
const keptKeys = es.data;
|
||||||
// The element already exists
|
// The element already exists
|
||||||
// We use the new feature to overwrite all the properties in the already existing eventsource
|
// We use the new feature to overwrite all the properties in the already existing eventsource
|
||||||
console.log("Merging multiple instances of ", elementId)
|
const debug_msg = []
|
||||||
let somethingChanged = false;
|
let somethingChanged = false;
|
||||||
for (const k in newProperties) {
|
for (const k in newProperties) {
|
||||||
const v = newProperties[k];
|
const v = newProperties[k];
|
||||||
|
|
||||||
if (keptKeys[k] !== v) {
|
if (keptKeys[k] !== v) {
|
||||||
keptKeys[k] = v;
|
|
||||||
|
if (v === undefined) {
|
||||||
|
// The new value is undefined; the tag might have been removed
|
||||||
|
// It might be a metatag as well
|
||||||
|
// In the latter case, we do keep the tag!
|
||||||
|
if (!k.startsWith("_")) {
|
||||||
|
delete keptKeys[k]
|
||||||
|
debug_msg.push(("Erased " + k))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keptKeys[k] = v;
|
||||||
|
debug_msg.push(k + " --> " + v)
|
||||||
|
}
|
||||||
|
|
||||||
somethingChanged = true;
|
somethingChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (somethingChanged) {
|
if (somethingChanged) {
|
||||||
|
console.trace(`Merging multiple instances of ${elementId}: ` + debug_msg.join(", ")+" newProperties: ", newProperties)
|
||||||
es.ping();
|
es.ping();
|
||||||
}
|
}
|
||||||
return es;
|
return es;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEventSourceById(elementId): UIEventSource<any> {
|
|
||||||
if (this._elements.has(elementId)) {
|
|
||||||
return this._elements.get(elementId);
|
|
||||||
}
|
|
||||||
console.error("Can not find eventsource with id ", elementId);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
has(id) {
|
|
||||||
return this._elements.has(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -3,16 +3,15 @@ import {UIEventSource} from "../UIEventSource";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
|
|
||||||
export default class RegisteringFeatureSource implements FeatureSource {
|
export default class RegisteringFeatureSource implements FeatureSource {
|
||||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||||
public readonly name;
|
public readonly name;
|
||||||
|
|
||||||
constructor(source: FeatureSource) {
|
constructor(source: FeatureSource) {
|
||||||
this.features = source.features;
|
this.features = source.features;
|
||||||
this.name = "RegisteringSource of "+source.name;
|
this.name = "RegisteringSource of " + source.name;
|
||||||
this.features.addCallbackAndRun(features => {
|
this.features.addCallbackAndRun(features => {
|
||||||
for (const feature of features ?? []) {
|
for (const feature of features ?? []) {
|
||||||
if (!State.state.allElements.has(feature.feature.properties.id)) {
|
State.state.allElements.addOrGetElement(feature.feature)
|
||||||
State.state.allElements.addOrGetElement(feature.feature)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,12 @@ export default class MetaTagging {
|
||||||
if (f === undefined) {
|
if (f === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
f({featuresPerLayer: featuresPerLayer, memberships: relations}, feature.feature)
|
try{
|
||||||
|
f({featuresPerLayer: featuresPerLayer, memberships: relations}, feature.feature)
|
||||||
|
}catch(e){
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -84,10 +89,21 @@ export default class MetaTagging {
|
||||||
}
|
}
|
||||||
const func = new Function("feat", "return " + code + ";");
|
const func = new Function("feat", "return " + code + ";");
|
||||||
|
|
||||||
|
try{
|
||||||
|
|
||||||
|
|
||||||
const f = (featuresPerLayer, feature: any) => {
|
const f = (featuresPerLayer, feature: any) => {
|
||||||
feature.properties[key] = func(feature);
|
try{
|
||||||
|
feature.properties[key] = func(feature);
|
||||||
|
}catch(e){
|
||||||
|
console.error("Could not calculate a metatag defined by "+code+" due to "+e+". This is code defined in the theme. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
functions.push(f)
|
functions.push(f)
|
||||||
|
}catch(e){
|
||||||
|
console.error("Could not create a dynamic function: ", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (params: Params, feature) => {
|
return (params: Params, feature) => {
|
||||||
const tags = feature.properties
|
const tags = feature.properties
|
||||||
|
|
|
@ -67,7 +67,46 @@ export abstract class OsmObject {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ParseObjects(elements: any[]) : OsmObject[]{
|
// bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
|
||||||
|
public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) {
|
||||||
|
const minlon = bounds[0][1]
|
||||||
|
const maxlon = bounds[1][1]
|
||||||
|
const minlat = bounds[1][0]
|
||||||
|
const maxlat = bounds[0][0];
|
||||||
|
const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
|
||||||
|
$.getJSON(url, data => {
|
||||||
|
const elements: any[] = data.elements;
|
||||||
|
const objects = OsmObject.ParseObjects(elements)
|
||||||
|
callback(objects);
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Loads an area from the OSM-api.
|
||||||
|
|
||||||
|
public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects: any) => void)) {
|
||||||
|
// local function which downloads all the objects one by one
|
||||||
|
// this is one big loop, running one download, then rerunning the entire function
|
||||||
|
if (neededIds.length == 0) {
|
||||||
|
continuation(knownElements);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const neededId = neededIds.pop();
|
||||||
|
|
||||||
|
if (neededId in knownElements) {
|
||||||
|
OsmObject.DownloadAll(neededIds, knownElements, continuation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OsmObject.DownloadObject(neededId,
|
||||||
|
function (element) {
|
||||||
|
knownElements[neededId] = element; // assign the element for later, continue downloading the next element
|
||||||
|
OsmObject.DownloadAll(neededIds, knownElements, continuation);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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>()
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
|
@ -96,44 +135,6 @@ export abstract class OsmObject {
|
||||||
}
|
}
|
||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Loads an area from the OSM-api.
|
|
||||||
// bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
|
|
||||||
public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) {
|
|
||||||
const minlon = bounds[0][1]
|
|
||||||
const maxlon = bounds[1][1]
|
|
||||||
const minlat = bounds[1][0]
|
|
||||||
const maxlat = bounds[0][0];
|
|
||||||
const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
|
|
||||||
$.getJSON(url, data => {
|
|
||||||
const elements: any[] = data.elements;
|
|
||||||
const objects = OsmObject.ParseObjects(elements)
|
|
||||||
callback(objects);
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects: any) => void)) {
|
|
||||||
// local function which downloads all the objects one by one
|
|
||||||
// this is one big loop, running one download, then rerunning the entire function
|
|
||||||
if (neededIds.length == 0) {
|
|
||||||
continuation(knownElements);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const neededId = neededIds.pop();
|
|
||||||
|
|
||||||
if (neededId in knownElements) {
|
|
||||||
OsmObject.DownloadAll(neededIds, knownElements, continuation);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
OsmObject.DownloadObject(neededId,
|
|
||||||
function (element) {
|
|
||||||
knownElements[neededId] = element; // assign the element for later, continue downloading the next element
|
|
||||||
OsmObject.DownloadAll(neededIds, knownElements, continuation);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The centerpoint of the feature, as [lat, lon]
|
// The centerpoint of the feature, as [lat, lon]
|
||||||
public abstract centerpoint(): [number, number];
|
public abstract centerpoint(): [number, number];
|
||||||
|
@ -149,10 +150,10 @@ export abstract class OsmObject {
|
||||||
TagsXML(): string {
|
TagsXML(): string {
|
||||||
let tags = "";
|
let tags = "";
|
||||||
for (const key in this.tags) {
|
for (const key in this.tags) {
|
||||||
if(key.startsWith("_")){
|
if (key.startsWith("_")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(key === "id"){
|
if (key === "id") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const v = this.tags[key];
|
const v = this.tags[key];
|
||||||
|
@ -168,24 +169,25 @@ export abstract class OsmObject {
|
||||||
const full = this.type !== "way" ? "" : "/full";
|
const full = this.type !== "way" ? "" : "/full";
|
||||||
const url = "https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id + full;
|
const url = "https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id + full;
|
||||||
$.getJSON(url, function (data) {
|
$.getJSON(url, function (data) {
|
||||||
|
|
||||||
const element = data.elements.pop();
|
const element = data.elements.pop();
|
||||||
|
|
||||||
let nodes = []
|
let nodes = []
|
||||||
if(data.elements.length > 2){
|
if (data.elements.length > 2) {
|
||||||
nodes = OsmObject.ParseObjects(data.elements)
|
nodes = OsmObject.ParseObjects(data.elements)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.LoadData(element)
|
self.LoadData(element)
|
||||||
self.SaveExtraData(element, nodes);
|
self.SaveExtraData(element, nodes);
|
||||||
|
const meta = {
|
||||||
continuation(self, {
|
|
||||||
"_last_edit:contributor": element.user,
|
"_last_edit:contributor": element.user,
|
||||||
"_last_edit:contributor:uid": element.uid,
|
"_last_edit:contributor:uid": element.uid,
|
||||||
"_last_edit:changeset": element.changeset,
|
"_last_edit:changeset": element.changeset,
|
||||||
"_last_edit:timestamp": new Date(element.timestamp),
|
"_last_edit:timestamp": new Date(element.timestamp),
|
||||||
"_version_number": element.version
|
"_version_number": element.version
|
||||||
});
|
}
|
||||||
|
|
||||||
|
continuation(self, meta);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return this;
|
return this;
|
||||||
|
@ -220,7 +222,7 @@ export abstract class OsmObject {
|
||||||
this.version = element.version;
|
this.version = element.version;
|
||||||
this.timestamp = element.timestamp;
|
this.timestamp = element.timestamp;
|
||||||
const tgs = this.tags;
|
const tgs = this.tags;
|
||||||
if(element.tags === undefined){
|
if (element.tags === undefined) {
|
||||||
// Simple node which is part of a way - not important
|
// Simple node which is part of a way - not important
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -230,8 +232,6 @@ export abstract class OsmObject {
|
||||||
tgs["_last_edit:timestamp"] = element.timestamp
|
tgs["_last_edit:timestamp"] = element.timestamp
|
||||||
tgs["_version_number"] = element.version
|
tgs["_version_number"] = element.version
|
||||||
tgs["id"] = this.type + "/" + this.id;
|
tgs["id"] = this.type + "/" + this.id;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ export class Overpass {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const geojson = OsmToGeoJson.default(json);
|
const geojson = OsmToGeoJson.default(json);
|
||||||
const osmTime = new Date(json.osm3s.timestamp_osm_base);
|
const osmTime = new Date(json.osm3s.timestamp_osm_base);
|
||||||
|
|
||||||
continuation(geojson, osmTime);
|
continuation(geojson, osmTime);
|
||||||
|
|
||||||
}).fail(onFail)
|
}).fail(onFail)
|
||||||
|
|
|
@ -31,17 +31,20 @@ export default class SimpleMetaTagger {
|
||||||
(feature) => {/*Note: also handled by 'UpdateTagsFromOsmAPI'*/
|
(feature) => {/*Note: also handled by 'UpdateTagsFromOsmAPI'*/
|
||||||
|
|
||||||
const tgs = feature.properties;
|
const tgs = feature.properties;
|
||||||
tgs["_last_edit:contributor"] = tgs["user"]
|
|
||||||
tgs["_last_edit:contributor:uid"] = tgs["uid"]
|
function move(src: string, target: string){
|
||||||
tgs["_last_edit:changeset"] = tgs["changeset"]
|
if(tgs[src] === undefined){
|
||||||
tgs["_last_edit:timestamp"] = tgs["timestamp"]
|
return;
|
||||||
tgs["_version_number"] = tgs["version"]
|
}
|
||||||
|
tgs[target] = tgs[src]
|
||||||
delete tgs["timestamp"]
|
delete tgs[src]
|
||||||
delete tgs["version"]
|
}
|
||||||
delete tgs["changeset"]
|
|
||||||
delete tgs["user"]
|
move("user","_last_edit:contributor")
|
||||||
delete tgs["uid"]
|
move("uid","_last_edit:contributor:uid")
|
||||||
|
move("changeset","_last_edit:changeset")
|
||||||
|
move("timestamp","_last_edit:timestamp")
|
||||||
|
move("version","_version_number")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static latlon = new SimpleMetaTagger({
|
private static latlon = new SimpleMetaTagger({
|
||||||
|
|
|
@ -16,6 +16,9 @@ export class RegexTag extends TagsFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean {
|
private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean {
|
||||||
|
if(fromTag === undefined){
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof possibleRegex === "string") {
|
if (typeof possibleRegex === "string") {
|
||||||
return fromTag === possibleRegex;
|
return fromTag === possibleRegex;
|
||||||
}
|
}
|
||||||
|
@ -42,6 +45,9 @@ export class RegexTag extends TagsFilter {
|
||||||
|
|
||||||
matchesProperties(tags: any): boolean {
|
matchesProperties(tags: any): boolean {
|
||||||
for (const key in tags) {
|
for (const key in tags) {
|
||||||
|
if(key === undefined){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (RegexTag.doesMatch(key, this.key)) {
|
if (RegexTag.doesMatch(key, this.key)) {
|
||||||
const value = tags[key]
|
const value = tags[key]
|
||||||
return RegexTag.doesMatch(value, this.value) != this.invert;
|
return RegexTag.doesMatch(value, this.value) != this.invert;
|
||||||
|
|
|
@ -53,7 +53,9 @@ export default class EditableTagRendering extends UIElement {
|
||||||
return this._question.Render();
|
return this._question.Render();
|
||||||
}
|
}
|
||||||
if(!this._configuration.IsKnown(this._tags.data)){
|
if(!this._configuration.IsKnown(this._tags.data)){
|
||||||
return ""
|
// Even though it is not known, we hide the question here
|
||||||
|
// It is the questionbox's task to show the question in edit mode
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Combine([this._answer,
|
return new Combine([this._answer,
|
||||||
|
|
|
@ -3,7 +3,6 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
|
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
|
||||||
import TagRenderingQuestion from "./TagRenderingQuestion";
|
import TagRenderingQuestion from "./TagRenderingQuestion";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {TagUtils} from "../../Logic/TagUtils";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,6 +43,7 @@ export default class QuestionBox extends UIElement {
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
self._skippedQuestions.setData([]);
|
self._skippedQuestions.setData([]);
|
||||||
})
|
})
|
||||||
|
this.SetClass("block")
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): string {
|
||||||
|
@ -57,7 +57,6 @@ export default class QuestionBox extends UIElement {
|
||||||
if (this._skippedQuestions.data.indexOf(i) >= 0) {
|
if (this._skippedQuestions.data.indexOf(i) >= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this value is NOT known
|
// this value is NOT known
|
||||||
return this._tagRenderingQuestions[i].Render();
|
return this._tagRenderingQuestions[i].Render();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue