From 583d1e137f080d3b9bd9189c28b8866ac946e2c7 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 14 Jul 2021 16:05:50 +0200 Subject: [PATCH 1/9] Refactoring of AvailableBaseLayer --- InitUiElements.ts | 2 +- Logic/Actors/AvailableBaseLayers.ts | 95 ++++++++++++++++++++--------- UI/Base/Minimap.ts | 2 +- UI/BigComponents/SimpleAddUI.ts | 22 ++++--- UI/Input/LocationInput.ts | 46 ++------------ 5 files changed, 89 insertions(+), 78 deletions(-) diff --git a/InitUiElements.ts b/InitUiElements.ts index 0f1143eba..0dbc7eaac 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -341,7 +341,7 @@ export class InitUiElements { private static InitBaseMap() { - State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers; + State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl); State.state.backgroundLayer = State.state.backgroundLayerId .map((selectedId: string) => { diff --git a/Logic/Actors/AvailableBaseLayers.ts b/Logic/Actors/AvailableBaseLayers.ts index 52b1c12de..eceadde89 100644 --- a/Logic/Actors/AvailableBaseLayers.ts +++ b/Logic/Actors/AvailableBaseLayers.ts @@ -7,7 +7,6 @@ import {UIEventSource} from "../UIEventSource"; import {GeoOperations} from "../GeoOperations"; import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; -import {isBoolean} from "util"; /** * Calculates which layers are available at the current location @@ -31,42 +30,82 @@ export default class AvailableBaseLayers { category: "osmbasedmap" } - public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); - public availableEditorLayers: UIEventSource; - constructor(location: UIEventSource) { - const self = this; - this.availableEditorLayers = - location.map( - (currentLocation) => { + public static AvailableLayersAt(location: UIEventSource): UIEventSource { + const source = location.map( + (currentLocation) => { - if (currentLocation === undefined) { - return AvailableBaseLayers.layerOverview; - } + if (currentLocation === undefined) { + return AvailableBaseLayers.layerOverview; + } - const currentLayers = self.availableEditorLayers?.data; - const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat); + const currentLayers = source?.data; // A bit unorthodox - I know + const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); - if (currentLayers === undefined) { + if (currentLayers === undefined) { + return newLayers; + } + if (newLayers.length !== currentLayers.length) { + return newLayers; + } + for (let i = 0; i < newLayers.length; i++) { + if (newLayers[i].name !== currentLayers[i].name) { return newLayers; } - if (newLayers.length !== currentLayers.length) { - return newLayers; - } - for (let i = 0; i < newLayers.length; i++) { - if (newLayers[i].name !== currentLayers[i].name) { - return newLayers; - } - } - - return currentLayers; - }); - + } + return currentLayers; + }); + return source; } - private static AvailableLayersAt(lon: number, lat: number): BaseLayer[] { + public static SelectBestLayerAccordingTo(location: UIEventSource, preferedCategory: UIEventSource): UIEventSource { + return AvailableBaseLayers.AvailableLayersAt(location).map(available => { + // First float all 'best layers' to the top + available.sort((a, b) => { + if (a.isBest && b.isBest) { + return 0; + } + if (!a.isBest) { + return 1 + } + + return -1; + } + ) + + if (preferedCategory.data === undefined) { + return available[0] + } + + let prefered: string [] + if (typeof preferedCategory.data === "string") { + prefered = [preferedCategory.data] + } else { + prefered = preferedCategory.data; + } + + prefered.reverse(); + for (const category of prefered) { + //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top + available.sort((a, b) => { + if (a.category === preferedCategory && b.category === preferedCategory) { + return 0; + } + if (a.category !== preferedCategory) { + return 1 + } + + return -1; + } + ) + } + return available[0] + }) + } + + private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { const availableLayers = [AvailableBaseLayers.osmCarto] const globalLayers = []; for (const layerOverviewItem of AvailableBaseLayers.layerOverview) { @@ -146,7 +185,7 @@ export default class AvailableBaseLayers { layer: leafletLayer, feature: layer, isBest: props.best ?? false, - category: props.category + category: props.category }); } return layers; diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 73bf2354a..2c38e8b74 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -52,7 +52,7 @@ export default class Minimap extends BaseUIElement { return wrapper; } - + private InitMap() { if (this._constructedHtmlElement === undefined) { // This element isn't initialized yet diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 05fb52c64..9d1fd1475 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -19,6 +19,7 @@ import {Translation} from "../i18n/Translation"; import LocationInput from "../Input/LocationInput"; import {InputElement} from "../Input/InputElement"; import Loc from "../../Models/Loc"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; /* * The SimpleAddUI is a single panel, which can have multiple states: @@ -115,14 +116,21 @@ export default class SimpleAddUI extends Toggle { let location = State.state.LastClickLocation; let preciseInput: InputElement = undefined if (preset.preciseInput !== undefined) { + const locationSrc = new UIEventSource({ + lat: location.data.lat, + lon: location.data.lon, + zoom: 19 + }); + + let backgroundLayer = undefined; + if(preset.preciseInput.preferredBackground){ + backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource(preset.preciseInput.preferredBackground)) + } + preciseInput = new LocationInput({ - preferCategory: preset.preciseInput.preferredBackground ?? State.state.backgroundLayer, - centerLocation: - new UIEventSource({ - lat: location.data.lat, - lon: location.data.lon, - zoom: 19 - }) + mapBackground: backgroundLayer, + centerLocation:locationSrc + }) preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") } diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index cee2f5cbf..c306153e3 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -2,30 +2,27 @@ import {InputElement} from "./InputElement"; import Loc from "../../Models/Loc"; import {UIEventSource} from "../../Logic/UIEventSource"; import Minimap from "../Base/Minimap"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import BaseLayer from "../../Models/BaseLayer"; import Combine from "../Base/Combine"; import Svg from "../../Svg"; +import State from "../../State"; export default class LocationInput extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private _centerLocation: UIEventSource; - private readonly preferCategory; + private readonly mapBackground : UIEventSource; constructor(options?: { + mapBackground?: UIEventSource, centerLocation?: UIEventSource, - preferCategory?: string | UIEventSource, }) { super(); options = options ?? {} options.centerLocation = options.centerLocation ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) this._centerLocation = options.centerLocation; - if(typeof options.preferCategory === "string"){ - options.preferCategory = new UIEventSource(options.preferCategory); - } - this.preferCategory = options.preferCategory ?? new UIEventSource(undefined) + this.mapBackground = options.mapBackground ?? State.state.backgroundLayer this.SetClass("block h-full") } @@ -38,43 +35,10 @@ export default class LocationInput extends InputElement { } protected InnerConstructElement(): HTMLElement { - const layer: UIEventSource = new AvailableBaseLayers(this._centerLocation).availableEditorLayers.map(allLayers => { - // First float all 'best layers' to the top - allLayers.sort((a, b) => { - if (a.isBest && b.isBest) { - return 0; - } - if (!a.isBest) { - return 1 - } - - return -1; - } - ) - if (this.preferCategory) { - const self = this; - //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top - allLayers.sort((a, b) => { - const preferred = self.preferCategory.data - if (a.category === preferred && b.category === preferred) { - return 0; - } - if (a.category !== preferred) { - return 1 - } - - return -1; - } - ) - } - return allLayers[0] - }, [this.preferCategory] - ) - layer.addCallbackAndRunD(layer => console.log(layer)) const map = new Minimap( { location: this._centerLocation, - background: layer + background: this.mapBackground } ) map.leafletMap.addCallbackAndRunD(leaflet => { From 4cc42c0842af3a3faf923a73b9b508410ec4c448 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 14 Jul 2021 16:06:34 +0200 Subject: [PATCH 2/9] WIP: length picker --- Svg.ts | 7 +- UI/Input/LengthInput.ts | 117 ++++++++++++++++++++++++++++++++ assets/svg/length-crosshair.svg | 73 ++++++++++++++++++++ test.ts | 9 +-- 4 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 UI/Input/LengthInput.ts create mode 100644 assets/svg/length-crosshair.svg diff --git a/Svg.ts b/Svg.ts index 0266e43e8..8b1e29307 100644 --- a/Svg.ts +++ b/Svg.ts @@ -184,6 +184,11 @@ export default class Svg { public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} + public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static length_crosshair_img = Img.AsImageElement(Svg.length_crosshair) + public static length_crosshair_svg() { return new Img(Svg.length_crosshair, true);} + public static length_crosshair_ui() { return new FixedUiElement(Svg.length_crosshair_img);} + public static logo = " image/svg+xml " public static logo_img = Img.AsImageElement(Svg.logo) public static logo_svg() { return new Img(Svg.logo, true);} @@ -344,4 +349,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-empty.svg": Svg.crosshair_empty,"crosshair-locked.svg": Svg.crosshair_locked,"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};} +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-empty.svg": Svg.crosshair_empty,"crosshair-locked.svg": Svg.crosshair_locked,"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,"length-crosshair.svg": Svg.length_crosshair,"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/Input/LengthInput.ts b/UI/Input/LengthInput.ts new file mode 100644 index 000000000..82b79ee0f --- /dev/null +++ b/UI/Input/LengthInput.ts @@ -0,0 +1,117 @@ +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"; +import Minimap from "../Base/Minimap"; + + +/** + * Selects a length after clicking on the minimap, in meters + */ +export default class LengthInput extends InputElement { + private readonly _location: UIEventSource; + + public readonly IsSelected: UIEventSource = new UIEventSource(false); + private readonly value: UIEventSource; + private background; + + constructor(mapBackground: UIEventSource, + location: UIEventSource, + value?: UIEventSource) { + super(); + this._location = location; + this.value = value ?? new UIEventSource(undefined); + this.background = mapBackground; + } + + GetValue(): UIEventSource { + return this.value; + } + + IsValid(str: string): boolean { + const t = Number(str); + return !isNaN(t) && t >= 0 && t <= 360; + } + + protected InnerConstructElement(): HTMLElement { + + let map: BaseUIElement = new FixedUiElement("") + if (!Utils.runningFromConsole) { + map = new Minimap({ + background: this.background, + allowMoving: true, + location: this._location + }) + } + + const element = new Combine([ + 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 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); height: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") + .ConstructElement() + + + this.value.addCallbackAndRunD(rotation => { + const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement + cone.style.transform = `rotate(${rotation}deg)`; + + }) + + this.RegisterTriggers(element) + element.style.overflow = "hidden" + + return element; + } + + private RegisterTriggers(htmlElement: HTMLElement) { + const self = this; + + function onPosChange(x: number, y: number) { + const rect = htmlElement.getBoundingClientRect(); + const dx = -(rect.left + rect.right) / 2 + x; + const dy = (rect.top + rect.bottom) / 2 - y; + const angle = 180 * Math.atan2(dy, dx) / Math.PI; + const angleGeo = Math.floor((450 - angle) % 360); + self.value.setData("" + angleGeo) + } + + + htmlElement.ontouchmove = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + ev.preventDefault(); + } + + htmlElement.ontouchstart = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + } + + let isDown = false; + + htmlElement.onmousedown = (ev: MouseEvent) => { + isDown = true; + onPosChange(ev.clientX, ev.clientY); + ev.preventDefault(); + } + + htmlElement.onmouseup = (ev) => { + isDown = false; + ev.preventDefault(); + } + + htmlElement.onmousemove = (ev: MouseEvent) => { + if (isDown) { + onPosChange(ev.clientX, ev.clientY); + } + ev.preventDefault(); + } + } + +} \ No newline at end of file diff --git a/assets/svg/length-crosshair.svg b/assets/svg/length-crosshair.svg new file mode 100644 index 000000000..6db2cf72b --- /dev/null +++ b/assets/svg/length-crosshair.svg @@ -0,0 +1,73 @@ + + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + image/svg+xml + + + + + + + + + diff --git a/test.ts b/test.ts index 5d077d354..23da820f3 100644 --- a/test.ts +++ b/test.ts @@ -10,6 +10,7 @@ import {Translation} from "./UI/i18n/Translation"; import LocationInput from "./UI/Input/LocationInput"; import Loc from "./Models/Loc"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import LengthInput from "./UI/Input/LengthInput"; /*import ValidatedTextField from "./UI/Input/ValidatedTextField"; import Combine from "./UI/Base/Combine"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; @@ -152,13 +153,7 @@ function TestMiniMap() { } //*/ -const li = new LocationInput({ - preferCategory:"photo", - centerLocation: - new UIEventSource({ - lat: 51.21576, lon: 3.22001, zoom: 19 - }) -}) +const li = new LengthInput() li.SetStyle("height: 20rem") .AttachTo("maindiv") From 4fa9159da10c4b214d975740e413321d450a8425 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 20 Jul 2021 01:33:58 +0200 Subject: [PATCH 3/9] First working version of a width measurment tool --- Customizations/JSON/TagRenderingConfig.ts | 17 +- Customizations/JSON/TagRenderingConfigJson.ts | 6 + Svg.ts | 2 +- UI/Base/Minimap.ts | 22 ++- UI/Input/LengthInput.ts | 160 +++++++++++++----- UI/Input/ValidatedTextField.ts | 127 ++++++++++---- UI/Popup/TagRenderingQuestion.ts | 5 +- UI/SpecialVisualizations.ts | 3 +- assets/svg/length-crosshair.svg | 44 +++-- assets/themes/widths/width.json | 8 +- index.ts | 3 + test.ts | 13 +- 12 files changed, 300 insertions(+), 110 deletions(-) diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index d3d440493..7b36dae44 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -27,7 +27,8 @@ export default class TagRenderingConfig { readonly type: string, readonly addExtraTags: TagsFilter[]; readonly inline: boolean, - readonly default?: string + readonly default?: string, + readonly helperArgs?: (string | number | boolean)[] }; readonly multiAnswer: boolean; @@ -76,8 +77,8 @@ export default class TagRenderingConfig { addExtraTags: json.freeform.addExtraTags?.map((tg, i) => FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [], inline: json.freeform.inline ?? false, - default: json.freeform.default - + default: json.freeform.default, + helperArgs: json.freeform.helperArgs } if (json.freeform["extraTags"] !== undefined) { @@ -336,20 +337,20 @@ export default class TagRenderingConfig { * Note: this might be hidden by conditions */ public hasMinimap(): boolean { - const translations : Translation[]= Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]); + const translations: Translation[] = Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]); for (const translation of translations) { for (const key in translation.translations) { - if(!translation.translations.hasOwnProperty(key)){ + if (!translation.translations.hasOwnProperty(key)) { continue } const template = translation.translations[key] const parts = SubstitutedTranslation.ExtractSpecialComponents(template) - const hasMiniMap = parts.filter(part =>part.special !== undefined ).some(special => special.special.func.funcName === "minimap") - if(hasMiniMap){ + const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap") + if (hasMiniMap) { return true; } } } return false; - } + } } \ No newline at end of file diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts index 89871ec74..843889525 100644 --- a/Customizations/JSON/TagRenderingConfigJson.ts +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -30,6 +30,7 @@ export interface TagRenderingConfigJson { * Allow freeform text input from the user */ freeform?: { + /** * If this key is present, then 'render' is used to display the value. * If this is undefined, the rendering is _always_ shown @@ -40,6 +41,11 @@ export interface TagRenderingConfigJson { * See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values */ type?: string, + /** + * Extra parameters to initialize the input helper arguments. + * For semantics, see the 'SpecialInputElements.md' + */ + helperArgs?: (string | number | boolean)[]; /** * If a value is added with the textfield, these extra tag is addded. * Useful to add a 'fixme=freeform textfield used - to be checked' diff --git a/Svg.ts b/Svg.ts index 26c5505ed..a3fe46cd7 100644 --- a/Svg.ts +++ b/Svg.ts @@ -184,7 +184,7 @@ export default class Svg { public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} - public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " public static length_crosshair_img = Img.AsImageElement(Svg.length_crosshair) public static length_crosshair_svg() { return new Img(Svg.length_crosshair, true);} public static length_crosshair_ui() { return new FixedUiElement(Svg.length_crosshair_img);} diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index a7066c9ee..6ebf37a75 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -5,6 +5,7 @@ import Loc from "../../Models/Loc"; import BaseLayer from "../../Models/BaseLayer"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import {Map} from "leaflet"; +import {Utils} from "../../Utils"; export default class Minimap extends BaseUIElement { @@ -15,11 +16,13 @@ export default class Minimap extends BaseUIElement { private readonly _location: UIEventSource; private _isInited = false; private _allowMoving: boolean; + private readonly _leafletoptions: any; constructor(options?: { background?: UIEventSource, location?: UIEventSource, - allowMoving?: boolean + allowMoving?: boolean, + leafletOptions?: any } ) { super() @@ -28,10 +31,11 @@ export default class Minimap extends BaseUIElement { this._location = options?.location ?? new UIEventSource(undefined) this._id = "minimap" + Minimap._nextId; this._allowMoving = options.allowMoving ?? true; + this._leafletoptions = options.leafletOptions ?? {} Minimap._nextId++ } - + protected InnerConstructElement(): HTMLElement { const div = document.createElement("div") div.id = this._id; @@ -52,7 +56,7 @@ export default class Minimap extends BaseUIElement { return wrapper; } - + private InitMap() { if (this._constructedHtmlElement === undefined) { // This element isn't initialized yet @@ -71,8 +75,8 @@ export default class Minimap extends BaseUIElement { 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], + const options = { + center: <[number, number]> [location.data?.lat ?? 0, location.data?.lon ?? 0], zoom: location.data?.zoom ?? 2, layers: [currentLayer], zoomControl: false, @@ -82,9 +86,13 @@ export default class Minimap extends BaseUIElement { doubleClickZoom: this._allowMoving, keyboard: this._allowMoving, touchZoom: this._allowMoving, - // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving, + // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving, fadeAnimation: this._allowMoving - }); + } + + Utils.Merge(this._leafletoptions, options) + + const map = L.map(this._id, options); map.setMaxBounds( [[-100, -200], [100, 200]] diff --git a/UI/Input/LengthInput.ts b/UI/Input/LengthInput.ts index 82b79ee0f..ea7530ce3 100644 --- a/UI/Input/LengthInput.ts +++ b/UI/Input/LengthInput.ts @@ -2,11 +2,12 @@ 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"; -import Minimap from "../Base/Minimap"; +import {GeoOperations} from "../../Logic/GeoOperations"; +import DirectionInput from "./DirectionInput"; +import {RadioButton} from "./RadioButton"; +import {FixedInputElement} from "./FixedInputElement"; /** @@ -19,13 +20,15 @@ export default class LengthInput extends InputElement { private readonly value: UIEventSource; private background; - constructor(mapBackground: UIEventSource, + constructor(mapBackground: UIEventSource, location: UIEventSource, value?: UIEventSource) { super(); this._location = location; this.value = value ?? new UIEventSource(undefined); this.background = mapBackground; + this.SetClass("block") + } GetValue(): UIEventSource { @@ -33,83 +36,150 @@ export default class LengthInput extends InputElement { } IsValid(str: string): boolean { - const t = Number(str); + const t = Number(str) return !isNaN(t) && t >= 0 && t <= 360; } protected InnerConstructElement(): HTMLElement { - - let map: BaseUIElement = new FixedUiElement("") + const modeElement = new RadioButton([ + new FixedInputElement("Measure", "measure"), + new FixedInputElement("Move", "move") + ]) + // @ts-ignore + let map = undefined if (!Utils.runningFromConsole) { - map = new Minimap({ + map = DirectionInput.constructMinimap({ background: this.background, - allowMoving: true, - location: this._location + allowMoving: false, + location: this._location, + leafletOptions: { + tap: true + } }) } - const element = new Combine([ - 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 relative") - .SetStyle("z-index: 1000"), - map.SetClass("w-full h-full absolute top-0 left-O rounded-full overflow-hidden"), + new Combine([Svg.length_crosshair_ui().SetStyle( + `visibility: hidden; position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`) + ]) + .SetClass("block length-crosshair-svg relative") + .SetStyle("z-index: 1000"), + map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"), ]) - .SetStyle("position:relative;display:block;width: min(100%, 25em); height: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") + .SetClass("relative block bg-white border border-black rounded-3xl overflow-hidden") .ConstructElement() - this.value.addCallbackAndRunD(rotation => { - const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement - cone.style.transform = `rotate(${rotation}deg)`; - - }) - - this.RegisterTriggers(element) + this.RegisterTriggers(element, map?.leafletMap) element.style.overflow = "hidden" - - return element; + element.style.display = "block" + + return element } - private RegisterTriggers(htmlElement: HTMLElement) { - const self = this; + private RegisterTriggers(htmlElement: HTMLElement, leafletMap: UIEventSource) { + + let firstClickXY: [number, number] = undefined + let lastClickXY: [number, number] = undefined + const self = this; + + + function onPosChange(x: number, y: number, isDown: boolean, isUp?: boolean) { + if (x === undefined || y === undefined) { + // Touch end + firstClickXY = undefined; + lastClickXY = undefined; + return; + } - function onPosChange(x: number, y: number) { const rect = htmlElement.getBoundingClientRect(); - const dx = -(rect.left + rect.right) / 2 + x; - const dy = (rect.top + rect.bottom) / 2 - y; - const angle = 180 * Math.atan2(dy, dx) / Math.PI; - const angleGeo = Math.floor((450 - angle) % 360); - self.value.setData("" + angleGeo) + // From the central part of location + const dx = x - rect.left; + const dy = y - rect.top; + if (isDown) { + if (lastClickXY === undefined && firstClickXY === undefined) { + firstClickXY = [dx, dy]; + } else if (firstClickXY !== undefined && lastClickXY === undefined) { + lastClickXY = [dx, dy] + } else if (firstClickXY !== undefined && lastClickXY !== undefined) { + // we measure again + firstClickXY = [dx, dy] + lastClickXY = undefined; + } + } + if (isUp) { + const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0])) + if (distance > 15) { + lastClickXY = [dx, dy] + } + + + } else if (lastClickXY !== undefined) { + return; + } + + + const measurementCrosshair = htmlElement.getElementsByClassName("length-crosshair-svg")[0] as HTMLElement + const measurementCrosshairInner: HTMLElement = measurementCrosshair.firstChild + if (firstClickXY === undefined) { + measurementCrosshair.style.visibility = "hidden" + } else { + measurementCrosshair.style.visibility = "unset" + measurementCrosshair.style.left = firstClickXY[0] + "px"; + measurementCrosshair.style.top = firstClickXY[1] + "px" + + const angle = 180 * Math.atan2(firstClickXY[1] - dy, firstClickXY[0] - dx) / Math.PI; + const angleGeo = (angle + 270) % 360 + measurementCrosshairInner.style.transform = `rotate(${angleGeo}deg)`; + + const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0])) + measurementCrosshairInner.style.width = (distance * 2) + "px" + measurementCrosshairInner.style.marginLeft = -distance + "px" + measurementCrosshairInner.style.marginTop = -distance + "px" + + + const leaflet = leafletMap?.data + if (leaflet) { + console.log(firstClickXY, [dx, dy], "pixel origin", leaflet.getPixelOrigin()) + const first = leaflet.layerPointToLatLng(firstClickXY) + const last = leaflet.layerPointToLatLng([dx, dy]) + console.log(first, last) + const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 100000) / 100 + console.log("First", first, "last", last, "d", geoDist) + self.value.setData("" + geoDist) + } + + } + } - htmlElement.ontouchmove = (ev: TouchEvent) => { - onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + htmlElement.ontouchstart = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true); ev.preventDefault(); } - htmlElement.ontouchstart = (ev: TouchEvent) => { - onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + htmlElement.ontouchmove = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false); + ev.preventDefault(); } - let isDown = false; + htmlElement.ontouchend = (ev: TouchEvent) => { + onPosChange(undefined, undefined, false, true); + ev.preventDefault(); + } htmlElement.onmousedown = (ev: MouseEvent) => { - isDown = true; - onPosChange(ev.clientX, ev.clientY); + onPosChange(ev.clientX, ev.clientY, true); ev.preventDefault(); } htmlElement.onmouseup = (ev) => { - isDown = false; + onPosChange(ev.clientX, ev.clientY, false, true); ev.preventDefault(); } htmlElement.onmousemove = (ev: MouseEvent) => { - if (isDown) { - onPosChange(ev.clientX, ev.clientY); - } + onPosChange(ev.clientX, ev.clientY, false); ev.preventDefault(); } } diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 2eeff8a54..809ff025f 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -13,6 +13,8 @@ import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; import {Unit} from "../../Customizations/JSON/Denomination"; import BaseUIElement from "../BaseUIElement"; +import LengthInput from "./LengthInput"; +import {GeoOperations} from "../../Logic/GeoOperations"; interface TextFieldDef { name: string, @@ -21,14 +23,16 @@ interface TextFieldDef { reformat?: ((s: string, country?: () => string) => string), inputHelper?: (value: UIEventSource, options?: { location: [number, number], - mapBackgroundLayer?: UIEventSource + mapBackgroundLayer?: UIEventSource, + args: (string | number | boolean)[] + feature?: any }) => InputElement, - inputmode?: string } export default class ValidatedTextField { + public static bestLayerAt: (location: UIEventSource, preferences: UIEventSource) => any public static tpList: TextFieldDef[] = [ ValidatedTextField.tp( @@ -63,6 +67,79 @@ export default class ValidatedTextField { return [year, month, day].join('-'); }, (value) => new SimpleDatePicker(value)), + ValidatedTextField.tp( + "direction", + "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)", + (str) => { + str = "" + str; + return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 + }, str => str, + (value, options) => { + const args = options.args ?? [] + let zoom = 19 + if (args[0]) { + zoom = Number(args[0]) + if (isNaN(zoom)) { + throw "Invalid zoom level for argument at 'length'-input" + } + } + const location = new UIEventSource({ + lat: options.location[0], + lon: options.location[1], + zoom: zoom + }) + if (args[1]) { + // We have a prefered map! + options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( + location, new UIEventSource(args[1].split(",")) + ) + } + const di = new DirectionInput(options.mapBackgroundLayer, location, value) + di.SetStyle("height: 20rem;"); + + return di; + }, + "numeric" + ), + ValidatedTextField.tp( + "length", + "A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma seperated) ], e.g. `[\"21\", \"map,photo\"]", + (str) => { + const t = Number(str) + return !isNaN(t) + }, + str => str, + (value, options) => { + const args = options.args ?? [] + let zoom = 19 + if (args[0]) { + zoom = Number(args[0]) + if (isNaN(zoom)) { + throw "Invalid zoom level for argument at 'length'-input" + } + } + + // Bit of a hack: we project the centerpoint to the closes point on the road - if available + if(options.feature){ + } + options.feature + + const location = new UIEventSource({ + lat: options.location[0], + lon: options.location[1], + zoom: zoom + }) + if (args[1]) { + // We have a prefered map! + options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( + location, new UIEventSource(args[1].split(",")) + ) + } + const li = new LengthInput(options.mapBackgroundLayer, location, value) + li.SetStyle("height: 20rem;") + return li; + } + ), ValidatedTextField.tp( "wikidata", "A wikidata identifier, e.g. Q42", @@ -113,22 +190,6 @@ export default class ValidatedTextField { undefined, undefined, "numeric"), - ValidatedTextField.tp( - "direction", - "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)", - (str) => { - str = "" + str; - return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 - }, str => str, - (value, options) => { - return new DirectionInput(options.mapBackgroundLayer , new UIEventSource({ - lat: options.location[0], - lon: options.location[1], - zoom: 19 - }),value); - }, - "numeric" - ), ValidatedTextField.tp( "float", "A decimal", @@ -222,6 +283,7 @@ export default class ValidatedTextField { * {string (typename) --> TextFieldDef} */ public static AllTypes = ValidatedTextField.allTypesDict(); + public static InputForType(type: string, options?: { placeholder?: string | BaseUIElement, value?: UIEventSource, @@ -233,7 +295,9 @@ export default class ValidatedTextField { country?: () => string, location?: [number /*lat*/, number /*lon*/], mapBackgroundLayer?: UIEventSource, - unit?: Unit + unit?: Unit, + args?: (string | number | boolean)[] // Extra arguments for the inputHelper, + feature?: any }): InputElement { options = options ?? {}; options.placeholder = options.placeholder ?? type; @@ -247,7 +311,7 @@ export default class ValidatedTextField { if (str === undefined) { return false; } - if(options.unit) { + if (options.unit) { str = options.unit.stripUnitParts(str) } return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country); @@ -268,7 +332,7 @@ export default class ValidatedTextField { }) } - if(options.unit) { + if (options.unit) { // We need to apply a unit. // This implies: // We have to create a dropdown with applicable denominations, and fuse those values @@ -288,17 +352,16 @@ export default class ValidatedTextField { input, unitDropDown, // combine the value from the textfield and the dropdown into the resulting value that should go into OSM - (text, denom) => denom?.canonicalValue(text, true) ?? undefined, + (text, denom) => denom?.canonicalValue(text, true) ?? undefined, (valueWithDenom: string) => { // Take the value from OSM and feed it into the textfield and the dropdown const withDenom = unit.findDenomination(valueWithDenom); - if(withDenom === undefined) - { + if (withDenom === undefined) { // Not a valid value at all - we give it undefined and leave the details up to the other elements return [undefined, undefined] } const [strippedText, denom] = withDenom - if(strippedText === undefined){ + if (strippedText === undefined) { return [undefined, undefined] } return [strippedText, denom] @@ -306,18 +369,20 @@ export default class ValidatedTextField { ).SetClass("flex") } if (tp.inputHelper) { - const helper = tp.inputHelper(input.GetValue(), { + const helper = tp.inputHelper(input.GetValue(), { location: options.location, - mapBackgroundLayer: options.mapBackgroundLayer - + mapBackgroundLayer: options.mapBackgroundLayer, + args: options.args, + feature: options.feature }) input = new CombinedInputElement(input, helper, (a, _) => a, // We can ignore b, as they are linked earlier a => [a, a] - ); + ); } return input; } + 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 @@ -329,7 +394,9 @@ export default class ValidatedTextField { reformat?: ((s: string, country?: () => string) => string), inputHelper?: (value: UIEventSource, options?: { location: [number, number], - mapBackgroundLayer: UIEventSource + mapBackgroundLayer: UIEventSource, + args: string[], + feature: any }) => InputElement, inputmode?: string): TextFieldDef { diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 20c0b00d2..c72375959 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -330,12 +330,15 @@ export default class TagRenderingQuestion extends Combine { } const tagsData = tags.data; + const feature = State.state.allElements.ContainingFeatures.get(tagsData.id) const input: InputElement = ValidatedTextField.InputForType(configuration.freeform.type, { isValid: (str) => (str.length <= 255), country: () => tagsData._country, location: [tagsData._lat, tagsData._lon], mapBackgroundLayer: State.state.backgroundLayer, - unit: applicableUnit + unit: applicableUnit, + args: configuration.freeform.helperArgs, + feature: feature }); input.GetValue().setData(tagsData[freeform.key] ?? freeform.default); diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 309060b36..5a38e8184 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -39,7 +39,8 @@ export default class SpecialVisualizations { static constructMiniMap: (options?: { background?: UIEventSource, location?: UIEventSource, - allowMoving?: boolean + allowMoving?: boolean, + leafletOptions?: any }) => BaseUIElement; static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource, layoutToUse: UIEventSource, enablePopups?: boolean, zoomToFeatures?: boolean) => any; public static specialVisualizations: SpecialVisualization[] = diff --git a/assets/svg/length-crosshair.svg b/assets/svg/length-crosshair.svg index 6db2cf72b..cb83789fb 100644 --- a/assets/svg/length-crosshair.svg +++ b/assets/svg/length-crosshair.svg @@ -26,17 +26,17 @@ guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" - inkscape:window-width="1680" - inkscape:window-height="1009" + inkscape:window-width="1920" + inkscape:window-height="999" id="namedview16" showgrid="false" showguides="true" inkscape:guide-bbox="true" - inkscape:zoom="0.25" - inkscape:cx="-448.31847" - inkscape:cy="144.08448" + inkscape:zoom="0.5" + inkscape:cx="108.3764" + inkscape:cy="623.05359" inkscape:window-x="0" - inkscape:window-y="15" + inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg14" inkscape:snap-smooth-nodes="true" /> @@ -54,20 +54,36 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017 + ry="427.81949" + transform="rotate(-90)" /> + + + diff --git a/assets/themes/widths/width.json b/assets/themes/widths/width.json index 48a1e883a..298b9a128 100644 --- a/assets/themes/widths/width.json +++ b/assets/themes/widths/width.json @@ -64,7 +64,13 @@ }, "tagRenderings": [ { - "render": "Deze straat is {width:carriageway}m breed" + "render": "Deze straat is {width:carriageway}m breed", + "question": "Hoe breed is deze straat?", + "freeform": { + "key": "width:carriageway", + "type": "length", + "helperArgs": [21, "map"] + } }, { "render": "Deze straat heeft {_width:difference}m te weinig:", diff --git a/index.ts b/index.ts index 70b06bf30..634ad8533 100644 --- a/index.ts +++ b/index.ts @@ -19,10 +19,13 @@ import DirectionInput from "./UI/Input/DirectionInput"; import SpecialVisualizations from "./UI/SpecialVisualizations"; import ShowDataLayer from "./UI/ShowDataLayer"; import * as L from "leaflet"; +import ValidatedTextField from "./UI/Input/ValidatedTextField"; +import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; // 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) +ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref) SpecialVisualizations.constructMiniMap = options => new Minimap(options) SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>, leafletMap: UIEventSource, diff --git a/test.ts b/test.ts index 23da820f3..21ca94b74 100644 --- a/test.ts +++ b/test.ts @@ -11,6 +11,7 @@ import LocationInput from "./UI/Input/LocationInput"; import Loc from "./Models/Loc"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; import LengthInput from "./UI/Input/LengthInput"; +import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; /*import ValidatedTextField from "./UI/Input/ValidatedTextField"; import Combine from "./UI/Base/Combine"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; @@ -153,8 +154,16 @@ function TestMiniMap() { } //*/ -const li = new LengthInput() - li.SetStyle("height: 20rem") +const loc = new UIEventSource({ + zoom: 24, + lat: 51.21043, + lon: 3.21389 +}) +const li = new LengthInput( + AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource("map","photo")), + loc +) + li.SetStyle("height: 30rem; background: aliceblue;") .AttachTo("maindiv") new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv") \ No newline at end of file From aa9045fd131a87e9ae77f9b9304a75d3ecc7b73f Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 20 Jul 2021 01:59:19 +0200 Subject: [PATCH 4/9] Automatically move the map onto the feature, add arguments to helpers --- Logic/GeoOperations.ts | 8 +++++ Svg.ts | 2 +- UI/Input/LengthInput.ts | 10 +++--- UI/Input/ValidatedTextField.ts | 4 +++ assets/svg/length-crosshair.svg | 56 ++++++++++++++++++++++++--------- 5 files changed, 58 insertions(+), 22 deletions(-) diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 31cb88ad2..768a5fe24 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -273,6 +273,14 @@ export class GeoOperations { } return undefined; } + /** + * Generates the closest point on a way from a given point + * @param way The road on which you want to find a point + * @param point Point defined as [lon, lat] + */ + public static nearestPoint(way, point: [number, number]){ + return turf.nearestPointOnLine(way, point, {units: "kilometers"}); + } } diff --git a/Svg.ts b/Svg.ts index a3fe46cd7..88e921626 100644 --- a/Svg.ts +++ b/Svg.ts @@ -184,7 +184,7 @@ export default class Svg { public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} - public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " public static length_crosshair_img = Img.AsImageElement(Svg.length_crosshair) public static length_crosshair_svg() { return new Img(Svg.length_crosshair, true);} public static length_crosshair_ui() { return new FixedUiElement(Svg.length_crosshair_img);} diff --git a/UI/Input/LengthInput.ts b/UI/Input/LengthInput.ts index ea7530ce3..0558069b2 100644 --- a/UI/Input/LengthInput.ts +++ b/UI/Input/LengthInput.ts @@ -58,11 +58,11 @@ export default class LengthInput extends InputElement { }) } const element = new Combine([ - new Combine([Svg.length_crosshair_ui().SetStyle( - `visibility: hidden; position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`) + new Combine([Svg.length_crosshair_svg().SetStyle( + `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`) ]) .SetClass("block length-crosshair-svg relative") - .SetStyle("z-index: 1000"), + .SetStyle("z-index: 1000; visibility: hidden"), map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"), ]) .SetClass("relative block bg-white border border-black rounded-3xl overflow-hidden") @@ -119,6 +119,7 @@ export default class LengthInput extends InputElement { const measurementCrosshair = htmlElement.getElementsByClassName("length-crosshair-svg")[0] as HTMLElement + const measurementCrosshairInner: HTMLElement = measurementCrosshair.firstChild if (firstClickXY === undefined) { measurementCrosshair.style.visibility = "hidden" @@ -139,12 +140,9 @@ export default class LengthInput extends InputElement { const leaflet = leafletMap?.data if (leaflet) { - console.log(firstClickXY, [dx, dy], "pixel origin", leaflet.getPixelOrigin()) const first = leaflet.layerPointToLatLng(firstClickXY) const last = leaflet.layerPointToLatLng([dx, dy]) - console.log(first, last) const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 100000) / 100 - console.log("First", first, "last", last, "d", geoDist) self.value.setData("" + geoDist) } diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 809ff025f..ec3aa62ce 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -121,6 +121,10 @@ export default class ValidatedTextField { // Bit of a hack: we project the centerpoint to the closes point on the road - if available if(options.feature){ + const lonlat: [number, number] = [...options.location] + lonlat.reverse() + options.location = <[number,number]> GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates + options.location.reverse() } options.feature diff --git a/assets/svg/length-crosshair.svg b/assets/svg/length-crosshair.svg index cb83789fb..0446f22c4 100644 --- a/assets/svg/length-crosshair.svg +++ b/assets/svg/length-crosshair.svg @@ -33,8 +33,8 @@ showguides="true" inkscape:guide-bbox="true" inkscape:zoom="0.5" - inkscape:cx="108.3764" - inkscape:cy="623.05359" + inkscape:cx="307.56567" + inkscape:cy="-35.669379" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -53,21 +53,26 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017 - + @@ -75,15 +80,36 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017 inkscape:connector-curvature="0" id="path814" d="M 429.76804,857.30628 V 428.78674" - style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:11.99999975,11.99999975;stroke-dashoffset:0" /> + style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:35.99999926,35.99999926;stroke-dashoffset:0" /> + d="M 857.32232,1.0332137 H 1.6833879 v 0" + style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:17.99999963, 17.99999963;stroke-dashoffset:0;stroke-opacity:1" /> + + From cdccfa27bedd7d1299620af0759b81fc44ef1f76 Mon Sep 17 00:00:00 2001 From: Vinicius Date: Tue, 20 Jul 2021 01:54:42 +0000 Subject: [PATCH 5/9] Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.3% (165 of 166 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/pt_BR/ --- langs/pt_BR.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/langs/pt_BR.json b/langs/pt_BR.json index 638ab0d39..268c8e4e6 100644 --- a/langs/pt_BR.json +++ b/langs/pt_BR.json @@ -122,8 +122,10 @@ "thanksForSharing": "Obrigado por compartilhar!", "copiedToClipboard": "Link copiado para a área de transferência", "addToHomeScreen": "

Adicionar à sua tela inicial

Você pode adicionar facilmente este site à tela inicial do smartphone para uma sensação nativa. Clique no botão 'adicionar à tela inicial' na barra de URL para fazer isso.", - "intro": "

Compartilhe este mapa

Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:" - } + "intro": "

Compartilhe este mapa

Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:", + "embedIntro": "

Incorpore em seu site

Por favor, incorpore este mapa em seu site.
Nós o encorajamos a fazer isso - você nem precisa pedir permissão.
É gratuito e sempre será. Quanto mais pessoas usarem isso, mais valioso se tornará." + }, + "aboutMapcomplete": "

Sobre o MapComplete

Com o MapComplete, você pode enriquecer o OpenStreetMap com informações sobre umúnico tema.Responda a algumas perguntas e, em minutos, suas contribuições estarão disponíveis em todo o mundo! Omantenedor do temadefine elementos, questões e linguagens para o tema.

Saiba mais

MapComplete sempreoferece a próxima etapapara saber mais sobre o OpenStreetMap.

  • Quando incorporado em um site, o iframe vincula-se a um MapComplete em tela inteira
  • A versão em tela inteira oferece informações sobre o OpenStreetMap
  • A visualização funciona sem login, mas a edição requer um login do OSM.
  • Se você não estiver conectado, será solicitado que você faça o login
  • Depois de responder a uma única pergunta, você pode adicionar novos aponta para o mapa
  • Depois de um tempo, as tags OSM reais são mostradas, posteriormente vinculadas ao wiki


Você percebeuum problema? Você tem umasolicitação de recurso ? Querajudar a traduzir? Acesse o código-fonteou rastreador de problemas.

Quer verseu progresso? Siga a contagem de edição emOsmCha.

" }, "index": { "pickTheme": "Escolha um tema abaixo para começar.", @@ -142,10 +144,13 @@ "no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!", "name_required": "É necessário um nome para exibir e criar comentários", "title_singular": "Um comentário", - "title": "{count} comentários" + "title": "{count} comentários", + "tos": "Se você criar um comentário, você concorda com o TOS e a política de privacidade de Mangrove.reviews ", + "affiliated_reviewer_warning": "(Revisão de afiliados)" }, "favourite": { "reload": "Recarregar dados", - "panelIntro": "

Seu tema pessoal

Ative suas camadas favoritas de todos os temas oficiais" + "panelIntro": "

Seu tema pessoal

Ative suas camadas favoritas de todos os temas oficiais", + "loginNeeded": "

Entrar

Um layout pessoal está disponível apenas para usuários do OpenStreetMap" } } From a8dcbaca193fe30094639471638a3c32a4681238 Mon Sep 17 00:00:00 2001 From: Jan Zabel Date: Mon, 19 Jul 2021 10:55:51 +0000 Subject: [PATCH 6/9] Translated using Weblate (German) Currently translated at 90.9% (10 of 11 strings) Translation: MapComplete/shared-questions Translate-URL: https://hosted.weblate.org/projects/mapcomplete/shared-questions/de/ --- langs/shared-questions/de.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/langs/shared-questions/de.json b/langs/shared-questions/de.json index ff0b97af8..6faff774e 100644 --- a/langs/shared-questions/de.json +++ b/langs/shared-questions/de.json @@ -6,6 +6,27 @@ "opening_hours": { "question": "Was sind die Öffnungszeiten von {name}?", "render": "

Öffnungszeiten

{opening_hours_table(opening_hours)}" + }, + "level": { + "mappings": { + "2": { + "then": "Ist im ersten Stock" + }, + "1": { + "then": "Ist im Erdgeschoss" + } + }, + "render": "Befindet sich im {level}ten Stock", + "question": "In welchem Stockwerk befindet sich dieses Objekt?" + }, + "description": { + "question": "Gibt es noch etwas, das die vorhergehenden Fragen nicht abgedeckt haben? Hier wäre Platz dafür.
Bitte keine bereits erhobenen Informationen." + }, + "website": { + "question": "Was ist die Website von {name}?" + }, + "email": { + "question": "Was ist die Mail-Adresse von {name}?" } } -} \ No newline at end of file +} From 4211b9c3ee4f599084ed554b39a71f02aeb1ad9d Mon Sep 17 00:00:00 2001 From: Rodrigo Tavares Date: Tue, 20 Jul 2021 10:17:39 +0000 Subject: [PATCH 7/9] Translated using Weblate (Portuguese (Brazil)) Currently translated at 72.7% (8 of 11 strings) Translation: MapComplete/shared-questions Translate-URL: https://hosted.weblate.org/projects/mapcomplete/shared-questions/pt_BR/ --- langs/shared-questions/pt_BR.json | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/langs/shared-questions/pt_BR.json b/langs/shared-questions/pt_BR.json index 0967ef424..9c577c396 100644 --- a/langs/shared-questions/pt_BR.json +++ b/langs/shared-questions/pt_BR.json @@ -1 +1,30 @@ -{} +{ + "undefined": { + "level": { + "render": "Localizado no {level}o andar", + "mappings": { + "2": { + "then": "Localizado no primeiro andar" + }, + "1": { + "then": "Localizado no térreo" + }, + "0": { + "then": "Localizado no subsolo" + } + } + }, + "opening_hours": { + "question": "Qual o horário de funcionamento de {name}?" + }, + "website": { + "question": "Qual o site de {name}?" + }, + "email": { + "question": "Qual o endereço de e-mail de {name}?" + }, + "phone": { + "question": "Qual o número de telefone de {name}?" + } + } +} From f6fca5e800b074d5930002d55cb9c07a1ecabf3e Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 18 Jul 2021 22:19:55 +0000 Subject: [PATCH 8/9] Translated using Weblate (Russian) Currently translated at 90.9% (10 of 11 strings) Translation: MapComplete/shared-questions Translate-URL: https://hosted.weblate.org/projects/mapcomplete/shared-questions/ru/ --- langs/shared-questions/ru.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/langs/shared-questions/ru.json b/langs/shared-questions/ru.json index a06bc7607..93c56dc44 100644 --- a/langs/shared-questions/ru.json +++ b/langs/shared-questions/ru.json @@ -15,6 +15,20 @@ "opening_hours": { "question": "Какое время работы у {name}?", "render": "

Часы работы

{opening_hours_table(opening_hours)}" + }, + "level": { + "mappings": { + "2": { + "then": "Расположено на первом этаже" + }, + "1": { + "then": "Расположено на первом этаже" + }, + "0": { + "then": "Расположено под землей" + } + }, + "render": "Расположено на {level}ом этаже" } } -} \ No newline at end of file +} From f6ebdc2b32fe45543da4de301d64731f1617cf27 Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 18 Jul 2021 20:59:27 +0000 Subject: [PATCH 9/9] Translated using Weblate (Russian) Currently translated at 54.7% (305 of 557 strings) Translation: MapComplete/Layer translations Translate-URL: https://hosted.weblate.org/projects/mapcomplete/layer-translations/ru/ --- langs/layers/ru.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/langs/layers/ru.json b/langs/layers/ru.json index 0cd328a6a..c0f535334 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -487,6 +487,11 @@ } } } + }, + "presets": { + "0": { + "title": "Обслуживание велосипедов/магазин" + } } }, "defibrillator": { @@ -1064,6 +1069,7 @@ "1": { "question": "Вы хотите добавить описание?" } - } + }, + "name": "Смотровая площадка" } -} \ No newline at end of file +}