forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
330930d5d4
77 changed files with 2462 additions and 581 deletions
|
@ -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;
|
||||
|
@ -44,7 +48,6 @@ export default class Minimap extends BaseUIElement {
|
|||
const self = this;
|
||||
// @ts-ignore
|
||||
const resizeObserver = new ResizeObserver(_ => {
|
||||
console.log("Change in size detected!")
|
||||
self.InitMap();
|
||||
self.leafletMap?.data?.invalidateSize()
|
||||
});
|
||||
|
@ -72,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,8 +85,14 @@ export default class Minimap extends BaseUIElement {
|
|||
scrollWheelZoom: this._allowMoving,
|
||||
doubleClickZoom: 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,
|
||||
fadeAnimation: this._allowMoving
|
||||
}
|
||||
|
||||
Utils.Merge(this._leafletoptions, options)
|
||||
|
||||
const map = L.map(this._id, options);
|
||||
|
||||
map.setMaxBounds(
|
||||
[[-100, -200], [100, 200]]
|
||||
|
|
|
@ -3,6 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import Loc from "../../Models/Loc";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export class Basemap {
|
||||
|
||||
|
@ -35,9 +36,8 @@ export class Basemap {
|
|||
);
|
||||
|
||||
this.map.attributionControl.setPrefix(
|
||||
"<span id='leaflet-attribution'></span> | <a href='https://osm.org'>OpenStreetMap</a>");
|
||||
"<span id='leaflet-attribution'>A</span>");
|
||||
|
||||
extraAttribution.AttachTo('leaflet-attribution')
|
||||
const self = this;
|
||||
|
||||
currentLayer.addCallbackAndRun(layer => {
|
||||
|
@ -77,6 +77,7 @@ export class Basemap {
|
|||
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
|
||||
});
|
||||
|
||||
extraAttribution.AttachTo('leaflet-attribution')
|
||||
|
||||
}
|
||||
|
||||
|
|
21
UI/BigComponents/ExportDataButton.ts
Normal file
21
UI/BigComponents/ExportDataButton.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import Translations from "../i18n/Translations";
|
||||
import State from "../../State";
|
||||
import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import Combine from "../Base/Combine";
|
||||
|
||||
export class ExportDataButton extends Combine {
|
||||
constructor() {
|
||||
const t = Translations.t.general.download
|
||||
const button = new SubtleButton(Svg.floppy_ui(), t.downloadGeojson.Clone().SetClass("font-bold"))
|
||||
.onClick(() => {
|
||||
const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline)
|
||||
const name = State.state.layoutToUse.data.id;
|
||||
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), `MapComplete_${name}_export_${new Date().toISOString().substr(0,19)}.geojson`);
|
||||
})
|
||||
|
||||
super([button, t.licenseInfo.Clone().SetClass("link-underline")])
|
||||
}
|
||||
}
|
|
@ -2,11 +2,12 @@ import State from "../../State";
|
|||
import BackgroundSelector from "./BackgroundSelector";
|
||||
import LayerSelection from "./LayerSelection";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {ExportDataButton} from "./ExportDataButton";
|
||||
|
||||
export default class LayerControlPanel extends ScrollableFullScreen {
|
||||
|
||||
|
@ -14,27 +15,34 @@ export default class LayerControlPanel extends ScrollableFullScreen {
|
|||
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown);
|
||||
}
|
||||
|
||||
private static GenTitle():BaseUIElement {
|
||||
private static GenTitle(): BaseUIElement {
|
||||
return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2")
|
||||
}
|
||||
|
||||
private static GeneratePanel() : BaseUIElement {
|
||||
let layerControlPanel: BaseUIElement = new FixedUiElement("");
|
||||
private static GeneratePanel(): BaseUIElement {
|
||||
const elements: BaseUIElement[] = []
|
||||
|
||||
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
||||
layerControlPanel = new BackgroundSelector();
|
||||
layerControlPanel.SetStyle("margin:1em");
|
||||
layerControlPanel.onClick(() => {
|
||||
const backgroundSelector = new BackgroundSelector();
|
||||
backgroundSelector.SetStyle("margin:1em");
|
||||
backgroundSelector.onClick(() => {
|
||||
});
|
||||
elements.push(backgroundSelector)
|
||||
}
|
||||
|
||||
if (State.state.filteredLayers.data.length > 1) {
|
||||
const layerSelection = new LayerSelection(State.state.filteredLayers);
|
||||
layerSelection.onClick(() => {
|
||||
});
|
||||
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]);
|
||||
}
|
||||
elements.push(new Toggle(
|
||||
new LayerSelection(State.state.filteredLayers),
|
||||
undefined,
|
||||
State.state.filteredLayers.map(layers => layers.length > 1)
|
||||
))
|
||||
|
||||
return layerControlPanel;
|
||||
elements.push(new Toggle(
|
||||
new ExportDataButton(),
|
||||
undefined,
|
||||
State.state.featureSwitchEnableExport
|
||||
))
|
||||
|
||||
return new Combine(elements).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
}
|
|
@ -74,7 +74,6 @@ export default class LayerSelection extends Combine {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
super(checkboxes)
|
||||
this.SetStyle("display:flex;flex-direction:column;")
|
||||
|
||||
|
|
|
@ -62,6 +62,10 @@ export default class MoreScreen extends Combine {
|
|||
let officialThemes = AllKnownLayouts.layoutsList
|
||||
|
||||
let buttons = officialThemes.map((layout) => {
|
||||
if(layout === undefined){
|
||||
console.trace("Layout is undefined")
|
||||
return undefined
|
||||
}
|
||||
const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass);
|
||||
if(layout.id === personal.id){
|
||||
return new VariableUiElement(
|
||||
|
|
|
@ -16,6 +16,10 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
|||
import Toggle from "../Input/Toggle";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import LocationInput from "../Input/LocationInput";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import Loc from "../../Models/Loc";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
|
@ -25,14 +29,18 @@ import {Translation} from "../i18n/Translation";
|
|||
* - A 'read your unread messages before adding a point'
|
||||
*/
|
||||
|
||||
/*private*/
|
||||
interface PresetInfo {
|
||||
description: string | Translation,
|
||||
name: string | BaseUIElement,
|
||||
icon: BaseUIElement,
|
||||
icon: () => BaseUIElement,
|
||||
tags: Tag[],
|
||||
layerToAddTo: {
|
||||
layerDef: LayerConfig,
|
||||
isDisplayed: UIEventSource<boolean>
|
||||
},
|
||||
preciseInput?: {
|
||||
preferredBackground?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,18 +56,16 @@ export default class SimpleAddUI extends Toggle {
|
|||
new SubtleButton(Svg.envelope_ui(),
|
||||
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false})
|
||||
]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
|
||||
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
|
||||
|
||||
function createNewPoint(tags: any[]){
|
||||
const loc = State.state.LastClickLocation.data;
|
||||
let feature = State.state.changes.createElement(tags, loc.lat, loc.lon);
|
||||
|
||||
function createNewPoint(tags: any[], location: { lat: number, lon: number }) {
|
||||
let feature = State.state.changes.createElement(tags, location.lat, location.lon);
|
||||
State.state.selectedElement.setData(feature);
|
||||
}
|
||||
|
||||
|
||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
||||
|
||||
const addUi = new VariableUiElement(
|
||||
|
@ -68,8 +74,8 @@ export default class SimpleAddUI extends Toggle {
|
|||
return presetsOverview
|
||||
}
|
||||
return SimpleAddUI.CreateConfirmButton(preset,
|
||||
tags => {
|
||||
createNewPoint(tags)
|
||||
(tags, location) => {
|
||||
createNewPoint(tags, location)
|
||||
selectedPreset.setData(undefined)
|
||||
}, () => {
|
||||
selectedPreset.setData(undefined)
|
||||
|
@ -86,7 +92,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
addUi,
|
||||
State.state.layerUpdater.runningQuery
|
||||
),
|
||||
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert") ,
|
||||
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
|
||||
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
|
||||
),
|
||||
readYourMessages,
|
||||
|
@ -103,22 +109,48 @@ export default class SimpleAddUI extends Toggle {
|
|||
}
|
||||
|
||||
|
||||
|
||||
private static CreateConfirmButton(preset: PresetInfo,
|
||||
confirm: (tags: any[]) => void,
|
||||
confirm: (tags: any[], location: { lat: number, lon: number }) => void,
|
||||
cancel: () => void): BaseUIElement {
|
||||
|
||||
let location = State.state.LastClickLocation;
|
||||
let preciseInput: InputElement<Loc> = undefined
|
||||
if (preset.preciseInput !== undefined) {
|
||||
const locationSrc = new UIEventSource({
|
||||
lat: location.data.lat,
|
||||
lon: location.data.lon,
|
||||
zoom: 19
|
||||
});
|
||||
|
||||
let backgroundLayer = undefined;
|
||||
if(preset.preciseInput.preferredBackground){
|
||||
backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
|
||||
}
|
||||
|
||||
preciseInput = new LocationInput({
|
||||
mapBackground: backgroundLayer,
|
||||
centerLocation:locationSrc
|
||||
|
||||
})
|
||||
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
|
||||
}
|
||||
|
||||
const confirmButton = new SubtleButton(preset.icon,
|
||||
|
||||
let confirmButton: BaseUIElement = new SubtleButton(preset.icon(),
|
||||
new Combine([
|
||||
Translations.t.general.add.addNew.Subs({category: preset.name}),
|
||||
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
|
||||
]).SetClass("flex flex-col")
|
||||
).SetClass("font-bold break-words")
|
||||
.onClick(() => confirm(preset.tags));
|
||||
.onClick(() => {
|
||||
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data);
|
||||
});
|
||||
|
||||
if (preciseInput !== undefined) {
|
||||
confirmButton = new Combine([preciseInput, confirmButton])
|
||||
}
|
||||
|
||||
|
||||
const openLayerControl =
|
||||
const openLayerControl =
|
||||
new SubtleButton(
|
||||
Svg.layers_ui(),
|
||||
new Combine([
|
||||
|
@ -128,9 +160,9 @@ export default class SimpleAddUI extends Toggle {
|
|||
Translations.t.general.add.openLayerControl
|
||||
])
|
||||
)
|
||||
|
||||
.onClick(() => State.state.layerControlIsOpened.setData(true))
|
||||
|
||||
|
||||
.onClick(() => State.state.layerControlIsOpened.setData(true))
|
||||
|
||||
const openLayerOrConfirm = new Toggle(
|
||||
confirmButton,
|
||||
openLayerControl,
|
||||
|
@ -140,12 +172,12 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
const cancelButton = new SubtleButton(Svg.close_ui(),
|
||||
Translations.t.general.cancel
|
||||
).onClick(cancel )
|
||||
).onClick(cancel)
|
||||
|
||||
return new Combine([
|
||||
Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
|
||||
State.state.osmConnection.userDetails.data.dryRun ?
|
||||
Translations.t.general.testing.Clone().SetClass("alert") : undefined ,
|
||||
State.state.osmConnection.userDetails.data.dryRun ?
|
||||
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
|
||||
openLayerOrConfirm,
|
||||
cancelButton,
|
||||
preset.description,
|
||||
|
@ -180,11 +212,11 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
}
|
||||
|
||||
private static CreatePresetSelectButton(preset: PresetInfo){
|
||||
private static CreatePresetSelectButton(preset: PresetInfo) {
|
||||
|
||||
const tagInfo =SimpleAddUI.CreateTagInfoFor(preset, false);
|
||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, false);
|
||||
return new SubtleButton(
|
||||
preset.icon,
|
||||
preset.icon(),
|
||||
new Combine([
|
||||
Translations.t.general.add.addNew.Subs({
|
||||
category: preset.name
|
||||
|
@ -194,29 +226,30 @@ export default class SimpleAddUI extends Toggle {
|
|||
]).SetClass("flex flex-col")
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Generates the list with all the buttons.*/
|
||||
|
||||
/*
|
||||
* Generates the list with all the buttons.*/
|
||||
private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
|
||||
const allButtons = [];
|
||||
for (const layer of State.state.filteredLayers.data) {
|
||||
|
||||
if(layer.isDisplayed.data === false && State.state.featureSwitchLayers){
|
||||
|
||||
if (layer.isDisplayed.data === false && State.state.featureSwitchLayers) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const presets = layer.layerDef.presets;
|
||||
for (const preset of presets) {
|
||||
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
|
||||
let icon:() => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
|
||||
.SetClass("w-12 h-12 block relative");
|
||||
const presetInfo: PresetInfo = {
|
||||
tags: preset.tags,
|
||||
layerToAddTo: layer,
|
||||
name: preset.title,
|
||||
description: preset.description,
|
||||
icon: icon
|
||||
icon: icon,
|
||||
preciseInput: preset.preciseInput
|
||||
}
|
||||
|
||||
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
|
||||
|
|
|
@ -66,6 +66,7 @@ export default class DirectionInput extends InputElement<string> {
|
|||
})
|
||||
|
||||
this.RegisterTriggers(element)
|
||||
element.style.overflow = "hidden"
|
||||
|
||||
return element;
|
||||
}
|
||||
|
|
35
UI/Input/InputElementWrapper.ts
Normal file
35
UI/Input/InputElementWrapper.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {SubstitutedTranslation} from "../SubstitutedTranslation";
|
||||
|
||||
export default class InputElementWrapper<T> extends InputElement<T> {
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
private readonly _inputElement: InputElement<T>;
|
||||
private readonly _renderElement: BaseUIElement
|
||||
|
||||
constructor(inputElement: InputElement<T>, translation: Translation, key: string, tags: UIEventSource<any>) {
|
||||
super()
|
||||
this._inputElement = inputElement;
|
||||
this.IsSelected = inputElement.IsSelected
|
||||
const mapping = new Map<string, BaseUIElement>()
|
||||
|
||||
mapping.set(key, inputElement)
|
||||
|
||||
this._renderElement = new SubstitutedTranslation(translation, tags, mapping)
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this._inputElement.GetValue();
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this._inputElement.IsValid(t);
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._renderElement.ConstructElement();
|
||||
}
|
||||
|
||||
}
|
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
76
UI/Input/LocationInput.ts
Normal file
76
UI/Input/LocationInput.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {InputElement} from "./InputElement";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Minimap from "../Base/Minimap";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import Combine from "../Base/Combine";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
|
||||
export default class LocationInput extends InputElement<Loc> {
|
||||
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private _centerLocation: UIEventSource<Loc>;
|
||||
private readonly mapBackground : UIEventSource<BaseLayer>;
|
||||
|
||||
constructor(options?: {
|
||||
mapBackground?: UIEventSource<BaseLayer>,
|
||||
centerLocation?: UIEventSource<Loc>,
|
||||
}) {
|
||||
super();
|
||||
options = options ?? {}
|
||||
options.centerLocation = options.centerLocation ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
|
||||
this._centerLocation = options.centerLocation;
|
||||
|
||||
this.mapBackground = options.mapBackground ?? State.state.backgroundLayer
|
||||
this.SetClass("block h-full")
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<Loc> {
|
||||
return this._centerLocation;
|
||||
}
|
||||
|
||||
IsValid(t: Loc): boolean {
|
||||
return t !== undefined;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const map = new Minimap(
|
||||
{
|
||||
location: this._centerLocation,
|
||||
background: this.mapBackground
|
||||
}
|
||||
)
|
||||
map.leafletMap.addCallbackAndRunD(leaflet => {
|
||||
console.log(leaflet.getBounds(), leaflet.getBounds().pad(0.15))
|
||||
leaflet.setMaxBounds(
|
||||
leaflet.getBounds().pad(0.15)
|
||||
)
|
||||
})
|
||||
|
||||
this.mapBackground.map(layer => {
|
||||
|
||||
const leaflet = map.leafletMap.data
|
||||
if (leaflet === undefined || layer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
leaflet.setMaxZoom(layer.max_zoom)
|
||||
leaflet.setMinZoom(layer.max_zoom - 3)
|
||||
leaflet.setZoom(layer.max_zoom - 1)
|
||||
|
||||
}, [map.leafletMap])
|
||||
return new Combine([
|
||||
new Combine([
|
||||
Svg.crosshair_empty_ui()
|
||||
.SetClass("block relative")
|
||||
.SetStyle("left: -1.25rem; top: -1.25rem; width: 2.5rem; height: 2.5rem")
|
||||
]).SetClass("block w-0 h-0 z-10 relative")
|
||||
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"),
|
||||
map
|
||||
.SetClass("z-0 relative block w-full h-full bg-gray-100")
|
||||
|
||||
]).ConstructElement();
|
||||
}
|
||||
|
||||
}
|
|
@ -103,7 +103,7 @@ export class RadioButton<T> extends InputElement<T> {
|
|||
const block = document.createElement("div")
|
||||
block.appendChild(input)
|
||||
block.appendChild(label)
|
||||
block.classList.add("flex","w-full","border", "rounded-full", "border-gray-400","m-1")
|
||||
block.classList.add("flex","w-full","border", "rounded-3xl", "border-gray-400","m-1")
|
||||
wrappers.push(block)
|
||||
|
||||
form.appendChild(block)
|
||||
|
|
|
@ -36,11 +36,11 @@ export class TextField extends InputElement<string> {
|
|||
this.SetClass("form-text-field")
|
||||
let inputEl: HTMLElement
|
||||
if (options.htmlType === "area") {
|
||||
this.SetClass("w-full box-border max-w-full")
|
||||
const el = document.createElement("textarea")
|
||||
el.placeholder = placeholder
|
||||
el.rows = options.textAreaRows
|
||||
el.cols = 50
|
||||
el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box"
|
||||
inputEl = el;
|
||||
} else {
|
||||
const el = document.createElement("input")
|
||||
|
|
|
@ -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,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<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(
|
||||
"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<Loc>({
|
||||
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<string>,
|
||||
|
@ -233,7 +299,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 +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
|
||||
|
@ -282,23 +350,22 @@ export default class ValidatedTextField {
|
|||
})
|
||||
)
|
||||
unitDropDown.GetValue().setData(unit.defaultDenom)
|
||||
unitDropDown.SetStyle("width: min-content")
|
||||
unitDropDown.SetClass("w-min")
|
||||
|
||||
input = new CombinedInputElement(
|
||||
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<string>, options?: {
|
||||
location: [number, number],
|
||||
mapBackgroundLayer: UIEventSource<any>
|
||||
mapBackgroundLayer: UIEventSource<any>,
|
||||
args: string[],
|
||||
feature: any
|
||||
}) => InputElement<string>,
|
||||
inputmode?: string): TextFieldDef {
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
.SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2");
|
||||
const titleIcons = new Combine(
|
||||
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
|
||||
"block w-8 h-8 align-baseline box-content sm:p-0.5")
|
||||
"block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem;")
|
||||
))
|
||||
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
|
||||
|
||||
|
|
|
@ -16,31 +16,31 @@ export default class TagRenderingAnswer extends VariableUiElement {
|
|||
throw "Trying to generate a tagRenderingAnswer without configuration..."
|
||||
}
|
||||
super(tagsSource.map(tags => {
|
||||
if(tags === undefined){
|
||||
if (tags === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(configuration.condition){
|
||||
if(!configuration.condition.matchesProperties(tags)){
|
||||
|
||||
if (configuration.condition) {
|
||||
if (!configuration.condition.matchesProperties(tags)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const trs = Utils.NoNull(configuration.GetRenderValues(tags));
|
||||
if(trs.length === 0){
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource))
|
||||
if(valuesToRender.length === 1){
|
||||
return valuesToRender[0];
|
||||
}else if(valuesToRender.length > 1){
|
||||
return new List(valuesToRender)
|
||||
}
|
||||
return undefined;
|
||||
}).map((element : BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle)))
|
||||
|
||||
this.SetClass("flex items-center flex-row text-lg link-underline tag-renering-answer")
|
||||
const trs = Utils.NoNull(configuration.GetRenderValues(tags));
|
||||
if (trs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource))
|
||||
if (valuesToRender.length === 1) {
|
||||
return valuesToRender[0];
|
||||
} else if (valuesToRender.length > 1) {
|
||||
return new List(valuesToRender)
|
||||
}
|
||||
return undefined;
|
||||
}).map((element: BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle)))
|
||||
|
||||
this.SetClass("flex items-center flex-row text-lg link-underline")
|
||||
this.SetStyle("word-wrap: anywhere;");
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||
import InputElementWrapper from "../Input/InputElementWrapper";
|
||||
|
||||
/**
|
||||
* Shows the question element.
|
||||
|
@ -128,7 +129,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
}
|
||||
return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined: m.ifnot))
|
||||
}
|
||||
const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource.data);
|
||||
const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource);
|
||||
const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0
|
||||
|
||||
if (mappings.length < 8 || configuration.multiAnswer || hasImages) {
|
||||
|
@ -289,7 +290,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
(t0, t1) => t1.isEquivalent(t0));
|
||||
}
|
||||
|
||||
private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tagsData: any): InputElement<TagsFilter> {
|
||||
private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>): InputElement<TagsFilter> {
|
||||
const freeform = configuration.freeform;
|
||||
if (freeform === undefined) {
|
||||
return undefined;
|
||||
|
@ -328,20 +329,34 @@ export default class TagRenderingQuestion extends Combine {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
let input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
|
||||
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[configuration.freeform.key]);
|
||||
input.GetValue().setData(tagsData[freeform.key] ?? freeform.default);
|
||||
|
||||
return new InputElementMap(
|
||||
let inputTagsFilter : InputElement<TagsFilter> = new InputElementMap(
|
||||
input, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
|
||||
pickString, toString
|
||||
);
|
||||
|
||||
if(freeform.inline){
|
||||
|
||||
inputTagsFilter.SetClass("w-16-imp")
|
||||
inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags)
|
||||
inputTagsFilter.SetClass("block")
|
||||
|
||||
}
|
||||
|
||||
return inputTagsFilter;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -80,9 +80,7 @@ export default class ShowDataLayer {
|
|||
|
||||
if (zoomToFeatures) {
|
||||
try {
|
||||
|
||||
mp.fitBounds(geoLayer.getBounds())
|
||||
|
||||
mp.fitBounds(geoLayer.getBounds(), {animate: false})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
@ -148,7 +146,9 @@ export default class ShowDataLayer {
|
|||
const popup = L.popup({
|
||||
autoPan: true,
|
||||
closeOnEscapeKey: true,
|
||||
closeButton: false
|
||||
closeButton: false,
|
||||
autoPanPaddingTopLeft: [15,15],
|
||||
|
||||
}, leafletLayer);
|
||||
|
||||
leafletLayer.bindPopup(popup);
|
||||
|
|
|
@ -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[] =
|
||||
|
@ -369,7 +370,6 @@ export default class SpecialVisualizations {
|
|||
if (unit === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return unit.asHumanLongValue(value);
|
||||
|
||||
},
|
||||
|
@ -379,6 +379,7 @@ export default class SpecialVisualizations {
|
|||
}
|
||||
|
||||
]
|
||||
|
||||
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
|
||||
private static GenHelpMessage() {
|
||||
|
||||
|
|
|
@ -7,19 +7,43 @@ import SpecialVisualizations, {SpecialVisualization} from "./SpecialVisualizatio
|
|||
import {Utils} from "../Utils";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import Combine from "./Base/Combine";
|
||||
import BaseUIElement from "./BaseUIElement";
|
||||
|
||||
export class SubstitutedTranslation extends VariableUiElement {
|
||||
|
||||
public constructor(
|
||||
translation: Translation,
|
||||
tagsSource: UIEventSource<any>) {
|
||||
tagsSource: UIEventSource<any>,
|
||||
mapping: Map<string, BaseUIElement> = undefined) {
|
||||
|
||||
const extraMappings: SpecialVisualization[] = [];
|
||||
|
||||
mapping?.forEach((value, key) => {
|
||||
console.log("KV:", key, value)
|
||||
extraMappings.push(
|
||||
{
|
||||
funcName: key,
|
||||
constr: (() => {
|
||||
return value
|
||||
}),
|
||||
docs: "Dynamically injected input element",
|
||||
args: [],
|
||||
example: ""
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
super(
|
||||
Locale.language.map(language => {
|
||||
const txt = translation.textFor(language)
|
||||
let txt = translation.textFor(language);
|
||||
if (txt === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt).map(
|
||||
mapping?.forEach((_, key) => {
|
||||
txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`)
|
||||
})
|
||||
|
||||
return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map(
|
||||
proto => {
|
||||
if (proto.fixed !== undefined) {
|
||||
return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags)));
|
||||
|
@ -36,30 +60,35 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
})
|
||||
)
|
||||
|
||||
|
||||
this.SetClass("w-full")
|
||||
}
|
||||
|
||||
|
||||
public static ExtractSpecialComponents(template: string): {
|
||||
fixed?: string, special?: {
|
||||
public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): {
|
||||
fixed?: string,
|
||||
special?: {
|
||||
func: SpecialVisualization,
|
||||
args: string[],
|
||||
style: string
|
||||
}
|
||||
}[] {
|
||||
|
||||
for (const knownSpecial of SpecialVisualizations.specialVisualizations) {
|
||||
if (extraMappings.length > 0) {
|
||||
|
||||
console.log("Extra mappings are", extraMappings)
|
||||
}
|
||||
|
||||
for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) {
|
||||
|
||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`);
|
||||
if (matched != null) {
|
||||
|
||||
// We found a special component that should be brought to live
|
||||
const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1]);
|
||||
const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1], extraMappings);
|
||||
const argument = matched[2].trim();
|
||||
const style = matched[3]?.substring(1) ?? ""
|
||||
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4]);
|
||||
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings);
|
||||
const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
|
||||
if (argument.length > 0) {
|
||||
const realArgs = argument.split(",").map(str => str.trim());
|
||||
|
@ -73,11 +102,13 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
}
|
||||
|
||||
let element;
|
||||
element = {special:{
|
||||
args: args,
|
||||
style: style,
|
||||
func: knownSpecial
|
||||
}}
|
||||
element = {
|
||||
special: {
|
||||
args: args,
|
||||
style: style,
|
||||
func: knownSpecial
|
||||
}
|
||||
}
|
||||
return [...partBefore, element, ...partAfter]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue