diff --git a/InitUiElements.ts b/InitUiElements.ts index a5b738e9c..65a4485e0 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -352,6 +352,11 @@ export class InitUiElements { State.state.backgroundLayer = State.state.backgroundLayerId .map((selectedId: string) => { + if(selectedId === undefined){ + return AvailableBaseLayers.osmCarto + } + + const available = State.state.availableBackgroundLayers.data; for (const layer of available) { if (layer.id === selectedId) { diff --git a/Logic/Actors/AvailableBaseLayers.ts b/Logic/Actors/AvailableBaseLayers.ts index e9855a1cb..1a025d5a0 100644 --- a/Logic/Actors/AvailableBaseLayers.ts +++ b/Logic/Actors/AvailableBaseLayers.ts @@ -14,18 +14,19 @@ import {Utils} from "../../Utils"; export default class AvailableBaseLayers { - public static osmCarto: BaseLayer = + public static osmCarto: BaseLayer = { id: "osm", - name: "OpenStreetMap", - layer: AvailableBaseLayers.CreateBackgroundLayer("osm", "OpenStreetMap", - "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", - 19, - false, false), + name: "OpenStreetMap", + layer: () => AvailableBaseLayers.CreateBackgroundLayer("osm", "OpenStreetMap", + "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", + 19, + false, false), feature: null, max_zoom: 19, min_zoom: 0 - } + } + public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); @@ -123,7 +124,7 @@ export default class AvailableBaseLayers { continue } - const leafletLayer = AvailableBaseLayers.CreateBackgroundLayer( + const leafletLayer: () => TileLayer = () => AvailableBaseLayers.CreateBackgroundLayer( props.id, props.name, props.url, @@ -150,10 +151,10 @@ export default class AvailableBaseLayers { private static LoadProviderIndex(): BaseLayer[] { // @ts-ignore X; // Import X to make sure the namespace is not optimized away - function l(id: string, name: string) { + function l(id: string, name: string) : BaseLayer{ try { - const layer: any = L.tileLayer.provider(id, undefined); - return { + const layer: any = () => L.tileLayer.provider(id, undefined); + const baseLayer : BaseLayer = { feature: null, id: id, name: name, @@ -161,6 +162,7 @@ export default class AvailableBaseLayers { min_zoom: layer.minzoom, max_zoom: layer.maxzoom } + return baseLayer } catch (e) { console.error("Could not find provided layer", name, e); return null; diff --git a/Models/BaseLayer.ts b/Models/BaseLayer.ts index 63c3abbc1..01eb8e9d7 100644 --- a/Models/BaseLayer.ts +++ b/Models/BaseLayer.ts @@ -3,7 +3,7 @@ import {TileLayer} from "leaflet"; export default interface BaseLayer { id: string, name: string, - layer: TileLayer, + layer: () => TileLayer, max_zoom: number, min_zoom: number; feature: any, diff --git a/Svg.ts b/Svg.ts index 3f0ff50fe..9a5c94b8f 100644 --- a/Svg.ts +++ b/Svg.ts @@ -104,11 +104,26 @@ export default class Svg { public static direction_svg() { return new Img(Svg.direction, true);} public static direction_ui() { return new FixedUiElement(Svg.direction_img);} - public static direction_gradient = " image/svg+xml " + public static direction_gradient = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " public static direction_gradient_img = Img.AsImageElement(Svg.direction_gradient) public static direction_gradient_svg() { return new Img(Svg.direction_gradient, true);} public static direction_gradient_ui() { return new FixedUiElement(Svg.direction_gradient_img);} + public static direction_masked = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static direction_masked_img = Img.AsImageElement(Svg.direction_masked) + public static direction_masked_svg() { return new Img(Svg.direction_masked, true);} + public static direction_masked_ui() { return new FixedUiElement(Svg.direction_masked_img);} + + public static direction_outline = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static direction_outline_img = Img.AsImageElement(Svg.direction_outline) + public static direction_outline_svg() { return new Img(Svg.direction_outline, true);} + public static direction_outline_ui() { return new FixedUiElement(Svg.direction_outline_img);} + + public static direction_stroke = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static direction_stroke_img = Img.AsImageElement(Svg.direction_stroke) + public static direction_stroke_svg() { return new Img(Svg.direction_stroke, true);} + public static direction_stroke_ui() { return new FixedUiElement(Svg.direction_stroke_img);} + public static down = " image/svg+xml " public static down_img = Img.AsImageElement(Svg.down) public static down_svg() { return new Img(Svg.down, true);} @@ -319,4 +334,4 @@ export default class Svg { public static wikipedia_svg() { return new Img(Svg.wikipedia, true);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} +public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts new file mode 100644 index 000000000..de7de1718 --- /dev/null +++ b/UI/Base/Minimap.ts @@ -0,0 +1,144 @@ +import BaseUIElement from "../BaseUIElement"; +import * as L from "leaflet"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Loc from "../../Models/Loc"; +import BaseLayer from "../../Models/BaseLayer"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import {Map} from "leaflet"; + +export default class Minimap extends BaseUIElement { + + private static _nextId = 0; + public readonly leafletMap: UIEventSource = new UIEventSource(undefined) + private readonly _id: string; + private readonly _background: UIEventSource; + private readonly _location: UIEventSource; + private _isInited = false; + private _allowMoving: boolean; + + constructor(options?: { + background?: UIEventSource, + location?: UIEventSource, + allowMoving?: boolean + } + ) { + super() + options = options ?? {} + this._background = options?.background ?? new UIEventSource(AvailableBaseLayers.osmCarto) + this._location = options?.location ?? new UIEventSource(undefined) + this._id = "minimap" + Minimap._nextId; + this._allowMoving = options.allowMoving ?? true; + Minimap._nextId++ + + } + + protected InnerConstructElement(): HTMLElement { + const div = document.createElement("div") + div.id = this._id; + div.style.height = "100%" + div.style.width = "100%" + div.style.minWidth = "40px" + div.style.minHeight = "40px" + const wrapper = document.createElement("div") + wrapper.appendChild(div) + const self = this; + // @ts-ignore + const resizeObserver = new ResizeObserver(_ => { + console.log("Change in size detected!") + self.InitMap(); + self.leafletMap?.data?.invalidateSize() + }); + + resizeObserver.observe(div); + return wrapper; + + } + + private InitMap() { + if (this._constructedHtmlElement === undefined) { + // This element isn't initialized yet + return; + } + + if (document.getElementById(this._id) === null) { + // not yet attached, we probably got some other event + return; + } + + if (this._isInited) { + return; + } + this._isInited = true; + const location = this._location; + + let currentLayer = this._background.data.layer() + const map = L.map(this._id, { + center: [location.data?.lat ?? 0, location.data?.lon ?? 0], + zoom: location.data?.zoom ?? 2, + layers: [currentLayer], + zoomControl: false, + attributionControl: false, + dragging: this._allowMoving, + scrollWheelZoom: this._allowMoving, + doubleClickZoom: this._allowMoving, + keyboard: this._allowMoving, + touchZoom: this._allowMoving + }); + + map.setMaxBounds( + [[-100, -200], [100, 200]] + ); + + this._background.addCallbackAndRun(layer => { + const newLayer = layer.layer() + if (currentLayer !== undefined) { + map.removeLayer(currentLayer); + } + currentLayer = newLayer; + map.addLayer(newLayer); + }) + + + let isRecursing = false; + map.on("moveend", function () { + if (isRecursing) { + return + } + if (map.getZoom() === location.data.zoom && + map.getCenter().lat === location.data.lat && + map.getCenter().lng === location.data.lon) { + return; + } + console.trace(map.getZoom(), map.getCenter(), location.data) + + location.data.zoom = map.getZoom(); + location.data.lat = map.getCenter().lat; + location.data.lon = map.getCenter().lng; + isRecursing = true; + location.ping(); + isRecursing = false; // This is ugly, I know + }) + + + location.addCallback(loc => { + const mapLoc = map.getCenter() + const dlat = Math.abs(loc.lat - mapLoc[0]) + const dlon = Math.abs(loc.lon - mapLoc[1]) + + if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) { + return; + } + map.setView([loc.lat, loc.lon], loc.zoom) + }) + + location.map(loc => loc.zoom) + .addCallback(zoom => { + if (Math.abs(map.getZoom() - zoom) > 0.1) { + map.setZoom(zoom, {}); + } + }) + + + this.leafletMap.setData(map) + } +} \ No newline at end of file diff --git a/UI/BigComponents/Basemap.ts b/UI/BigComponents/Basemap.ts index 5c2844bd0..4ad5bc8f6 100644 --- a/UI/BigComponents/Basemap.ts +++ b/UI/BigComponents/Basemap.ts @@ -14,10 +14,14 @@ export class Basemap { currentLayer: UIEventSource, lastClickLocation?: UIEventSource<{ lat: number, lon: number }>, extraAttribution?: BaseUIElement) { + + console.log("Currentlayer is" ,currentLayer, currentLayer.data, currentLayer.data?.id) + let previousLayer = currentLayer.data.layer(); + this.map = L.map(leafletElementId, { center: [location.data.lat ?? 0, location.data.lon ?? 0], zoom: location.data.zoom ?? 2, - layers: [currentLayer.data.layer], + layers: [previousLayer], zoomControl: false, attributionControl: extraAttribution !== undefined }); @@ -42,16 +46,16 @@ export class Basemap { extraAttribution.AttachTo('leaflet-attribution') const self = this; - let previousLayer = currentLayer.data; currentLayer.addCallbackAndRun(layer => { - if (layer === previousLayer) { + const newLayer = layer.layer() + if (newLayer === previousLayer) { return; } if (previousLayer !== undefined) { - self.map.removeLayer(previousLayer.layer); + self.map.removeLayer(previousLayer); } - previousLayer = layer; - self.map.addLayer(layer.layer); + previousLayer = newLayer; + self.map.addLayer(newLayer); }) diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index 4be40b99e..c356d34a8 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -2,21 +2,30 @@ import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; import Svg from "../../Svg"; +import BaseUIElement from "../BaseUIElement"; import {FixedUiElement} from "../Base/FixedUiElement"; +import {Utils} from "../../Utils"; +import Loc from "../../Models/Loc"; /** * Selects a direction in degrees */ export default class DirectionInput extends InputElement { + public static constructMinimap: ((any) => BaseUIElement); + private readonly _location: UIEventSource; public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly value: UIEventSource; + private background; - constructor(value?: UIEventSource) { + constructor(mapBackground: UIEventSource, + location: UIEventSource, + value?: UIEventSource) { super(); + this._location = location; this.value = value ?? new UIEventSource(undefined); - + this.background = mapBackground; } GetValue(): UIEventSource { @@ -30,16 +39,23 @@ export default class DirectionInput extends InputElement { protected InnerConstructElement(): HTMLElement { + let map: BaseUIElement = new FixedUiElement("") + if (!Utils.runningFromConsole) { + map = DirectionInput.constructMinimap({ + background: this.background, + allowMoving: false, + location: this._location + }) + } const element = new Combine([ - new FixedUiElement("").SetClass("w-full h-full absolute top-0 left-O rounded-full"), - Svg.direction_svg().SetStyle( + Svg.direction_stroke_svg().SetStyle( `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`) - .SetClass("direction-svg"), - Svg.compass_svg().SetStyle( - "position: absolute;top: 0;left: 0;width: 100%;height: 100%;") + .SetClass("direction-svg relative") + .SetStyle("z-index: 1000"), + map.SetClass("w-full h-full absolute top-0 left-O rounded-full overflow-hidden"), ]) - .SetStyle("position:relative;display:block;width: min(100%, 25em); padding-top: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") + .SetStyle("position:relative;display:block;width: min(100%, 25em); height: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") .ConstructElement() diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 51b24b2e4..f1ce4dd69 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -1,7 +1,6 @@ import {DropDown} from "./DropDown"; import * as EmailValidator from "email-validator"; import {parsePhoneNumberFromString} from "libphonenumber-js"; -import InputElementMap from "./InputElementMap"; import {InputElement} from "./InputElement"; import {TextField} from "./TextField"; import {UIElement} from "../UIElement"; @@ -12,6 +11,7 @@ import OpeningHoursInput from "../OpeningHours/OpeningHoursInput"; import DirectionInput from "./DirectionInput"; import ColorPicker from "./ColorPicker"; import {Utils} from "../../Utils"; +import Loc from "../../Models/Loc"; interface TextFieldDef { name: string, @@ -19,7 +19,8 @@ interface TextFieldDef { isValid: ((s: string, country?: () => string) => boolean), reformat?: ((s: string, country?: () => string) => string), inputHelper?: (value: UIEventSource, options?: { - location: [number, number] + location: [number, number], + mapBackgroundLayer?: UIEventSource }) => InputElement, inputmode?: string @@ -118,8 +119,12 @@ export default class ValidatedTextField { str = "" + str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 }, str => str, - (value) => { - return new DirectionInput(value); + (value, options) => { + return new DirectionInput(options.mapBackgroundLayer , new UIEventSource({ + lat: options.location[0], + lon: options.location[1], + zoom: 19 + }),value); }, "numeric" ), @@ -235,7 +240,8 @@ export default class ValidatedTextField { textAreaRows?: number, isValid?: ((s: string, country: () => string) => boolean), country?: () => string, - location?: [number /*lat*/, number /*lon*/] + location?: [number /*lat*/, number /*lon*/], + mapBackgroundLayer?: UIEventSource }): InputElement { options = options ?? {}; options.placeholder = options.placeholder ?? type; @@ -269,87 +275,12 @@ export default class ValidatedTextField { if (tp.inputHelper) { input = new CombinedInputElement(input, tp.inputHelper(input.GetValue(), { - location: options.location + location: options.location, + mapBackgroundLayer: options.mapBackgroundLayer })); } return input; } - - public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined): InputElement { - const isValid = ValidatedTextField.AllTypes[type].isValid; - extraValidation = extraValidation ?? (() => true) - - const fromString = str => { - if (!isValid(str)) { - return undefined; - } - const n = Number(str); - if (!extraValidation(n)) { - return undefined; - } - return n; - }; - const toString = num => { - if (num === undefined) { - return undefined; - } - return "" + num; - }; - const textField = ValidatedTextField.InputForType(type); - return new InputElementMap(textField, (n0, n1) => n0 === n1, fromString, toString) - } - - public static KeyInput(allowEmpty: boolean = false): InputElement { - - function fromString(str) { - if (str?.match(/^[a-zA-Z][a-zA-Z0-9:_-]*$/)) { - return str; - } - if (str === "" && allowEmpty) { - return ""; - } - - return undefined - } - - const toString = str => str - - function isSame(str0, str1) { - return str0 === str1; - } - - const textfield = new TextField({ - placeholder: "key", - isValid: str => fromString(str) !== undefined, - value: new UIEventSource("") - }); - - return new InputElementMap(textfield, isSame, fromString, toString); - } - - static Mapped(fromString: (str) => T, toString: (T) => string, options?: { - placeholder?: string | UIElement, - type?: string, - value?: UIEventSource, - startValidated?: boolean, - textArea?: boolean, - textAreaRows?: number, - isValid?: ((string: string) => boolean), - country?: () => string - }): InputElement { - let textField: InputElement; - if (options?.type) { - textField = ValidatedTextField.InputForType(options.type, options); - } else { - textField = new TextField(options); - } - return new InputElementMap( - textField, (a, b) => a === b, - fromString, toString - ); - - } - public static HelpText(): string { const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n") return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations @@ -360,7 +291,8 @@ export default class ValidatedTextField { isValid?: ((s: string, country?: () => string) => boolean), reformat?: ((s: string, country?: () => string) => string), inputHelper?: (value: UIEventSource, options?: { - location: [number, number] + location: [number, number], + mapBackgroundLayer: UIEventSource }) => InputElement, inputmode?: string): TextFieldDef { diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 2b6dd7bc4..57c51ac5d 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -326,7 +326,8 @@ export default class TagRenderingQuestion extends UIElement { const textField = ValidatedTextField.InputForType(this._configuration.freeform.type, { isValid: (str) => (str.length <= 255), country: () => this._tags.data._country, - location: [this._tags.data._lat, this._tags.data._lon] + location: [this._tags.data._lat, this._tags.data._lon], + mapBackgroundLayer: State.state.backgroundLayer }); textField.GetValue().setData(this._tags.data[this._configuration.freeform.key]); diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 77e5eff9b..b383e1925 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -15,13 +15,15 @@ export default class ShowDataLayer { private _layerDict; private readonly _leafletMap: UIEventSource; private _cleanCount = 0; + private readonly _enablePopups: boolean; constructor(features: UIEventSource<{ feature: any, freshness: Date }[]>, leafletMap: UIEventSource, - layoutToUse: UIEventSource) { + layoutToUse: UIEventSource, + enablePopups= true) { this._leafletMap = leafletMap; + this._enablePopups = enablePopups; const self = this; - const mp = leafletMap.data; self._layerDict = {}; layoutToUse.addCallbackAndRun(layoutToUse => { @@ -39,7 +41,9 @@ export default class ShowDataLayer { if (features.data === undefined) { return; } - if (leafletMap.data === undefined) { + const mp = leafletMap.data; + + if(mp === undefined){ return; } @@ -119,6 +123,11 @@ export default class ShowDataLayer { // No popup action defined -> Don't do anything return; } + if(!this._enablePopups){ + // Probably a map in the popup - no popups needed! + return; + } + const popup = L.popup({ autoPan: true, closeOnEscapeKey: true, @@ -171,15 +180,15 @@ export default class ShowDataLayer { } private CreateGeojsonLayer(): L.Layer { - const self = this; - const data = { - type: "FeatureCollection", - features: [] - } - // @ts-ignore - return L.geoJSON(data, { - style: feature => self.createStyleFor(feature), - pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng), + const self = this; + const data = { + type: "FeatureCollection", + features: [] + } + // @ts-ignore + return L.geoJSON(data, { + style: feature => self.createStyleFor(feature), + pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng), onEachFeature: (feature, leafletLayer) => self.postProcessFeature(feature, leafletLayer) }); diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 1ee996af1..1375a4ff8 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -21,10 +21,13 @@ import LayerConfig from "../Customizations/JSON/LayerConfig"; import Title from "./Base/Title"; import Table from "./Base/Table"; import Histogram from "./BigComponents/Histogram"; +import Loc from "../Models/Loc"; +import ShowDataLayer from "./ShowDataLayer"; +import Minimap from "./Base/Minimap"; export default class SpecialVisualizations { - + public static specialVisualizations: { funcName: string, constr: ((state: State, tagSource: UIEventSource, argument: string[]) => BaseUIElement), @@ -32,7 +35,6 @@ export default class SpecialVisualizations { example?: string, args: { name: string, defaultValue?: string, doc: string }[] }[] = - [{ funcName: "all_tags", docs: "Prints all key-value pairs of the object - used for debugging", @@ -85,7 +87,57 @@ export default class SpecialVisualizations { return new ImageUploadFlow(tags, args[0]) } }, + { + funcName: "minimap", + docs: "A small map showing the selected feature. Note that no styling is applied, wrap this in a div", + args: [ + { + doc: "The zoomlevel: the higher, the more zoomed in with 1 being the entire world and 19 being really close", + name: "zoomlevel", + defaultValue: "18" + } + ], + example: "`{minimap()}`", + constr: (state, tagSource, args) => { + const properties = tagSource.data; + const feature = state.allElements.ContainingFeatures.get(properties.id) + let zoom = 18 + if(args[0] ){ + const parsed = Number(args[0]) + if(!isNaN(parsed) && parsed > 0 && parsed < 25){ + zoom = parsed; + } + } + const minimap = new Minimap( + { + background: state.backgroundLayer, + location: new UIEventSource({ + lat: Number(properties._lat), + lon: Number(properties._lon), + zoom: zoom + }), + allowMoving: false + } + ) + + new ShowDataLayer( + new UIEventSource<{ feature: any; freshness: Date }[]>([ + { + freshness: new Date(), + feature: feature + } + ]), + minimap.leafletMap, + State.state.layoutToUse, + false + ) + + minimap.SetStyle("overflow: hidden; pointer-events: none;") + return minimap; + + } + }, { funcName: "reviews", docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten", diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index c99699dda..2c7f573ad 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -17,14 +17,14 @@ export class SubstitutedTranslation extends VariableUiElement { super( tagsSource.map(tags => { const txt = Utils.SubstituteKeys(translation.txt, tags) - if (txt === undefined) { + if (txt === undefined) { return undefined } - return new Combine(SubstitutedTranslation.EvaluateSpecialComponents(txt, tagsSource)) + return new Combine(SubstitutedTranslation.EvaluateSpecialComponents(txt, tagsSource)) }, [Locale.language]) ) - - + + this.SetClass("w-full") } @@ -34,13 +34,14 @@ export class SubstitutedTranslation extends VariableUiElement { for (const knownSpecial of SpecialVisualizations.specialVisualizations) { // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' - const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)}(.*)`); + const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`); if (matched != null) { // We found a special component that should be brought to live const partBefore = SubstitutedTranslation.EvaluateSpecialComponents(matched[1], tags); const argument = matched[2].trim(); - const partAfter = SubstitutedTranslation.EvaluateSpecialComponents(matched[3], tags); + const style = matched[3] ?? "" + const partAfter = SubstitutedTranslation.EvaluateSpecialComponents(matched[4], tags); try { const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); if (argument.length > 0) { @@ -56,13 +57,14 @@ export class SubstitutedTranslation extends VariableUiElement { let element: BaseUIElement = new FixedUiElement(`Constructing ${knownSpecial}(${args.join(", ")})`) - try{ - element = knownSpecial.constr(State.state, tags, args); - }catch(e){ + try { + element = knownSpecial.constr(State.state, tags, args); + element.SetStyle(style) + } catch (e) { console.error("SPECIALRENDERING FAILED for", tags.data.id, e) element = new FixedUiElement(`Could not generate special rendering for ${knownSpecial}(${args.join(", ")}) ${e}`).SetClass("alert") } - + return [...partBefore, element, ...partAfter] } catch (e) { console.error(e); diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index a7f632e39..16eb121e3 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -6,7 +6,7 @@ "render": "{reviews()}" }, "minimap": { - "render": "
{minimap(19, {id})}
" + "render": "{minimap(19, id): width:100%; height:6rem; border-radius:999rem; overflow: hidden; pointer-events: none;}" }, "phone": { "question": { diff --git a/index.ts b/index.ts index 472fb943f..83abe95d4 100644 --- a/index.ts +++ b/index.ts @@ -14,10 +14,12 @@ import Translations from "./UI/i18n/Translations"; import CountryCoder from "latlon2country" import SimpleMetaTagger from "./Logic/SimpleMetaTagger"; +import Minimap from "./UI/Base/Minimap"; +import DirectionInput from "./UI/Input/DirectionInput"; -// Workaround for a stupid crash: inject the function +// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); - +DirectionInput.constructMinimap = options => new Minimap(options) let defaultLayout = "" // --------------------- Special actions based on the parameters ----------------- diff --git a/test.html b/test.html index 839114efe..8b6c44878 100644 --- a/test.html +++ b/test.html @@ -3,6 +3,7 @@ Small tests + diff --git a/test.ts b/test.ts index cdc00c93f..3bd1a5f99 100644 --- a/test.ts +++ b/test.ts @@ -10,11 +10,15 @@ import {FixedUiElement} from "./UI/Base/FixedUiElement"; import Img from "./UI/Base/Img"; import {AttributedImage} from "./UI/Image/AttributedImage"; import {Imgur} from "./Logic/ImageProviders/Imgur"; -import ReviewForm from "./UI/Reviews/ReviewForm"; -import {OsmConnection} from "./Logic/Osm/OsmConnection"; +import Minimap from "./UI/Base/Minimap"; +import Loc from "./Models/Loc"; +import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; +import ShowDataLayer from "./UI/ShowDataLayer"; +import LayoutConfig from "./Customizations/JSON/LayoutConfig"; +import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; -function TestSlideshow(){ +function TestSlideshow() { const elems = new UIEventSource([ new FixedUiElement("A"), new FixedUiElement("qmsldkfjqmlsdkjfmqlskdjfmqlksdf").SetClass("text-xl"), @@ -25,17 +29,17 @@ function TestSlideshow(){ new SlideShow(elems).AttachTo("maindiv") } -function TestTagRendering(){ +function TestTagRendering() { State.state = new State(undefined) const tagsSource = new UIEventSource({ - id:"node/1" + id: "node/1" }) new TagRenderingQuestion( tagsSource, new TagRenderingConfig({ multiAnswer: false, freeform: { - key:"valve" + key: "valve" }, question: "What valves are supported?", render: "This pump supports {valve}", @@ -45,25 +49,94 @@ function TestTagRendering(){ then: "This pump supports dunlop" }, { - if:"valve=shrader", - then:"shrader is supported", + if: "valve=shrader", + then: "shrader is supported", } ], - + }, undefined, "test") ).AttachTo("maindiv") new VariableUiElement(tagsSource.map(tags => tags["valves"])).SetClass("alert").AttachTo("extradiv") } -function TestAllInputMethods(){ +function TestAllInputMethods() { new Combine(ValidatedTextField.tpList.map(tp => { const tf = ValidatedTextField.InputForType(tp.name); return new Combine([tf, new VariableUiElement(tf.GetValue()).SetClass("alert")]); - })).AttachTo("maindiv") + })).AttachTo("maindiv") } -new ReviewForm(() => { - return undefined; -}, new OsmConnection(true, new UIEventSource(undefined), "test")).AttachTo("maindiv"); \ No newline at end of file + +const location = new UIEventSource({ + lon: 4.84771728515625, + lat: 51.17920846421931, + zoom: 14 +}) +const map0 = new Minimap({ + location: location, + allowMoving: true, + background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[2]) +}) +map0.SetStyle("width: 500px; height: 250px; overflow: hidden; border: 2px solid red") + .AttachTo("maindiv") + +const layout = AllKnownLayouts.layoutsList[1] +State.state = new State(layout) +console.log("LAYOUT is", layout.id) + +const feature = { + "type": "Feature", + _matching_layer_id: "bike_repair_station", + "properties": { + id: "node/-1", + "amenity": "bicycle_repair_station" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 4.84771728515625, + 51.17920846421931 + ] + } + } + +; + +State.state.allElements.addOrGetElement(feature) + +const featureSource = new UIEventSource([{ + freshness: new Date(), + feature: feature +}]) + +new ShowDataLayer( + featureSource, + map0.leafletMap, + new UIEventSource(layout) +) + +const map1 = new Minimap({ + location: location, + allowMoving: true, + background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[5]) + }, +) + +map1.SetStyle("width: 500px; height: 250px; overflow: hidden; border : 2px solid black") + .AttachTo("extradiv") + + + + + +new ShowDataLayer( + featureSource, + map1.leafletMap, + new UIEventSource(layout) +) + +featureSource.ping() + +// */ \ No newline at end of file