diff --git a/Docs/TagInfo/mapcomplete_notes.json b/Docs/TagInfo/mapcomplete_notes.json new file mode 100644 index 000000000..580b68620 --- /dev/null +++ b/Docs/TagInfo/mapcomplete_notes.json @@ -0,0 +1,23 @@ +{ + "data_format": 1, + "project": { + "name": "MapComplete Notes on OpenStreetMap", + "description": "A note is a pin on the map with some text to indicate something wrong", + "project_url": "https://mapcomplete.osm.be/notes", + "doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/", + "icon_url": "https://mapcomplete.osm.be/assets/svg/resolved.svg", + "contact_name": "Pieter Vander Vennet, MapComplete", + "contact_email": "pietervdvn@posteo.net" + }, + "tags": [ + { + "key": "id", + "description": "The MapComplete theme Notes on OpenStreetMap has a layer OpenStreetMap notes showing features with this tag" + }, + { + "key": "id", + "description": "The MapComplete theme Notes on OpenStreetMap has a layer Your track showing features with this tag", + "value": "location_track" + } + ] +} \ No newline at end of file diff --git a/Logic/Actors/StrayClickHandler.ts b/Logic/Actors/StrayClickHandler.ts index 1cde81871..7c9a51fa0 100644 --- a/Logic/Actors/StrayClickHandler.ts +++ b/Logic/Actors/StrayClickHandler.ts @@ -1,9 +1,9 @@ import * as L from "leaflet"; import {UIEventSource} from "../UIEventSource"; import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"; -import AddNewMarker from "../../UI/BigComponents/AddNewMarker"; import FilteredLayer from "../../Models/FilteredLayer"; import Constants from "../../Models/Constants"; +import BaseUIElement from "../../UI/BaseUIElement"; /** * The stray-click-hanlders adds a marker to the map if no feature was clicked. @@ -13,37 +13,41 @@ export default class StrayClickHandler { private _lastMarker; constructor( - lastClickLocation: UIEventSource<{ lat: number, lon: number }>, - selectedElement: UIEventSource, - filteredLayers: UIEventSource, - leafletMap: UIEventSource, - uiToShow: ScrollableFullScreen) { + state: { + LastClickLocation: UIEventSource<{ lat: number, lon: number }>, + selectedElement: UIEventSource, + filteredLayers: UIEventSource, + leafletMap: UIEventSource + }, + uiToShow: ScrollableFullScreen, + iconToShow: BaseUIElement) { const self = this; - filteredLayers.data.forEach((filteredLayer) => { + const leafletMap = state.leafletMap + state.filteredLayers.data.forEach((filteredLayer) => { filteredLayer.isDisplayed.addCallback(isEnabled => { if (isEnabled && self._lastMarker && leafletMap.data !== undefined) { // When a layer is activated, we remove the 'last click location' in order to force the user to reclick // This reclick might be at a location where a feature now appeared... - leafletMap.data.removeLayer(self._lastMarker); + state.leafletMap.data.removeLayer(self._lastMarker); } }) }) - lastClickLocation.addCallback(function (lastClick) { + state.LastClickLocation.addCallback(function (lastClick) { if (self._lastMarker !== undefined) { - leafletMap.data?.removeLayer(self._lastMarker); + state.leafletMap.data?.removeLayer(self._lastMarker); } if (lastClick === undefined) { return; } - selectedElement.setData(undefined); + state.selectedElement.setData(undefined); const clickCoor: [number, number] = [lastClick.lat, lastClick.lon] self._lastMarker = L.marker(clickCoor, { icon: L.divIcon({ - html: new AddNewMarker(filteredLayers).ConstructElement(), + html: iconToShow.ConstructElement(), iconSize: [50, 50], iconAnchor: [25, 50], popupAnchor: [0, -45] @@ -71,7 +75,7 @@ export default class StrayClickHandler { }); }); - selectedElement.addCallback(() => { + state.selectedElement.addCallback(() => { if (self._lastMarker !== undefined) { leafletMap.data.removeLayer(self._lastMarker); this._lastMarker = undefined; diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 634547764..7f2067c7c 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -223,7 +223,7 @@ export class OsmConnection { if ((text ?? "") !== "") { textSuffix = "?text=" + encodeURIComponent(text) } - if(this._dryRun){ + if (this._dryRun) { console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) return new Promise((ok, error) => { ok() @@ -232,7 +232,7 @@ export class OsmConnection { return new Promise((ok, error) => { this.auth.xhr({ method: 'POST', - path: `/api/0.6/notes/${id}/close${textSuffix}` + path: `/api/0.6/notes/${id}/close${textSuffix}`, }, function (err, response) { if (err !== null) { error(err) @@ -246,7 +246,7 @@ export class OsmConnection { } public reopenNote(id: number | string, text?: string): Promise { - if(this._dryRun){ + if (this._dryRun) { console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) return new Promise((ok, error) => { ok() @@ -272,9 +272,40 @@ export class OsmConnection { } + public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { + if (this._dryRun) { + console.warn("Dryrun enabled - not actually opening note with text ", text) + return new Promise((ok, error) => { + ok() + }); + } + const auth = this.auth; + const content = {lat, lon, text} + return new Promise((ok, error) => { + auth.xhr({ + method: 'POST', + path: `/api/0.6/notes.json`, + options: {header: + {'Content-Type': 'application/json'}}, + content: JSON.stringify(content) + + }, function (err, response) { + if (err !== null) { + error(err) + } else { + const id = Number(response.children[0].children[0].children.item("id").innerHTML) + console.log("OPENED NOTE", id) + ok({id}) + } + }) + + }) + + } + public addCommentToNode(id: number | string, text: string): Promise { - if(this._dryRun){ - console.warn("Dryrun enabled - not actually adding comment ",text, "to note ", id) + if (this._dryRun) { + console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id) return new Promise((ok, error) => { ok() }); @@ -286,7 +317,8 @@ export class OsmConnection { return new Promise((ok, error) => { this.auth.xhr({ method: 'POST', - path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}` + + path: `/api/0.6/notes.json/${id}/comment?text=${encodeURIComponent(text)}` }, function (err, response) { if (err !== null) { error(err) diff --git a/Models/Constants.ts b/Models/Constants.ts index 187d3e0c5..f01908f07 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import {Utils} from "../Utils"; export default class Constants { - public static vNumber = "0.14.0-alpha-2"; + public static vNumber = "0.14.0-alpha-3"; public static ImgurApiKey = '7070e7167f0a25a' public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" diff --git a/UI/BigComponents/AddNewMarker.ts b/UI/BigComponents/AddNewMarker.ts index e51152c7d..ccee99b50 100644 --- a/UI/BigComponents/AddNewMarker.ts +++ b/UI/BigComponents/AddNewMarker.ts @@ -27,6 +27,9 @@ export default class AddNewMarker extends Combine { } } } + if(icons.length === 0){ + return undefined + } if (icons.length === 1) { return icons[0] } diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 9d5b03e64..56c7e9b6e 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -138,9 +138,6 @@ export default class SimpleAddUI extends Toggle { loginButton, state.osmConnection.isLoggedIn ) - - - this.SetStyle("font-size:large"); } diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index c07dbf7bb..d2eef312c 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -23,6 +23,9 @@ import Lazy from "./Base/Lazy"; import {DefaultGuiState} from "./DefaultGuiState"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import * as home_location_json from "../assets/layers/home_location/home_location.json"; +import NewNoteUi from "./Popup/NewNoteUi"; +import Combine from "./Base/Combine"; +import AddNewMarker from "./BigComponents/AddNewMarker"; /** @@ -54,45 +57,63 @@ export default class DefaultGUI { public setupClickDialogOnMap(filterViewIsOpened: UIEventSource, state: FeaturePipelineState) { + const hasPresets = state.layoutToUse.layers.some(layer => layer.presets.length > 0); + const noteLayer= state.filteredLayers.data.filter(l => l.layerDef.id === "note")[0] + let addNewNoteDialog : () => BaseUIElement = undefined; + if(noteLayer !== undefined){ + addNewNoteDialog = () => new NewNoteUi(state.LastClickLocation, state) + } + function setup() { - let presetCount = 0; - for (const layer of state.layoutToUse.layers) { - for (const preset of layer.presets) { - presetCount++; - } - } - if (presetCount == 0) { - return; - } + console.warn("Settuping ") + if(!hasPresets && addNewNoteDialog === undefined){ + return; // nothing to do + } + + const newPointDialogIsShown = new UIEventSource(false); const addNewPoint = new ScrollableFullScreen( - () => Translations.t.general.add.title.Clone(), - () => new SimpleAddUI(newPointDialogIsShown, filterViewIsOpened, state), + () => hasPresets ? Translations.t.general.add.title : Translations.t.notes.createNoteTitle, + () => { + let addNew = undefined; + if(hasPresets){ + addNew = new SimpleAddUI(newPointDialogIsShown, filterViewIsOpened, state); + } + let addNote = undefined; + if(noteLayer !== undefined){ + addNote = addNewNoteDialog() + } + return new Combine([addNew, addNote]).SetClass("flex flex-col font-lg text-lg") + }, "new", newPointDialogIsShown ); + addNewPoint.isShown.addCallback((isShown) => { if (!isShown) { + // Clear the 'last-click'-location when the dialog is closed - this causes the popup and the marker to be removed state.LastClickLocation.setData(undefined); } }); new StrayClickHandler( - state.LastClickLocation, - state.selectedElement, - state.filteredLayers, - state.leafletMap, - addNewPoint + state, + addNewPoint, + hasPresets ? new AddNewMarker(state.filteredLayers) : new Combine([Svg.note_svg().SetStyle("height: 40px"), Svg.addSmall_svg().SetClass("absolute bottom-0 right-0 w-6 animate-pulse")]).SetClass("block relative") ); } - state.featureSwitchAddNew.addCallbackAndRunD(addNewAllowed => { - if (addNewAllowed) { - setup() - return true; - } - }) + if(noteLayer !== undefined){ + setup() + }else{ + state.featureSwitchAddNew.addCallbackAndRunD(addNewAllowed => { + if (addNewAllowed) { + setup() + return true; + } + }) + } } diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index a2e5cfa55..23178dcc3 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -7,18 +7,19 @@ import {Tag} from "../../Logic/Tags/Tag"; import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; import {Changes} from "../../Logic/Osm/Changes"; import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; export default class DeleteImage extends Toggle { - constructor(key: string, tags: UIEventSource, state: {changes?: Changes, osmConnection?: OsmConnection}) { + constructor(key: string, tags: UIEventSource, state: {layoutToUse: LayoutConfig, changes?: Changes, osmConnection?: OsmConnection}) { const oldValue = tags.data[key] const isDeletedBadge = Translations.t.image.isDeleted.Clone() .SetClass("rounded-full p-1") .SetStyle("color:white;background:#ff8c8c") .onClick(async () => { await state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, { - changeType: "answer", - theme: "test" + changeType: "delete-image", + theme: state.layoutToUse.id })) }); diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 1d4d7ef43..ffa880bf0 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -8,12 +8,13 @@ import Toggle from "../Input/Toggle"; import ImageProvider from "../../Logic/ImageProviders/ImageProvider"; import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import {Changes} from "../../Logic/Osm/Changes"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; export class ImageCarousel extends Toggle { - constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider }[]>, + constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider}[]>, tags: UIEventSource, - state: {osmConnection?: OsmConnection, changes?: Changes}) { + state: {osmConnection?: OsmConnection, changes?: Changes, layoutToUse: LayoutConfig }) { const uiElements = images.map((imageURLS: { key: string, url: string, provider: ImageProvider }[]) => { const uiElements: BaseUIElement[] = []; for (const url of imageURLS) { diff --git a/UI/Popup/NewNoteUi.ts b/UI/Popup/NewNoteUi.ts new file mode 100644 index 000000000..9addf0931 --- /dev/null +++ b/UI/Popup/NewNoteUi.ts @@ -0,0 +1,57 @@ +import Combine from "../Base/Combine"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import Translations from "../i18n/Translations"; +import Title from "../Base/Title"; +import ValidatedTextField from "../Input/ValidatedTextField"; +import {SubtleButton} from "../Base/SubtleButton"; +import Svg from "../../Svg"; +import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; +import Toggle from "../Input/Toggle"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; + +export default class NewNoteUi extends Toggle { + + constructor(lastClickLocation: UIEventSource<{ lat: number, lon: number }>, + state: { osmConnection: OsmConnection, layoutToUse: LayoutConfig, featurePipeline: FeaturePipeline }) { + + const t = Translations.t.notes; + const isCreated = new UIEventSource(false); + const text = ValidatedTextField.InputForType("text", { + value: LocalStorageSource.Get("note-text") + }) + text.SetClass("border rounded-sm border-grey-500") + + const postNote = new SubtleButton(Svg.addSmall_svg().SetClass("max-h-7"), t.createNote) + postNote.onClick(async () => { + let txt = text.GetValue().data + if (txt === undefined || txt === "") { + return; + } + txt += "\n\n #MapComplete #" + state?.layoutToUse?.id + const loc = lastClickLocation.data; + state?.osmConnection?.openNote(loc.lat, loc.lon, txt) + text.GetValue().setData("") + isCreated.setData(true) + }) + const createNoteDialog = new Combine([ + new Title(t.createNoteTitle), + t.createNoteIntro, + text, + new Combine([new Toggle(undefined, t.warnAnonymous, state?.osmConnection?.isLoggedIn).SetClass("alert"), postNote]).SetClass("flex justify-end items-center") + ]).SetClass("flex flex-col border-2 border-black rounded-xl p-4"); + + + super( + new Toggle(t.isCreated.SetClass("thanks"), + createNoteDialog, + isCreated + ), + undefined, + new UIEventSource(true) + ) + } + + +} diff --git a/all_themes_index.ts b/all_themes_index.ts index d18acadbd..dec4d6873 100644 --- a/all_themes_index.ts +++ b/all_themes_index.ts @@ -7,7 +7,11 @@ const layout = QueryParameters.GetQueryParameter("layout", undefined).data ?? "" const customLayout = QueryParameters.GetQueryParameter("userlayout", undefined).data ?? "" const l = window.location; if( layout !== ""){ - window.location.replace(l.protocol + "//" + window.location.host+"/"+layout+".html"+ l.search + l.hash); + if(window.location.host.startsWith("127.0.0.1")){ + window.location.replace(l.protocol + "//" + window.location.host+"/theme.html"+ l.search + "&layout="+layout + l.hash); + }else{ + window.location.replace(l.protocol + "//" + window.location.host+"/"+layout+".html"+ l.search + l.hash); + } }else if (customLayout !== ""){ window.location.replace(l.protocol + "//" + window.location.host+"/theme.html"+ l.search + l.hash); } diff --git a/assets/layers/note/note.json b/assets/layers/note/note.json index b4f28782c..63c447570 100644 --- a/assets/layers/note/note.json +++ b/assets/layers/note/note.json @@ -3,7 +3,7 @@ "name": { "en": "OpenStreetMap notes" }, - "description": "This layer shows notes on OpenStreetMap.", + "description": "This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)", "source": { "osmTags": "id~*", "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=7&bbox={x_min},{y_min},{x_max},{y_max}", diff --git a/assets/layers/note_import/note_import.json b/assets/layers/note_import/note_import.json index 58b0919f4..acb25411a 100644 --- a/assets/layers/note_import/note_import.json +++ b/assets/layers/note_import/note_import.json @@ -28,10 +28,12 @@ ], "isShown": { "render": "yes", - "mappings": [{ - "if": "_trigger_index=", - "then": "no" - }] + "mappings": [ + { + "if": "_trigger_index=", + "then": "no" + } + ] }, "titleIcons": [ { @@ -59,7 +61,6 @@ "id": "close_note_mapped", "render": "{close_note(Already mapped, ./assets/svg/checkmark.svg, id, Already mapped)}" }, - { "id": "comment", "render": "{add_note_comment()}" diff --git a/langs/en.json b/langs/en.json index 525041959..50f036e5b 100644 --- a/langs/en.json +++ b/langs/en.json @@ -441,6 +441,13 @@ "anonymous": "Anonymous user", "loginToAddComment": "Login to add a comment", "loginToAddPicture": "Login to add a picture", - "loginToClose": "Login to close this note" + "loginToClose": "Login to close this note", + "createNoteTitle": "Create a new note here", + "createNote": "Create a new note", + "noteIsPublic": "This will be visible to everyone", + "createNoteIntro": "Is something wrong or missing on the map? Create a note here. These will be checked by volunteers", + "warnAnonymous": "You are not logged in. We won't be able to contact you to resolve your issue.", + "notesLayerMustBeEnabled": "The 'notes'-layer is disabled. Enable it to add a note", + "isCreated": "Your note has been created!" } } diff --git a/langs/layers/en.json b/langs/layers/en.json index b5cf5b23d..674c9ac5f 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -3375,6 +3375,7 @@ } }, "note_import": { + "name": "Possible bookcases", "title": { "render": "Possible feature" } diff --git a/test.ts b/test.ts index e4f2d196d..ec4354d34 100644 --- a/test.ts +++ b/test.ts @@ -1 +1,2 @@ -console.log("Hello world") \ No newline at end of file +import NewNoteUi from "./UI/Popup/NewNoteUi"; +import {UIEventSource} from "./Logic/UIEventSource";