First working version of a width measurment tool

This commit is contained in:
pietervdvn 2021-07-20 01:33:58 +02:00
parent 40400591d0
commit 4fa9159da1
12 changed files with 300 additions and 110 deletions

View file

@ -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;
}
}
}

View file

@ -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'

2
Svg.ts
View file

@ -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 = " <svg xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" version=\"1.0\" width=\"859.53607pt\" height=\"858.4754pt\" viewBox=\"0 0 859.53607 858.4754\" preserveAspectRatio=\"xMidYMid meet\" id=\"svg14\" sodipodi:docname=\"length-crosshair.svg\" inkscape:version=\"0.92.5 (2060ec1f9f, 2020-04-08)\"> <defs id=\"defs18\" /> <sodipodi:namedview pagecolor=\"#ffffff\" bordercolor=\"#666666\" borderopacity=\"1\" objecttolerance=\"10\" gridtolerance=\"10\" guidetolerance=\"10\" inkscape:pageopacity=\"0\" inkscape:pageshadow=\"2\" inkscape:window-width=\"1680\" inkscape:window-height=\"1009\" id=\"namedview16\" showgrid=\"false\" showguides=\"true\" inkscape:guide-bbox=\"true\" inkscape:zoom=\"0.25\" inkscape:cx=\"-448.31847\" inkscape:cy=\"144.08448\" inkscape:window-x=\"0\" inkscape:window-y=\"15\" inkscape:window-maximized=\"1\" inkscape:current-layer=\"svg14\" inkscape:snap-smooth-nodes=\"true\" /> <metadata id=\"metadata2\"> Created by potrace 1.15, written by Peter Selinger 2001-2017 <rdf:RDF> <cc:Work rdf:about=\"\"> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" /> <dc:title /> </cc:Work> </rdf:RDF> </metadata> <ellipse style=\"fill: none !important;fill-opacity:1;stroke:#000000;stroke-width:2.83639622;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" id=\"path816\" cx=\"429.76804\" cy=\"429.2377\" rx=\"428.34982\" ry=\"427.81949\" /> <path style=\"fill: none !important;stroke:#000000;stroke-width:0.75px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\" d=\"M 1.4182129,429.2377 H 858.11788\" id=\"path820\" inkscape:connector-curvature=\"0\" /> <path style=\"fill: none !important;stroke:#000000;stroke-width:0.75px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\" d=\"M 429.76803,857.05715 V 1.4182129 v 0\" id=\"path822\" inkscape:connector-curvature=\"0\" /> </svg> "
public static length_crosshair = " <svg xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" version=\"1.0\" width=\"859.53607pt\" height=\"858.4754pt\" viewBox=\"0 0 859.53607 858.4754\" preserveAspectRatio=\"xMidYMid meet\" id=\"svg14\" sodipodi:docname=\"length-crosshair.svg\" inkscape:version=\"0.92.5 (2060ec1f9f, 2020-04-08)\"> <defs id=\"defs18\" /> <sodipodi:namedview pagecolor=\"#ffffff\" bordercolor=\"#666666\" borderopacity=\"1\" objecttolerance=\"10\" gridtolerance=\"10\" guidetolerance=\"10\" inkscape:pageopacity=\"0\" inkscape:pageshadow=\"2\" inkscape:window-width=\"1920\" inkscape:window-height=\"999\" id=\"namedview16\" showgrid=\"false\" showguides=\"true\" inkscape:guide-bbox=\"true\" inkscape:zoom=\"0.5\" inkscape:cx=\"108.3764\" inkscape:cy=\"623.05359\" inkscape:window-x=\"0\" inkscape:window-y=\"0\" inkscape:window-maximized=\"1\" inkscape:current-layer=\"svg14\" inkscape:snap-smooth-nodes=\"true\" /> <metadata id=\"metadata2\"> Created by potrace 1.15, written by Peter Selinger 2001-2017 <rdf:RDF> <cc:Work rdf:about=\"\"> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" /> <dc:title /> </cc:Work> </rdf:RDF> </metadata> <ellipse style=\"fill: none !important;fill-opacity:1;stroke:#000000;stroke-width:2.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" id=\"path816\" cx=\"-429.2377\" cy=\"429.76804\" rx=\"428.34982\" ry=\"427.81949\" transform=\"rotate(-90)\" /> <path style=\"fill: none !important;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"m 429.76804,430.08754 0,-429.19968\" id=\"path820\" inkscape:connector-curvature=\"0\" /> <path style=\"fill: none !important;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:8.99999982,8.99999982;stroke-dashoffset:0\" d=\"m 857.58749,429.23771 -855.6389371,0 v 0\" id=\"path822\" inkscape:connector-curvature=\"0\" /> <path inkscape:connector-curvature=\"0\" id=\"path814\" d=\"M 429.76804,857.30628 V 428.78674\" style=\"fill: none !important;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\" /> <path inkscape:connector-curvature=\"0\" id=\"path826\" d=\"M 857.58749,0.23771855 H 1.9485529 v 0\" style=\"fill: none !important;stroke:#000000;stroke-width:2.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:17.99999963,17.99999963;stroke-dashoffset:0;stroke-opacity:1\" /> <path inkscape:connector-curvature=\"0\" id=\"path828\" d=\"M 857.58749,858.2377 H 1.9485529 v 0\" style=\"fill: none !important;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:8.99999982, 8.99999982;stroke-dashoffset:0;stroke-opacity:1\" /> </svg> "
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);}

View file

@ -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<Loc>;
private _isInited = false;
private _allowMoving: boolean;
private readonly _leafletoptions: any;
constructor(options?: {
background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>,
allowMoving?: boolean
allowMoving?: boolean,
leafletOptions?: any
}
) {
super()
@ -28,10 +31,11 @@ export default class Minimap extends BaseUIElement {
this._location = options?.location ?? new UIEventSource<Loc>(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]]

View file

@ -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<string> {
private readonly value: UIEventSource<string>;
private background;
constructor(mapBackground: UIEventSource<any>,
constructor(mapBackground: UIEventSource<any>,
location: UIEventSource<Loc>,
value?: UIEventSource<string>) {
super();
this._location = location;
this.value = value ?? new UIEventSource<string>(undefined);
this.background = mapBackground;
this.SetClass("block")
}
GetValue(): UIEventSource<string> {
@ -33,83 +36,150 @@ export default class LengthInput extends InputElement<string> {
}
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<L.Map>) {
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 = <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();
}
}

View file

@ -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<string>, options?: {
location: [number, number],
mapBackgroundLayer?: UIEventSource<any>
mapBackgroundLayer?: UIEventSource<any>,
args: (string | number | boolean)[]
feature?: any
}) => InputElement<string>,
inputmode?: string
}
export default class ValidatedTextField {
public static bestLayerAt: (location: UIEventSource<Loc>, preferences: UIEventSource<string[]>) => 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<Loc>({
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<string[]>(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<Loc>({
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<string[]>(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<Loc>({
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<string>,
@ -233,7 +295,9 @@ export default class ValidatedTextField {
country?: () => string,
location?: [number /*lat*/, number /*lon*/],
mapBackgroundLayer?: UIEventSource<any>,
unit?: Unit
unit?: Unit,
args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
feature?: any
}): InputElement<string> {
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<string>, options?: {
location: [number, number],
mapBackgroundLayer: UIEventSource<any>
mapBackgroundLayer: UIEventSource<any>,
args: string[],
feature: any
}) => InputElement<string>,
inputmode?: string): TextFieldDef {

View file

@ -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<string> = 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);

View file

@ -39,7 +39,8 @@ export default class SpecialVisualizations {
static constructMiniMap: (options?: {
background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>,
allowMoving?: boolean
allowMoving?: boolean,
leafletOptions?: any
}) => BaseUIElement;
static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
public static specialVisualizations: SpecialVisualization[] =

View file

@ -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
</rdf:RDF>
</metadata>
<ellipse
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.83639622;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path816"
cx="429.76804"
cy="429.2377"
cx="-429.2377"
cy="429.76804"
rx="428.34982"
ry="427.81949" />
ry="427.81949"
transform="rotate(-90)" />
<path
style="fill:none;stroke:#000000;stroke-width:0.75px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 1.4182129,429.2377 H 858.11788"
style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 429.76804,430.08754 0,-429.19968"
id="path820"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:0.75px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 429.76803,857.05715 V 1.4182129 v 0"
style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:8.99999982,8.99999982;stroke-dashoffset:0"
d="m 857.58749,429.23771 -855.6389371,0 v 0"
id="path822"
inkscape:connector-curvature="0" />
<path
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" />
<path
inkscape:connector-curvature="0"
id="path826"
d="M 857.58749,0.23771855 H 1.9485529 v 0"
style="fill:none;stroke:#000000;stroke-width:2.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:17.99999963,17.99999963;stroke-dashoffset:0;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path828"
d="M 857.58749,858.2377 H 1.9485529 v 0"
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:8.99999982, 8.99999982;stroke-dashoffset:0;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -64,7 +64,13 @@
},
"tagRenderings": [
{
"render": "Deze straat is <b>{width:carriageway}m</b> breed"
"render": "Deze straat is <b>{width:carriageway}m</b> breed",
"question": "Hoe breed is deze straat?",
"freeform": {
"key": "width:carriageway",
"type": "length",
"helperArgs": [21, "map"]
}
},
{
"render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig:",

View file

@ -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<L.Map>,

13
test.ts
View file

@ -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<Loc>({
zoom: 24,
lat: 51.21043,
lon: 3.21389
})
const li = new LengthInput(
AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource<string | string[]>("map","photo")),
loc
)
li.SetStyle("height: 30rem; background: aliceblue;")
.AttachTo("maindiv")
new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv")