First steps for a delete button
This commit is contained in:
parent
b7798a470c
commit
985e97d43b
10 changed files with 246 additions and 75 deletions
|
@ -13,6 +13,7 @@ export default class UserDetails {
|
||||||
|
|
||||||
public loggedIn = false;
|
public loggedIn = false;
|
||||||
public name = "Not logged in";
|
public name = "Not logged in";
|
||||||
|
public uid: number;
|
||||||
public csCount = 0;
|
public csCount = 0;
|
||||||
public img: string;
|
public img: string;
|
||||||
public unreadMessages = 0;
|
public unreadMessages = 0;
|
||||||
|
@ -167,6 +168,7 @@ export class OsmConnection {
|
||||||
data.loggedIn = true;
|
data.loggedIn = true;
|
||||||
console.log("Login completed, userinfo is ", userInfo);
|
console.log("Login completed, userinfo is ", userInfo);
|
||||||
data.name = userInfo.getAttribute('display_name');
|
data.name = userInfo.getAttribute('display_name');
|
||||||
|
data.uid= Number(userInfo.getAttribute("id"))
|
||||||
data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count");
|
data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count");
|
||||||
|
|
||||||
data.img = undefined;
|
data.img = undefined;
|
||||||
|
|
|
@ -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 splitted = id.split("/");
|
||||||
const type = splitted[0];
|
const type = splitted[0];
|
||||||
const idN = splitted[1];
|
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 elements: any[] = data.elements;
|
||||||
const osmObjects: OsmObject[] = []
|
const osmObjects: OsmObject[] = []
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
|
|
|
@ -7,11 +7,13 @@ export default class Constants {
|
||||||
// The user journey states thresholds when a new feature gets unlocked
|
// The user journey states thresholds when a new feature gets unlocked
|
||||||
public static userJourney = {
|
public static userJourney = {
|
||||||
moreScreenUnlock: 1,
|
moreScreenUnlock: 1,
|
||||||
personalLayoutUnlock: 15,
|
personalLayoutUnlock: 5,
|
||||||
historyLinkVisible: 20,
|
historyLinkVisible: 10,
|
||||||
|
deletePointsOfOthersUnlock: 15,
|
||||||
tagsVisibleAt: 25,
|
tagsVisibleAt: 25,
|
||||||
mapCompleteHelpUnlock: 50,
|
|
||||||
tagsVisibleAndWikiLinked: 30,
|
tagsVisibleAndWikiLinked: 30,
|
||||||
|
|
||||||
|
mapCompleteHelpUnlock: 50,
|
||||||
themeGeneratorReadOnlyUnlock: 50,
|
themeGeneratorReadOnlyUnlock: 50,
|
||||||
themeGeneratorFullUnlock: 500,
|
themeGeneratorFullUnlock: 500,
|
||||||
addNewPointWithUnreadMessagesUnlock: 500,
|
addNewPointWithUnreadMessagesUnlock: 500,
|
||||||
|
|
7
UI/Base/Loading.ts
Normal file
7
UI/Base/Loading.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import {FixedUiElement} from "./FixedUiElement";
|
||||||
|
|
||||||
|
export default class Loading extends FixedUiElement {
|
||||||
|
constructor() {
|
||||||
|
super("Loading..."); // TODO to be improved
|
||||||
|
}
|
||||||
|
}
|
72
UI/Popup/DeleteButton.ts
Normal file
72
UI/Popup/DeleteButton.ts
Normal file
|
@ -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<boolean> = new UIEventSource<boolean>(null)
|
||||||
|
OsmObject.DownloadReferencingRelations(id, (rels) => {
|
||||||
|
hasRelations.setData(rels.length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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<boolean>(id.startsWith("node"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
1
Utils.ts
1
Utils.ts
|
@ -343,6 +343,7 @@ export class Utils {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
xhr.open('GET', url);
|
xhr.open('GET', url);
|
||||||
|
xhr.setRequestHeader("accept","application/json")
|
||||||
xhr.send();
|
xhr.send();
|
||||||
}catch(e){
|
}catch(e){
|
||||||
reject(e)
|
reject(e)
|
||||||
|
|
|
@ -27,6 +27,15 @@
|
||||||
"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",
|
||||||
|
"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": {
|
"general": {
|
||||||
"loginWithOpenStreetMap": "Login with OpenStreetMap",
|
"loginWithOpenStreetMap": "Login with OpenStreetMap",
|
||||||
"welcomeBack": "You are logged in, welcome back!",
|
"welcomeBack": "You are logged in, welcome back!",
|
||||||
|
|
138
test.ts
138
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 Combine from "./UI/Base/Combine";
|
||||||
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
||||||
import {UIEventSource} from "./Logic/UIEventSource";
|
import {UIEventSource} from "./Logic/UIEventSource";
|
||||||
|
@ -69,75 +73,77 @@ function TestAllInputMethods() {
|
||||||
})).AttachTo("maindiv")
|
})).AttachTo("maindiv")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TestMiniMap() {
|
||||||
|
|
||||||
const location = new UIEventSource<Loc>({
|
const location = new UIEventSource<Loc>({
|
||||||
lon: 4.84771728515625,
|
lon: 4.84771728515625,
|
||||||
lat: 51.17920846421931,
|
lat: 51.17920846421931,
|
||||||
zoom: 14
|
zoom: 14
|
||||||
})
|
})
|
||||||
const map0 = new Minimap({
|
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<LayoutConfig>(layout)
|
|
||||||
)
|
|
||||||
|
|
||||||
const map1 = new Minimap({
|
|
||||||
location: location,
|
location: location,
|
||||||
allowMoving: true,
|
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")
|
const layout = AllKnownLayouts.layoutsList[1]
|
||||||
.AttachTo("extradiv")
|
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<LayoutConfig>(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<LayoutConfig>(layout)
|
||||||
|
)
|
||||||
|
|
||||||
|
featureSource.ping()
|
||||||
|
}
|
||||||
new ShowDataLayer(
|
//*/
|
||||||
featureSource,
|
State.state= new State(undefined)
|
||||||
map1.leafletMap,
|
new Combine([
|
||||||
new UIEventSource<LayoutConfig>(layout)
|
new DeleteButton("node/8598664388"),
|
||||||
)
|
]).AttachTo("maindiv")
|
||||||
|
|
||||||
featureSource.ping()
|
|
||||||
|
|
||||||
// */
|
|
||||||
|
|
32
test/OsmObject.spec.ts
Normal file
32
test/OsmObject.spec.ts
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import {Utils} from "../Utils";
|
import {Utils} from "../Utils";Utils.runningFromConsole = true;
|
||||||
Utils.runningFromConsole = true;
|
|
||||||
import TagSpec from "./Tag.spec";
|
import TagSpec from "./Tag.spec";
|
||||||
import ImageAttributionSpec from "./ImageAttribution.spec";
|
import ImageAttributionSpec from "./ImageAttribution.spec";
|
||||||
import GeoOperationsSpec from "./GeoOperations.spec";
|
import GeoOperationsSpec from "./GeoOperations.spec";
|
||||||
|
@ -10,6 +9,10 @@ import OsmConnectionSpec from "./OsmConnection.spec";
|
||||||
import T from "./TestHelper";
|
import T from "./TestHelper";
|
||||||
import {FixedUiElement} from "../UI/Base/FixedUiElement";
|
import {FixedUiElement} from "../UI/Base/FixedUiElement";
|
||||||
import Combine from "../UI/Base/Combine";
|
import Combine from "../UI/Base/Combine";
|
||||||
|
import OsmObjectSpec from "./OsmObject.spec";
|
||||||
|
import ScriptUtils from "../scripts/ScriptUtils";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default class TestAll {
|
export default class TestAll {
|
||||||
private needsBrowserTests: T[] = [new OsmConnectionSpec()]
|
private needsBrowserTests: T[] = [new OsmConnectionSpec()]
|
||||||
|
@ -26,8 +29,9 @@ export default class TestAll {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ScriptUtils.fixUtils()
|
||||||
const allTests = [
|
const allTests = [
|
||||||
|
new OsmObjectSpec(),
|
||||||
new TagSpec(),
|
new TagSpec(),
|
||||||
new ImageAttributionSpec(),
|
new ImageAttributionSpec(),
|
||||||
new GeoOperationsSpec(),
|
new GeoOperationsSpec(),
|
||||||
|
@ -39,6 +43,6 @@ const allTests = [
|
||||||
|
|
||||||
for (const test of allTests) {
|
for (const test of allTests) {
|
||||||
if (test.failures.length > 0) {
|
if (test.failures.length > 0) {
|
||||||
throw "Some test failed"
|
throw "Some test failed: "+test.failures.join(", ")
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue