diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index c085c5738..0ef3c5ba2 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -13,6 +13,7 @@ export default class UserDetails { public loggedIn = false; public name = "Not logged in"; + public uid: number; public csCount = 0; public img: string; public unreadMessages = 0; @@ -167,6 +168,7 @@ export class OsmConnection { data.loggedIn = true; console.log("Login completed, userinfo is ", userInfo); data.name = userInfo.getAttribute('display_name'); + data.uid= Number(userInfo.getAttribute("id")) data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count"); data.img = undefined; diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index 363678d8d..b09f515d0 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -43,11 +43,47 @@ export abstract class OsmObject { } } - public static DownloadHistory(id: string, continuation: (versions: OsmObject[]) => void): void { + /** + * 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`) + .then(data => { + const ways = data.elements.map(wayInfo => { + const way = new OsmWay(wayInfo.id) + way.LoadData(wayInfo) + return way + }) + continuation(ways) + }) + } + /** + * 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`) + .then(data => { + const rels = data.elements.map(wayInfo => { + const rel = new OsmRelation(wayInfo.id) + rel.LoadData(wayInfo) + return rel + }) + continuation(rels) + }) + } + public static DownloadHistory(id: string, continuation: (versions: OsmObject[]) => void): void{ const splitted = id.split("/"); const type = splitted[0]; const idN = splitted[1]; - $.getJSON("https://openStreetMap.org/api/0.6/" + type + "/" + idN + "/history", data => { + $.getJSON("https://www.openStreetMap.org/api/0.6/" + type + "/" + idN + "/history", data => { const elements: any[] = data.elements; const osmObjects: OsmObject[] = [] for (const element of elements) { diff --git a/Models/Constants.ts b/Models/Constants.ts index 4cbec2930..3482b3193 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -7,11 +7,13 @@ export default class Constants { // The user journey states thresholds when a new feature gets unlocked public static userJourney = { moreScreenUnlock: 1, - personalLayoutUnlock: 15, - historyLinkVisible: 20, + personalLayoutUnlock: 5, + historyLinkVisible: 10, + deletePointsOfOthersUnlock: 15, tagsVisibleAt: 25, - mapCompleteHelpUnlock: 50, tagsVisibleAndWikiLinked: 30, + + mapCompleteHelpUnlock: 50, themeGeneratorReadOnlyUnlock: 50, themeGeneratorFullUnlock: 500, addNewPointWithUnreadMessagesUnlock: 500, diff --git a/UI/Base/Loading.ts b/UI/Base/Loading.ts new file mode 100644 index 000000000..8711dec0d --- /dev/null +++ b/UI/Base/Loading.ts @@ -0,0 +1,7 @@ +import {FixedUiElement} from "./FixedUiElement"; + +export default class Loading extends FixedUiElement { + constructor() { + super("Loading..."); // TODO to be improved + } +} \ No newline at end of file diff --git a/UI/Popup/DeleteButton.ts b/UI/Popup/DeleteButton.ts new file mode 100644 index 000000000..a0bc50cf3 --- /dev/null +++ b/UI/Popup/DeleteButton.ts @@ -0,0 +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"; + + +export default class DeleteButton extends Toggle { + constructor(id: string) { + + const hasRelations: UIEventSource = new UIEventSource(null) + OsmObject.DownloadReferencingRelations(id, (rels) => { + hasRelations.setData(rels.length > 0) + }) + + const hasWays: UIEventSource = new UIEventSource(null) + OsmObject.DownloadReferencingWays(id, (ways) => { + hasWays.setData(ways.length > 0) + }) + + const previousEditors = new UIEventSource(null) + OsmObject.DownloadHistory(id, versions => { + const uids = versions.map(version => version.tags["_last_edit:contributor:uid"]) + previousEditors.setData(uids) + }) + const allByMyself = previousEditors.map(previous => { + if (previous === null) { + return null; + } + const userId = State.state.osmConnection.userDetails.data.uid; + return !previous.some(editor => editor !== userId) + }, [State.state.osmConnection.userDetails]) + + const t = Translations.t.deleteButton + + 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]) + ), + t.onlyEditedByLoggedInUser.Clone().onClick(State.state.osmConnection.AttemptLogin), + State.state.osmConnection.isLoggedIn), + t.isntAPoint, + new UIEventSource(id.startsWith("node")) + ); + } +} \ No newline at end of file diff --git a/Utils.ts b/Utils.ts index fa3e55ef8..81576df9f 100644 --- a/Utils.ts +++ b/Utils.ts @@ -343,6 +343,7 @@ export class Utils { } }; xhr.open('GET', url); + xhr.setRequestHeader("accept","application/json") xhr.send(); }catch(e){ reject(e) diff --git a/langs/en.json b/langs/en.json index dda19d600..0c574bca7 100644 --- a/langs/en.json +++ b/langs/en.json @@ -27,6 +27,15 @@ "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", + "loginToDelete": "You must be logged in to delete a point", + "checkingDeletability": "Inspecting properties to check if this feature can be 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" + }, "general": { "loginWithOpenStreetMap": "Login with OpenStreetMap", "welcomeBack": "You are logged in, welcome back!", diff --git a/test.ts b/test.ts index 8ae5dc012..25608b2b0 100644 --- a/test.ts +++ b/test.ts @@ -1,4 +1,8 @@ -import ValidatedTextField from "./UI/Input/ValidatedTextField"; +import {OsmObject} from "./Logic/Osm/OsmObject"; +import DeleteButton from "./UI/Popup/DeleteButton"; +import Combine from "./UI/Base/Combine"; +import State from "./State"; +/*import ValidatedTextField from "./UI/Input/ValidatedTextField"; import Combine from "./UI/Base/Combine"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; import {UIEventSource} from "./Logic/UIEventSource"; @@ -69,75 +73,77 @@ function TestAllInputMethods() { })).AttachTo("maindiv") } +function TestMiniMap() { -const location = new UIEventSource({ - lon: 4.84771728515625, - lat: 51.17920846421931, - zoom: 14 -}) -const map0 = new Minimap({ - location: location, - allowMoving: true, - background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[2]) -}) -map0.SetStyle("width: 500px; height: 250px; overflow: hidden; border: 2px solid red") - .AttachTo("maindiv") - -const layout = AllKnownLayouts.layoutsList[1] -State.state = new State(layout) -console.log("LAYOUT is", layout.id) - -const feature = { - "type": "Feature", - _matching_layer_id: "bike_repair_station", - "properties": { - id: "node/-1", - "amenity": "bicycle_repair_station" - }, - "geometry": { - "type": "Point", - "coordinates": [ - 4.84771728515625, - 51.17920846421931 - ] - } - } - -; - -State.state.allElements.addOrGetElement(feature) - -const featureSource = new UIEventSource([{ - freshness: new Date(), - feature: feature -}]) - -new ShowDataLayer( - featureSource, - map0.leafletMap, - new UIEventSource(layout) -) - -const map1 = new Minimap({ + const location = new UIEventSource({ + lon: 4.84771728515625, + lat: 51.17920846421931, + zoom: 14 + }) + const map0 = new Minimap({ location: location, allowMoving: true, - background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[5]) - }, -) + background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[2]) + }) + map0.SetStyle("width: 500px; height: 250px; overflow: hidden; border: 2px solid red") + .AttachTo("maindiv") -map1.SetStyle("width: 500px; height: 250px; overflow: hidden; border : 2px solid black") - .AttachTo("extradiv") + const layout = AllKnownLayouts.layoutsList[1] + State.state = new State(layout) + console.log("LAYOUT is", layout.id) + + const feature = { + "type": "Feature", + _matching_layer_id: "bike_repair_station", + "properties": { + id: "node/-1", + "amenity": "bicycle_repair_station" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 4.84771728515625, + 51.17920846421931 + ] + } + } + + ; + + State.state.allElements.addOrGetElement(feature) + + const featureSource = new UIEventSource([{ + freshness: new Date(), + feature: feature + }]) + + new ShowDataLayer( + featureSource, + map0.leafletMap, + new UIEventSource(layout) + ) + + const map1 = new Minimap({ + location: location, + allowMoving: true, + background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[5]) + }, + ) + + map1.SetStyle("width: 500px; height: 250px; overflow: hidden; border : 2px solid black") + .AttachTo("extradiv") + new ShowDataLayer( + featureSource, + map1.leafletMap, + new UIEventSource(layout) + ) - - -new ShowDataLayer( - featureSource, - map1.leafletMap, - new UIEventSource(layout) -) - -featureSource.ping() - -// */ \ No newline at end of file + featureSource.ping() +} +//*/ +State.state= new State(undefined) +new Combine([ + new DeleteButton("node/8598664388"), +]).AttachTo("maindiv") diff --git a/test/OsmObject.spec.ts b/test/OsmObject.spec.ts new file mode 100644 index 000000000..f5145a41e --- /dev/null +++ b/test/OsmObject.spec.ts @@ -0,0 +1,32 @@ +import T from "./TestHelper"; +import {OsmObject} from "../Logic/Osm/OsmObject"; +import ScriptUtils from "../scripts/ScriptUtils"; + +export default class OsmObjectSpec extends T { + constructor() { + super("OsmObject", [ + [ + "Download referencing ways", + () => { + let downloaded = false; + OsmObject.DownloadReferencingWays("node/1124134958", ways => { + downloaded = true; + console.log(ways) + }) + let timeout = 10 + while (!downloaded && timeout >= 0) { + ScriptUtils.sleep(1000) + + timeout--; + } + if(!downloaded){ + throw "Timeout: referencing ways not found" + } + } + + ] + + + ]); + } +} \ No newline at end of file diff --git a/test/TestAll.ts b/test/TestAll.ts index d34d4c5e6..5174e2ad0 100644 --- a/test/TestAll.ts +++ b/test/TestAll.ts @@ -1,5 +1,4 @@ -import {Utils} from "../Utils"; -Utils.runningFromConsole = true; +import {Utils} from "../Utils";Utils.runningFromConsole = true; import TagSpec from "./Tag.spec"; import ImageAttributionSpec from "./ImageAttribution.spec"; import GeoOperationsSpec from "./GeoOperations.spec"; @@ -10,6 +9,10 @@ import OsmConnectionSpec from "./OsmConnection.spec"; import T from "./TestHelper"; import {FixedUiElement} from "../UI/Base/FixedUiElement"; import Combine from "../UI/Base/Combine"; +import OsmObjectSpec from "./OsmObject.spec"; +import ScriptUtils from "../scripts/ScriptUtils"; + + export default class TestAll { private needsBrowserTests: T[] = [new OsmConnectionSpec()] @@ -26,8 +29,9 @@ export default class TestAll { } } } - +ScriptUtils.fixUtils() const allTests = [ + new OsmObjectSpec(), new TagSpec(), new ImageAttributionSpec(), new GeoOperationsSpec(), @@ -39,6 +43,6 @@ const allTests = [ for (const test of allTests) { if (test.failures.length > 0) { - throw "Some test failed" + throw "Some test failed: "+test.failures.join(", ") } } \ No newline at end of file