Refactor OsmObject to use eventsources, add first version of the delete button
This commit is contained in:
parent
ec7833b2ee
commit
bbfcee686f
15 changed files with 553 additions and 229 deletions
|
@ -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);
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
|
||||
changes +=
|
||||
`<delete><${object.type} id="${object.id}" version="${object.version}" changeset="${csId}" lat="${lat}" lon="${lon}" /></delete>`;
|
||||
changes += "</osmChange>";
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
207
Logic/Osm/DeleteAction.ts
Normal file
207
Logic/Osm/DeleteAction.ts
Normal file
|
@ -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<boolean> {
|
||||
const isDeleted = new UIEventSource<boolean>(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<number[]>(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<boolean> = new UIEventSource<boolean>(null)
|
||||
const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(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
|
||||
})
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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<UserDetails>(new UserDetails(), "userDetails");
|
||||
this.userDetails = new UIEventSource<UserDetails>(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<string> {
|
||||
|
|
|
@ -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<string, UIEventSource<OsmObject>>();
|
||||
private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>();
|
||||
private static referencingRelationsCache = new Map<string, UIEventSource<OsmRelation[]>>();
|
||||
private static historyCache = new Map<string, UIEventSource<OsmObject[]>>();
|
||||
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<OsmObject> {
|
||||
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<OsmObject>(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<OsmWay[]> {
|
||||
if (OsmObject.referencingWaysCache.has(id)) {
|
||||
return OsmObject.referencingWaysCache.get(id);
|
||||
}
|
||||
const waysSrc = new UIEventSource<OsmWay[]>([])
|
||||
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<OsmRelation[]> {
|
||||
if (OsmObject.referencingRelationsCache.has(id)) {
|
||||
return OsmObject.referencingRelationsCache.get(id);
|
||||
}
|
||||
const relsSrc = new UIEventSource<OsmRelation[]>([])
|
||||
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<OsmObject []> {
|
||||
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[]>([]);
|
||||
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<string, { values: Set<string>, blacklist: boolean }> {
|
||||
const result = new Map<string, { values: Set<string>, blacklist: boolean }>();
|
||||
public static DownloadAll(neededIds): UIEventSource<OsmObject[]> {
|
||||
// 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<OsmObject> [] = 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<string>(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<string, { values: Set<string>, blacklist: boolean }> {
|
||||
const result = new Map<string, { values: Set<string>, 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<string>(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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ export default class PublicHolidayInput extends InputElement<string> {
|
|||
this._value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this._value;
|
||||
}
|
||||
|
@ -25,18 +25,56 @@ export default class PublicHolidayInput extends InputElement<string> {
|
|||
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<string>, startTime: UIEventSource<string>, endTime: UIEventSource<string>) {
|
||||
|
||||
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<string> {
|
|||
}, [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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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<boolean> = new UIEventSource<boolean>(null)
|
||||
OsmObject.DownloadReferencingRelations(id, (rels) => {
|
||||
hasRelations.setData(rels.length > 0)
|
||||
})
|
||||
const deleteAction = new DeleteAction(id);
|
||||
|
||||
const deleteReasons = new RadioButton<string>(
|
||||
[new FixedInputElement(
|
||||
t.reasons.test, "test"
|
||||
),
|
||||
new FixedInputElement(t.reasons.disused, "disused"),
|
||||
new FixedInputElement(t.reasons.notFound, "not found"),
|
||||
new TextField()]
|
||||
|
||||
)
|
||||
|
||||
const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null)
|
||||
OsmObject.DownloadReferencingWays(id, (ways) => {
|
||||
hasWays.setData(ways.length > 0)
|
||||
})
|
||||
|
||||
const previousEditors = new UIEventSource<number[]>(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<boolean>(id.startsWith("node"))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()) {
|
||||
|
|
3
index.ts
3
index.ts
|
@ -45,7 +45,8 @@ if (location.href.indexOf("buurtnatuur.be") >= 0) {
|
|||
|
||||
|
||||
let testing: UIEventSource<string>;
|
||||
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")
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue