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)
|
State.state.osmConnection.userDetails.map((userDetails: UserDetails) => userDetails?.home)
|
||||||
.addCallbackAndRun(home => {
|
.addCallbackAndRunD(home => {
|
||||||
if (home === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color")
|
const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color")
|
||||||
const icon = L.icon({
|
const icon = L.icon({
|
||||||
iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)),
|
iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)),
|
||||||
|
@ -286,10 +283,8 @@ export class InitUiElements {
|
||||||
isOpened.setData(false);
|
isOpened.setData(false);
|
||||||
})
|
})
|
||||||
|
|
||||||
State.state.selectedElement.addCallbackAndRun(selected => {
|
State.state.selectedElement.addCallbackAndRunD(_ => {
|
||||||
if (selected !== undefined) {
|
|
||||||
isOpened.setData(false);
|
isOpened.setData(false);
|
||||||
}
|
|
||||||
})
|
})
|
||||||
isOpened.setData(Hash.hash.data === undefined || Hash.hash.data === "" || Hash.hash.data == "welcome")
|
isOpened.setData(Hash.hash.data === undefined || Hash.hash.data === "" || Hash.hash.data == "welcome")
|
||||||
}
|
}
|
||||||
|
@ -337,11 +332,9 @@ export class InitUiElements {
|
||||||
copyrightButton.isEnabled.setData(false);
|
copyrightButton.isEnabled.setData(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
State.state.selectedElement.addCallbackAndRun(feature => {
|
State.state.selectedElement.addCallbackAndRunD(_ => {
|
||||||
if (feature !== undefined) {
|
|
||||||
layerControlButton.isEnabled.setData(false);
|
layerControlButton.isEnabled.setData(false);
|
||||||
copyrightButton.isEnabled.setData(false);
|
copyrightButton.isEnabled.setData(false);
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default class SelectedFeatureHandler {
|
||||||
return; // No valid feature selected
|
return; // No valid feature selected
|
||||||
}
|
}
|
||||||
// We should have a valid osm-ID and zoom to it
|
// We should have a valid osm-ID and zoom to it
|
||||||
OsmObject.DownloadObject(hash, (element: OsmObject, meta: OsmObjectMeta) => {
|
OsmObject.DownloadObject(hash).addCallbackAndRunD(element => {
|
||||||
const centerpoint = element.centerpoint();
|
const centerpoint = element.centerpoint();
|
||||||
console.log("Zooming to location for select point: ", centerpoint)
|
console.log("Zooming to location for select point: ", centerpoint)
|
||||||
location.data.lat = centerpoint[0]
|
location.data.lat = centerpoint[0]
|
||||||
|
|
|
@ -228,9 +228,9 @@ export class Changes implements FeatureSource{
|
||||||
}
|
}
|
||||||
|
|
||||||
neededIds = Utils.Dedup(neededIds);
|
neededIds = Utils.Dedup(neededIds);
|
||||||
OsmObject.DownloadAll(neededIds, {}, (knownElements) => {
|
OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => {
|
||||||
self.uploadChangesWithLatestVersions(knownElements, newElements, pending)
|
self.uploadChangesWithLatestVersions(knownElements, newElements, pending)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,6 +7,7 @@ import State from "../../State";
|
||||||
import Locale from "../../UI/i18n/Locale";
|
import Locale from "../../UI/i18n/Locale";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
|
import {OsmObject} from "./OsmObject";
|
||||||
|
|
||||||
export class ChangesetHandler {
|
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(
|
public UploadChangeset(
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
generateChangeXML: (csid: string) => string,
|
generateChangeXML: (csid: string) => string) {
|
||||||
continuation: () => void) {
|
|
||||||
|
|
||||||
if (this.userDetails.data.csCount == 0) {
|
if (this.userDetails.data.csCount == 0) {
|
||||||
// The user became a contributor!
|
// The user became a contributor!
|
||||||
|
@ -62,7 +72,6 @@ export class ChangesetHandler {
|
||||||
if (this._dryRun) {
|
if (this._dryRun) {
|
||||||
const changesetXML = generateChangeXML("123456");
|
const changesetXML = generateChangeXML("123456");
|
||||||
console.log(changesetXML);
|
console.log(changesetXML);
|
||||||
continuation();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +106,7 @@ export class ChangesetHandler {
|
||||||
// Mark the CS as closed...
|
// Mark the CS as closed...
|
||||||
this.currentChangeset.setData("");
|
this.currentChangeset.setData("");
|
||||||
// ... and try again. As the cs is closed, no recursive loop can exist
|
// ... 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) {
|
if (changesetId === undefined) {
|
||||||
changesetId = this.currentChangeset.data;
|
changesetId = this.currentChangeset.data;
|
||||||
|
@ -133,15 +195,25 @@ export class ChangesetHandler {
|
||||||
|
|
||||||
private OpenChangeset(
|
private OpenChangeset(
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
continuation: (changesetId: string) => void) {
|
continuation: (changesetId: string) => void,
|
||||||
|
isDeletionCS: boolean = false,
|
||||||
|
deletionReason: string = undefined) {
|
||||||
|
|
||||||
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
|
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;
|
let path = window.location.pathname;
|
||||||
path = path.substr(1, path.lastIndexOf("/"));
|
path = path.substr(1, path.lastIndexOf("/"));
|
||||||
const metadata = [
|
const metadata = [
|
||||||
["created_by", `MapComplete ${Constants.vNumber}`],
|
["created_by", `MapComplete ${Constants.vNumber}`],
|
||||||
["comment", `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`],
|
["comment", comment],
|
||||||
|
["deletion", isDeletionCS ? "yes" : undefined],
|
||||||
["theme", layout.id],
|
["theme", layout.id],
|
||||||
["language", Locale.language.data],
|
["language", Locale.language.data],
|
||||||
["host", window.location.host],
|
["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,
|
private AddChange(changesetId: string,
|
||||||
changesetXML: string,
|
changesetXML: string,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
continuation: ((changesetId: string, idMapping: any) => void),
|
continuation: ((changesetId: string, idMapping: any) => void),
|
||||||
onFail: ((changesetId: string) => void) = undefined) {
|
onFail: ((changesetId: string, reason: string) => void) = undefined) {
|
||||||
this.auth.xhr({
|
this.auth.xhr({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
options: {header: {'Content-Type': 'text/xml'}},
|
options: {header: {'Content-Type': 'text/xml'}},
|
||||||
|
@ -186,7 +268,7 @@ export class ChangesetHandler {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
console.log("err", err);
|
console.log("err", err);
|
||||||
if (onFail) {
|
if (onFail) {
|
||||||
onFail(changesetId);
|
onFail(changesetId, err);
|
||||||
}
|
}
|
||||||
return;
|
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 LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import Img from "../../UI/Base/Img";
|
import Img from "../../UI/Base/Img";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
|
import {OsmObject} from "./OsmObject";
|
||||||
|
|
||||||
export default class UserDetails {
|
export default class UserDetails {
|
||||||
|
|
||||||
|
@ -20,6 +21,11 @@ export default class UserDetails {
|
||||||
public totalMessages = 0;
|
public totalMessages = 0;
|
||||||
public dryRun: boolean;
|
public dryRun: boolean;
|
||||||
home: { lon: number; lat: number };
|
home: { lon: number; lat: number };
|
||||||
|
public backend: string;
|
||||||
|
|
||||||
|
constructor(backend: string) {
|
||||||
|
this.backend = backend;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OsmConnection {
|
export class OsmConnection {
|
||||||
|
@ -62,9 +68,10 @@ export class OsmConnection {
|
||||||
this._singlePage = singlePage;
|
this._singlePage = singlePage;
|
||||||
this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm;
|
this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm;
|
||||||
console.debug("Using backend", this._oauth_config.url)
|
console.debug("Using backend", this._oauth_config.url)
|
||||||
|
OsmObject.SetBackendUrl(this._oauth_config.url + "/")
|
||||||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
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;
|
this.userDetails.data.dryRun = dryRun;
|
||||||
const self =this;
|
const self =this;
|
||||||
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
|
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
|
||||||
|
@ -103,10 +110,8 @@ export class OsmConnection {
|
||||||
public UploadChangeset(
|
public UploadChangeset(
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
generateChangeXML: (csid: string) => string,
|
generateChangeXML: (csid: string) => string) {
|
||||||
continuation: () => void = () => {
|
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML);
|
||||||
}) {
|
|
||||||
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, continuation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
import * as $ from "jquery"
|
import * as $ from "jquery"
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import * as polygon_features from "../../assets/polygon-features.json";
|
import * as polygon_features from "../../assets/polygon-features.json";
|
||||||
|
import {UIEventSource} from "../UIEventSource";
|
||||||
|
|
||||||
|
|
||||||
export abstract class OsmObject {
|
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;
|
type: string;
|
||||||
id: number;
|
id: number;
|
||||||
tags: {} = {};
|
tags: {} = {};
|
||||||
|
@ -12,9 +19,6 @@ export abstract class OsmObject {
|
||||||
public changed: boolean = false;
|
public changed: boolean = false;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
|
|
||||||
|
|
||||||
private static polygonFeatures = OsmObject.constructPolygonFeatures()
|
|
||||||
|
|
||||||
protected constructor(type: string, id: number) {
|
protected constructor(type: string, id: number) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.type = type;
|
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 splitted = id.split("/");
|
||||||
const type = splitted[0];
|
const type = splitted[0];
|
||||||
const idN = splitted[1];
|
const idN = splitted[1];
|
||||||
|
|
||||||
const newContinuation = (element: OsmObject, meta: OsmObjectMeta) => {
|
const src = new UIEventSource<OsmObject>(undefined)
|
||||||
continuation(element, meta);
|
OsmObject.objectCache.set(id, src);
|
||||||
|
const newContinuation = (element: OsmObject) => {
|
||||||
|
src.setData(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case("node"):
|
case("node"):
|
||||||
return new OsmNode(idN).Download(newContinuation);
|
new OsmNode(idN).Download(newContinuation);
|
||||||
|
break;
|
||||||
case("way"):
|
case("way"):
|
||||||
return new OsmWay(idN).Download(newContinuation);
|
new OsmWay(idN).Download(newContinuation);
|
||||||
|
break;
|
||||||
case("relation"):
|
case("relation"):
|
||||||
return new OsmRelation(idN).Download(newContinuation);
|
new OsmRelation(idN).Download(newContinuation);
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
return src;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads the ways that are using this node.
|
* Downloads the ways that are using this node.
|
||||||
* Beware: their geometry will be incomplete!
|
* Beware: their geometry will be incomplete!
|
||||||
* @param id
|
|
||||||
* @param continuation
|
|
||||||
* @constructor
|
|
||||||
*/
|
*/
|
||||||
public static DownloadReferencingWays(id: string, continuation: (referencingWays: OsmWay[]) => void){
|
public static DownloadReferencingWays(id: string): UIEventSource<OsmWay[]> {
|
||||||
Utils.downloadJson(`https://www.openStreetMap.org/api/0.6/${id}/ways`)
|
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 => {
|
.then(data => {
|
||||||
const ways = data.elements.map(wayInfo => {
|
const ways = data.elements.map(wayInfo => {
|
||||||
const way = new OsmWay(wayInfo.id)
|
const way = new OsmWay(wayInfo.id)
|
||||||
way.LoadData(wayInfo)
|
way.LoadData(wayInfo)
|
||||||
return way
|
return way
|
||||||
})
|
})
|
||||||
continuation(ways)
|
waysSrc.setData(ways)
|
||||||
})
|
})
|
||||||
|
return waysSrc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads the relations that are using this feature.
|
* Downloads the relations that are using this feature.
|
||||||
* Beware: their geometry will be incomplete!
|
* Beware: their geometry will be incomplete!
|
||||||
* @param id
|
|
||||||
* @param continuation
|
|
||||||
* @constructor
|
|
||||||
*/
|
*/
|
||||||
public static DownloadReferencingRelations(id: string, continuation: (referencingRelations: OsmRelation[]) => void){
|
public static DownloadReferencingRelations(id: string): UIEventSource<OsmRelation[]> {
|
||||||
Utils.downloadJson(`https://www.openStreetMap.org/api/0.6/${id}/relations`)
|
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 => {
|
.then(data => {
|
||||||
const rels = data.elements.map(wayInfo => {
|
const rels = data.elements.map(wayInfo => {
|
||||||
const rel = new OsmRelation(wayInfo.id)
|
const rel = new OsmRelation(wayInfo.id)
|
||||||
rel.LoadData(wayInfo)
|
rel.LoadData(wayInfo)
|
||||||
return rel
|
return rel
|
||||||
})
|
})
|
||||||
continuation(rels)
|
relsSrc.setData(rels)
|
||||||
})
|
})
|
||||||
|
return relsSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloadHistory(id: string): UIEventSource<OsmObject []> {
|
||||||
|
if (OsmObject.historyCache.has(id)) {
|
||||||
|
return OsmObject.historyCache.get(id)
|
||||||
}
|
}
|
||||||
public static DownloadHistory(id: string, continuation: (versions: OsmObject[]) => void): void{
|
|
||||||
const splitted = id.split("/");
|
const splitted = id.split("/");
|
||||||
const type = splitted[0];
|
const type = splitted[0];
|
||||||
const idN = splitted[1];
|
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 elements: any[] = data.elements;
|
||||||
const osmObjects: OsmObject[] = []
|
const osmObjects: OsmObject[] = []
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
|
@ -103,30 +139,42 @@ export abstract class OsmObject {
|
||||||
osmObject?.SaveExtraData(element, []);
|
osmObject?.SaveExtraData(element, []);
|
||||||
osmObjects.push(osmObject)
|
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 }> {
|
public static DownloadAll(neededIds): UIEventSource<OsmObject[]> {
|
||||||
const result = new Map<string, { values: Set<string>, blacklist: boolean }>();
|
// 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 allSources: UIEventSource<OsmObject> [] = neededIds.map(id => OsmObject.DownloadObject(id))
|
||||||
const key = polygonFeature.key;
|
const allCompleted = new UIEventSource(undefined).map(_ => {
|
||||||
|
return !allSources.some(uiEventSource => uiEventSource.data === undefined)
|
||||||
if (polygonFeature.polygon === "all") {
|
}, allSources)
|
||||||
result.set(key, {values: null, blacklist: false})
|
return allCompleted.map(completed => {
|
||||||
continue
|
if (completed) {
|
||||||
|
return allSources.map(src => src.data)
|
||||||
}
|
}
|
||||||
|
return []
|
||||||
const blacklist = polygonFeature.polygon === "blacklist"
|
});
|
||||||
result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected static isPolygon(tags: any): boolean {
|
protected static isPolygon(tags: any): boolean {
|
||||||
for (const tagsKey in tags) {
|
for (const tagsKey in tags) {
|
||||||
if (!tags.hasOwnProperty(tagsKey)) {
|
if (!tags.hasOwnProperty(tagsKey)) {
|
||||||
|
@ -145,43 +193,23 @@ export abstract class OsmObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
|
private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> {
|
||||||
public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) {
|
const result = new Map<string, { values: Set<string>, blacklist: boolean }>();
|
||||||
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);
|
|
||||||
|
|
||||||
})
|
for (const polygonFeature of polygon_features) {
|
||||||
|
const key = polygonFeature.key;
|
||||||
|
|
||||||
|
if (polygonFeature.polygon === "all") {
|
||||||
|
result.set(key, {values: null, blacklist: false})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
//Loads an area from the OSM-api.
|
const blacklist = polygonFeature.polygon === "blacklist"
|
||||||
|
result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist})
|
||||||
|
|
||||||
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,
|
return result;
|
||||||
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[] {
|
private static ParseObjects(elements: any[]): OsmObject[] {
|
||||||
|
@ -245,7 +273,7 @@ export abstract class OsmObject {
|
||||||
Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
|
Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
|
||||||
const self = this;
|
const self = this;
|
||||||
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 = `${OsmObject.backendURL}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();
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default class UserBadge extends Toggle {
|
||||||
let messageSpan =
|
let messageSpan =
|
||||||
new Link(
|
new Link(
|
||||||
new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle),
|
new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle),
|
||||||
'https://www.openstreetmap.org/messages/inbox',
|
`${user.backend}/messages/inbox`,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,14 +64,14 @@ export default class UserBadge extends Toggle {
|
||||||
const csCount =
|
const csCount =
|
||||||
new Link(
|
new Link(
|
||||||
new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle),
|
new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle),
|
||||||
`https://www.openstreetmap.org/user/${user.name}/history`,
|
`${user.backend}/user/${user.name}/history`,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
|
||||||
if (user.unreadMessages > 0) {
|
if (user.unreadMessages > 0) {
|
||||||
messageSpan = new Link(
|
messageSpan = new Link(
|
||||||
new Combine([Svg.envelope, "" + user.unreadMessages]),
|
new Combine([Svg.envelope, "" + user.unreadMessages]),
|
||||||
'https://www.openstreetmap.org/messages/inbox',
|
'${user.backend}/messages/inbox',
|
||||||
true
|
true
|
||||||
).SetClass("alert")
|
).SetClass("alert")
|
||||||
}
|
}
|
||||||
|
@ -83,22 +83,22 @@ export default class UserBadge extends Toggle {
|
||||||
|
|
||||||
const settings =
|
const settings =
|
||||||
new Link(Svg.gear_svg(),
|
new Link(Svg.gear_svg(),
|
||||||
`https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`,
|
`${user.backend}/user/${encodeURIComponent(user.name)}/account`,
|
||||||
true)
|
true)
|
||||||
|
|
||||||
|
|
||||||
const userIcon = new Link(
|
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")
|
.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
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const userName = new Link(
|
const userName = new Link(
|
||||||
new FixedUiElement(user.name),
|
new FixedUiElement(user.name),
|
||||||
`https://www.openstreetmap.org/user/${user.name}`,
|
`${user.backend}/user/${user.name}`,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,61 +26,13 @@ export default class PublicHolidayInput extends InputElement<string> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
mode.setData(parsed.mode)
|
|
||||||
startTime.setData(parsed.start)
|
|
||||||
endTime.setData(parsed.end)
|
|
||||||
})
|
|
||||||
|
|
||||||
// We use this as a 'addCallbackAndRun'
|
|
||||||
mode.map(mode => {
|
|
||||||
if (mode === undefined || mode === "") {
|
|
||||||
// not known
|
|
||||||
value.setData(undefined)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (mode === "off") {
|
|
||||||
value.setData("PH off");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mode === "open") {
|
|
||||||
value.setData("PH open");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Open during PH with special hours
|
|
||||||
if (startTime.data === undefined || endTime.data === undefined) {
|
|
||||||
// hours not filled in - not saveable
|
|
||||||
value.setData(undefined)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const oh = `PH ${startTime.data}-${endTime.data}`
|
|
||||||
value.setData(oh)
|
|
||||||
|
|
||||||
|
|
||||||
}, [startTime, endTime]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected InnerConstructElement(): HTMLElement {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
const dropdown = new DropDown(
|
const dropdown = new DropDown(
|
||||||
Translations.t.general.opening_hours.open_during_ph.Clone(),
|
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_not_known.Clone(), value: ""},
|
||||||
{shown: Translations.t.general.opening_hours.ph_closed.Clone(), value: "off"},
|
{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_as_usual.Clone(), value: "open"},
|
||||||
{shown: Translations.t.general.opening_hours.ph_open.Clone(), value: " "},
|
{shown: Translations.t.general.opening_hours.ph_open.Clone(), value: " "},
|
||||||
]
|
]
|
||||||
).SetClass("inline-block");
|
).SetClass("inline-block");
|
||||||
|
@ -118,4 +70,45 @@ export default class PublicHolidayInput extends InputElement<string> {
|
||||||
]).ConstructElement()
|
]).ConstructElement()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SetupDataSync(mode: UIEventSource<string>, startTime: UIEventSource<string>, endTime: UIEventSource<string>) {
|
||||||
|
|
||||||
|
const value = this._value;
|
||||||
|
value.map(ph => OH.ParsePHRule(ph))
|
||||||
|
.addCallbackAndRunD(parsed => {
|
||||||
|
mode.setData(parsed.mode)
|
||||||
|
startTime.setData(parsed.start)
|
||||||
|
endTime.setData(parsed.end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// We use this as a 'addCallbackAndRun'
|
||||||
|
mode.map(mode => {
|
||||||
|
if (mode === undefined || mode === "") {
|
||||||
|
// not known
|
||||||
|
value.setData(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mode === "off") {
|
||||||
|
value.setData("PH off");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode === "open") {
|
||||||
|
value.setData("PH open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Open during PH with special hours
|
||||||
|
if (startTime.data === undefined || endTime.data === undefined) {
|
||||||
|
// hours not filled in - not saveable
|
||||||
|
value.setData(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const oh = `PH ${startTime.data}-${endTime.data}`
|
||||||
|
value.setData(oh)
|
||||||
|
|
||||||
|
|
||||||
|
}, [startTime, endTime]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,72 +1,72 @@
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
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 State from "../../State";
|
||||||
import Toggle from "../Input/Toggle";
|
import Toggle from "../Input/Toggle";
|
||||||
import Translations from "../i18n/Translations";
|
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 {SubtleButton} from "../Base/SubtleButton";
|
||||||
import Svg from "../../Svg";
|
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 {
|
export default class DeleteWizard extends Toggle {
|
||||||
constructor(id: string) {
|
/**
|
||||||
|
* 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)
|
const deleteAction = new DeleteAction(id);
|
||||||
OsmObject.DownloadReferencingRelations(id, (rels) => {
|
|
||||||
hasRelations.setData(rels.length > 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null)
|
const deleteReasons = new RadioButton<string>(
|
||||||
OsmObject.DownloadReferencingWays(id, (ways) => {
|
[new FixedInputElement(
|
||||||
hasWays.setData(ways.length > 0)
|
t.reasons.test, "test"
|
||||||
})
|
),
|
||||||
|
new FixedInputElement(t.reasons.disused, "disused"),
|
||||||
|
new FixedInputElement(t.reasons.notFound, "not found"),
|
||||||
|
new TextField()]
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
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])
|
const deleteButton = new SubtleButton(
|
||||||
|
Svg.delete_icon_svg(),
|
||||||
|
t.delete.Clone()
|
||||||
|
).onClick(() => deleteAction.DoDelete(deleteReasons.GetValue().data))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
super(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
new Toggle(
|
||||||
|
deleteButton,
|
||||||
|
new VariableUiElement(deleteAction.canBeDeleted.map(cbd => cbd.reason.Clone())),
|
||||||
|
deleteAction.canBeDeleted.map(cbd => cbd.canBeDeleted)
|
||||||
),
|
),
|
||||||
t.onlyEditedByLoggedInUser.Clone().onClick(State.state.osmConnection.AttemptLogin),
|
|
||||||
State.state.osmConnection.isLoggedIn),
|
|
||||||
t.isntAPoint,
|
|
||||||
new UIEventSource<boolean>(id.startsWith("node"))
|
t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin),
|
||||||
);
|
State.state.osmConnection.isLoggedIn
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -35,7 +35,7 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
configuration: TagRenderingConfig,
|
configuration: TagRenderingConfig,
|
||||||
units: Unit[],
|
units: Unit[],
|
||||||
afterSave?: () => void,
|
afterSave?: () => void,
|
||||||
cancelButton?: BaseUIElement
|
cancelButton?: BaseUIElement,
|
||||||
) {
|
) {
|
||||||
if (configuration === undefined) {
|
if (configuration === undefined) {
|
||||||
throw "A question is needed for a question visualization"
|
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);
|
const allFeats = features.data.map(ff => ff.feature);
|
||||||
geoLayer = self.CreateGeojsonLayer();
|
geoLayer = self.CreateGeojsonLayer();
|
||||||
for (const feat of allFeats) {
|
for (const feat of allFeats) {
|
||||||
|
if(feat === undefined){
|
||||||
|
continue
|
||||||
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
geoLayer.addData(feat);
|
geoLayer.addData(feat);
|
||||||
}
|
}
|
||||||
|
@ -76,7 +79,13 @@ export default class ShowDataLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zoomToFeatures) {
|
if (zoomToFeatures) {
|
||||||
|
try{
|
||||||
|
|
||||||
mp.fitBounds(geoLayer.getBounds())
|
mp.fitBounds(geoLayer.getBounds())
|
||||||
|
|
||||||
|
}catch(e){
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -169,8 +178,8 @@ export default class ShowDataLayer {
|
||||||
infobox.Activate();
|
infobox.Activate();
|
||||||
});
|
});
|
||||||
const self = this;
|
const self = this;
|
||||||
State.state.selectedElement.addCallbackAndRun(selected => {
|
State.state.selectedElement.addCallbackAndRunD(selected => {
|
||||||
if (selected === undefined || self._leafletMap.data === undefined) {
|
if ( self._leafletMap.data === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (leafletLayer.getPopup().isOpen()) {
|
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>;
|
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");
|
testing = QueryParameters.GetQueryParameter("test", "true");
|
||||||
// Set to true if testing and changes should NOT be saved
|
// Set to true if testing and changes should NOT be saved
|
||||||
testing.setData(testing.data ?? "true")
|
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.",
|
"intro": "MapComplete is an OpenStreetMap-viewer and editor, which shows you information about a specific theme.",
|
||||||
"pickTheme": "Pick a theme below to get started."
|
"pickTheme": "Pick a theme below to get started."
|
||||||
},
|
},
|
||||||
"deleteButton": {
|
"delete": {
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"loginToDelete": "You must be logged in to delete a point",
|
"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",
|
"isntAPoint": "Only points can be deleted",
|
||||||
"onlyEditedByLoggedInUser": "This point has only be edited by yourself, you can safely delete it",
|
"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",
|
"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": {
|
"general": {
|
||||||
"loginWithOpenStreetMap": "Login with OpenStreetMap",
|
"loginWithOpenStreetMap": "Login with OpenStreetMap",
|
||||||
|
|
Loading…
Reference in a new issue