forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
cc22c5b0fb
17 changed files with 537 additions and 134 deletions
|
@ -27,7 +27,8 @@ export default class TagRenderingConfig {
|
||||||
readonly type: string,
|
readonly type: string,
|
||||||
readonly addExtraTags: TagsFilter[];
|
readonly addExtraTags: TagsFilter[];
|
||||||
readonly inline: boolean,
|
readonly inline: boolean,
|
||||||
readonly default?: string
|
readonly default?: string,
|
||||||
|
readonly helperArgs?: (string | number | boolean)[]
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly multiAnswer: boolean;
|
readonly multiAnswer: boolean;
|
||||||
|
@ -76,8 +77,8 @@ export default class TagRenderingConfig {
|
||||||
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
|
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
|
||||||
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [],
|
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [],
|
||||||
inline: json.freeform.inline ?? false,
|
inline: json.freeform.inline ?? false,
|
||||||
default: json.freeform.default
|
default: json.freeform.default,
|
||||||
|
helperArgs: json.freeform.helperArgs
|
||||||
|
|
||||||
}
|
}
|
||||||
if (json.freeform["extraTags"] !== undefined) {
|
if (json.freeform["extraTags"] !== undefined) {
|
||||||
|
@ -336,20 +337,20 @@ export default class TagRenderingConfig {
|
||||||
* Note: this might be hidden by conditions
|
* Note: this might be hidden by conditions
|
||||||
*/
|
*/
|
||||||
public hasMinimap(): boolean {
|
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 translation of translations) {
|
||||||
for (const key in translation.translations) {
|
for (const key in translation.translations) {
|
||||||
if(!translation.translations.hasOwnProperty(key)){
|
if (!translation.translations.hasOwnProperty(key)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const template = translation.translations[key]
|
const template = translation.translations[key]
|
||||||
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
|
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
|
||||||
const hasMiniMap = parts.filter(part =>part.special !== undefined ).some(special => special.special.func.funcName === "minimap")
|
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
|
||||||
if(hasMiniMap){
|
if (hasMiniMap) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -30,6 +30,7 @@ export interface TagRenderingConfigJson {
|
||||||
* Allow freeform text input from the user
|
* Allow freeform text input from the user
|
||||||
*/
|
*/
|
||||||
freeform?: {
|
freeform?: {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this key is present, then 'render' is used to display the value.
|
* If this key is present, then 'render' is used to display the value.
|
||||||
* If this is undefined, the rendering is _always_ shown
|
* 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
|
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
|
||||||
*/
|
*/
|
||||||
type?: string,
|
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.
|
* If a value is added with the textfield, these extra tag is addded.
|
||||||
* Useful to add a 'fixme=freeform textfield used - to be checked'
|
* Useful to add a 'fixme=freeform textfield used - to be checked'
|
||||||
|
|
7
Svg.ts
7
Svg.ts
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,7 @@ import Loc from "../../Models/Loc";
|
||||||
import BaseLayer from "../../Models/BaseLayer";
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||||
import {Map} from "leaflet";
|
import {Map} from "leaflet";
|
||||||
|
import {Utils} from "../../Utils";
|
||||||
|
|
||||||
export default class Minimap extends BaseUIElement {
|
export default class Minimap extends BaseUIElement {
|
||||||
|
|
||||||
|
@ -15,11 +16,13 @@ export default class Minimap extends BaseUIElement {
|
||||||
private readonly _location: UIEventSource<Loc>;
|
private readonly _location: UIEventSource<Loc>;
|
||||||
private _isInited = false;
|
private _isInited = false;
|
||||||
private _allowMoving: boolean;
|
private _allowMoving: boolean;
|
||||||
|
private readonly _leafletoptions: any;
|
||||||
|
|
||||||
constructor(options?: {
|
constructor(options?: {
|
||||||
background?: UIEventSource<BaseLayer>,
|
background?: UIEventSource<BaseLayer>,
|
||||||
location?: UIEventSource<Loc>,
|
location?: UIEventSource<Loc>,
|
||||||
allowMoving?: boolean
|
allowMoving?: boolean,
|
||||||
|
leafletOptions?: any
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -28,10 +31,11 @@ export default class Minimap extends BaseUIElement {
|
||||||
this._location = options?.location ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
|
this._location = options?.location ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
|
||||||
this._id = "minimap" + Minimap._nextId;
|
this._id = "minimap" + Minimap._nextId;
|
||||||
this._allowMoving = options.allowMoving ?? true;
|
this._allowMoving = options.allowMoving ?? true;
|
||||||
|
this._leafletoptions = options.leafletOptions ?? {}
|
||||||
Minimap._nextId++
|
Minimap._nextId++
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerConstructElement(): HTMLElement {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
const div = document.createElement("div")
|
const div = document.createElement("div")
|
||||||
div.id = this._id;
|
div.id = this._id;
|
||||||
|
@ -53,7 +57,7 @@ export default class Minimap extends BaseUIElement {
|
||||||
return wrapper;
|
return wrapper;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private InitMap() {
|
private InitMap() {
|
||||||
if (this._constructedHtmlElement === undefined) {
|
if (this._constructedHtmlElement === undefined) {
|
||||||
// This element isn't initialized yet
|
// This element isn't initialized yet
|
||||||
|
@ -72,8 +76,8 @@ export default class Minimap extends BaseUIElement {
|
||||||
const location = this._location;
|
const location = this._location;
|
||||||
|
|
||||||
let currentLayer = this._background.data.layer()
|
let currentLayer = this._background.data.layer()
|
||||||
const map = L.map(this._id, {
|
const options = {
|
||||||
center: [location.data?.lat ?? 0, location.data?.lon ?? 0],
|
center: <[number, number]> [location.data?.lat ?? 0, location.data?.lon ?? 0],
|
||||||
zoom: location.data?.zoom ?? 2,
|
zoom: location.data?.zoom ?? 2,
|
||||||
layers: [currentLayer],
|
layers: [currentLayer],
|
||||||
zoomControl: false,
|
zoomControl: false,
|
||||||
|
@ -83,9 +87,13 @@ export default class Minimap extends BaseUIElement {
|
||||||
doubleClickZoom: this._allowMoving,
|
doubleClickZoom: this._allowMoving,
|
||||||
keyboard: this._allowMoving,
|
keyboard: this._allowMoving,
|
||||||
touchZoom: 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
|
fadeAnimation: this._allowMoving
|
||||||
});
|
}
|
||||||
|
|
||||||
|
Utils.Merge(this._leafletoptions, options)
|
||||||
|
|
||||||
|
const map = L.map(this._id, options);
|
||||||
|
|
||||||
map.setMaxBounds(
|
map.setMaxBounds(
|
||||||
[[-100, -200], [100, 200]]
|
[[-100, -200], [100, 200]]
|
||||||
|
|
185
UI/Input/LengthInput.ts
Normal file
185
UI/Input/LengthInput.ts
Normal file
|
@ -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<string> {
|
||||||
|
private readonly _location: UIEventSource<Loc>;
|
||||||
|
|
||||||
|
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
|
private readonly value: UIEventSource<string>;
|
||||||
|
private background;
|
||||||
|
|
||||||
|
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> {
|
||||||
|
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<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = <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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ import {Utils} from "../../Utils";
|
||||||
import Loc from "../../Models/Loc";
|
import Loc from "../../Models/Loc";
|
||||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import LengthInput from "./LengthInput";
|
||||||
|
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||||
|
|
||||||
interface TextFieldDef {
|
interface TextFieldDef {
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -21,14 +23,16 @@ interface TextFieldDef {
|
||||||
reformat?: ((s: string, country?: () => string) => string),
|
reformat?: ((s: string, country?: () => string) => string),
|
||||||
inputHelper?: (value: UIEventSource<string>, options?: {
|
inputHelper?: (value: UIEventSource<string>, options?: {
|
||||||
location: [number, number],
|
location: [number, number],
|
||||||
mapBackgroundLayer?: UIEventSource<any>
|
mapBackgroundLayer?: UIEventSource<any>,
|
||||||
|
args: (string | number | boolean)[]
|
||||||
|
feature?: any
|
||||||
}) => InputElement<string>,
|
}) => InputElement<string>,
|
||||||
|
|
||||||
inputmode?: string
|
inputmode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ValidatedTextField {
|
export default class ValidatedTextField {
|
||||||
|
|
||||||
|
public static bestLayerAt: (location: UIEventSource<Loc>, preferences: UIEventSource<string[]>) => any
|
||||||
|
|
||||||
public static tpList: TextFieldDef[] = [
|
public static tpList: TextFieldDef[] = [
|
||||||
ValidatedTextField.tp(
|
ValidatedTextField.tp(
|
||||||
|
@ -63,6 +67,83 @@ export default class ValidatedTextField {
|
||||||
return [year, month, day].join('-');
|
return [year, month, day].join('-');
|
||||||
},
|
},
|
||||||
(value) => new SimpleDatePicker(value)),
|
(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){
|
||||||
|
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<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(
|
ValidatedTextField.tp(
|
||||||
"wikidata",
|
"wikidata",
|
||||||
"A wikidata identifier, e.g. Q42",
|
"A wikidata identifier, e.g. Q42",
|
||||||
|
@ -113,22 +194,6 @@ export default class ValidatedTextField {
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
"numeric"),
|
"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(
|
ValidatedTextField.tp(
|
||||||
"float",
|
"float",
|
||||||
"A decimal",
|
"A decimal",
|
||||||
|
@ -222,6 +287,7 @@ export default class ValidatedTextField {
|
||||||
* {string (typename) --> TextFieldDef}
|
* {string (typename) --> TextFieldDef}
|
||||||
*/
|
*/
|
||||||
public static AllTypes = ValidatedTextField.allTypesDict();
|
public static AllTypes = ValidatedTextField.allTypesDict();
|
||||||
|
|
||||||
public static InputForType(type: string, options?: {
|
public static InputForType(type: string, options?: {
|
||||||
placeholder?: string | BaseUIElement,
|
placeholder?: string | BaseUIElement,
|
||||||
value?: UIEventSource<string>,
|
value?: UIEventSource<string>,
|
||||||
|
@ -233,7 +299,9 @@ export default class ValidatedTextField {
|
||||||
country?: () => string,
|
country?: () => string,
|
||||||
location?: [number /*lat*/, number /*lon*/],
|
location?: [number /*lat*/, number /*lon*/],
|
||||||
mapBackgroundLayer?: UIEventSource<any>,
|
mapBackgroundLayer?: UIEventSource<any>,
|
||||||
unit?: Unit
|
unit?: Unit,
|
||||||
|
args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
|
||||||
|
feature?: any
|
||||||
}): InputElement<string> {
|
}): InputElement<string> {
|
||||||
options = options ?? {};
|
options = options ?? {};
|
||||||
options.placeholder = options.placeholder ?? type;
|
options.placeholder = options.placeholder ?? type;
|
||||||
|
@ -247,7 +315,7 @@ export default class ValidatedTextField {
|
||||||
if (str === undefined) {
|
if (str === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(options.unit) {
|
if (options.unit) {
|
||||||
str = options.unit.stripUnitParts(str)
|
str = options.unit.stripUnitParts(str)
|
||||||
}
|
}
|
||||||
return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country);
|
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.
|
// We need to apply a unit.
|
||||||
// This implies:
|
// This implies:
|
||||||
// We have to create a dropdown with applicable denominations, and fuse those values
|
// We have to create a dropdown with applicable denominations, and fuse those values
|
||||||
|
@ -288,17 +356,16 @@ export default class ValidatedTextField {
|
||||||
input,
|
input,
|
||||||
unitDropDown,
|
unitDropDown,
|
||||||
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM
|
// 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) => {
|
(valueWithDenom: string) => {
|
||||||
// Take the value from OSM and feed it into the textfield and the dropdown
|
// Take the value from OSM and feed it into the textfield and the dropdown
|
||||||
const withDenom = unit.findDenomination(valueWithDenom);
|
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
|
// Not a valid value at all - we give it undefined and leave the details up to the other elements
|
||||||
return [undefined, undefined]
|
return [undefined, undefined]
|
||||||
}
|
}
|
||||||
const [strippedText, denom] = withDenom
|
const [strippedText, denom] = withDenom
|
||||||
if(strippedText === undefined){
|
if (strippedText === undefined) {
|
||||||
return [undefined, undefined]
|
return [undefined, undefined]
|
||||||
}
|
}
|
||||||
return [strippedText, denom]
|
return [strippedText, denom]
|
||||||
|
@ -306,18 +373,20 @@ export default class ValidatedTextField {
|
||||||
).SetClass("flex")
|
).SetClass("flex")
|
||||||
}
|
}
|
||||||
if (tp.inputHelper) {
|
if (tp.inputHelper) {
|
||||||
const helper = tp.inputHelper(input.GetValue(), {
|
const helper = tp.inputHelper(input.GetValue(), {
|
||||||
location: options.location,
|
location: options.location,
|
||||||
mapBackgroundLayer: options.mapBackgroundLayer
|
mapBackgroundLayer: options.mapBackgroundLayer,
|
||||||
|
args: options.args,
|
||||||
|
feature: options.feature
|
||||||
})
|
})
|
||||||
input = new CombinedInputElement(input, helper,
|
input = new CombinedInputElement(input, helper,
|
||||||
(a, _) => a, // We can ignore b, as they are linked earlier
|
(a, _) => a, // We can ignore b, as they are linked earlier
|
||||||
a => [a, a]
|
a => [a, a]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HelpText(): string {
|
public static HelpText(): string {
|
||||||
const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n")
|
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
|
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),
|
reformat?: ((s: string, country?: () => string) => string),
|
||||||
inputHelper?: (value: UIEventSource<string>, options?: {
|
inputHelper?: (value: UIEventSource<string>, options?: {
|
||||||
location: [number, number],
|
location: [number, number],
|
||||||
mapBackgroundLayer: UIEventSource<any>
|
mapBackgroundLayer: UIEventSource<any>,
|
||||||
|
args: string[],
|
||||||
|
feature: any
|
||||||
}) => InputElement<string>,
|
}) => InputElement<string>,
|
||||||
inputmode?: string): TextFieldDef {
|
inputmode?: string): TextFieldDef {
|
||||||
|
|
||||||
|
|
|
@ -333,12 +333,15 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsData = tags.data;
|
const tagsData = tags.data;
|
||||||
|
const feature = State.state.allElements.ContainingFeatures.get(tagsData.id)
|
||||||
const input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
|
const input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
|
||||||
isValid: (str) => (str.length <= 255),
|
isValid: (str) => (str.length <= 255),
|
||||||
country: () => tagsData._country,
|
country: () => tagsData._country,
|
||||||
location: [tagsData._lat, tagsData._lon],
|
location: [tagsData._lat, tagsData._lon],
|
||||||
mapBackgroundLayer: State.state.backgroundLayer,
|
mapBackgroundLayer: State.state.backgroundLayer,
|
||||||
unit: applicableUnit
|
unit: applicableUnit,
|
||||||
|
args: configuration.freeform.helperArgs,
|
||||||
|
feature: feature
|
||||||
});
|
});
|
||||||
|
|
||||||
input.GetValue().setData(tagsData[freeform.key] ?? freeform.default);
|
input.GetValue().setData(tagsData[freeform.key] ?? freeform.default);
|
||||||
|
|
|
@ -39,7 +39,8 @@ export default class SpecialVisualizations {
|
||||||
static constructMiniMap: (options?: {
|
static constructMiniMap: (options?: {
|
||||||
background?: UIEventSource<BaseLayer>,
|
background?: UIEventSource<BaseLayer>,
|
||||||
location?: UIEventSource<Loc>,
|
location?: UIEventSource<Loc>,
|
||||||
allowMoving?: boolean
|
allowMoving?: boolean,
|
||||||
|
leafletOptions?: any
|
||||||
}) => BaseUIElement;
|
}) => BaseUIElement;
|
||||||
static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
|
static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
|
||||||
public static specialVisualizations: SpecialVisualization[] =
|
public static specialVisualizations: SpecialVisualization[] =
|
||||||
|
|
115
assets/svg/length-crosshair.svg
Normal file
115
assets/svg/length-crosshair.svg
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<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="307.56567"
|
||||||
|
inkscape:cy="-35.669379"
|
||||||
|
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>
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:71.99999853,71.99999853;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="path816"
|
||||||
|
transform="rotate(-89.47199)"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="-425.24921"
|
||||||
|
sodipodi:cy="433.71375"
|
||||||
|
sodipodi:rx="428.34982"
|
||||||
|
sodipodi:ry="427.81949"
|
||||||
|
sodipodi:start="0"
|
||||||
|
sodipodi:end="4.7117019"
|
||||||
|
sodipodi:open="true"
|
||||||
|
d="M 3.1006165,433.71375 A 428.34982,427.81949 0 0 1 -425.1511,861.53322 428.34982,427.81949 0 0 1 -853.59898,433.90971 428.34982,427.81949 0 0 1 -425.54352,5.8943576" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:4.49999991;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: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.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:35.99999926,35.99999926;stroke-dashoffset:0" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path826"
|
||||||
|
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" />
|
||||||
|
<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" />
|
||||||
|
<path
|
||||||
|
cx="-429.2377"
|
||||||
|
cy="429.76804"
|
||||||
|
rx="428.34982"
|
||||||
|
ry="427.81949"
|
||||||
|
transform="rotate(-90)"
|
||||||
|
id="path825"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:11.99999975;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="M -5.3639221,-424.71887 A 428.34982,427.81949 0 0 1 -429.83855,3.0831087 428.34982,427.81949 0 0 1 -861.99345,-416.97839"
|
||||||
|
sodipodi:open="true"
|
||||||
|
sodipodi:end="3.1234988"
|
||||||
|
sodipodi:start="0"
|
||||||
|
sodipodi:ry="427.81949"
|
||||||
|
sodipodi:rx="428.34982"
|
||||||
|
sodipodi:cy="-424.71887"
|
||||||
|
sodipodi:cx="-433.71375"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
transform="rotate(-179.47199)"
|
||||||
|
id="path827"
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.7 KiB |
|
@ -64,7 +64,13 @@
|
||||||
},
|
},
|
||||||
"tagRenderings": [
|
"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:",
|
"render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig:",
|
||||||
|
|
3
index.ts
3
index.ts
|
@ -19,10 +19,13 @@ import DirectionInput from "./UI/Input/DirectionInput";
|
||||||
import SpecialVisualizations from "./UI/SpecialVisualizations";
|
import SpecialVisualizations from "./UI/SpecialVisualizations";
|
||||||
import ShowDataLayer from "./UI/ShowDataLayer";
|
import ShowDataLayer from "./UI/ShowDataLayer";
|
||||||
import * as L from "leaflet";
|
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
|
// 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/");
|
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
|
||||||
DirectionInput.constructMinimap = options => new Minimap(options)
|
DirectionInput.constructMinimap = options => new Minimap(options)
|
||||||
|
ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
|
||||||
SpecialVisualizations.constructMiniMap = options => new Minimap(options)
|
SpecialVisualizations.constructMiniMap = options => new Minimap(options)
|
||||||
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
|
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
|
||||||
leafletMap: UIEventSource<L.Map>,
|
leafletMap: UIEventSource<L.Map>,
|
||||||
|
|
|
@ -487,6 +487,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"0": {
|
||||||
|
"title": "Обслуживание велосипедов/магазин"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defibrillator": {
|
"defibrillator": {
|
||||||
|
@ -1064,6 +1069,7 @@
|
||||||
"1": {
|
"1": {
|
||||||
"question": "Вы хотите добавить описание?"
|
"question": "Вы хотите добавить описание?"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"name": "Смотровая площадка"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,8 +122,10 @@
|
||||||
"thanksForSharing": "Obrigado por compartilhar!",
|
"thanksForSharing": "Obrigado por compartilhar!",
|
||||||
"copiedToClipboard": "Link copiado para a área de transferência",
|
"copiedToClipboard": "Link copiado para a área de transferência",
|
||||||
"addToHomeScreen": "<h3>Adicionar à sua tela inicial</h3>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.",
|
"addToHomeScreen": "<h3>Adicionar à sua tela inicial</h3>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": "<h3>Compartilhe este mapa</h3> Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:"
|
"intro": "<h3>Compartilhe este mapa</h3> Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:",
|
||||||
}
|
"embedIntro": "<h3>Incorpore em seu site</h3>Por favor, incorpore este mapa em seu site.<br>Nós o encorajamos a fazer isso - você nem precisa pedir permissão.<br>É gratuito e sempre será. Quanto mais pessoas usarem isso, mais valioso se tornará."
|
||||||
|
},
|
||||||
|
"aboutMapcomplete": "<h3>Sobre o MapComplete</h3><p>Com o MapComplete, você pode enriquecer o OpenStreetMap com informações sobre um<b>único tema.</b>Responda a algumas perguntas e, em minutos, suas contribuições estarão disponíveis em todo o mundo! O<b>mantenedor do tema</b>define elementos, questões e linguagens para o tema.</p><h3>Saiba mais</h3><p>MapComplete sempre<b>oferece a próxima etapa</b>para saber mais sobre o OpenStreetMap.</p><ul><li>Quando incorporado em um site, o iframe vincula-se a um MapComplete em tela inteira</li><li>A versão em tela inteira oferece informações sobre o OpenStreetMap</li><li>A visualização funciona sem login, mas a edição requer um login do OSM.</li><li>Se você não estiver conectado, será solicitado que você faça o login</li><li>Depois de responder a uma única pergunta, você pode adicionar novos aponta para o mapa </li><li> Depois de um tempo, as tags OSM reais são mostradas, posteriormente vinculadas ao wiki </li></ul><p></p><br><p>Você percebeu<b>um problema</b>? Você tem uma<b>solicitação de recurso </b>? Quer<b>ajudar a traduzir</b>? Acesse <a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">o código-fonte</a>ou <a href=\"https: //github.com/pietervdvn/MapComplete / issues \" target=\" _ blank \">rastreador de problemas.</a></p><p>Quer ver<b>seu progresso</b>? Siga a contagem de edição em<a href=\"https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D\" target=\"_blank\">OsmCha</a>.</p>"
|
||||||
},
|
},
|
||||||
"index": {
|
"index": {
|
||||||
"pickTheme": "Escolha um tema abaixo para começar.",
|
"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!",
|
"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",
|
"name_required": "É necessário um nome para exibir e criar comentários",
|
||||||
"title_singular": "Um comentário",
|
"title_singular": "Um comentário",
|
||||||
"title": "{count} comentários"
|
"title": "{count} comentários",
|
||||||
|
"tos": "Se você criar um comentário, você concorda com <a href=\"https://mangrove.reviews/terms\" target=\"_blank\"> o TOS e a política de privacidade de Mangrove.reviews </a>",
|
||||||
|
"affiliated_reviewer_warning": "(Revisão de afiliados)"
|
||||||
},
|
},
|
||||||
"favourite": {
|
"favourite": {
|
||||||
"reload": "Recarregar dados",
|
"reload": "Recarregar dados",
|
||||||
"panelIntro": "<h3>Seu tema pessoal</h3>Ative suas camadas favoritas de todos os temas oficiais"
|
"panelIntro": "<h3>Seu tema pessoal</h3>Ative suas camadas favoritas de todos os temas oficiais",
|
||||||
|
"loginNeeded": "<h3>Entrar</h3> Um layout pessoal está disponível apenas para usuários do OpenStreetMap"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,27 @@
|
||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"question": "Was sind die Öffnungszeiten von {name}?",
|
"question": "Was sind die Öffnungszeiten von {name}?",
|
||||||
"render": "<h3>Öffnungszeiten</h3>{opening_hours_table(opening_hours)}"
|
"render": "<h3>Öffnungszeiten</h3>{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.<br/><span style='font-size: small'>Bitte keine bereits erhobenen Informationen.</span>"
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"question": "Was ist die Website von {name}?"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"question": "Was ist die Mail-Adresse von {name}?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,20 @@
|
||||||
"opening_hours": {
|
"opening_hours": {
|
||||||
"question": "Какое время работы у {name}?",
|
"question": "Какое время работы у {name}?",
|
||||||
"render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}"
|
"render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"mappings": {
|
||||||
|
"2": {
|
||||||
|
"then": "Расположено на первом этаже"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"then": "Расположено на первом этаже"
|
||||||
|
},
|
||||||
|
"0": {
|
||||||
|
"then": "Расположено под землей"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"render": "Расположено на {level}ом этаже"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
76
test.ts
76
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)))
|
|
Loading…
Add table
Add a link
Reference in a new issue