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 f1d208983..c34fed806 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);} @@ -349,4 +354,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,"scissors.svg": Svg.scissors,"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,"scissors.svg": Svg.scissors,"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 index a3b8ae001..0c063b672 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({lat: 0, lon: 0, zoom: 1}) 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; @@ -53,7 +57,7 @@ export default class Minimap extends BaseUIElement { return wrapper; } - + private InitMap() { if (this._constructedHtmlElement === undefined) { // This element isn't initialized yet @@ -72,8 +76,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, @@ -83,9 +87,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 new file mode 100644 index 000000000..0558069b2 --- /dev/null +++ b/UI/Input/LengthInput.ts @@ -0,0 +1,185 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import Svg from "../../Svg"; +import {Utils} from "../../Utils"; +import Loc from "../../Models/Loc"; +import {GeoOperations} from "../../Logic/GeoOperations"; +import DirectionInput from "./DirectionInput"; +import {RadioButton} from "./RadioButton"; +import {FixedInputElement} from "./FixedInputElement"; + + +/** + * 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; + this.SetClass("block") + + } + + GetValue(): UIEventSource { + return this.value; + } + + IsValid(str: string): boolean { + const t = Number(str) + return !isNaN(t) && t >= 0 && t <= 360; + } + + protected InnerConstructElement(): HTMLElement { + const modeElement = new RadioButton([ + new FixedInputElement("Measure", "measure"), + new FixedInputElement("Move", "move") + ]) + // @ts-ignore + let map = undefined + if (!Utils.runningFromConsole) { + map = DirectionInput.constructMinimap({ + background: this.background, + allowMoving: false, + location: this._location, + leafletOptions: { + tap: true + } + }) + } + const element = new Combine([ + 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; 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") + .ConstructElement() + + + this.RegisterTriggers(element, map?.leafletMap) + element.style.overflow = "hidden" + element.style.display = "block" + + return element + } + + 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; + } + + const rect = htmlElement.getBoundingClientRect(); + // 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) { + const first = leaflet.layerPointToLatLng(firstClickXY) + const last = leaflet.layerPointToLatLng([dx, dy]) + const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 100000) / 100 + self.value.setData("" + geoDist) + } + + } + + } + + + htmlElement.ontouchstart = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true); + ev.preventDefault(); + } + + htmlElement.ontouchmove = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false); + ev.preventDefault(); + } + + htmlElement.ontouchend = (ev: TouchEvent) => { + onPosChange(undefined, undefined, false, true); + ev.preventDefault(); + } + + htmlElement.onmousedown = (ev: MouseEvent) => { + onPosChange(ev.clientX, ev.clientY, true); + ev.preventDefault(); + } + + htmlElement.onmouseup = (ev) => { + onPosChange(ev.clientX, ev.clientY, false, true); + ev.preventDefault(); + } + + htmlElement.onmousemove = (ev: MouseEvent) => { + onPosChange(ev.clientX, ev.clientY, false); + ev.preventDefault(); + } + } + +} \ No newline at end of file diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 2eeff8a54..ec3aa62ce 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,83 @@ 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){ + const lonlat: [number, number] = [...options.location] + lonlat.reverse() + options.location = <[number,number]> GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates + options.location.reverse() + } + 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 +194,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 +287,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 +299,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 +315,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 +336,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 +356,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 +373,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 +398,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 8d63325d4..2a6e5c939 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -333,12 +333,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 8b9475c32..ec6c34b5c 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 new file mode 100644 index 000000000..0446f22c4 --- /dev/null +++ b/assets/svg/length-crosshair.svg @@ -0,0 +1,115 @@ + + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + image/svg+xml + + + + + + + + + + + + + + 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/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 +} 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" } } 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 +} 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}?" + } + } +} 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 +} diff --git a/test.ts b/test.ts index b47699b0b..e69de29bb 100644 --- a/test.ts +++ b/test.ts @@ -1,76 +0,0 @@ -import SplitAction from "./Logic/Osm/Actions/SplitAction"; -import {GeoOperations} from "./Logic/GeoOperations"; - -const way = { - "type": "Feature", - "properties": { - "highway": "residential", - "maxweight": "3.5", - "maxweight:conditional": "none @ delivery", - "name": "Silsstraat", - "_last_edit:contributor": "Jorisbo", - "_last_edit:contributor:uid": 1983103, - "_last_edit:changeset": 70963524, - "_last_edit:timestamp": "2019-06-05T18:20:44Z", - "_version_number": 9, - "id": "way/23583625" - }, - "geometry": { - "type": "LineString", - "coordinates": [ - [ - 4.4889691, - 51.2049831 - ], - [ - 4.4895496, - 51.2047718 - ], - [ - 4.48966, - 51.2047147 - ], - [ - 4.4897439, - 51.2046548 - ], - [ - 4.4898162, - 51.2045921 - ], - [ - 4.4902997, - 51.2038418 - ] - ] - } -} - -let splitPoint = { - "type": "Feature", - "properties": {}, - "geometry": { - "type": "Point", - "coordinates": [ - 4.490211009979248, - 51.2041509326002 - ] - } -} - - -let splitClose = { - "type": "Feature", - "properties": {}, - "geometry": { - "type": "Point", - "coordinates": [ - 4.489563927054405, - 51.2047546593862 - ] - } -} -// State.state = new State(AllKnownLayouts.allKnownLayouts.get("fietsstraten")); -// add road to state -// State.state.allElements.addOrGetElement(way); -new SplitAction(way).DoSplit([splitPoint, splitClose].map(p => GeoOperations.nearestPoint(way,<[number, number]> p.geometry.coordinates)))