diff --git a/InitUiElements.ts b/InitUiElements.ts
index 65a4485e0..0f1143eba 100644
--- a/InitUiElements.ts
+++ b/InitUiElements.ts
@@ -154,10 +154,7 @@ export class InitUiElements {
}
State.state.osmConnection.userDetails.map((userDetails: UserDetails) => userDetails?.home)
- .addCallbackAndRun(home => {
- if (home === undefined) {
- return;
- }
+ .addCallbackAndRunD(home => {
const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color")
const icon = L.icon({
iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)),
@@ -286,10 +283,8 @@ export class InitUiElements {
isOpened.setData(false);
})
- State.state.selectedElement.addCallbackAndRun(selected => {
- if (selected !== undefined) {
+ State.state.selectedElement.addCallbackAndRunD(_ => {
isOpened.setData(false);
- }
})
isOpened.setData(Hash.hash.data === undefined || Hash.hash.data === "" || Hash.hash.data == "welcome")
}
@@ -337,11 +332,9 @@ export class InitUiElements {
copyrightButton.isEnabled.setData(false);
});
- State.state.selectedElement.addCallbackAndRun(feature => {
- if (feature !== undefined) {
+ State.state.selectedElement.addCallbackAndRunD(_ => {
layerControlButton.isEnabled.setData(false);
copyrightButton.isEnabled.setData(false);
- }
})
}
diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts
index e74708811..3aa5d8e5b 100644
--- a/Logic/Actors/SelectedFeatureHandler.ts
+++ b/Logic/Actors/SelectedFeatureHandler.ts
@@ -61,7 +61,7 @@ export default class SelectedFeatureHandler {
return; // No valid feature selected
}
// We should have a valid osm-ID and zoom to it
- OsmObject.DownloadObject(hash, (element: OsmObject, meta: OsmObjectMeta) => {
+ OsmObject.DownloadObject(hash).addCallbackAndRunD(element => {
const centerpoint = element.centerpoint();
console.log("Zooming to location for select point: ", centerpoint)
location.data.lat = centerpoint[0]
diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts
index e06fd2ada..e7fd28698 100644
--- a/Logic/Osm/Changes.ts
+++ b/Logic/Osm/Changes.ts
@@ -228,9 +228,9 @@ export class Changes implements FeatureSource{
}
neededIds = Utils.Dedup(neededIds);
- OsmObject.DownloadAll(neededIds, {}, (knownElements) => {
+ OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => {
self.uploadChangesWithLatestVersions(knownElements, newElements, pending)
- });
+ })
}
}
\ No newline at end of file
diff --git a/Logic/Osm/ChangesetHandler.ts b/Logic/Osm/ChangesetHandler.ts
index 5280510ff..88ad246e5 100644
--- a/Logic/Osm/ChangesetHandler.ts
+++ b/Logic/Osm/ChangesetHandler.ts
@@ -7,6 +7,7 @@ import State from "../../State";
import Locale from "../../UI/i18n/Locale";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import Constants from "../../Models/Constants";
+import {OsmObject} from "./OsmObject";
export class ChangesetHandler {
@@ -47,11 +48,20 @@ export class ChangesetHandler {
}
}
+ /**
+ * The full logic to upload a change to one or more elements.
+ *
+ * This method will attempt to reuse an existing, open changeset for this theme (or open one if none available).
+ * Then, it will upload a changes-xml within this changeset (and leave the changeset open)
+ * When upload is successfull, eventual id-rewriting will be handled (aka: don't worry about that)
+ *
+ * If 'dryrun' is specified, the changeset XML will be printed to console instead of being uploaded
+ *
+ */
public UploadChangeset(
layout: LayoutConfig,
allElements: ElementStorage,
- generateChangeXML: (csid: string) => string,
- continuation: () => void) {
+ generateChangeXML: (csid: string) => string) {
if (this.userDetails.data.csCount == 0) {
// The user became a contributor!
@@ -62,7 +72,6 @@ export class ChangesetHandler {
if (this._dryRun) {
const changesetXML = generateChangeXML("123456");
console.log(changesetXML);
- continuation();
return;
}
@@ -97,7 +106,7 @@ export class ChangesetHandler {
// Mark the CS as closed...
this.currentChangeset.setData("");
// ... and try again. As the cs is closed, no recursive loop can exist
- self.UploadChangeset(layout, allElements, generateChangeXML, continuation);
+ self.UploadChangeset(layout, allElements, generateChangeXML);
}
)
@@ -105,7 +114,60 @@ export class ChangesetHandler {
}
}
- public CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
+
+ /**
+ * Deletes the element with the given ID from the OSM database.
+ * DOES NOT PERFORM ANY SAFETY CHECKS!
+ *
+ * For the deletion of an element, a new, seperate changeset is created with a slightly changed comment and some extra flags set.
+ * The CS will be closed afterwards.
+ *
+ * If dryrun is specified, will not actually delete the point but print the CS-XML to console instead
+ *
+ */
+ public DeleteElement(object: OsmObject,
+ layout: LayoutConfig,
+ reason: string,
+ allElements: ElementStorage,
+ continuation: () => void) {
+
+ function generateChangeXML(csId: string) {
+ let [lat, lon] = object.centerpoint();
+
+ let changes = ``;
+ changes +=
+ `<${object.type} id="${object.id}" version="${object.version}" changeset="${csId}" lat="${lat}" lon="${lon}" />`;
+ changes += "";
+
+ return changes;
+
+ }
+
+
+ if (this._dryRun) {
+ const changesetXML = generateChangeXML("123456");
+ console.log(changesetXML);
+ return;
+ }
+
+ const self = this;
+ this.OpenChangeset(layout, (csId: string) => {
+
+ // The cs is open - let us actually upload!
+ const changes = generateChangeXML(csId)
+
+ self.AddChange(csId, changes, allElements, (csId) => {
+ console.log("Successfully deleted ", object.id)
+ self.CloseChangeset(csId, continuation)
+ }, (csId) => {
+ alert("Deletion failed... Should not happend")
+ // FAILED
+ self.CloseChangeset(csId, continuation)
+ })
+ }, true, reason)
+ }
+
+ private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
}) {
if (changesetId === undefined) {
changesetId = this.currentChangeset.data;
@@ -133,15 +195,25 @@ export class ChangesetHandler {
private OpenChangeset(
layout: LayoutConfig,
- continuation: (changesetId: string) => void) {
+ continuation: (changesetId: string) => void,
+ isDeletionCS: boolean = false,
+ deletionReason: string = undefined) {
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
+ let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`
+ if (isDeletionCS) {
+ comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}`
+ if(deletionReason){
+ comment += ": "+deletionReason;
+ }
+ }
let path = window.location.pathname;
path = path.substr(1, path.lastIndexOf("/"));
const metadata = [
["created_by", `MapComplete ${Constants.vNumber}`],
- ["comment", `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`],
+ ["comment", comment],
+ ["deletion", isDeletionCS ? "yes" : undefined],
["theme", layout.id],
["language", Locale.language.data],
["host", window.location.host],
@@ -172,11 +244,21 @@ export class ChangesetHandler {
});
}
+ /**
+ * Upload a changesetXML
+ * @param changesetId
+ * @param changesetXML
+ * @param allElements
+ * @param continuation
+ * @param onFail
+ * @constructor
+ * @private
+ */
private AddChange(changesetId: string,
changesetXML: string,
allElements: ElementStorage,
continuation: ((changesetId: string, idMapping: any) => void),
- onFail: ((changesetId: string) => void) = undefined) {
+ onFail: ((changesetId: string, reason: string) => void) = undefined) {
this.auth.xhr({
method: 'POST',
options: {header: {'Content-Type': 'text/xml'}},
@@ -186,7 +268,7 @@ export class ChangesetHandler {
if (response == null) {
console.log("err", err);
if (onFail) {
- onFail(changesetId);
+ onFail(changesetId, err);
}
return;
}
diff --git a/Logic/Osm/DeleteAction.ts b/Logic/Osm/DeleteAction.ts
new file mode 100644
index 000000000..8694a522f
--- /dev/null
+++ b/Logic/Osm/DeleteAction.ts
@@ -0,0 +1,207 @@
+import {UIEventSource} from "../UIEventSource";
+import {Translation} from "../../UI/i18n/Translation";
+import Translations from "../../UI/i18n/Translations";
+import {OsmObject} from "./OsmObject";
+import State from "../../State";
+import Constants from "../../Models/Constants";
+
+export default class DeleteAction {
+
+ public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>;
+ private readonly _id: string;
+
+ constructor(id: string) {
+ this._id = id;
+
+ this.canBeDeleted = new UIEventSource<{canBeDeleted?: boolean; reason: Translation}>({
+ canBeDeleted : false,
+ reason: Translations.t.delete.loading
+ })
+
+ this.CheckDeleteability()
+ }
+
+
+ public DoDelete(reason: string): UIEventSource {
+ const isDeleted = new UIEventSource(false)
+
+ const self = this;
+ let deletionStarted = false;
+ this.canBeDeleted.addCallbackAndRun(
+ canBeDeleted => {
+ if (!canBeDeleted) {
+ // We are not allowed to delete (yet), this might change in the future though
+ return;
+ }
+
+ if (isDeleted.data) {
+ // Already deleted...
+ return;
+ }
+
+ if (deletionStarted) {
+ // Deletion is already running...
+ return;
+ }
+ deletionStarted = true;
+ OsmObject.DownloadObject(self._id).addCallbackAndRun(obj => {
+ if(obj === undefined){
+ return;
+ }
+ State.state.osmConnection.changesetHandler.DeleteElement(
+ obj,
+ State.state.layoutToUse.data,
+ reason,
+ State.state.allElements,
+ () => {
+ isDeleted.setData(true)
+ }
+ )
+ })
+
+ }
+ )
+
+ return isDeleted;
+ }
+
+ /**
+ * Checks if the currently logged in user can delete the current point.
+ * State is written into this._canBeDeleted
+ * @constructor
+ * @private
+ */
+ private CheckDeleteability(): void {
+ const t = Translations.t.delete;
+ const id = this._id;
+ const state = this.canBeDeleted
+ if (!id.startsWith("node")) {
+ this.canBeDeleted.setData({
+ canBeDeleted: false,
+ reason: t.isntAPoint
+ })
+ return;
+ }
+
+ // Does the currently logged in user have enough experience to delete this point?
+
+ const deletingPointsOfOtherAllowed = State.state.osmConnection.userDetails.map(ud => {
+ if (ud === undefined) {
+ return undefined;
+ }
+ if(!ud.loggedIn){
+ return false;
+ }
+ return ud.csCount >= Constants.userJourney.deletePointsOfOthersUnlock;
+ })
+
+ const previousEditors = new UIEventSource(undefined)
+
+ const allByMyself = previousEditors.map(previous => {
+ if (previous === null || previous === undefined) {
+ // Not yet downloaded
+ return null;
+ }
+ const userId = State.state.osmConnection.userDetails.data.uid;
+ return !previous.some(editor => editor !== userId)
+ }, [State.state.osmConnection.userDetails])
+
+
+ // User allowed OR only edited by self?
+ const deletetionAllowed = deletingPointsOfOtherAllowed.map(isAllowed => {
+ if (isAllowed === undefined) {
+ // No logged in user => definitively not allowed to delete!
+ return false;
+ }
+ if (isAllowed === true) {
+ return true;
+ }
+
+ // At this point, the logged in user is not allowed to delete points created/edited by _others_
+ // however, we query OSM and if it turns out the current point has only be edited by the current user, deletion is allowed after all!
+
+ if (allByMyself.data === null) {
+ // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
+ OsmObject.DownloadHistory(id).map(versions => versions.map(version => version.tags["_last_edit:contributor:uid"])).syncWith(previousEditors)
+ }
+ if (allByMyself.data === true) {
+ // Yay! We can download!
+ return true;
+ }
+ if (allByMyself.data === false) {
+ // Nope, downloading not allowed...
+ return false;
+ }
+
+
+ // At this point, we don't have enough information yet to decide if the user is allowed to delete the current point...
+ return undefined;
+ }, [allByMyself])
+
+
+ const hasRelations: UIEventSource = new UIEventSource(null)
+ const hasWays: UIEventSource = new UIEventSource(null)
+ deletetionAllowed.addCallbackAndRunD(deletetionAllowed => {
+
+ if (deletetionAllowed === false) {
+ // Nope, we are not allowed to delete
+ state.setData({
+ canBeDeleted: false,
+ reason: t.notEnoughExperience
+ })
+ return;
+ }
+
+
+ // All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations
+ OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD(rels => {
+ hasRelations.setData(rels.length > 0)
+ })
+
+ OsmObject.DownloadReferencingWays(id).addCallbackAndRunD(ways => {
+ hasWays.setData(ways.length > 0)
+ })
+ })
+
+
+ const hasWaysOrRelations = hasRelations.map(hasRelationsData => {
+ if (hasRelationsData === true) {
+ return true;
+ }
+ if (hasWays.data === true) {
+ return true;
+ }
+ if (hasWays.data === false && hasRelationsData === false) {
+ return false;
+ }
+ return null;
+ }, [hasWays])
+
+ hasWaysOrRelations.addCallbackAndRun(
+ waysOrRelations => {
+ if (waysOrRelations == null) {
+ // Not yet loaded - we still wait a little bit
+ return;
+ }
+ if (waysOrRelations) {
+ // not deleteble by mapcomplete
+ state.setData({
+ canBeDeleted: false,
+ reason: t.partOfOthers
+ })
+ }
+
+ // alright, this point can be safely deleted!
+ state.setData({
+ canBeDeleted: true,
+ reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete
+ })
+
+ }
+ )
+
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts
index 0ef3c5ba2..a3df9be9f 100644
--- a/Logic/Osm/OsmConnection.ts
+++ b/Logic/Osm/OsmConnection.ts
@@ -8,6 +8,7 @@ import Svg from "../../Svg";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import Img from "../../UI/Base/Img";
import {Utils} from "../../Utils";
+import {OsmObject} from "./OsmObject";
export default class UserDetails {
@@ -20,6 +21,11 @@ export default class UserDetails {
public totalMessages = 0;
public dryRun: boolean;
home: { lon: number; lat: number };
+ public backend: string;
+
+ constructor(backend: string) {
+ this.backend = backend;
+ }
}
export class OsmConnection {
@@ -62,9 +68,10 @@ export class OsmConnection {
this._singlePage = singlePage;
this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm;
console.debug("Using backend", this._oauth_config.url)
+ OsmObject.SetBackendUrl(this._oauth_config.url + "/")
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
- this.userDetails = new UIEventSource(new UserDetails(), "userDetails");
+ this.userDetails = new UIEventSource(new UserDetails(this._oauth_config.url), "userDetails");
this.userDetails.data.dryRun = dryRun;
const self =this;
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
@@ -103,10 +110,8 @@ export class OsmConnection {
public UploadChangeset(
layout: LayoutConfig,
allElements: ElementStorage,
- generateChangeXML: (csid: string) => string,
- continuation: () => void = () => {
- }) {
- this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, continuation);
+ generateChangeXML: (csid: string) => string) {
+ this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML);
}
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource {
diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts
index b09f515d0..b9d1b9076 100644
--- a/Logic/Osm/OsmObject.ts
+++ b/Logic/Osm/OsmObject.ts
@@ -1,10 +1,17 @@
import * as $ from "jquery"
import {Utils} from "../../Utils";
import * as polygon_features from "../../assets/polygon-features.json";
+import {UIEventSource} from "../UIEventSource";
export abstract class OsmObject {
+ protected static backendURL = "https://www.openstreetmap.org/"
+ private static polygonFeatures = OsmObject.constructPolygonFeatures()
+ private static objectCache = new Map>();
+ private static referencingWaysCache = new Map>();
+ private static referencingRelationsCache = new Map>();
+ private static historyCache = new Map>();
type: string;
id: number;
tags: {} = {};
@@ -12,9 +19,6 @@ export abstract class OsmObject {
public changed: boolean = false;
timestamp: Date;
-
- private static polygonFeatures = OsmObject.constructPolygonFeatures()
-
protected constructor(type: string, id: number) {
this.id = id;
this.type = type;
@@ -23,67 +27,99 @@ export abstract class OsmObject {
}
}
- static DownloadObject(id, continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
+ public static SetBackendUrl(url: string) {
+ if (!url.endsWith("/")) {
+ throw "Backend URL must end with a '/'"
+ }
+ if (!url.startsWith("http")) {
+ throw "Backend URL must begin with http"
+ }
+ this.backendURL = url;
+ }
+
+ static DownloadObject(id): UIEventSource {
+ if (OsmObject.objectCache.has(id)) {
+ return OsmObject.objectCache.get(id)
+ }
const splitted = id.split("/");
const type = splitted[0];
const idN = splitted[1];
- const newContinuation = (element: OsmObject, meta: OsmObjectMeta) => {
- continuation(element, meta);
+ const src = new UIEventSource(undefined)
+ OsmObject.objectCache.set(id, src);
+ const newContinuation = (element: OsmObject) => {
+ src.setData(element)
}
switch (type) {
case("node"):
- return new OsmNode(idN).Download(newContinuation);
+ new OsmNode(idN).Download(newContinuation);
+ break;
case("way"):
- return new OsmWay(idN).Download(newContinuation);
+ new OsmWay(idN).Download(newContinuation);
+ break;
case("relation"):
- return new OsmRelation(idN).Download(newContinuation);
+ new OsmRelation(idN).Download(newContinuation);
+ break;
}
+ return src;
}
/**
* Downloads the ways that are using this node.
* Beware: their geometry will be incomplete!
- * @param id
- * @param continuation
- * @constructor
*/
- public static DownloadReferencingWays(id: string, continuation: (referencingWays: OsmWay[]) => void){
- Utils.downloadJson(`https://www.openStreetMap.org/api/0.6/${id}/ways`)
+ public static DownloadReferencingWays(id: string): UIEventSource {
+ if (OsmObject.referencingWaysCache.has(id)) {
+ return OsmObject.referencingWaysCache.get(id);
+ }
+ const waysSrc = new UIEventSource([])
+ OsmObject.referencingWaysCache.set(id, waysSrc);
+ Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/ways`)
.then(data => {
- const ways = data.elements.map(wayInfo => {
+ const ways = data.elements.map(wayInfo => {
const way = new OsmWay(wayInfo.id)
way.LoadData(wayInfo)
return way
})
- continuation(ways)
+ waysSrc.setData(ways)
})
+ return waysSrc;
}
+
/**
* Downloads the relations that are using this feature.
* Beware: their geometry will be incomplete!
- * @param id
- * @param continuation
- * @constructor
*/
- public static DownloadReferencingRelations(id: string, continuation: (referencingRelations: OsmRelation[]) => void){
- Utils.downloadJson(`https://www.openStreetMap.org/api/0.6/${id}/relations`)
+ public static DownloadReferencingRelations(id: string): UIEventSource {
+ if (OsmObject.referencingRelationsCache.has(id)) {
+ return OsmObject.referencingRelationsCache.get(id);
+ }
+ const relsSrc = new UIEventSource([])
+ OsmObject.referencingRelationsCache.set(id, relsSrc);
+ Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`)
.then(data => {
const rels = data.elements.map(wayInfo => {
const rel = new OsmRelation(wayInfo.id)
rel.LoadData(wayInfo)
return rel
})
- continuation(rels)
+ relsSrc.setData(rels)
})
+ return relsSrc;
}
- public static DownloadHistory(id: string, continuation: (versions: OsmObject[]) => void): void{
+
+ public static DownloadHistory(id: string): UIEventSource {
+ if (OsmObject.historyCache.has(id)) {
+ return OsmObject.historyCache.get(id)
+ }
const splitted = id.split("/");
const type = splitted[0];
const idN = splitted[1];
- $.getJSON("https://www.openStreetMap.org/api/0.6/" + type + "/" + idN + "/history", data => {
+ const src = new UIEventSource([]);
+ OsmObject.historyCache.set(id, src);
+ Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`).then(data => {
const elements: any[] = data.elements;
const osmObjects: OsmObject[] = []
for (const element of elements) {
@@ -103,30 +139,42 @@ export abstract class OsmObject {
osmObject?.SaveExtraData(element, []);
osmObjects.push(osmObject)
}
- continuation(osmObjects)
+ src.setData(osmObjects)
+ })
+ return src;
+ }
+
+ // 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 = `${OsmObject.backendURL}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);
+
})
}
- private static constructPolygonFeatures(): Map, blacklist: boolean }> {
- const result = new Map, blacklist: boolean }>();
+ public static DownloadAll(neededIds): UIEventSource {
+ // local function which downloads all the objects one by one
+ // this is one big loop, running one download, then rerunning the entire function
- for (const polygonFeature of polygon_features) {
- const key = polygonFeature.key;
-
- if (polygonFeature.polygon === "all") {
- result.set(key, {values: null, blacklist: false})
- continue
+ const allSources: UIEventSource [] = neededIds.map(id => OsmObject.DownloadObject(id))
+ const allCompleted = new UIEventSource(undefined).map(_ => {
+ return !allSources.some(uiEventSource => uiEventSource.data === undefined)
+ }, allSources)
+ return allCompleted.map(completed => {
+ if (completed) {
+ return allSources.map(src => src.data)
}
-
- const blacklist = polygonFeature.polygon === "blacklist"
- result.set(key, {values: new Set(polygonFeature.values), blacklist: blacklist})
-
- }
-
- return result;
+ return []
+ });
}
-
protected static isPolygon(tags: any): boolean {
for (const tagsKey in tags) {
if (!tags.hasOwnProperty(tagsKey)) {
@@ -145,43 +193,23 @@ export abstract class 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);
+ private static constructPolygonFeatures(): Map, blacklist: boolean }> {
+ const result = new Map, blacklist: boolean }>();
- })
- }
+ for (const polygonFeature of polygon_features) {
+ const key = polygonFeature.key;
- //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);
+ if (polygonFeature.polygon === "all") {
+ result.set(key, {values: null, blacklist: false})
+ continue
}
- );
+
+ const blacklist = polygonFeature.polygon === "blacklist"
+ result.set(key, {values: new Set(polygonFeature.values), blacklist: blacklist})
+
+ }
+
+ return result;
}
private static ParseObjects(elements: any[]): OsmObject[] {
@@ -245,7 +273,7 @@ export abstract class OsmObject {
Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
const self = this;
const full = this.type !== "way" ? "" : "/full";
- const url = "https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id + full;
+ const url = `${OsmObject.backendURL}api/0.6/${this.type}/${this.id}${full}`;
$.getJSON(url, function (data) {
const element = data.elements.pop();
diff --git a/UI/BigComponents/UserBadge.ts b/UI/BigComponents/UserBadge.ts
index 26214661a..6b830a5c6 100644
--- a/UI/BigComponents/UserBadge.ts
+++ b/UI/BigComponents/UserBadge.ts
@@ -56,7 +56,7 @@ export default class UserBadge extends Toggle {
let messageSpan =
new Link(
new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle),
- 'https://www.openstreetmap.org/messages/inbox',
+ `${user.backend}/messages/inbox`,
true
)
@@ -64,14 +64,14 @@ export default class UserBadge extends Toggle {
const csCount =
new Link(
new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle),
- `https://www.openstreetmap.org/user/${user.name}/history`,
+ `${user.backend}/user/${user.name}/history`,
true);
if (user.unreadMessages > 0) {
messageSpan = new Link(
new Combine([Svg.envelope, "" + user.unreadMessages]),
- 'https://www.openstreetmap.org/messages/inbox',
+ '${user.backend}/messages/inbox',
true
).SetClass("alert")
}
@@ -83,22 +83,22 @@ export default class UserBadge extends Toggle {
const settings =
new Link(Svg.gear_svg(),
- `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`,
+ `${user.backend}/user/${encodeURIComponent(user.name)}/account`,
true)
const userIcon = new Link(
- new Img(user.img)
+ user.img === undefined ? Svg.osm_logo_ui() : new Img(user.img)
.SetClass("rounded-full opacity-0 m-0 p-0 duration-500 w-16 h16 float-left")
,
- `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}`,
+ `${user.backend}/user/${encodeURIComponent(user.name)}`,
true
);
const userName = new Link(
new FixedUiElement(user.name),
- `https://www.openstreetmap.org/user/${user.name}`,
+ `${user.backend}/user/${user.name}`,
true);
diff --git a/UI/OpeningHours/PublicHolidayInput.ts b/UI/OpeningHours/PublicHolidayInput.ts
index dd3c7b9f5..8c1049a29 100644
--- a/UI/OpeningHours/PublicHolidayInput.ts
+++ b/UI/OpeningHours/PublicHolidayInput.ts
@@ -17,7 +17,7 @@ export default class PublicHolidayInput extends InputElement {
this._value = value;
}
-
+
GetValue(): UIEventSource {
return this._value;
}
@@ -25,18 +25,56 @@ export default class PublicHolidayInput extends InputElement {
IsValid(t: string): boolean {
return true;
}
-
+
+ protected InnerConstructElement(): HTMLElement {
+ const dropdown = new DropDown(
+ Translations.t.general.opening_hours.open_during_ph.Clone(),
+ [
+ {shown: Translations.t.general.opening_hours.ph_not_known.Clone(), value: ""},
+ {shown: Translations.t.general.opening_hours.ph_closed.Clone(), value: "off"},
+ {shown: Translations.t.general.opening_hours.ph_open_as_usual.Clone(), value: "open"},
+ {shown: Translations.t.general.opening_hours.ph_open.Clone(), value: " "},
+ ]
+ ).SetClass("inline-block");
+ /*
+ * Either "" (unknown), " " (opened) or "off" (closed)
+ * */
+ const mode = dropdown.GetValue();
+
+
+ const start = new TextField({
+ placeholder: "starthour",
+ htmlType: "time"
+ }).SetClass("inline-block");
+ const end = new TextField({
+ placeholder: "starthour",
+ htmlType: "time"
+ }).SetClass("inline-block");
+
+ const askHours = new Toggle(
+ new Combine([
+ Translations.t.general.opening_hours.opensAt.Clone(),
+ start,
+ Translations.t.general.opening_hours.openTill.Clone(),
+ end
+ ]),
+ undefined,
+ mode.map(mode => mode === " ")
+ )
+
+ this.SetupDataSync(mode, start.GetValue(), end.GetValue())
+
+ return new Combine([
+ dropdown,
+ askHours
+ ]).ConstructElement()
+ }
+
private SetupDataSync(mode: UIEventSource, startTime: UIEventSource, endTime: UIEventSource) {
const value = this._value;
- value.addCallbackAndRun(ph => {
- if (ph === undefined) {
- return;
- }
- const parsed = OH.ParsePHRule(ph);
- if (parsed === null) {
- return;
- }
+ value.map(ph => OH.ParsePHRule(ph))
+ .addCallbackAndRunD(parsed => {
mode.setData(parsed.mode)
startTime.setData(parsed.start)
endTime.setData(parsed.end)
@@ -72,50 +110,5 @@ export default class PublicHolidayInput extends InputElement {
}, [startTime, endTime]
)
}
-
-
- protected InnerConstructElement(): HTMLElement {
- const dropdown = new DropDown(
- Translations.t.general.opening_hours.open_during_ph.Clone(),
- [
- {shown: Translations.t.general.opening_hours.ph_not_known.Clone(), value: ""},
- {shown: Translations.t.general.opening_hours.ph_closed.Clone(), value: "off"},
- {shown:Translations.t.general.opening_hours.ph_open_as_usual.Clone(), value: "open"},
- {shown: Translations.t.general.opening_hours.ph_open.Clone(), value: " "},
- ]
- ).SetClass("inline-block");
- /*
- * Either "" (unknown), " " (opened) or "off" (closed)
- * */
- const mode = dropdown.GetValue();
-
-
- const start = new TextField({
- placeholder: "starthour",
- htmlType: "time"
- }).SetClass("inline-block");
- const end = new TextField({
- placeholder: "starthour",
- htmlType: "time"
- }).SetClass("inline-block");
-
- const askHours = new Toggle(
- new Combine([
- Translations.t.general.opening_hours.opensAt.Clone(),
- start,
- Translations.t.general.opening_hours.openTill.Clone(),
- end
- ]),
- undefined,
- mode.map(mode => mode === " ")
- )
-
- this.SetupDataSync(mode, start.GetValue(), end.GetValue())
-
- return new Combine([
- dropdown,
- askHours
- ]).ConstructElement()
- }
}
\ No newline at end of file
diff --git a/UI/Popup/DeleteButton.ts b/UI/Popup/DeleteButton.ts
index a0bc50cf3..37e0d1270 100644
--- a/UI/Popup/DeleteButton.ts
+++ b/UI/Popup/DeleteButton.ts
@@ -1,72 +1,72 @@
import {VariableUiElement} from "../Base/VariableUIElement";
-import {OsmObject} from "../../Logic/Osm/OsmObject";
-import {UIEventSource} from "../../Logic/UIEventSource";
-import {Translation} from "../i18n/Translation";
import State from "../../State";
import Toggle from "../Input/Toggle";
import Translations from "../i18n/Translations";
-import Loading from "../Base/Loading";
-import UserDetails from "../../Logic/Osm/OsmConnection";
-import Constants from "../../Models/Constants";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
-import {Utils} from "../../Utils";
+import DeleteAction from "../../Logic/Osm/DeleteAction";
+import {Tag} from "../../Logic/Tags/Tag";
+import CheckBoxes from "../Input/Checkboxes";
+import {RadioButton} from "../Input/RadioButton";
+import {FixedInputElement} from "../Input/FixedInputElement";
+import {TextField} from "../Input/TextField";
-export default class DeleteButton extends Toggle {
- constructor(id: string) {
+export default class DeleteWizard extends Toggle {
+ /**
+ * The UI-element which triggers 'deletion' (either soft or hard).
+ *
+ * - A 'hard deletion' is if the point is actually deleted from the OSM database
+ * - A 'soft deletion' is if the point is not deleted, but the tagging is modified which will result in the point not being picked up by the filters anymore.
+ * Apart having needing theme-specific tags added (which must be supplied by the theme creator), fixme='marked for deletion' will be added too
+ *
+ * A deletion is only possible if the user is logged in.
+ * A soft deletion is only possible if tags are provided
+ * A hard deletion is only possible if the user has sufficient rigts
+ *
+ * If no deletion is possible at all, the delete button will not be shown - but a reason will be shown instead.
+ *
+ * @param id: The id of the element to remove
+ * @param softDeletionTags: the tags to apply if the user doesn't have permission to delete, e.g. 'disused:amenity=public_bookcase', 'amenity='. After applying, the element should not be picked up on the map anymore. If undefined, the wizard will only show up if the point can be (hard) deleted
+ */
+ constructor(id: string, softDeletionTags? : Tag[]) {
+ const t = Translations.t.delete
- const hasRelations: UIEventSource = new UIEventSource(null)
- OsmObject.DownloadReferencingRelations(id, (rels) => {
- hasRelations.setData(rels.length > 0)
- })
+ const deleteAction = new DeleteAction(id);
+
+ const deleteReasons = new RadioButton(
+ [new FixedInputElement(
+ t.reasons.test, "test"
+ ),
+ new FixedInputElement(t.reasons.disused, "disused"),
+ new FixedInputElement(t.reasons.notFound, "not found"),
+ new TextField()]
+
+ )
- const hasWays: UIEventSource = new UIEventSource(null)
- OsmObject.DownloadReferencingWays(id, (ways) => {
- hasWays.setData(ways.length > 0)
- })
-
- const previousEditors = new UIEventSource(null)
- OsmObject.DownloadHistory(id, versions => {
- const uids = versions.map(version => version.tags["_last_edit:contributor:uid"])
- previousEditors.setData(uids)
- })
- const allByMyself = previousEditors.map(previous => {
- if (previous === null) {
- return null;
- }
- const userId = State.state.osmConnection.userDetails.data.uid;
- return !previous.some(editor => editor !== userId)
- }, [State.state.osmConnection.userDetails])
-
- const t = Translations.t.deleteButton
+ const deleteButton = new SubtleButton(
+ Svg.delete_icon_svg(),
+ t.delete.Clone()
+ ).onClick(() => deleteAction.DoDelete(deleteReasons.GetValue().data))
+
+
+
super(
+
+
+
new Toggle(
- new VariableUiElement(
- hasRelations.map(hasRelations => {
- if (hasRelations === null || hasWays.data === null) {
- return new Loading()
- }
- if (hasWays.data || hasRelations) {
- return t.partOfOthers.Clone()
- }
+ deleteButton,
+ new VariableUiElement(deleteAction.canBeDeleted.map(cbd => cbd.reason.Clone())),
+ deleteAction.canBeDeleted.map(cbd => cbd.canBeDeleted)
+ ),
+
+
+
+ t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin),
+ State.state.osmConnection.isLoggedIn
+ )
- return new Toggle(
- new SubtleButton(Svg.delete_icon_svg(), t.delete.Clone()),
- t.notEnoughExperience.Clone(),
- State.state.osmConnection.userDetails.map(userinfo =>
- allByMyself.data ||
- userinfo.csCount >= Constants.userJourney.deletePointsOfOthersUnlock,
- [allByMyself])
- )
-
- }, [hasWays])
- ),
- t.onlyEditedByLoggedInUser.Clone().onClick(State.state.osmConnection.AttemptLogin),
- State.state.osmConnection.isLoggedIn),
- t.isntAPoint,
- new UIEventSource(id.startsWith("node"))
- );
}
}
\ No newline at end of file
diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts
index a5fe60199..97dcbefda 100644
--- a/UI/Popup/FeatureInfoBox.ts
+++ b/UI/Popup/FeatureInfoBox.ts
@@ -69,7 +69,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
if (!hasMinimap) {
renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap")))
}
-
+
renderings.push(
new VariableUiElement(
State.state.osmConnection.userDetails.map(userdetails => {
diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts
index 92568e9fb..4fc48f2cc 100644
--- a/UI/Popup/TagRenderingQuestion.ts
+++ b/UI/Popup/TagRenderingQuestion.ts
@@ -35,7 +35,7 @@ export default class TagRenderingQuestion extends Combine {
configuration: TagRenderingConfig,
units: Unit[],
afterSave?: () => void,
- cancelButton?: BaseUIElement
+ cancelButton?: BaseUIElement,
) {
if (configuration === undefined) {
throw "A question is needed for a question visualization"
diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts
index 6aa8407c9..5c55bb3b4 100644
--- a/UI/ShowDataLayer.ts
+++ b/UI/ShowDataLayer.ts
@@ -62,6 +62,9 @@ export default class ShowDataLayer {
const allFeats = features.data.map(ff => ff.feature);
geoLayer = self.CreateGeojsonLayer();
for (const feat of allFeats) {
+ if(feat === undefined){
+ continue
+ }
// @ts-ignore
geoLayer.addData(feat);
}
@@ -76,7 +79,13 @@ export default class ShowDataLayer {
}
if (zoomToFeatures) {
+ try{
+
mp.fitBounds(geoLayer.getBounds())
+
+ }catch(e){
+ console.error(e)
+ }
}
@@ -169,8 +178,8 @@ export default class ShowDataLayer {
infobox.Activate();
});
const self = this;
- State.state.selectedElement.addCallbackAndRun(selected => {
- if (selected === undefined || self._leafletMap.data === undefined) {
+ State.state.selectedElement.addCallbackAndRunD(selected => {
+ if ( self._leafletMap.data === undefined) {
return;
}
if (leafletLayer.getPopup().isOpen()) {
diff --git a/index.ts b/index.ts
index ccba3d5ac..70b06bf30 100644
--- a/index.ts
+++ b/index.ts
@@ -45,7 +45,8 @@ if (location.href.indexOf("buurtnatuur.be") >= 0) {
let testing: UIEventSource;
-if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
+if (QueryParameters.GetQueryParameter("backend", undefined).data !== "osm-test" &&
+ (location.hostname === "localhost" || location.hostname === "127.0.0.1")) {
testing = QueryParameters.GetQueryParameter("test", "true");
// Set to true if testing and changes should NOT be saved
testing.setData(testing.data ?? "true")
diff --git a/langs/en.json b/langs/en.json
index 0c574bca7..9ec787501 100644
--- a/langs/en.json
+++ b/langs/en.json
@@ -27,14 +27,20 @@
"intro": "MapComplete is an OpenStreetMap-viewer and editor, which shows you information about a specific theme.",
"pickTheme": "Pick a theme below to get started."
},
- "deleteButton": {
+ "delete": {
"delete": "Delete",
"loginToDelete": "You must be logged in to delete a point",
- "checkingDeletability": "Inspecting properties to check if this feature can be deleted",
+ "safeDelete": "This point can be safely deleted",
"isntAPoint": "Only points can be deleted",
"onlyEditedByLoggedInUser": "This point has only be edited by yourself, you can safely delete it",
"notEnoughExperience": "You don't have enough experience to delete points made by other people. Make more edits to improve your skills",
- "partOfOthers": "This point is part of some way or relation, so you can not delete it"
+ "partOfOthers": "This point is part of some way or relation, so you can not delete it",
+ "loading": "Inspecting properties to check if this feature can be deleted",
+ "reasons": {
+ "test": "This was a testing point - the feature was never actually there",
+ "disused": "This feature is disused or removed",
+ "notFound": "This feature couldn't be found"
+ }
},
"general": {
"loginWithOpenStreetMap": "Login with OpenStreetMap",