From 0b4016b65def6f5d5fcc91eb68d3454b0bcf55fe Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 27 Jun 2020 03:06:51 +0200 Subject: [PATCH] Further work on infobox, styling everything, removing clutter --- .gitignore | 2 + Helpers.ts | 34 +-- LayerDefinition.ts | 43 +--- Layers/Bookcases.ts | 3 - Layers/Bos.ts | 6 +- Layers/CommonTagMappings.ts | 5 +- Layers/KnownSet.ts | 2 +- Layers/NatureReserves.ts | 10 +- Layers/Park.ts | 7 +- Layers/Toilets.ts | 3 - Logic/Basemap.ts | 23 +- Logic/Changes.ts | 40 +++- Logic/FilteredLayer.ts | 35 +-- Logic/ImageSearcher.ts | 11 +- Logic/LayerUpdater.ts | 13 +- Logic/OsmConnection.ts | 33 ++- Logic/OsmImageUploadHandler.ts | 13 +- Logic/Overpass.ts | 3 +- Logic/Question.ts | 2 +- Logic/Wikimedia.ts | 10 +- Quests.ts | 3 +- UI/CenterMessageBox.ts | 2 +- UI/FeatureInfoBox.ts | 115 ++++++++++ UI/Image/ImageCarousel.ts | 2 +- UI/Image/WikimediaImage.ts | 54 +++-- UI/ImageUploadFlow.ts | 31 ++- UI/Img.ts | 14 ++ UI/LoginDependendMessage.ts | 31 --- UI/MessageBoxHandler.ts | 58 +++++ UI/PendingChanges.ts | 86 +++----- UI/QuestionPicker.ts | 2 +- UI/SlideShow.ts | 31 +-- UI/UIElement.ts | 20 ++ UI/UIEventSource.ts | 11 +- UI/UserBadge.ts | 48 +++- UI/VariableUIElement.ts | 12 +- assets/arrow-left-smooth.svg | 76 +++++++ assets/arrow-right-both.svg | 82 +++++++ assets/arrow-right-sharp.svg | 77 +++++++ assets/arrow-right-smooth.svg | 76 +++++++ assets/test.json | 3 - assets/wikimedia-commons-white.svg | 22 ++ assets/wikimedia-commons.svg | 22 ++ index.css | 337 +++++++++++++++++++++++------ index.html | 24 +- index.ts | 95 ++++++-- test.html | 3 +- test.ts | 102 +++++---- 48 files changed, 1283 insertions(+), 454 deletions(-) create mode 100644 UI/FeatureInfoBox.ts create mode 100644 UI/Img.ts delete mode 100644 UI/LoginDependendMessage.ts create mode 100644 UI/MessageBoxHandler.ts create mode 100644 assets/arrow-left-smooth.svg create mode 100644 assets/arrow-right-both.svg create mode 100644 assets/arrow-right-sharp.svg create mode 100644 assets/arrow-right-smooth.svg create mode 100644 assets/wikimedia-commons-white.svg create mode 100644 assets/wikimedia-commons.svg diff --git a/.gitignore b/.gitignore index a3edb6aa7..5b305a5a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ dist/* node_modules +.cache/* +.idea/* diff --git a/Helpers.ts b/Helpers.ts index 4ef63d838..fd3f164b6 100644 --- a/Helpers.ts +++ b/Helpers.ts @@ -1,25 +1,23 @@ import {OsmConnection} from "./Logic/OsmConnection"; import {Changes} from "./Logic/Changes"; import {UIEventSource} from "./UI/UIEventSource"; -import {PendingChanges} from "./UI/PendingChanges"; export class Helpers { - static SetupAutoSave(changes: Changes, secondsTillChangesAreSaved : UIEventSource) { + static SetupAutoSave(changes: Changes, millisTillChangesAreSaved : UIEventSource, saveAfterXMillis : number) { // This little function triggers the actual upload: // Either when more then three answers are selected, or when no new answer has been added for the last 20s // @ts-ignore window.decreaseTime = function () { - var time = secondsTillChangesAreSaved.data; + var time = millisTillChangesAreSaved.data; if (time <= 0) { - if (changes._pendingChanges.length > 0) { + if (changes.pendingChangesES.data > 0) { changes.uploadAll(undefined); } } else { - secondsTillChangesAreSaved.setData(time - 1000); - + millisTillChangesAreSaved.setData(time - 1000); } window.setTimeout('decreaseTime()', 1000); }; @@ -27,15 +25,15 @@ export class Helpers { changes.pendingChangesES.addCallback(function () { - var c = changes._pendingChanges.length; + var c = changes.pendingChangesES.data; if (c > 10) { - secondsTillChangesAreSaved.setData(0); + millisTillChangesAreSaved.setData(0); changes.uploadAll(undefined); return; } if (c > 0) { - secondsTillChangesAreSaved.setData(5000); + millisTillChangesAreSaved.setData(saveAfterXMillis); } }); @@ -44,22 +42,6 @@ export class Helpers { window.decreaseTime(); // The timer keeps running... } - /** - * All elements with class 'activate-osm-authentication' are loaded and get an 'onclick' to authenticate - * @param osmConnection - */ - static registerActivateOsmAUthenticationClass(osmConnection: OsmConnection) { - - const authElements = document.getElementsByClassName("activate-osm-authentication"); - for (let i = 0; i < authElements.length; i++) { - let element = authElements.item(i); - // @ts-ignore - element.onclick = function () { - osmConnection.AttemptLogin(); - } - } - } - /* @@ -72,7 +54,7 @@ export class Helpers { window.addEventListener("beforeunload", function (e) { // Quickly save everyting! - if (changes._pendingChanges.length == 0) { + if (changes.pendingChangesES.data == 0) { return ""; } diff --git a/LayerDefinition.ts b/LayerDefinition.ts index ebdbb73eb..6f21be0fb 100644 --- a/LayerDefinition.ts +++ b/LayerDefinition.ts @@ -28,54 +28,21 @@ export class LayerDefinition { questions: QuestionDefinition[]; // Questions are shown below elementsToShow in a questionPicker style: (tags: any) => any; - - removeContainedElements : boolean = false; + + removeContainedElements: boolean = false; removeTouchingElements: boolean = false; - asLayer(basemap: Basemap, allElements: ElementStorage, changes: Changes, userDetails: UserDetails): + asLayer(basemap: Basemap, allElements: ElementStorage, changes: Changes, userDetails: UIEventSource, selectedElement: UIEventSource): FilteredLayer { const self = this; - - function generateInfoBox(tagsES: UIEventSource) { - - var infoboxes: UIElement[] = []; - for (const uiElement of self.elementsToShow) { - if (uiElement instanceof QuestionDefinition) { - const questionDef = uiElement as QuestionDefinition; - const question = new Question(changes, questionDef); - infoboxes.push(question.CreateHtml(tagsES)); - } else if (uiElement instanceof TagMappingOptions) { - const tagMappingOpt = uiElement as TagMappingOptions; - infoboxes.push(new TagMapping(tagMappingOpt, tagsES)) - } else { - const ui = uiElement as UIElement; - infoboxes.push(ui); - } - - } - infoboxes.push(new ImageCarousel(tagsES)); - - infoboxes.push(new FixedUiElement("
")); - - infoboxes.push(new OsmImageUploadHandler( - tagsES, userDetails, changes - ).getUI()); - - const qbox = new QuestionPicker(changes.asQuestions(self.questions), tagsES); - infoboxes.push(qbox); - - return new VerticalCombine(infoboxes); - - } - return new FilteredLayer( this.name, basemap, allElements, changes, this.overpassFilter, this.removeContainedElements, this.removeTouchingElements, - generateInfoBox, - this.style); + this.style, + selectedElement); } diff --git a/Layers/Bookcases.ts b/Layers/Bookcases.ts index f34ba2d15..f6463a141 100644 --- a/Layers/Bookcases.ts +++ b/Layers/Bookcases.ts @@ -58,9 +58,6 @@ export class Bookcases extends LayerDefinition { new TagMappingOptions({key: "ref", template: "Referentienummer {ref}"}), new TagMappingOptions({key: "description", template: "Extra beschrijving:

{description}

"}), - - CommonTagMappings.osmLink - ] ; } diff --git a/Layers/Bos.ts b/Layers/Bos.ts index d1a2b037f..cf8db1f77 100644 --- a/Layers/Bos.ts +++ b/Layers/Bos.ts @@ -32,14 +32,12 @@ export class Bos extends LayerDefinition { this.elementsToShow = [ new TagMappingOptions({ key: "name", - template: "

{name}

", - missing: "

Naamloos bos

" + template: "{name}", + missing: "Naamloos bos" }), CommonTagMappings.access, CommonTagMappings.operator, - CommonTagMappings.osmLink - ]; } diff --git a/Layers/CommonTagMappings.ts b/Layers/CommonTagMappings.ts index 664da0070..6417bfc40 100644 --- a/Layers/CommonTagMappings.ts +++ b/Layers/CommonTagMappings.ts @@ -1,4 +1,5 @@ import {TagMappingOptions} from "../UI/TagMapping"; +import {Img} from "../UI/Img"; export class CommonTagMappings { @@ -28,6 +29,8 @@ export class CommonTagMappings { mapping: { "node/-1": "Over enkele momenten sturen we je punt naar OpenStreetMap" }, - template: " Op OSM" + template: "" + + Img.osmAbstractLogo + + "" }) } \ No newline at end of file diff --git a/Layers/KnownSet.ts b/Layers/KnownSet.ts index 9f5418314..580e06582 100644 --- a/Layers/KnownSet.ts +++ b/Layers/KnownSet.ts @@ -49,7 +49,7 @@ export class KnownSet { static groen = new KnownSet("groen", "Buurtnatuur", - [new NatureReserves(), new Park(), new Bos(), new Playground()], + [new NatureReserves(), new Park(), new Bos()], 14, 51.2, 3.2, diff --git a/Layers/NatureReserves.ts b/Layers/NatureReserves.ts index 6aeb0acd4..b010e6804 100644 --- a/Layers/NatureReserves.ts +++ b/Layers/NatureReserves.ts @@ -3,7 +3,7 @@ import {Quests} from "../Quests"; import {TagMappingOptions} from "../UI/TagMapping"; import L from "leaflet" import {CommonTagMappings} from "./CommonTagMappings"; -import {Tag} from "../Logic/TagsFilter"; +import {Or, Tag} from "../Logic/TagsFilter"; export class NatureReserves extends LayerDefinition { @@ -11,7 +11,8 @@ export class NatureReserves extends LayerDefinition { super(); this.name = "natuurgebied"; this.icon = "./assets/tree_white_background.svg"; - this.overpassFilter = new Tag("leisure", "nature_reserve"); + this.overpassFilter = + new Or([new Tag("leisure", "nature_reserve"), new Tag("boundary","protected_area")]); this.removeTouchingElements = true; this.newElementTags = [new Tag("leisure", "nature_reserve"), @@ -22,12 +23,11 @@ export class NatureReserves extends LayerDefinition { this.elementsToShow = [ new TagMappingOptions({ key: "name", - template: "

{name}

", - missing: "

Naamloos gebied

" + template: "{name}", + missing: "Naamloos gebied" }), CommonTagMappings.access, CommonTagMappings.operator, - CommonTagMappings.osmLink ]; } diff --git a/Layers/Park.ts b/Layers/Park.ts index f7a6da383..dbdb24402 100644 --- a/Layers/Park.ts +++ b/Layers/Park.ts @@ -22,15 +22,12 @@ export class Park extends LayerDefinition { this.elementsToShow = [ new TagMappingOptions({ key: "name", - template: "

{name}

", - missing: "

Naamloos park

" + template: "{name}", + missing: "Naamloos park" }), CommonTagMappings.access, CommonTagMappings.operator, - CommonTagMappings.osmLink, - - ]; } diff --git a/Layers/Toilets.ts b/Layers/Toilets.ts index 892221d3c..9c07abb7c 100644 --- a/Layers/Toilets.ts +++ b/Layers/Toilets.ts @@ -82,9 +82,6 @@ export class Toilets extends LayerDefinition{ } }), - - CommonTagMappings.osmLink - ]; } diff --git a/Logic/Basemap.ts b/Logic/Basemap.ts index 7f79ecf8e..db53e4830 100644 --- a/Logic/Basemap.ts +++ b/Logic/Basemap.ts @@ -58,21 +58,36 @@ export class Basemap { constructor(leafletElementId: string, location: UIEventSource<{ zoom: number, lat: number, lon: number }>) { - this. map = L.map(leafletElementId, { + this.map = L.map(leafletElementId, { center: [location.data.lat, location.data.lon], zoom: location.data.zoom, - layers: [this.osmLayer] + layers: [this.osmLayer], + attributionControl: false }); + + L.control.attribution({ + position: 'bottomleft' + }).addTo(this.map); + this.Location = location; - L.control.layers(this.baseLayers).addTo(this.map); + const layerControl = L.control.layers(this.baseLayers, null, + { + position: 'bottomleft', + hideSingleBase: true + }) + layerControl.addTo(this.map); + this.map.zoomControl.setPosition("bottomleft"); const self = this; + this.map.on("moveend", function () { location.data.zoom = self.map.getZoom(); location.data.lat = self.map.getCenter().lat; location.data.lon = self.map.getCenter().lon; location.ping(); - }) + }); + + } diff --git a/Logic/Changes.ts b/Logic/Changes.ts index 6c6fcf08e..43a6af0b6 100644 --- a/Logic/Changes.ts +++ b/Logic/Changes.ts @@ -16,16 +16,23 @@ export class Changes { private readonly login: OsmConnection; public readonly _allElements: ElementStorage; - public _pendingChanges: { elementId: string, key: string, value: string }[] = []; // Gets reset on uploadAll + private _pendingChanges: { elementId: string, key: string, value: string }[] = []; // Gets reset on uploadAll private newElements: OsmObject[] = []; // Gets reset on uploadAll - public readonly pendingChangesES = new UIEventSource(this._pendingChanges); - private readonly centerMessage: UIEventSource; + public readonly pendingChangesES = new UIEventSource(this._pendingChanges.length); + public readonly isSaving = new UIEventSource(false); + private readonly _changesetComment: string; + private readonly _centerMessage: UIEventSource; - constructor(login: OsmConnection, allElements: ElementStorage, centerMessage: UIEventSource) { + constructor( + changesetComment: string, + login: OsmConnection, + allElements: ElementStorage, + centerMessage: UIEventSource) { + this._changesetComment = changesetComment; this.login = login; this._allElements = allElements; - this.centerMessage = centerMessage; + this._centerMessage = centerMessage; } /** @@ -37,15 +44,15 @@ export class Changes { addChange(elementId: string, key: string, value: string) { if (!this.login.userDetails.data.loggedIn) { - this.centerMessage.setData( + this._centerMessage.setData( "

Bedankt voor je antwoord!

" + - "

Gelieve in te loggen op OpenStreetMap om dit op te slaan.

"+ + "

Gelieve in te loggen op OpenStreetMap om dit op te slaan.

" + "

Nog geen account? Registreer hier

" ); const self = this; this.login.userDetails.addCallback(() => { if (self.login.userDetails.data.loggedIn) { - self.centerMessage.setData(""); + self._centerMessage.setData(""); } }); return; @@ -67,7 +74,7 @@ export class Changes { eventSource.ping(); // We get the id from the event source, as that ID might be rewritten this._pendingChanges.push({elementId: eventSource.data.id, key: key, value: value}); - this.pendingChangesES.ping(); + this.pendingChangesES.setData(this._pendingChanges.length); } @@ -114,9 +121,18 @@ export class Changes { public uploadAll(optionalContinuation: (() => void)) { const self = this; + this.isSaving.setData(true); + const optionalContinuationWrapped = function () { + self.isSaving.setData(false); + if (optionalContinuation) { + optionalContinuation(); + } + } + + const pending: { elementId: string; key: string; value: string }[] = this._pendingChanges; this._pendingChanges = []; - this.pendingChangesES.setData(this._pendingChanges); + this.pendingChangesES.setData(this._pendingChanges.length); const newElements = this.newElements; this.newElements = []; @@ -203,7 +219,7 @@ export class Changes { console.log("Beginning upload..."); // At last, we build the changeset and upload - self.login.UploadChangeset("Updaten van metadata met Mapcomplete", + self.login.UploadChangeset(self._changesetComment, function (csId) { let modifications = ""; @@ -243,7 +259,7 @@ export class Changes { return changes; }, handleMapping, - optionalContinuation); + optionalContinuationWrapped); }); } diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index 9fafddd4d..e627028bd 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -25,8 +25,6 @@ export class FilteredLayer { private readonly _removeContainedElements; private readonly _removeTouchingElements; - private readonly _popupContent: ((source: UIEventSource) => UIElement); - private readonly _style: (properties) => any; private readonly _storage: ElementStorage; @@ -41,6 +39,7 @@ export class FilteredLayer { * The leaflet layer object which should be removed on rerendering */ private _geolayer; + private _selectedElement: UIEventSource; constructor( name: string, @@ -49,8 +48,9 @@ export class FilteredLayer { filters: TagsFilter, removeContainedElements: boolean, removeTouchingElements: boolean, - popupContent: ((source: UIEventSource) => UIElement), - style: ((properties) => any)) { + style: ((properties) => any), + selectedElement: UIEventSource) { + this._selectedElement = selectedElement; if (style === undefined) { style = function () { @@ -60,7 +60,6 @@ export class FilteredLayer { this.name = name; this._map = map; this.filters = filters; - this._popupContent = popupContent; this._style = style; this._storage = storage; this._removeContainedElements = removeContainedElements; @@ -167,8 +166,6 @@ export class FilteredLayer { }, pointToLayer: function (feature, latLng) { - - const eventSource = self._storage.addOrGetElement(feature); const style = self._style(feature.properties); let marker; if (style.icon === undefined) { @@ -180,19 +177,6 @@ export class FilteredLayer { }); } - eventSource.addCallback(function () { - self.updateStyle(); - }); - const content = self._popupContent(eventSource) - marker.bindPopup( - "
" + - content.Render() + - "
" - ).on("popupopen", function () { - content.Activate(); - content.Update(); - }); - return marker; }, @@ -203,14 +187,9 @@ export class FilteredLayer { eventSource.addCallback(function () { self.updateStyle(); }); - const content = self._popupContent(eventSource) - layer.bindPopup( - "
" + - content.Render() + - "
" - ).on("popupopen", function () { - content.Activate(); - content.Update(); + layer.on("click", function(){ + console.log("Selected ",feature) + self._selectedElement.setData(feature.properties); }); } }); diff --git a/Logic/ImageSearcher.ts b/Logic/ImageSearcher.ts index d0622d1ab..84f3335bc 100644 --- a/Logic/ImageSearcher.ts +++ b/Logic/ImageSearcher.ts @@ -34,7 +34,9 @@ export class ImageSearcher extends UIEventSource { self.AddImage(wd.image); Wikimedia.GetCategoryFiles(wd.commonsWiki, (images: ImagesInCategory) => { for (const image of images.images) { - self.AddImage(image.filename); + if (image.startsWith("File:")) { + self.AddImage(image); + } } }) }) @@ -48,7 +50,10 @@ export class ImageSearcher extends UIEventSource { if (commons.startsWith("Category:")) { Wikimedia.GetCategoryFiles(commons, (images: ImagesInCategory) => { for (const image of images.images) { - self.AddImage(image.filename); + // @ts-ignore + if (image.startsWith("File:")) { + self.AddImage(image); + } } }) } else { // @ts-ignore @@ -125,7 +130,7 @@ export class ImageSearcher extends UIEventSource { const urlSource = new UIEventSource(url); // @ts-ignore if (url.startsWith("File:")) { - return new WikimediaImage(urlSource); + return new WikimediaImage(urlSource.data); } else { return new SimpleImageElement(urlSource); } diff --git a/Logic/LayerUpdater.ts b/Logic/LayerUpdater.ts index b51ab87f8..1c3faa9b5 100644 --- a/Logic/LayerUpdater.ts +++ b/Logic/LayerUpdater.ts @@ -57,9 +57,13 @@ export class LayerUpdater { } private handleFail(reason: any) { - this.runningQuery.setData(false); console.log("QUERY FAILED", reason); - // TODO + console.log("Retrying in 1s") + this.previousBounds = undefined; + const self = this; + window.setTimeout( + function(){self.update()}, 1000 + ) } @@ -89,7 +93,7 @@ export class LayerUpdater { } - buildBboxFor(): string { + private buildBboxFor(): string { const b = this._map.map.getBounds(); const latDiff = Math.abs(b.getNorth() - b.getSouth()); const lonDiff = Math.abs(b.getEast() - b.getWest()); @@ -101,8 +105,7 @@ export class LayerUpdater { this.previousBounds = {north: n, east: e, south: s, west: w}; - const bbox = "[bbox:" + s + "," + w + "," + n + "," + e + "]"; - return bbox; + return "[bbox:" + s + "," + w + "," + n + "," + e + "]"; } private IsInBounds(): boolean { diff --git a/Logic/OsmConnection.ts b/Logic/OsmConnection.ts index 163e53b3d..076ec664e 100644 --- a/Logic/OsmConnection.ts +++ b/Logic/OsmConnection.ts @@ -9,6 +9,9 @@ export class UserDetails { public csCount = 0; public img: string; public unreadMessages = 0; + public totalMessages = 0; + public osmConnection : OsmConnection; + public dryRun : boolean; } @@ -26,10 +29,9 @@ export class OsmConnection { constructor(dryRun: boolean) { this.userDetails = new UIEventSource(new UserDetails()); + this.userDetails.data.osmConnection = this; + this.userDetails.data.dryRun = dryRun; this._dryRun = dryRun; - if(dryRun){ - alert("Opgelet: testmode actief. Wijzigingen worden NIET opgeslaan") - } if (this.auth.authenticated()) { this.AttemptLogin(); // Also updates the user badge @@ -61,23 +63,43 @@ export class OsmConnection { self.userDetails.ping(); } - if(details == null){ + if (details == null) { return; } // details is an XML DOM of user details let userInfo = details.getElementsByTagName("user")[0]; + // let moreDetails = new DOMParser().parseFromString(userInfo.innerHTML, "text/xml"); + let data = self.userDetails.data; data.loggedIn = true; console.log(userInfo); data.name = userInfo.getAttribute('display_name'); data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count"); data.img = userInfo.getElementsByTagName("img")[0].getAttribute("href"); - data.unreadMessages = userInfo.getElementsByTagName("received")[0].getAttribute("unread"); + const messages = userInfo.getElementsByTagName("messages")[0].getElementsByTagName("received")[0]; + data.unreadMessages = parseInt(messages.getAttribute("unread")); + data.totalMessages = parseInt(messages.getAttribute("count")); self.userDetails.ping(); }); } + /** + * All elements with class 'activate-osm-authentication' are loaded and get an 'onclick' to authenticate + * @param osmConnection + */ + registerActivateOsmAUthenticationClass() { + + const authElements = document.getElementsByClassName("activate-osm-authentication"); + for (let i = 0; i < authElements.length; i++) { + let element = authElements.item(i); + // @ts-ignore + element.onclick = function () { + this.AttemptLogin(); + } + } + } + private static parseUploadChangesetResponse(response: XMLDocument) { const nodes = response.getElementsByTagName("node"); const mapping = {}; @@ -102,6 +124,7 @@ export class OsmConnection { console.log("NOT UPLOADING as dryrun is true"); var changesetXML = generateChangeXML("123456"); console.log(changesetXML); + continuation(); return; } diff --git a/Logic/OsmImageUploadHandler.ts b/Logic/OsmImageUploadHandler.ts index f86d7c76b..bf6212cff 100644 --- a/Logic/OsmImageUploadHandler.ts +++ b/Logic/OsmImageUploadHandler.ts @@ -9,29 +9,26 @@ import {UserDetails} from "./OsmConnection"; export class OsmImageUploadHandler { private _tags: UIEventSource; private _changeHandler: Changes; - private _userdetails: UserDetails; + private _userdetails: UIEventSource; constructor(tags: UIEventSource, - userdetails: UserDetails, + userdetails: UIEventSource, changeHandler: Changes ) { if (tags === undefined || userdetails === undefined || changeHandler === undefined) { throw "Something is undefined" } - console.log(tags, changeHandler, userdetails) this._tags = tags; this._changeHandler = changeHandler; this._userdetails = userdetails; } private generateOptions(license: string) { - console.log(this) - console.log(this._tags, this._changeHandler, this._userdetails) const tags = this._tags.data; const title = tags.name ?? "Unknown area"; const description = [ - "author:" + this._userdetails.name, + "author:" + this._userdetails.data.name, "license:" + license, "wikidata:" + tags.wikidata, "osmid:" + tags.id, @@ -60,7 +57,9 @@ export class OsmImageUploadHandler { getUI(): ImageUploadFlow { const self = this; - return new ImageUploadFlow(function (license) { + return new ImageUploadFlow( + this._userdetails, + function (license) { return self.generateOptions(license) } ); diff --git a/Logic/Overpass.ts b/Logic/Overpass.ts index 4f52d3e3b..f0a84fa38 100644 --- a/Logic/Overpass.ts +++ b/Logic/Overpass.ts @@ -26,8 +26,7 @@ export class Overpass { const query = '[out:json][timeout:25]' + bbox + ';(' + filter + ');out body;>;out skel qt;'; console.log(query); - const url = "https://overpass-api.de/api/interpreter?data=" + encodeURIComponent(query); - return url; + return "https://overpass-api.de/api/interpreter?data=" + encodeURIComponent(query); } diff --git a/Logic/Question.ts b/Logic/Question.ts index ffc7eb663..3ffd6469a 100644 --- a/Logic/Question.ts +++ b/Logic/Question.ts @@ -81,7 +81,7 @@ export class QuestionUI extends UIElement { const embeddedScriptSave = 'questionAnswered(' + this._qid + ', "' + this._tags.data.id + '", false )'; const embeddedScriptSkip = 'questionAnswered(' + this._qid + ', "' + this._tags.data.id + '", true )'; const saveButton = ""; - const skip = ""; + const skip = ""; return q.question + "
" + answers + saveButton + skip; } diff --git a/Logic/Wikimedia.ts b/Logic/Wikimedia.ts index e36a69be8..a5f3ff13e 100644 --- a/Logic/Wikimedia.ts +++ b/Logic/Wikimedia.ts @@ -68,8 +68,7 @@ export class Wikimedia { for (const member of members) { - imageOverview.images.push( - {filename: member.title, fileid: member.pageid}); + imageOverview.images.push(member.title); } if (response.continue === undefined || alreadyLoaded > 30) { handleCategory(imageOverview); @@ -96,7 +95,10 @@ export class Wikimedia { wd.commonsWiki = commons?.title; // P18 is the claim 'depicted in this image' - wd.image = "File:" + entity.claims.P18?.[0]?.mainsnak?.datavalue?.value; + const image = entity.claims.P18?.[0]?.mainsnak?.datavalue?.value; + if (image) { + wd.image = "File:" + image; + } handleWikidata(wd); }); } @@ -114,7 +116,7 @@ export class Wikidata { export class ImagesInCategory { // Filenames of relevant images - images: { filename: string, fileid: number }[] = []; + images: string[] = []; } export class LicenseInfo { diff --git a/Quests.ts b/Quests.ts index 76c20dcf1..5b559937b 100644 --- a/Quests.ts +++ b/Quests.ts @@ -54,7 +54,8 @@ export class Quests { ).addUnrequiredTag("seamark:type", "restricted_area"); static nameOf(name: string) : QuestionDefinition { - return QuestionDefinition.noNameOrNameQuestion("Wat is de naam van dit " + name + "?", + return QuestionDefinition.noNameOrNameQuestion("Wat is de officiƫle naam van dit " + name + "?
" + + "Gelieve geen naam uit te vinden", "Dit " + name + " heeft geen naam", 20); } diff --git a/UI/CenterMessageBox.ts b/UI/CenterMessageBox.ts index 143312342..c72561ed5 100644 --- a/UI/CenterMessageBox.ts +++ b/UI/CenterMessageBox.ts @@ -62,7 +62,7 @@ export class CenterMessageBox extends UIElement { if (this._centermessage.data != "") { pstyle.opacity = "1"; pstyle.pointerEvents = "all"; - Helpers.registerActivateOsmAUthenticationClass(this._osmConnection); + this._osmConnection.registerActivateOsmAUthenticationClass(); return; } pstyle.pointerEvents = "none"; diff --git a/UI/FeatureInfoBox.ts b/UI/FeatureInfoBox.ts new file mode 100644 index 000000000..1431da52b --- /dev/null +++ b/UI/FeatureInfoBox.ts @@ -0,0 +1,115 @@ +import {UIElement} from "./UIElement"; +import {TagMapping, TagMappingOptions} from "./TagMapping"; +import {Question, QuestionDefinition} from "../Logic/Question"; +import {UIEventSource} from "./UIEventSource"; +import {VerticalCombine} from "./VerticalCombine"; +import {QuestionPicker} from "./QuestionPicker"; +import {OsmImageUploadHandler} from "../Logic/OsmImageUploadHandler"; +import {ImageCarousel} from "./Image/ImageCarousel"; +import {Changes} from "../Logic/Changes"; +import {UserDetails} from "../Logic/OsmConnection"; +import {Img} from "./Img"; +import {CommonTagMappings} from "../Layers/CommonTagMappings"; +import {Tag} from "../Logic/TagsFilter"; + +export class FeatureInfoBox extends UIElement { + + private _tagsES: UIEventSource; + + + private _title: UIElement; + private _osmLink: UIElement; + private _infoElements: UIElement[] + + + private _questions: QuestionPicker; + + private _changes: Changes; + private _userDetails: UIEventSource; + private _imageElement: ImageCarousel; + + + constructor( + tagsES: UIEventSource, + elementsToShow: (TagMappingOptions | QuestionDefinition | UIElement)[], + questions: QuestionDefinition[], + changes: Changes, + userDetails: UIEventSource, + ) { + super(tagsES); + this._tagsES = tagsES; + this._changes = changes; + this._userDetails = userDetails; + + this._imageElement = new ImageCarousel(this._tagsES); + + this._questions = new QuestionPicker( + this._changes.asQuestions(questions), this._tagsES); + + var infoboxes: UIElement[] = []; + for (const uiElement of elementsToShow) { + if (uiElement instanceof QuestionDefinition) { + const questionDef = uiElement as QuestionDefinition; + const question = new Question(this._changes, questionDef); + infoboxes.push(question.CreateHtml(this._tagsES)); + } else if (uiElement instanceof TagMappingOptions) { + const tagMappingOpt = uiElement as TagMappingOptions; + infoboxes.push(new TagMapping(tagMappingOpt, this._tagsES)) + } else { + const ui = uiElement as UIElement; + infoboxes.push(ui); + } + + } + + this._title = infoboxes.shift(); + this._infoElements = infoboxes; + + this._osmLink = new TagMapping(CommonTagMappings.osmLink, this._tagsES); + + + } + + InnerRender(): string { + + + return "
" + + "
" + + "" + this._title.Render() + "" + + this._osmLink.Render() + + "
" + + + "
" + + + this._imageElement.Render() + + + new VerticalCombine(this._infoElements).Render() + + " " + + this._questions.Render() + + " " + + "
" + + "" + + "
"; + } + + Activate() { + super.Activate(); + this._imageElement.Activate(); + } + + Update() { + super.Update(); + this._imageElement.Update(); + } + + private generateInfoBox() { + var infoboxes: UIElement[] = []; + + infoboxes.push(new OsmImageUploadHandler( + this._tagsES, this._userDetails, this._changes + ).getUI()); + + + return new VerticalCombine(infoboxes); + } +} diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 00f618bcd..5200d58e7 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -35,7 +35,7 @@ export class ImageCarousel extends UIElement { this.slideshow = new SlideShow( new FixedUiElement("Afbeeldingen"), uiElements, - new FixedUiElement("Geen afbeeldingen gevonden")); + new FixedUiElement("")).HideOnEmpty(true); } InnerRender(): string { diff --git a/UI/Image/WikimediaImage.ts b/UI/Image/WikimediaImage.ts index d12051be9..9b5c589f3 100644 --- a/UI/Image/WikimediaImage.ts +++ b/UI/Image/WikimediaImage.ts @@ -1,36 +1,52 @@ import {UIEventSource} from "../UIEventSource"; import {UIElement} from "../UIElement"; -import {SimpleImageElement} from "./SimpleImageElement"; import {LicenseInfo, Wikimedia} from "../../Logic/Wikimedia"; export class WikimediaImage extends UIElement { + + static allLicenseInfos: any = {}; private _imageMeta: UIEventSource; + private _imageLocation : string; + constructor(source: string) { + super(undefined) + this._imageLocation = source; + if (WikimediaImage.allLicenseInfos[source] !== undefined) { + this._imageMeta = WikimediaImage.allLicenseInfos[source]; + } else { + this._imageMeta = new UIEventSource(new LicenseInfo()); + WikimediaImage.allLicenseInfos[source] = this._imageMeta; + } - constructor(source: UIEventSource) { - super(source) - const meta = new UIEventSource(new LicenseInfo()); - this.ListenTo(meta); - this._imageMeta = meta; - this._source.addCallback(() => { - Wikimedia.LicenseData(source.data, (info) => { - meta.setData(info); - }) - }); - this._source.ping(); + this.ListenTo(this._imageMeta); + + const self = this; + Wikimedia.LicenseData(source, (info) => { + self._imageMeta.setData(info); + }) } protected InnerRender(): string { - let url = Wikimedia.ImageNameToUrl(this._source.data); + let url = Wikimedia.ImageNameToUrl(this._imageLocation, 500, 400); url = url.replace(/'/g, '%27'); - return "
" + - "
" + - "" + (this._source.data) + "
" + - (this._imageMeta.data.artist ?? "Unknown artist") + " " + (this._imageMeta.data.licenseShortName ?? "") + - "
" + + + const wikimediaLink = + "" + + "Wikimedia Commons Logo" + + " "; + + const attribution = + "" + (this._imageMeta.data.artist ?? "") + "" + " " + (this._imageMeta.data.licenseShortName ?? "") + ""; + const image = ""; + + return "
" + + image + + "
" + + wikimediaLink + + attribution + + "
" + "
"; } diff --git a/UI/ImageUploadFlow.ts b/UI/ImageUploadFlow.ts index c1a0333ae..998807a4b 100644 --- a/UI/ImageUploadFlow.ts +++ b/UI/ImageUploadFlow.ts @@ -4,22 +4,28 @@ import {UIRadioButton} from "./UIRadioButton"; import {VariableUiElement} from "./VariableUIElement"; import $ from "jquery" import {Imgur} from "../Logic/Imgur"; +import {UserDetails} from "../Logic/OsmConnection"; export class ImageUploadFlow extends UIElement { private _licensePicker: UIRadioButton; private _licenseExplanation: UIElement; private _isUploading: UIEventSource = new UIEventSource(0) private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) }; + private _userdetails: UIEventSource; - constructor(uploadOptions: ((license: string) => - { - title: string, - description: string, - handleURL: ((url: string) => void), - allDone: (() => void) - }) + constructor( + userInfo: UIEventSource, + uploadOptions: ((license: string) => + { + title: string, + description: string, + handleURL: ((url: string) => void), + allDone: (() => void) + }) ) { super(undefined); + this._userdetails = userInfo; + this.ListenTo(userInfo); this._uploadOptions = uploadOptions; this.ListenTo(this._isUploading); this._licensePicker = UIRadioButton.FromStrings( @@ -49,6 +55,10 @@ export class ImageUploadFlow extends UIElement { protected InnerRender(): string { + if (!this._userdetails.data.loggedIn) { + return "
Gelieve je aan te melden om een foto toe te voegen
"; + } + if (this._isUploading.data > 0) { return "Bezig met uploaden, nog " + this._isUploading.data + " foto's te gaan..." } @@ -63,6 +73,13 @@ export class ImageUploadFlow extends UIElement { InnerUpdate(htmlElement: HTMLElement) { super.InnerUpdate(htmlElement); + const user = this._userdetails.data; + if(!user.loggedIn){ + htmlElement.onclick = function(){ + user.osmConnection.AttemptLogin(); + } + } + this._licensePicker.Update(); const selector = document.getElementById('fileselector-' + this.id); const self = this; diff --git a/UI/Img.ts b/UI/Img.ts new file mode 100644 index 000000000..c4636f758 --- /dev/null +++ b/UI/Img.ts @@ -0,0 +1,14 @@ +export class Img { + + static osmAbstractLogo: string = + ""; + + +} + diff --git a/UI/LoginDependendMessage.ts b/UI/LoginDependendMessage.ts deleted file mode 100644 index f3769726c..000000000 --- a/UI/LoginDependendMessage.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {UIElement} from "./UIElement"; -import {UserDetails} from "../Logic/OsmConnection"; -import {UIEventSource} from "./UIEventSource"; - -export class LoginDependendMessage extends UIElement { - private _noLoginMsg: string; - private _loginMsg: string; - private _userDetails: UserDetails; - - constructor(loginData: UIEventSource, - noLoginMsg: string, - loginMsg: string) { - super(loginData); - this._userDetails = loginData.data; - this._noLoginMsg = noLoginMsg; - this._loginMsg = loginMsg; - } - - protected InnerRender(): string { - if (this._userDetails.loggedIn) { - return this._loginMsg; - } else { - return this._noLoginMsg; - } - } - - InnerUpdate(htmlElement: HTMLElement) { - // pass - } - -} \ No newline at end of file diff --git a/UI/MessageBoxHandler.ts b/UI/MessageBoxHandler.ts new file mode 100644 index 000000000..b0188228f --- /dev/null +++ b/UI/MessageBoxHandler.ts @@ -0,0 +1,58 @@ +/** + * Keeps 'messagebox' and 'messageboxmobile' in sync, shows a 'close' button on the latter one + */ +import {UIEventSource} from "./UIEventSource"; +import {UIElement} from "./UIElement"; +import {FixedUiElement} from "./FixedUiElement"; +import {VariableUiElement} from "./VariableUIElement"; + +export class MessageBoxHandler { + private _uielement: UIEventSource<() => UIElement>; + + constructor(uielement: UIEventSource<() => UIElement>, + onClear: (() => void)) { + this._uielement = uielement; + this.listenTo(uielement); + this.update(); + + new VariableUiElement(new UIEventSource("

Naar de kaart >

"), + (htmlElement) => { + htmlElement.onclick = function () { + uielement.setData(undefined); + onClear(); + } + } + ).AttachTo("to-the-map"); + + } + + listenTo(uiEventSource: UIEventSource) { + const self = this; + uiEventSource.addCallback(function () { + self.update(); + }) + } + + update() { + const wrapper = document.getElementById("messagesboxmobilewrapper"); + const gen = this._uielement.data; + console.log("Generator: ", gen); + if (gen === undefined) { + wrapper.classList.add("hidden"); + return; + } + wrapper.classList.remove("hidden"); + gen() + ?.HideOnEmpty(true) + ?.AttachTo("messagesbox") + ?.Activate(); + + gen() + ?.HideOnEmpty(true) + ?.AttachTo("messagesboxmobile") + ?.Activate(); + + + } + +} \ No newline at end of file diff --git a/UI/PendingChanges.ts b/UI/PendingChanges.ts index 2466973f4..de76d1748 100644 --- a/UI/PendingChanges.ts +++ b/UI/PendingChanges.ts @@ -1,65 +1,37 @@ import {UIElement} from "./UIElement"; import {UIEventSource} from "./UIEventSource"; -import {Changes} from "../Logic/Changes"; -export class PendingChanges extends UIElement{ +export class PendingChanges extends UIElement { + private _pendingChangesCount: UIEventSource; + private _countdown: UIEventSource; + private _isSaving: UIEventSource; - private readonly changes; - - constructor(changes: Changes, countdown: UIEventSource) { - super(undefined); // We do everything manually here! - this.changes = changes; - - - countdown.addCallback(function () { - - const percentage = Math.max(0, 100 * countdown.data / 20000); - - let bar = document.getElementById("pending-bar"); - if (bar === undefined) { - return; - } - const style = bar.style; - style.width = percentage + "%"; - style["margin-left"] = (50 - (percentage / 2)) + "%"; - - }); - - changes.pendingChangesES.addCallback(function () { - const c = changes._pendingChanges.length; - const text = document.getElementById("pending-text"); - if (c == 0) { - text.style.opacity = "0"; - text.innerText = "Saving..."; - } else { - text.innerText = c + " pending"; - text.style.opacity = "1"; - } - - - const bar = document.getElementById("pending-bar"); - - if (bar === null) { - return; - } - - - if (c == 0) { - bar.style.opacity = "0"; - } else { - bar.style.opacity = "0.5"; - } - - }); + constructor(pendingChangesCount: UIEventSource, + countdown: UIEventSource, + isSaving: UIEventSource) { + super(pendingChangesCount); + this.ListenTo(isSaving); + this.ListenTo(countdown); + this._pendingChangesCount = pendingChangesCount; + this._countdown = countdown; + this._isSaving = isSaving; } - + protected InnerRender(): string { - return "
" + - "
"; - } + if (this._isSaving.data) { + return "Saving"; + } + if (this._pendingChangesCount.data == 0) { + return ""; + } + + var restingSeconds = this._countdown.data / 1000; + var dots = ""; + while (restingSeconds > 0) { + dots += "."; + restingSeconds = restingSeconds - 1; + } + return "Saving "+this._pendingChangesCount.data; + } - InnerUpdate(htmlElement: HTMLElement) { - } - - } \ No newline at end of file diff --git a/UI/QuestionPicker.ts b/UI/QuestionPicker.ts index d8462de86..a550af628 100644 --- a/UI/QuestionPicker.ts +++ b/UI/QuestionPicker.ts @@ -36,7 +36,7 @@ export class QuestionPicker extends UIElement { if (highestQ === undefined) { - return "De vragen zijn op!"; + return ""; } return highestQ.CreateHtml(this.source).Render(); diff --git a/UI/SlideShow.ts b/UI/SlideShow.ts index 186a5250e..1810a2d3a 100644 --- a/UI/SlideShow.ts +++ b/UI/SlideShow.ts @@ -6,7 +6,6 @@ export class SlideShow extends UIElement { private readonly _embeddedElements: UIEventSource private readonly _currentSlide: UIEventSource = new UIEventSource(0); - private readonly _title: UIElement; private readonly _noimages: UIElement; constructor( @@ -14,7 +13,6 @@ export class SlideShow extends UIElement { embeddedElements: UIEventSource, noImages: UIElement) { super(embeddedElements); - this._title = title; this._embeddedElements = embeddedElements; this.ListenTo(this._currentSlide); this._noimages = noImages; @@ -24,30 +22,37 @@ export class SlideShow extends UIElement { if (this._embeddedElements.data.length == 0) { return this._noimages.Render(); } - const prevBtn = "" - const nextBtn = "" - let header = this._title.Render(); - if (this._embeddedElements.data.length > 1) { - header = header + prevBtn + (this._currentSlide.data + 1) + "/" + this._embeddedElements.data.length + nextBtn; + + if (this._embeddedElements.data.length == 1) { + return "
"+this._embeddedElements.data[0].Render()+"
"; } - let body = "" + + const prevBtn = "
" + const nextBtn = "
" + + let slides = "" for (let i = 0; i < this._embeddedElements.data.length; i++) { let embeddedElement = this._embeddedElements.data[i]; let state = "hidden" if (this._currentSlide.data === i) { state = "active-slide"; } - body += "
" + embeddedElement.Render() + "
\n"; + slides += "
" + embeddedElement.Render() + "
\n"; } - return "" + header + body + ""; + return "
" + + prevBtn + + "
" + slides + "
" + + nextBtn + + "
"; } InnerUpdate(htmlElement) { - const nextButton = htmlElement.getElementsByClassName('next-button')[0]; - if(nextButton === undefined){ + const nextButton = document.getElementById("nextbtn-"+this.id); + if(nextButton === undefined || nextButton === null){ return; } - const prevButton = htmlElement.getElementsByClassName('prev-button')[0]; + + const prevButton = document.getElementById("prevbtn-"+this.id); const self = this; nextButton.onclick = () => { const current = self._currentSlide.data; diff --git a/UI/UIElement.ts b/UI/UIElement.ts index 919f080b5..a690bdc4f 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -6,6 +6,8 @@ export abstract class UIElement { public readonly id: string; public readonly _source: UIEventSource; + + private _hideIfEmpty = false; protected constructor(source: UIEventSource) { this.id = "ui-element-" + UIElement.nextId; @@ -33,9 +35,23 @@ export abstract class UIElement { } element.innerHTML = this.InnerRender(); + if(this._hideIfEmpty){ + if(element.innerHTML === ""){ + element.parentElement.style.display = "none"; + }else{ + element.parentElement.style.display = undefined; + } + } + this.InnerUpdate(element); } + HideOnEmpty(hide : boolean){ + this._hideIfEmpty = hide; + this.Update(); + return this; + } + // Called after the HTML has been replaced. Can be used for css tricks InnerUpdate(htmlElement : HTMLElement){} @@ -45,6 +61,10 @@ export abstract class UIElement { AttachTo(divId: string) { let element = document.getElementById(divId); + if(element === null){ + console.log("SEVERE: could not attach UIElement to ", divId); + return; + } element.innerHTML = this.Render(); this.Update(); return this; diff --git a/UI/UIEventSource.ts b/UI/UIEventSource.ts index 4fa020515..1cf6f54e5 100644 --- a/UI/UIEventSource.ts +++ b/UI/UIEventSource.ts @@ -8,7 +8,7 @@ export class UIEventSource{ } - public addCallback(callback: (() => void)) { + public addCallback(callback: ((latestData) => void)) { this._callbacks.push(callback); return this; } @@ -23,18 +23,19 @@ export class UIEventSource{ public ping(): void { for (let i in this._callbacks) { - this._callbacks[i](); + this._callbacks[i](this.data); } } public map(f: ((T) => J)): UIEventSource { - const newSource = new UIEventSource( - f(this.data) - ); const self = this; this.addCallback(function () { newSource.setData(f(self.data)); + newSource.ping(); }); + const newSource = new UIEventSource( + f(this.data) + ); return newSource; diff --git a/UI/UserBadge.ts b/UI/UserBadge.ts index dd5a93d1a..eaefbf762 100644 --- a/UI/UserBadge.ts +++ b/UI/UserBadge.ts @@ -7,11 +7,14 @@ import {UIEventSource} from "./UIEventSource"; */ export class UserBadge extends UIElement { private _userDetails: UIEventSource; + private _pendingChanges: UIElement; - constructor(userDetails: UIEventSource) { + constructor(userDetails: UIEventSource, + pendingChanges : UIElement) { super(userDetails); this._userDetails = userDetails; + this._pendingChanges = pendingChanges; userDetails.addCallback(function () { const profilePic = document.getElementById("profile-pic"); @@ -27,17 +30,50 @@ export class UserBadge extends UIElement { if (!user.loggedIn) { return "
Klik hier om aan te melden bij OSM
"; } + + + let messageSpan = "" + + " " + + user.totalMessages + + ""; + + if (user.unreadMessages > 0) { + messageSpan = "" + + " " + + " " + + "" + + user.unreadMessages.toString() + + ""; + } + + let dryrun = ""; + if (user.dryRun) { + dryrun = " TESTING"; + } return " " + - "
"+ - "
" + - "" + + "
" + + "

" + + "" + user.name + "" + + dryrun + + "

" + + "

" + + messageSpan + + " " + + " " + user.csCount + + " " + + this._pendingChanges.Render() + + "

" + + "
"; } InnerUpdate(htmlElement: HTMLElement) { + this._pendingChanges.Update(); + } + + Activate() { + this._pendingChanges.Activate(); } } \ No newline at end of file diff --git a/UI/VariableUIElement.ts b/UI/VariableUIElement.ts index b47dceff7..d48bc6155 100644 --- a/UI/VariableUIElement.ts +++ b/UI/VariableUIElement.ts @@ -3,15 +3,25 @@ import {UIEventSource} from "./UIEventSource"; export class VariableUiElement extends UIElement { private _html: UIEventSource; + private _innerUpdate: (htmlElement: HTMLElement) => void; - constructor(html: UIEventSource) { + constructor(html: UIEventSource, innerUpdate : ((htmlElement : HTMLElement) => void) = undefined) { super(html); this._html = html; + this._innerUpdate = innerUpdate; + } protected InnerRender(): string { return this._html.data; } + + InnerUpdate(htmlElement: HTMLElement) { + super.InnerUpdate(htmlElement); + if(this._innerUpdate !== undefined){ + this._innerUpdate(htmlElement); + } + } } \ No newline at end of file diff --git a/assets/arrow-left-smooth.svg b/assets/arrow-left-smooth.svg new file mode 100644 index 000000000..1139447fa --- /dev/null +++ b/assets/arrow-left-smooth.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/arrow-right-both.svg b/assets/arrow-right-both.svg new file mode 100644 index 000000000..39a3fa916 --- /dev/null +++ b/assets/arrow-right-both.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/assets/arrow-right-sharp.svg b/assets/arrow-right-sharp.svg new file mode 100644 index 000000000..49d18106e --- /dev/null +++ b/assets/arrow-right-sharp.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/arrow-right-smooth.svg b/assets/arrow-right-smooth.svg new file mode 100644 index 000000000..7cc17c681 --- /dev/null +++ b/assets/arrow-right-smooth.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/test.json b/assets/test.json index 00131d898..3d707d0d2 100644 --- a/assets/test.json +++ b/assets/test.json @@ -577,7 +577,6 @@ ], "tags": { "leisure": "park", - "name": "Koning Albert I-park", "name:etymology:wikidata": "Q12974", "surface": "wood", "wikidata": "Q54817470", @@ -634,7 +633,6 @@ ], "tags": { "leisure": "park", - "name": "Begijnvest", "wikidata": "Q4499623", "wikipedia": "nl:Begijnenvest (Brugge)" } @@ -17215,7 +17213,6 @@ ], "tags": { "leisure": "park", - "name": "Begijnvest", "type": "multipolygon", "wikidata": "Q4499623", "wikipedia": "nl:Begijnenvest (Brugge)" diff --git a/assets/wikimedia-commons-white.svg b/assets/wikimedia-commons-white.svg new file mode 100644 index 000000000..9fc0c792a --- /dev/null +++ b/assets/wikimedia-commons-white.svg @@ -0,0 +1,22 @@ + + +Wikimedia Commons Logo + + + + + + + + + + + + + + + + + + + diff --git a/assets/wikimedia-commons.svg b/assets/wikimedia-commons.svg new file mode 100644 index 000000000..6496dec84 --- /dev/null +++ b/assets/wikimedia-commons.svg @@ -0,0 +1,22 @@ + + +Wikimedia Commons Logo + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.css b/index.css index 5d470423f..9402a8eb8 100644 --- a/index.css +++ b/index.css @@ -12,18 +12,59 @@ body { height: 100%; } +img { + border-radius: 1em; +} + +/**************** GENERIC ****************/ + +.uielement { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +.alert { + background-color: #fee4d1; + font-weight: bold; + border-radius: 1em; + padding: 0.3em; + margin: 0.25em; + text-align: center; + padding-top: 0.15em; + padding-bottom: 0.15em; +} + + +.activate-osm-authentication { + cursor: pointer; + color: blue; + text-decoration: underline; +} + + +/**************** USER BADGE ****************/ + + .star { fill: black; width: 1em; height: 1em; } +.envelope { + width: 1em; + height: 1em; +} + + #profile-pic { float: left; width: 4em; height: 4em; - padding:0; - margin:0; + padding: 0; + margin: 0; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; @@ -31,11 +72,12 @@ body { transition: opacity 500ms linear; } -#usertext a { +#username { text-decoration: none; color: black; } + #usertext { width: auto; margin:0; @@ -50,81 +92,76 @@ body { background-color: white; background-size: 100%; display: block; - + line-height: 0.75em; - - } -#username { - display: block ruby; +#usertext a { text-decoration: none; color: black; } -.imgWithAttr { - max-height: 20em; - -} - - -#userbadge{ +#userbadge { + display: inline-block; text-align: center; background-color: white; -webkit-border-radius: 2em; -moz-border-radius: 2em; border-radius: 2em; transition: all 500ms linear; + margin: 1em; + margin-left: 0; + margin-top: 0; + min-width: 20em; + pointer-events: all; + } -#authbox { + +#userbadge p { + margin: 0; + padding-top: 0.2em; + padding-bottom: 0.2em; +} + + +/**************************************/ + + +#topleft-tools { + display: block; position: absolute; margin: 1em; margin-right: 0; margin-bottom: 0; - padding:0; + padding: 0; z-index: 5000; transition: all 500ms linear; - max-width: 25%; overflow: hidden; - -} - -#pendingchangesbox { - position:relative; -} - -#pending-bar { - height: 1.2em; - margin: 0; - z-index: 5005; - background-color: white; - border-radius:1.2em; - padding: 0; - transition: all 1s linear; - opacity: 0; - -} - -#pending-text { - margin: 0; - margin-top: -1.2em; - z-index: 5000; - border-radius:1.2em; - height: 1.2em; - text-align: center; - padding: 0; - opacity: 0; - transition: opacity 1s linear; + pointer-events: none; } +#welcomeMessage { + display: inline-block; + width: 30em; +} -#zoomin { +#messagesboxmobilewrapper { + display: none; /*Only shown on small screens*/ +} + +#messagesboxmobile { + padding-left: 1.5em; + padding-right: 1.5em; + padding-top: 1em; + padding-bottom: 0; + height: 90%; } #messagesbox { + /*Only shown on big screens*/ padding: 2em; padding-top: 1em; padding-bottom: 1em; @@ -132,6 +169,33 @@ body { transition: all 500ms linear; background-color: white; border-radius: 2em; + pointer-events: all; +} + + +@media only screen and (max-width: 600px) { + #messagesbox { + display: none; + } +} + + +@media only screen and (max-width: 600px) { + #messagesboxmobilewrapper { + position: absolute; + padding: 0; + margin: 0; + z-index: 5050; + transition: all 500ms linear; + overflow: hidden; + border-radius: 0; + width: 100%; + height: 100%; + display: block; + overflow-y: auto; + background-color: white; + + } } #logo { @@ -202,41 +266,170 @@ body { font-weight: bold; } -.uielement{ - width:100%; - height:100%; - margin:0; - padding:0; + +/************ Slideshow *****************/ + + +.image-slideshow { + position: relative; + text-align: center; + max-width: 100%; + margin-top: 1em; + margin-bottom: 1em; } -.activate-osm-authentication { - cursor: pointer; - color: blue; - text-decoration: underline; +.slides { } -.popupImg { - max-height: 150px; - max-width: 300px; +.prev-button { + background-color: black; + opacity: 30%; + width: 3.0em; + + height: 100%; + padding-left: 0.5em; + padding-right: 0.5em; + position: absolute; + top: 50%; + left: 0; + transform: translate(0, -50%); + + z-index: 5060; + + content:url(assets/arrow-left-smooth.svg); + + border-bottom-left-radius: 1em; + border-top-left-radius: 1em; } -.image-slidehow{ + +.next-button { + background-color: black; + opacity: 30%; + width: 3.0em; + height: 100%; + padding-left: 0.5em; + padding-right: 0.5em; + position: absolute; + top: 50%; + right: 0; + transform: translate(0, -50%); + border-bottom-right-radius: 1em; + border-top-right-radius: 1em; + + + z-index: 5060; + + content:url(assets/arrow-right-smooth.svg); +} + + +.slide > span > img { width: 500px; - height: 250px; + max-width: 100%; + height: auto; + } -.slide { -} - -.slide img{ - max-height: 200px; - max-width: 500px; -} .hidden { - display: none; + /* This is used by the slideshow, to hide non-active slides*/ + display: none !important; + } -.osmlink{ - font-size: xx-small; + +.imgWithAttr { + max-height: 20em; + + position: relative; + overflow: hidden; + +} + +.wikimedia-link { + width: 1.5em; + height: auto; +} + +.attribution { + background-color: rgba(0, 0, 0, 0.5); + color: white; + font-weight: bold; + font-size: smaller; + + position: absolute; + bottom: 0; + left: 5em; /* Offset for the go left button*/ + padding: 0.25em; + margin-bottom: 0.25em; + border-radius: 0.5em; + +} + +.attribution-author { + display: inline-block; +} + +.license { + font-size: small; + font-weight: lighter; +} + +.attribution a { + color: white; +} + +/***************** Info box (box containing features and questions ******************/ + +.featureinfobox { +} + +.featureinfoboxtitle { + position: relative; +} + +.osmlink { + position: absolute; + right: 0; +} + + +.osm-logo path { + fill: #80cf36; +} + + +.featureinfoboxtitle span { + font-weight: bold; + font-size: x-large; +} + +.featureinfoboxtitle a { + float: right; + margin-left: 1em; +} + + +.infoboxcontents { + +} + + +.infboxcontents-left { + width: auto; + display: inline-block; + float: left; + padding: 0.1em; +} + +.infboxcontents-right { + padding: 0.1em; + overflow: hidden; + display: inline-block; +} + +.infobox-questions { + max-width: 25em; + display: inline-block; } \ No newline at end of file diff --git a/index.html b/index.html index f11109241..0bf670451 100644 --- a/index.html +++ b/index.html @@ -12,42 +12,34 @@ +
+
Return to map
+
+
-
+

-
-
-
- -
- -
-
ADD
- +
+ - - - - - - + diff --git a/index.ts b/index.ts index cda9270b0..8fa5aa562 100644 --- a/index.ts +++ b/index.ts @@ -5,30 +5,34 @@ import {UIEventSource} from "./UI/UIEventSource"; import {UserBadge} from "./UI/UserBadge"; import {Basemap} from "./Logic/Basemap"; import {PendingChanges} from "./UI/PendingChanges"; -import {FixedUiElement} from "./UI/FixedUiElement"; import {CenterMessageBox} from "./UI/CenterMessageBox"; import {Helpers} from "./Helpers"; import {KnownSet} from "./Layers/KnownSet"; -import {AddButton} from "./UI/AddButton"; -import {Tag} from "./Logic/TagsFilter"; +import {Tag, TagsFilter, TagUtils} from "./Logic/TagsFilter"; import {FilteredLayer} from "./Logic/FilteredLayer"; import {LayerUpdater} from "./Logic/LayerUpdater"; -import {LoginDependendMessage} from "./UI/LoginDependendMessage"; +import {VariableUiElement} from "./UI/VariableUIElement"; +import {UIElement} from "./UI/UIElement"; +import {MessageBoxHandler} from "./UI/MessageBoxHandler"; +import {Overpass} from "./Logic/Overpass"; +import {FixedUiElement} from "./UI/FixedUiElement"; +import {FeatureInfoBox} from "./UI/FeatureInfoBox"; let dryRun = false; if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { // Set to true if testing and changes should NOT be saved - // dryRun = true; + dryRun = true; // If you have a testfile somewhere, enable this to spoof overpass // This should be hosted independantly, e.g. with `cd assets; webfsd -p 8080` + a CORS plugin to disable cors rules - // Overpass.testUrl = "http://127.0.0.1:8080/test.json"; + Overpass.testUrl = "http://127.0.0.1:8080/test.json"; } // ----------------- SELECT THE RIGHT QUESTSET ----------------- + let questSetToRender = KnownSet.groen; if (window.location.search) { const params = window.location.search.substr(1).split("&"); @@ -56,7 +60,12 @@ const centerMessage = new UIEventSource(""); // The countdown: if set to e.g. ten, it'll start counting down. When reaching zero, changes will be saved. NB: this is implemented later, not in the eventSource const secondsTillChangesAreSaved = new UIEventSource(0); -var locationControl = new UIEventSource({ +const leftMessage = new UIEventSource<() => UIElement>(undefined); + +const selectedElement = new UIEventSource(undefined); + + +const locationControl = new UIEventSource({ zoom: questSetToRender.startzoom, lat: questSetToRender.startLat, lon: questSetToRender.startLon @@ -65,10 +74,12 @@ var locationControl = new UIEventSource({ // ----------------- Prepare the important objects ----------------- - +const saveTimeout = 5000; // After this many milliseconds without changes, saves are sent of to OSM const allElements = new ElementStorage(); const osmConnection = new OsmConnection(dryRun); -const changes = new Changes(osmConnection, allElements, centerMessage); +const changes = new Changes( + "Beantwoorden van vragen met MapComplete voor vragenset #" + questSetToRender.name, + osmConnection, allElements, centerMessage); const bm = new Basemap("leafletDiv", locationControl); @@ -86,7 +97,7 @@ const flayers: FilteredLayer[] = [] for (const layer of questSetToRender.layers) { - const flayer = layer.asLayer(bm, allElements, changes, osmConnection.userDetails.data); + const flayer = layer.asLayer(bm, allElements, changes, osmConnection.userDetails, selectedElement); const addButton = { name: layer.name, @@ -103,23 +114,64 @@ const layerUpdater = new LayerUpdater(bm, questSetToRender.startzoom, flayers); // ------------------ Setup various UI elements ------------ - +/* const addButton = new AddButton(bm, changes, addButtons); addButton.AttachTo("bottomRight"); addButton.Update(); + */ + +/** + * Show the questions and information for the selected element on the leftMessage + */ +selectedElement.addCallback((data) => { + console.log("Got selection"); + // Which is the applicable set? + for (const layer of questSetToRender.layers) { + + const applicable = layer.overpassFilter.matches(TagUtils.proprtiesToKV(data)); + if (applicable) { + // This layer is the layer that gives the questions + leftMessage.setData(() => + new FeatureInfoBox( + allElements.getElement(data.id), + layer.elementsToShow, + layer.questions, + changes, + osmConnection.userDetails + )); + break; + } + } -new UserBadge(osmConnection.userDetails) + } +); + +const pendingChanges = new PendingChanges( + changes.pendingChangesES, secondsTillChangesAreSaved, changes.isSaving); + +new UserBadge(osmConnection.userDetails, pendingChanges) .AttachTo('userbadge'); -new FixedUiElement(questSetToRender.welcomeMessage) - .AttachTo("welcomeMessage"); +var welcomeMessage = () => { + return new VariableUiElement( + osmConnection.userDetails.map((userdetails) => { + var login = questSetToRender.gettingStartedPlzLogin; + if (userdetails.loggedIn) { + login = questSetToRender.welcomeBackMessage; + } + return "
" + + questSetToRender.welcomeMessage + login + + "
"; + }), + function (html) { + osmConnection.registerActivateOsmAUthenticationClass() + }); +} +leftMessage.setData(welcomeMessage); -new LoginDependendMessage(osmConnection.userDetails, questSetToRender.gettingStartedPlzLogin, questSetToRender.welcomeBackMessage) - .AttachTo("gettingStartedBox"); -new PendingChanges(changes, secondsTillChangesAreSaved) - .AttachTo("pendingchangesbox"); +var messageBox = new MessageBoxHandler(leftMessage, () => {selectedElement.setData(undefined)}); new CenterMessageBox( questSetToRender.startzoom, @@ -130,11 +182,14 @@ new CenterMessageBox( .AttachTo("centermessage"); -Helpers.SetupAutoSave(changes, secondsTillChangesAreSaved); +Helpers.SetupAutoSave(changes, secondsTillChangesAreSaved, saveTimeout); Helpers.LastEffortSave(changes); -Helpers.registerActivateOsmAUthenticationClass(osmConnection); + + +osmConnection.registerActivateOsmAUthenticationClass(); // --------------- Send a ping to start various action -------- locationControl.ping(); +messageBox.update(); diff --git a/test.html b/test.html index 70c4e248f..e7381bacd 100644 --- a/test.html +++ b/test.html @@ -5,8 +5,7 @@ -
Hello World
- +
Hello World
diff --git a/test.ts b/test.ts index 4d3a78bb6..7891377cf 100644 --- a/test.ts +++ b/test.ts @@ -1,50 +1,70 @@ -import {FixedUiElement} from "./UI/FixedUiElement"; -import $ from "jquery" -import {Imgur} from "./Logic/Imgur"; -import {ImageUploadFlow} from "./UI/ImageUploadFlow"; -import {UserDetails} from "./Logic/OsmConnection"; +// The message that should be shown at the center of the screen import {UIEventSource} from "./UI/UIEventSource"; -import {UIRadioButton} from "./UI/UIRadioButton"; import {UIElement} from "./UI/UIElement"; +import {ElementStorage} from "./Logic/ElementStorage"; +import {OsmConnection} from "./Logic/OsmConnection"; +import {Changes} from "./Logic/Changes"; +import {Basemap} from "./Logic/Basemap"; +import {KnownSet} from "./Layers/KnownSet"; +import {Overpass} from "./Logic/Overpass"; +import {FeatureInfoBox} from "./UI/FeatureInfoBox"; +import {TagMapping, TagMappingOptions} from "./UI/TagMapping"; +import {CommonTagMappings} from "./Layers/CommonTagMappings"; +import {ImageCarousel} from "./UI/Image/ImageCarousel"; +import {WikimediaImage} from "./UI/Image/WikimediaImage"; +const centerMessage = new UIEventSource(""); -var tags = { - "name": "Astridpark Brugge", - "wikidata":"Q1234", - "leisure":"park" -} +const dryRun = true; +// If you have a testfile somewhere, enable this to spoof overpass +// This should be hosted independantly, e.g. with `cd assets; webfsd -p 8080` + a CORS plugin to disable cors rules +Overpass.testUrl = "http://127.0.0.1:8080/test.json"; +// The countdown: if set to e.g. ten, it'll start counting down. When reaching zero, changes will be saved. NB: this is implemented later, not in the eventSource +const secondsTillChangesAreSaved = new UIEventSource(0); -var userdetails = new UserDetails() -userdetails.loggedIn = true; -userdetails.name = "Pietervdvn"; +const leftMessage = new UIEventSource<() => UIElement>(undefined); +const selectedElement = new UIEventSource(undefined); -new ImageUploadFlow( -).AttachTo("maindiv") //*/ +const questSetToRender = KnownSet.groen; - - -/* -$('document').ready(function () { - $('input[type=file]').on('change', function () { - var $files = $(this).get(0).files; - - if ($files.length) { - // Reject big files - if ($files[0].size > $(this).data('max-size') * 1024) { - console.log('Please select a smaller file'); - return false; - } - - // Begin file upload - console.log('Uploading file to Imgur..'); - - const imgur = new Imgur(); - imgur.uploadImage("KorenBloem", "Een korenbloem, ergens", $files[0], - (url) => { - console.log("URL: ", url); - }) - } - }); +const locationControl = new UIEventSource({ + zoom: questSetToRender.startzoom, + lat: questSetToRender.startLat, + lon: questSetToRender.startLon }); -*/ \ No newline at end of file + + +// ----------------- Prepare the important objects ----------------- + +const saveTimeout = 5000; // After this many milliseconds without changes, saves are sent of to OSM +const allElements = new ElementStorage(); +const osmConnection = new OsmConnection(dryRun); +const changes = new Changes( + "Beantwoorden van vragen met MapComplete voor vragenset #" + questSetToRender.name, + osmConnection, allElements, centerMessage); + + +const layer = questSetToRender.layers[0]; +const tags ={ + id: "way/123", + // access: "yes", + barrier: "fence", + curator: "Arnout Zwaenepoel", + description: "Heide en heischraal landschap met landduin en grote soortenverscheidenheid", + dog: "no", + email: "arnoutenregine@skynet.be", + image: "https://natuurpuntbrugge.be/wp-content/uploads/2017/05/Schobbejakshoogte-schapen-PDG-1024x768.jpg", + leisure: "nature_reserve", + name: "Schobbejakshoogte", + operator: "Natuurpunt Brugge", + phone: "+32 50 82 26 97", + website: "https://natuurpuntbrugge.be/schobbejakshoogte/", + wikidata: "Q4499623", + wikipedia: "nl:Schobbejakshoogte" +}; +const tagsES = allElements.addElement({properties: tags}); + +new ImageCarousel(tagsES).AttachTo("maindiv").Activate(); + +// new WikimediaImage(new UIEventSource("File:Brugge_cobblestone_path.jpg")).AttachTo("maindiv");