Refactor OsmObject to use eventsources, add first version of the delete button

This commit is contained in:
pietervdvn 2021-06-30 18:48:23 +02:00
parent ec7833b2ee
commit bbfcee686f
15 changed files with 553 additions and 229 deletions

View file

@ -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);
}
})
}

View file

@ -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]

View file

@ -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)
});
})
}
}

View file

@ -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
View 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
})
}
)
}
}

View file

@ -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> {

View file

@ -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();

View file

@ -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);

View file

@ -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()
}
}

View file

@ -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"))
);
}
}

View file

@ -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 => {

View file

@ -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"

View file

@ -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()) {

View file

@ -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")

View file

@ -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",