forked from MapComplete/MapComplete
refactoring: fix basic flow to add a new point
This commit is contained in:
parent
52a0810ea9
commit
0241f89d3d
109 changed files with 1931 additions and 1446 deletions
|
@ -1,75 +0,0 @@
|
|||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import Svg from "../../Svg"
|
||||
|
||||
/**
|
||||
* The icon with the 'plus'-sign and the preset icons spinning
|
||||
*
|
||||
*/
|
||||
export default class AddNewMarker extends Combine {
|
||||
constructor(filteredLayers: UIEventSource<FilteredLayer[]>) {
|
||||
const icons = new VariableUiElement(
|
||||
filteredLayers.map((filteredLayers) => {
|
||||
const icons = []
|
||||
let last = undefined
|
||||
for (const filteredLayer of filteredLayers) {
|
||||
const layer = filteredLayer.layerDef
|
||||
if (layer.name === undefined && !filteredLayer.isDisplayed.data) {
|
||||
continue
|
||||
}
|
||||
for (const preset of filteredLayer.layerDef.presets) {
|
||||
const tags = TagUtils.KVtoProperties(preset.tags)
|
||||
const icon = layer.mapRendering[0]
|
||||
.RenderIcon(new ImmutableStore<any>(tags), false)
|
||||
.html.SetClass("block relative")
|
||||
.SetStyle("width: 42px; height: 42px;")
|
||||
icons.push(icon)
|
||||
if (last === undefined) {
|
||||
last = layer.mapRendering[0]
|
||||
.RenderIcon(new ImmutableStore<any>(tags), false)
|
||||
.html.SetClass("block relative")
|
||||
.SetStyle("width: 42px; height: 42px;")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (icons.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
if (icons.length === 1) {
|
||||
return icons[0]
|
||||
}
|
||||
icons.push(last)
|
||||
const elem = new Combine(icons).SetClass("flex")
|
||||
elem.SetClass("slide min-w-min").SetStyle(
|
||||
"animation: slide " + icons.length + "s linear infinite;"
|
||||
)
|
||||
return elem
|
||||
})
|
||||
)
|
||||
const label = Translations.t.general.add.addNewMapLabel
|
||||
.Clone()
|
||||
.SetClass(
|
||||
"block center absolute text-sm min-w-min pl-1 pr-1 bg-gray-400 rounded-3xl text-white opacity-65 whitespace-nowrap"
|
||||
)
|
||||
.SetStyle("top: 65px; transform: translateX(-50%)")
|
||||
super([
|
||||
new Combine([
|
||||
Svg.add_pin_svg()
|
||||
.SetClass("absolute")
|
||||
.SetStyle("width: 50px; filter: drop-shadow(grey 0 0 10px"),
|
||||
new Combine([icons])
|
||||
.SetStyle("width: 50px")
|
||||
.SetClass("absolute p-1 rounded-full overflow-hidden"),
|
||||
Svg.addSmall_svg()
|
||||
.SetClass("absolute animate-pulse")
|
||||
.SetStyle("width: 30px; left: 30px; top: 35px;"),
|
||||
]).SetClass("absolute"),
|
||||
new Combine([label]).SetStyle("position: absolute; left: 50%"),
|
||||
])
|
||||
this.SetClass("block relative")
|
||||
}
|
||||
}
|
|
@ -1,223 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import Svg from "../../Svg"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
class SingleLayerSelectionButton extends Toggle {
|
||||
public readonly activate: () => void
|
||||
|
||||
/**
|
||||
*
|
||||
* The SingeLayerSelectionButton also acts as an actor to keep the layers in check
|
||||
*
|
||||
* It works the following way:
|
||||
*
|
||||
* - It has a boolean state to indicate wether or not the button is active
|
||||
* - It keeps track of the available layers
|
||||
*/
|
||||
constructor(
|
||||
locationControl: UIEventSource<Loc>,
|
||||
options: {
|
||||
currentBackground: UIEventSource<BaseLayer>
|
||||
preferredType: string
|
||||
preferredLayer?: BaseLayer
|
||||
notAvailable?: () => void
|
||||
}
|
||||
) {
|
||||
const prefered = options.preferredType
|
||||
const previousLayer = new UIEventSource(options.preferredLayer)
|
||||
|
||||
const unselected = SingleLayerSelectionButton.getIconFor(prefered).SetClass(
|
||||
"rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-invisible"
|
||||
)
|
||||
|
||||
const selected = SingleLayerSelectionButton.getIconFor(prefered).SetClass(
|
||||
"rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-attention-catch"
|
||||
)
|
||||
|
||||
const available = AvailableBaseLayers.SelectBestLayerAccordingTo(
|
||||
locationControl,
|
||||
new UIEventSource<string | string[]>(options.preferredType)
|
||||
)
|
||||
|
||||
let toggle: BaseUIElement = new Toggle(
|
||||
selected,
|
||||
unselected,
|
||||
options.currentBackground.map((bg) => bg?.category === options.preferredType)
|
||||
)
|
||||
|
||||
super(
|
||||
toggle,
|
||||
undefined,
|
||||
available.map((av) => av?.category === options.preferredType)
|
||||
)
|
||||
|
||||
/**
|
||||
* Checks that the previous layer is still usable on the current location.
|
||||
* If not, clears the 'previousLayer'
|
||||
*/
|
||||
function checkPreviousLayer() {
|
||||
if (previousLayer.data === undefined) {
|
||||
return
|
||||
}
|
||||
if (previousLayer.data.feature === null || previousLayer.data.feature === undefined) {
|
||||
// Global layer
|
||||
return
|
||||
}
|
||||
const loc = locationControl.data
|
||||
if (!GeoOperations.inside([loc.lon, loc.lat], previousLayer.data.feature)) {
|
||||
// The previous layer is out of bounds
|
||||
previousLayer.setData(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
unselected.onClick(() => {
|
||||
// Note: a check if 'available' has the correct type is not needed:
|
||||
// Unselected will _not_ be visible if availableBaseLayer has a wrong type!
|
||||
checkPreviousLayer()
|
||||
|
||||
previousLayer.setData(previousLayer.data ?? available.data)
|
||||
options.currentBackground.setData(previousLayer.data)
|
||||
})
|
||||
|
||||
options.currentBackground.addCallbackAndRunD((background) => {
|
||||
if (background.category === options.preferredType) {
|
||||
previousLayer.setData(background)
|
||||
}
|
||||
})
|
||||
|
||||
available.addCallbackD((availableLayer) => {
|
||||
// Called whenever a better layer is available
|
||||
|
||||
if (previousLayer.data === undefined) {
|
||||
// PreviousLayer is unset -> we definitively weren't using this category -> no need to switch
|
||||
return
|
||||
}
|
||||
if (options.currentBackground.data?.id !== previousLayer.data?.id) {
|
||||
// The previously used layer doesn't match the current layer -> no need to switch
|
||||
return
|
||||
}
|
||||
|
||||
// Is the previous layer still valid? If so, we don't bother to switch
|
||||
if (
|
||||
previousLayer.data.feature === null ||
|
||||
GeoOperations.inside(
|
||||
[locationControl.data.lon, locationControl.data.lat],
|
||||
previousLayer.data.feature
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (availableLayer.category === options.preferredType) {
|
||||
// Allright, we can set this different layer
|
||||
options.currentBackground.setData(availableLayer)
|
||||
previousLayer.setData(availableLayer)
|
||||
} else {
|
||||
// Uh oh - no correct layer is available... We pass the torch!
|
||||
if (options.notAvailable !== undefined) {
|
||||
options.notAvailable()
|
||||
} else {
|
||||
// Fallback to OSM carto
|
||||
options.currentBackground.setData(AvailableBaseLayers.osmCarto)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.activate = () => {
|
||||
checkPreviousLayer()
|
||||
if (available.data.category !== options.preferredType) {
|
||||
// This object can't help either - pass the torch!
|
||||
if (options.notAvailable !== undefined) {
|
||||
options.notAvailable()
|
||||
} else {
|
||||
// Fallback to OSM carto
|
||||
options.currentBackground.setData(AvailableBaseLayers.osmCarto)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
previousLayer.setData(previousLayer.data ?? available.data)
|
||||
options.currentBackground.setData(previousLayer.data)
|
||||
}
|
||||
}
|
||||
|
||||
private static getIconFor(type: string) {
|
||||
switch (type) {
|
||||
case "map":
|
||||
return Svg.generic_map_svg()
|
||||
case "photo":
|
||||
return Svg.satellite_svg()
|
||||
case "osmbasedmap":
|
||||
return Svg.osm_logo_svg()
|
||||
default:
|
||||
return Svg.generic_map_svg()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class BackgroundMapSwitch extends Combine {
|
||||
/**
|
||||
* Three buttons to easily switch map layers between OSM, aerial and some map.
|
||||
* @param state
|
||||
* @param currentBackground
|
||||
* @param options
|
||||
*/
|
||||
constructor(
|
||||
state: {
|
||||
locationControl: UIEventSource<Loc>
|
||||
backgroundLayer: UIEventSource<BaseLayer>
|
||||
},
|
||||
currentBackground: UIEventSource<BaseLayer>,
|
||||
options?: {
|
||||
preferredCategory?: string
|
||||
allowedCategories?: ("osmbasedmap" | "photo" | "map")[]
|
||||
enableHotkeys?: boolean
|
||||
}
|
||||
) {
|
||||
const allowedCategories = options?.allowedCategories ?? ["osmbasedmap", "photo", "map"]
|
||||
|
||||
const previousLayer = state.backgroundLayer.data
|
||||
const buttons = []
|
||||
let activatePrevious: () => void = undefined
|
||||
for (const category of allowedCategories) {
|
||||
let preferredLayer = undefined
|
||||
if (previousLayer?.category === category) {
|
||||
preferredLayer = previousLayer
|
||||
}
|
||||
|
||||
const button = new SingleLayerSelectionButton(state.locationControl, {
|
||||
preferredType: category,
|
||||
preferredLayer: preferredLayer,
|
||||
currentBackground: currentBackground,
|
||||
notAvailable: activatePrevious,
|
||||
})
|
||||
// Fall back to the first option: OSM
|
||||
activatePrevious = activatePrevious ?? button.activate
|
||||
if (category === options?.preferredCategory) {
|
||||
button.activate()
|
||||
}
|
||||
|
||||
if (options?.enableHotkeys) {
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: category.charAt(0).toUpperCase() },
|
||||
Translations.t.hotkeyDocumentation.selectBackground.Subs({ category }),
|
||||
() => {
|
||||
button.activate()
|
||||
}
|
||||
)
|
||||
}
|
||||
buttons.push(button)
|
||||
}
|
||||
|
||||
// Selects the initial map
|
||||
|
||||
super(buttons)
|
||||
this.SetClass("flex")
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import Combine from "../Base/Combine"
|
||||
|
@ -6,18 +5,9 @@ import Translations from "../i18n/Translations"
|
|||
import { Translation } from "../i18n/Translation"
|
||||
import Svg from "../../Svg"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
|
||||
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
||||
import ValidatedTextField from "../Input/ValidatedTextField"
|
||||
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import { InputElement } from "../Input/InputElement"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { BackToThemeOverview } from "./ActionButtons"
|
||||
|
||||
export default class FilterView extends VariableUiElement {
|
||||
constructor(
|
||||
|
@ -31,11 +21,6 @@ export default class FilterView extends VariableUiElement {
|
|||
readonly featureSwitchMoreQuests: Store<boolean>
|
||||
}
|
||||
) {
|
||||
const backgroundSelector = new Toggle(
|
||||
new BackgroundSelector(state),
|
||||
undefined,
|
||||
state.featureSwitchBackgroundSelection ?? new ImmutableStore(false)
|
||||
)
|
||||
super(
|
||||
filteredLayer.map((filteredLayers) => {
|
||||
// Create the views which toggle layers (and filters them) ...
|
||||
|
@ -51,10 +36,6 @@ export default class FilterView extends VariableUiElement {
|
|||
tileLayers.map((tl) => FilterView.createOverlayToggle(state, tl))
|
||||
)
|
||||
|
||||
elements.push(
|
||||
backgroundSelector,
|
||||
new BackToThemeOverview(state, { imgSize: "h-6 w-6" }).SetClass("block mt-12")
|
||||
)
|
||||
return elements
|
||||
})
|
||||
)
|
||||
|
@ -73,17 +54,8 @@ export default class FilterView extends VariableUiElement {
|
|||
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2")
|
||||
const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2")
|
||||
|
||||
const zoomStatus = new Toggle(
|
||||
undefined,
|
||||
Translations.t.general.layerSelection.zoomInToSeeThisLayer
|
||||
.SetClass("alert")
|
||||
.SetStyle("display: block ruby;width:min-content;"),
|
||||
state.locationControl?.map((location) => location.zoom >= config.config.minzoom) ??
|
||||
new ImmutableStore(false)
|
||||
)
|
||||
|
||||
const style = "display:flex;align-items:center;padding:0.5rem 0;"
|
||||
const layerChecked = new Combine([icon, styledNameChecked, zoomStatus])
|
||||
const layerChecked = new Combine([icon, styledNameChecked])
|
||||
.SetStyle(style)
|
||||
.onClick(() => config.isDisplayed.setData(false))
|
||||
|
||||
|
@ -93,188 +65,4 @@ export default class FilterView extends VariableUiElement {
|
|||
|
||||
return new Toggle(layerChecked, layerNotChecked, config.isDisplayed)
|
||||
}
|
||||
|
||||
private static createOneFilteredLayerElement(
|
||||
filteredLayer: FilteredLayer,
|
||||
state: { featureSwitchIsDebugging?: Store<boolean>; locationControl?: Store<Loc> }
|
||||
) {
|
||||
if (filteredLayer.layerDef.name === undefined) {
|
||||
// Name is not defined: we hide this one
|
||||
return new Toggle(
|
||||
new FixedUiElement(filteredLayer?.layerDef?.id).SetClass("block"),
|
||||
undefined,
|
||||
state?.featureSwitchIsDebugging ?? new ImmutableStore(false)
|
||||
)
|
||||
}
|
||||
const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;"
|
||||
|
||||
const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle)
|
||||
const layer = filteredLayer.layerDef
|
||||
|
||||
const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(iconStyle)
|
||||
|
||||
const name: Translation = filteredLayer.layerDef.name.Clone()
|
||||
|
||||
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3")
|
||||
|
||||
const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3")
|
||||
|
||||
const zoomStatus = new Toggle(
|
||||
undefined,
|
||||
Translations.t.general.layerSelection.zoomInToSeeThisLayer
|
||||
.SetClass("alert")
|
||||
.SetStyle("display: block ruby;width:min-content;"),
|
||||
state?.locationControl?.map(
|
||||
(location) => location.zoom >= filteredLayer.layerDef.minzoom
|
||||
) ?? new ImmutableStore(false)
|
||||
)
|
||||
|
||||
const toggleClasses = "layer-toggle flex flex-wrap items-center pt-2 pb-2 px-0"
|
||||
const layerIcon = layer.defaultIcon()?.SetClass("flex-shrink-0 w-8 h-8 ml-2")
|
||||
const layerIconUnchecked = layer
|
||||
.defaultIcon()
|
||||
?.SetClass("flex-shrink-0 opacity-50 w-8 h-8 ml-2")
|
||||
|
||||
const layerChecked = new Combine([icon, layerIcon, styledNameChecked, zoomStatus])
|
||||
.SetClass(toggleClasses)
|
||||
.onClick(() => filteredLayer.isDisplayed.setData(false))
|
||||
|
||||
const layerNotChecked = new Combine([
|
||||
iconUnselected,
|
||||
layerIconUnchecked,
|
||||
styledNameUnChecked,
|
||||
])
|
||||
.SetClass(toggleClasses)
|
||||
.onClick(() => filteredLayer.isDisplayed.setData(true))
|
||||
|
||||
const filterPanel: BaseUIElement = new LayerFilterPanel(state, filteredLayer)
|
||||
|
||||
return new Toggle(
|
||||
new Combine([layerChecked, filterPanel]),
|
||||
layerNotChecked,
|
||||
filteredLayer.isDisplayed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class LayerFilterPanel extends Combine {
|
||||
public constructor(state: any, flayer: FilteredLayer) {
|
||||
const layer = flayer.layerDef
|
||||
if (layer.filters.length === 0) {
|
||||
super([])
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toShow: BaseUIElement[] = []
|
||||
|
||||
for (const filter of layer.filters) {
|
||||
const [ui, actualTags] = LayerFilterPanel.createFilter(state, filter)
|
||||
|
||||
ui.SetClass("mt-1")
|
||||
toShow.push(ui)
|
||||
actualTags.addCallbackAndRun((tagsToFilterFor) => {
|
||||
flayer.appliedFilters.data.set(filter.id, tagsToFilterFor)
|
||||
flayer.appliedFilters.ping()
|
||||
})
|
||||
flayer.appliedFilters
|
||||
.map((dict) => dict.get(filter.id))
|
||||
.addCallbackAndRun((filters) => actualTags.setData(filters))
|
||||
}
|
||||
|
||||
super(toShow)
|
||||
this.SetClass("flex flex-col p-2 ml-12 pl-1 pt-0 layer-filters")
|
||||
}
|
||||
|
||||
// Filter which uses one or more textfields
|
||||
private static createFilterWithFields(
|
||||
state: any,
|
||||
filterConfig: FilterConfig
|
||||
): [BaseUIElement, UIEventSource<FilterState>] {
|
||||
const filter = filterConfig.options[0]
|
||||
const mappings = new Map<string, BaseUIElement>()
|
||||
let allValid: Store<boolean> = new ImmutableStore(true)
|
||||
var allFields: InputElement<string>[] = []
|
||||
const properties = new UIEventSource<any>({})
|
||||
for (const { name, type } of filter.fields) {
|
||||
const value = QueryParameters.GetQueryParameter(
|
||||
"filter-" + filterConfig.id + "-" + name,
|
||||
"",
|
||||
"Value for filter " + filterConfig.id
|
||||
)
|
||||
|
||||
const field = ValidatedTextField.ForType(type)
|
||||
.ConstructInputElement({
|
||||
value,
|
||||
})
|
||||
.SetClass("inline-block")
|
||||
mappings.set(name, field)
|
||||
const stable = value.stabilized(250)
|
||||
stable.addCallbackAndRunD((v) => {
|
||||
properties.data[name] = v.toLowerCase()
|
||||
properties.ping()
|
||||
})
|
||||
allFields.push(field)
|
||||
allValid = allValid.map(
|
||||
(previous) => previous && field.IsValid(stable.data) && stable.data !== "",
|
||||
[stable]
|
||||
)
|
||||
}
|
||||
const tr = new SubstitutedTranslation(
|
||||
filter.question,
|
||||
new UIEventSource<any>({ id: filterConfig.id }),
|
||||
state,
|
||||
mappings
|
||||
)
|
||||
const trigger: Store<FilterState> = allValid.map(
|
||||
(isValid) => {
|
||||
if (!isValid) {
|
||||
return undefined
|
||||
}
|
||||
const props = properties.data
|
||||
// Replace all the field occurences in the tags...
|
||||
const tagsSpec = Utils.WalkJson(filter.originalTagsSpec, (v) => {
|
||||
if (typeof v !== "string") {
|
||||
return v
|
||||
}
|
||||
|
||||
for (const key in props) {
|
||||
v = (<string>v).replace("{" + key + "}", props[key])
|
||||
}
|
||||
|
||||
return v
|
||||
})
|
||||
const tagsFilter = TagUtils.Tag(tagsSpec)
|
||||
return {
|
||||
currentFilter: tagsFilter,
|
||||
state: JSON.stringify(props),
|
||||
}
|
||||
},
|
||||
[properties]
|
||||
)
|
||||
|
||||
const settableFilter = new UIEventSource<FilterState>(undefined)
|
||||
trigger.addCallbackAndRun((state) => settableFilter.setData(state))
|
||||
settableFilter.addCallback((state) => {
|
||||
if (state === undefined) {
|
||||
// still initializing
|
||||
return
|
||||
}
|
||||
if (state.currentFilter === undefined) {
|
||||
allFields.forEach((f) => f.GetValue().setData(undefined))
|
||||
}
|
||||
})
|
||||
|
||||
return [tr, settableFilter]
|
||||
}
|
||||
|
||||
private static createFilter(
|
||||
state: {},
|
||||
filterConfig: FilterConfig
|
||||
): [BaseUIElement, UIEventSource<FilterState>] {
|
||||
if (filterConfig.options[0].fields.length > 0) {
|
||||
return LayerFilterPanel.createFilterWithFields(state, filterConfig)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">/**
|
||||
* The FilterView shows the various options to enable/disable a single layer.
|
||||
* The FilterView shows the various options to enable/disable a single layer or to only show a subset of the data.
|
||||
*/
|
||||
import type FilteredLayer from "../../Models/FilteredLayer";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
|
@ -10,14 +10,19 @@ import type { Writable } from "svelte/store";
|
|||
import If from "../Base/If.svelte";
|
||||
import Dropdown from "../Base/Dropdown.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import FilterviewWithFields from "./FilterviewWithFields.svelte";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export let filteredLayer: FilteredLayer;
|
||||
export let zoomlevel: number;
|
||||
export let highlightedLayer: UIEventSource<string> | undefined;
|
||||
export let zoomlevel: UIEventSource<number>;
|
||||
let layer: LayerConfig = filteredLayer.layerDef;
|
||||
let isDisplayed: boolean = filteredLayer.isDisplayed.data;
|
||||
onDestroy(filteredLayer.isDisplayed.addCallbackAndRunD(d => {
|
||||
isDisplayed = d;
|
||||
return false
|
||||
return false;
|
||||
}));
|
||||
|
||||
/**
|
||||
|
@ -34,9 +39,20 @@ function getBooleanStateFor(option: FilterConfig): Writable<boolean> {
|
|||
function getStateFor(option: FilterConfig): Writable<number> {
|
||||
return filteredLayer.appliedFilters.get(option.id);
|
||||
}
|
||||
|
||||
let mainElem: HTMLElement;
|
||||
$: onDestroy(
|
||||
highlightedLayer.addCallbackAndRun(highlightedLayer => {
|
||||
if (highlightedLayer === filteredLayer.layerDef.id) {
|
||||
mainElem?.classList?.add("glowing-shadow");
|
||||
} else {
|
||||
mainElem?.classList?.remove("glowing-shadow");
|
||||
}
|
||||
})
|
||||
);
|
||||
</script>
|
||||
{#if filteredLayer.layerDef.name}
|
||||
<div>
|
||||
<div bind:this={mainElem}>
|
||||
<label class="flex gap-1">
|
||||
<Checkbox selected={filteredLayer.isDisplayed} />
|
||||
<If condition={filteredLayer.isDisplayed}>
|
||||
|
@ -45,6 +61,13 @@ function getStateFor(option: FilterConfig): Writable<number> {
|
|||
</If>
|
||||
|
||||
{filteredLayer.layerDef.name}
|
||||
|
||||
{#if $zoomlevel < layer.minzoom}
|
||||
<span class="alert">
|
||||
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
</label>
|
||||
<If condition={filteredLayer.isDisplayed}>
|
||||
<div id="subfilters" class="flex flex-col gap-y-1 mb-4 ml-4">
|
||||
|
@ -59,6 +82,12 @@ function getStateFor(option: FilterConfig): Writable<number> {
|
|||
</label>
|
||||
{/if}
|
||||
|
||||
{#if filter.options.length === 1 && filter.options[0].fields.length > 0}
|
||||
<FilterviewWithFields id={filter.id} filteredLayer={filteredLayer}
|
||||
option={filter.options[0]}></FilterviewWithFields>
|
||||
|
||||
{/if}
|
||||
|
||||
{#if filter.options.length > 1}
|
||||
<Dropdown value={getStateFor(filter)}>
|
||||
{#each filter.options as option, i}
|
||||
|
|
57
UI/BigComponents/FilterviewWithFields.svelte
Normal file
57
UI/BigComponents/FilterviewWithFields.svelte
Normal file
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts">
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import type { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig";
|
||||
import Locale from "../i18n/Locale";
|
||||
import ValidatedInput from "../InputElement/ValidatedInput.svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let filteredLayer: FilteredLayer;
|
||||
export let option: FilterConfigOption;
|
||||
export let id: string;
|
||||
let parts: string[];
|
||||
let language = Locale.language;
|
||||
$: {
|
||||
parts = option.question.textFor($language).split("{");
|
||||
}
|
||||
let fieldValues: Record<string, UIEventSource<string>> = {};
|
||||
let fieldTypes: Record<string, string> = {};
|
||||
let appliedFilter = <UIEventSource<string>>filteredLayer.appliedFilters.get(id);
|
||||
let initialState: Record<string, string> = JSON.parse(appliedFilter.data ?? "{}");
|
||||
|
||||
function setFields() {
|
||||
const properties: Record<string, string> = {};
|
||||
for (const key in fieldValues) {
|
||||
const v = fieldValues[key].data;
|
||||
const k = key.substring(0, key.length - 1);
|
||||
if (v === undefined) {
|
||||
properties[k] = undefined;
|
||||
} else {
|
||||
properties[k] = v;
|
||||
}
|
||||
}
|
||||
appliedFilter.setData(FilteredLayer.fieldsToString(properties));
|
||||
}
|
||||
|
||||
for (const field of option.fields) {
|
||||
// A bit of cheating: the 'parts' will have '}' suffixed for fields
|
||||
fieldTypes[field.name + "}"] = field.type;
|
||||
const src = new UIEventSource<string>(initialState[field.name] ?? "");
|
||||
fieldValues[field.name + "}"] = src;
|
||||
onDestroy(src.addCallback(v => {
|
||||
setFields();
|
||||
}));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#each parts as part, i}
|
||||
{#if part.endsWith("}")}
|
||||
<!-- This is a field! -->
|
||||
<ValidatedInput value={fieldValues[part]} type={fieldTypes[part]} />
|
||||
{:else}
|
||||
{part}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
|
@ -15,10 +15,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
|||
import { Utils } from "../../Utils"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import Loc from "../../Models/Loc"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import PrivacyPolicy from "./PrivacyPolicy"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
|
||||
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
||||
|
@ -84,12 +81,6 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
tabs.push({ header: Svg.share_img, content: new ShareScreen(state) })
|
||||
}
|
||||
|
||||
const privacy = {
|
||||
header: Svg.eye_svg(),
|
||||
content: new PrivacyPolicy(),
|
||||
}
|
||||
tabs.push(privacy)
|
||||
|
||||
return tabs
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import type { Feature } from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
|
@ -11,9 +10,9 @@
|
|||
import Hotkeys from "../Base/Hotkeys";
|
||||
import { Geocoding } from "../../Logic/Osm/Geocoding";
|
||||
import { BBox } from "../../Logic/BBox";
|
||||
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
|
||||
Translations.t;
|
||||
export let state: SpecialVisualizationState
|
||||
export let bounds: UIEventSource<BBox>
|
||||
export let selectedElement: UIEventSource<Feature>;
|
||||
export let selectedLayer: UIEventSource<LayerConfig>;
|
||||
|
@ -50,6 +49,7 @@
|
|||
const [lat0, lat1, lon0, lon1] = poi.boundingbox
|
||||
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01))
|
||||
const id = poi.osm_type + "/" + poi.osm_id
|
||||
const perLayer = state.perLayer
|
||||
const layers = Array.from(perLayer.values())
|
||||
for (const layer of layers) {
|
||||
const found = layer.features.data.find(f => f.properties.id === id)
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import MapControlButton from "../MapControlButton"
|
||||
import Svg from "../../Svg"
|
||||
import AllDownloads from "./AllDownloads"
|
||||
import FilterView from "./FilterView"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import BackgroundMapSwitch from "./BackgroundMapSwitch"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
|
||||
export default class LeftControls extends Combine {
|
||||
|
@ -74,32 +69,7 @@ export default class LeftControls extends Combine {
|
|||
)
|
||||
)
|
||||
|
||||
new ScrollableFullScreen(
|
||||
() => Translations.t.general.layerSelection.title.Clone(),
|
||||
() =>
|
||||
new FilterView(state.filteredLayers, state.overlayToggles, state).SetClass(
|
||||
"block p-1"
|
||||
),
|
||||
"filters",
|
||||
guiState.filterViewIsOpened
|
||||
)
|
||||
state.featureSwitchFilter.addCallbackAndRun((f) => {
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "B" },
|
||||
Translations.t.hotkeyDocumentation.openLayersPanel,
|
||||
() => {
|
||||
guiState.filterViewIsOpened.setData(!guiState.filterViewIsOpened.data)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const mapSwitch = new Toggle(
|
||||
new BackgroundMapSwitch(state, state.backgroundLayer, { enableHotkeys: true }),
|
||||
undefined,
|
||||
state.featureSwitchBackgroundSelection
|
||||
)
|
||||
|
||||
super([currentViewAction, filterButton, downloadButton, mapSwitch])
|
||||
super([currentViewAction, downloadButton])
|
||||
|
||||
this.SetClass("flex flex-col")
|
||||
}
|
||||
|
|
100
UI/BigComponents/NewPointLocationInput.svelte
Normal file
100
UI/BigComponents/NewPointLocationInput.svelte
Normal file
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import LocationInput from "../InputElement/Helpers/LocationInput.svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { Tiles } from "../../Models/TileRange";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import { BBox } from "../../Logic/BBox";
|
||||
import type { MapProperties } from "../../Models/MapProperties";
|
||||
import ShowDataLayer from "../Map/ShowDataLayer";
|
||||
import type { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource";
|
||||
|
||||
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource";
|
||||
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import { Utils } from "../../Utils";
|
||||
|
||||
/**
|
||||
* An advanced location input, which has support to:
|
||||
* - Show more layers
|
||||
* - Snap to layers
|
||||
*
|
||||
* This one is mostly used to insert new points
|
||||
*/
|
||||
export let state: SpecialVisualizationState;
|
||||
/**
|
||||
* The start coordinate
|
||||
*/
|
||||
export let coordinate: { lon: number, lat: number };
|
||||
export let snapToLayers: string[] | undefined;
|
||||
export let targetLayer: LayerConfig;
|
||||
export let maxSnapDistance: number = undefined;
|
||||
|
||||
export let snappedTo: UIEventSource<string | undefined>;
|
||||
export let value: UIEventSource<{ lon: number, lat: number }>;
|
||||
if (value.data === undefined) {
|
||||
value.setData(coordinate);
|
||||
}
|
||||
|
||||
let preciseLocation: UIEventSource<{ lon: number, lat: number }> = new UIEventSource<{ lon: number; lat: number }>(coordinate);
|
||||
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16);
|
||||
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let initialMapProperties: Partial<MapProperties> = {
|
||||
zoom: new UIEventSource<number>(19),
|
||||
maxbounds: new UIEventSource(undefined),
|
||||
/*If no snapping needed: the value is simply the map location;
|
||||
* If snapping is needed: the value will be set later on by the snapping feature source
|
||||
* */
|
||||
location: snapToLayers.length === 0 ? value : new UIEventSource<{ lon: number; lat: number }>(coordinate),
|
||||
bounds: new UIEventSource<BBox>(undefined),
|
||||
allowMoving: new UIEventSource<boolean>(true),
|
||||
allowZooming: new UIEventSource<boolean>(true),
|
||||
minzoom: new UIEventSource<number>(18)
|
||||
};
|
||||
|
||||
initialMapProperties.bounds.addCallbackAndRunD((bounds: BBox) => {
|
||||
const max = bounds.pad(3).squarify();
|
||||
initialMapProperties.maxbounds.setData(max);
|
||||
return true; // unregister
|
||||
});
|
||||
|
||||
if (snapToLayers?.length > 0) {
|
||||
|
||||
const snapSources: FeatureSource[] = [];
|
||||
for (const layerId of (snapToLayers ?? [])) {
|
||||
const layer: FeatureSourceForLayer = state.perLayer.get(layerId);
|
||||
snapSources.push(layer);
|
||||
if (layer.features === undefined) {
|
||||
continue;
|
||||
}
|
||||
new ShowDataLayer(map, {
|
||||
layer: layer.layer.layerDef,
|
||||
zoomToFeatures: false,
|
||||
features: layer
|
||||
});
|
||||
}
|
||||
const snappedLocation = new SnappingFeatureSource(
|
||||
new FeatureSourceMerger(...Utils.NoNull(snapSources)),
|
||||
// We snap to the (constantly updating) map location
|
||||
initialMapProperties.location,
|
||||
{
|
||||
maxDistance: maxSnapDistance ?? 15,
|
||||
allowUnsnapped: true,
|
||||
snappedTo,
|
||||
snapLocation: value
|
||||
}
|
||||
);
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
features: snappedLocation
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="w-full h-64">
|
||||
<LocationInput {map} mapProperties={initialMapProperties}
|
||||
value={preciseLocation}></LocationInput>
|
||||
</div>
|
|
@ -9,21 +9,16 @@ import BaseUIElement from "../BaseUIElement"
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Loc from "../../Models/Loc"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { InputElement } from "../Input/InputElement"
|
||||
import { CheckBox } from "../Input/Checkboxes"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import LZString from "lz-string"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export default class ShareScreen extends Combine {
|
||||
constructor(state: {
|
||||
layoutToUse: LayoutConfig
|
||||
locationControl: UIEventSource<Loc>
|
||||
backgroundLayer: UIEventSource<BaseLayer>
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
}) {
|
||||
const layout = state?.layoutToUse
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
const layout = state?.layout
|
||||
const tr = Translations.t.general.sharescreen
|
||||
|
||||
const optionCheckboxes: InputElement<boolean>[] = []
|
||||
|
@ -32,7 +27,8 @@ export default class ShareScreen extends Combine {
|
|||
const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true)
|
||||
optionCheckboxes.push(includeLocation)
|
||||
|
||||
const currentLocation = state.locationControl
|
||||
const currentLocation = state.mapProperties.location
|
||||
const zoom = state.mapProperties.zoom
|
||||
|
||||
optionParts.push(
|
||||
includeLocation.GetValue().map(
|
||||
|
@ -42,7 +38,7 @@ export default class ShareScreen extends Combine {
|
|||
}
|
||||
if (includeL) {
|
||||
return [
|
||||
["z", currentLocation.data?.zoom],
|
||||
["z", zoom.data],
|
||||
["lat", currentLocation.data?.lat],
|
||||
["lon", currentLocation.data?.lon],
|
||||
]
|
||||
|
@ -53,7 +49,7 @@ export default class ShareScreen extends Combine {
|
|||
return null
|
||||
}
|
||||
},
|
||||
[currentLocation]
|
||||
[currentLocation, zoom]
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -67,8 +63,8 @@ export default class ShareScreen extends Combine {
|
|||
return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
|
||||
}
|
||||
|
||||
const currentLayer: UIEventSource<{ id: string; name: string; layer: any }> =
|
||||
state.backgroundLayer
|
||||
const currentLayer: Store<{ id: string; name: string } | undefined> =
|
||||
state.mapProperties.rasterLayer.map((l) => l?.properties)
|
||||
const currentBackground = new VariableUiElement(
|
||||
currentLayer.map((layer) => {
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" })
|
||||
|
@ -96,7 +92,9 @@ export default class ShareScreen extends Combine {
|
|||
includeLayerChoices.GetValue().map(
|
||||
(includeLayerSelection) => {
|
||||
if (includeLayerSelection) {
|
||||
return Utils.NoNull(state.filteredLayers.data.map(fLayerToParam)).join("&")
|
||||
return Utils.NoNull(
|
||||
state.layerState.filteredLayers.map(fLayerToParam)
|
||||
).join("&")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -1,29 +1,22 @@
|
|||
/**
|
||||
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
|
||||
*/
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Svg from "../../Svg"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Combine from "../Base/Combine"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"
|
||||
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
||||
import PresetConfig from "../../Models/ThemeConfig/PresetConfig"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"
|
||||
import Loading from "../Base/Loading"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
import { WayId } from "../../Models/OsmFeature"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Feature } from "geojson"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
|
@ -40,33 +33,18 @@ export interface PresetInfo extends PresetConfig {
|
|||
boundsFactor?: 0.25 | number
|
||||
}
|
||||
|
||||
export default class SimpleAddUI extends LoginToggle {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class SimpleAddUI extends Toggle {
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
const readYourMessages = new Combine([
|
||||
Translations.t.general.readYourMessages.Clone().SetClass("alert"),
|
||||
new SubtleButton(Svg.envelope_ui(), Translations.t.general.goToInbox, {
|
||||
url: "https://www.openstreetmap.org/messages/inbox",
|
||||
newTab: false,
|
||||
}),
|
||||
])
|
||||
|
||||
const filterViewIsOpened = state.guistate.filterViewIsOpened
|
||||
const takeLocationFrom = state.mapProperties.lastClickLocation
|
||||
const selectedPreset = new UIEventSource<PresetInfo>(undefined)
|
||||
|
||||
takeLocationFrom.addCallback((_) => selectedPreset.setData(undefined))
|
||||
|
||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state)
|
||||
|
||||
async function createNewPoint(
|
||||
tags: Tag[],
|
||||
location: { lat: number; lon: number },
|
||||
snapOntoWay?: OsmWay
|
||||
): Promise<void> {
|
||||
tags.push(new Tag(Tag.newlyCreated.key, new Date().toISOString()))
|
||||
if (snapOntoWay) {
|
||||
tags.push(new Tag("_referencing_ways", "way/" + snapOntoWay.id))
|
||||
}
|
||||
|
@ -86,10 +64,6 @@ export default class SimpleAddUI extends LoginToggle {
|
|||
|
||||
const addUi = new VariableUiElement(
|
||||
selectedPreset.map((preset) => {
|
||||
if (preset === undefined) {
|
||||
return presetsOverview
|
||||
}
|
||||
|
||||
function confirm(
|
||||
tags: any[],
|
||||
location: { lat: number; lon: number },
|
||||
|
@ -113,7 +87,7 @@ export default class SimpleAddUI extends LoginToggle {
|
|||
{ category: preset.name },
|
||||
preset.name["context"]
|
||||
)
|
||||
return new ConfirmLocationOfPoint(
|
||||
return new FixedUiElement("ConfirmLocationOfPoint...") /*ConfirmLocationOfPoint(
|
||||
state,
|
||||
filterViewIsOpened,
|
||||
preset,
|
||||
|
@ -128,140 +102,14 @@ export default class SimpleAddUI extends LoginToggle {
|
|||
cancelIcon: Svg.back_svg(),
|
||||
cancelText: Translations.t.general.add.backToSelect,
|
||||
}
|
||||
)
|
||||
)*/
|
||||
})
|
||||
)
|
||||
|
||||
super(
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
new Loading(Translations.t.general.add.stillLoading).SetClass("alert"),
|
||||
addUi,
|
||||
state.dataIsLoading
|
||||
),
|
||||
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
|
||||
state.mapProperties.zoom.map(
|
||||
(zoom) => zoom >= Constants.minZoomLevelToAddNewPoint
|
||||
)
|
||||
),
|
||||
readYourMessages,
|
||||
state.osmConnection.userDetails.map(
|
||||
(userdetails: UserDetails) =>
|
||||
userdetails.csCount >=
|
||||
Constants.userJourney.addNewPointWithUnreadMessagesUnlock ||
|
||||
userdetails.unreadMessages == 0
|
||||
)
|
||||
),
|
||||
Translations.t.general.add.pleaseLogin,
|
||||
state
|
||||
new Loading(Translations.t.general.add.stillLoading).SetClass("alert"),
|
||||
addUi,
|
||||
state.dataIsLoading
|
||||
)
|
||||
}
|
||||
|
||||
public static CreateTagInfoFor(
|
||||
preset: PresetInfo,
|
||||
osmConnection: OsmConnection,
|
||||
optionallyLinkToWiki = true
|
||||
) {
|
||||
const csCount = osmConnection.userDetails.data.csCount
|
||||
return new Toggle(
|
||||
Translations.t.general.add.presetInfo
|
||||
.Subs({
|
||||
tags: preset.tags
|
||||
.map((t) =>
|
||||
t.asHumanString(
|
||||
optionallyLinkToWiki &&
|
||||
csCount > Constants.userJourney.tagsVisibleAndWikiLinked,
|
||||
true
|
||||
)
|
||||
)
|
||||
.join("&"),
|
||||
})
|
||||
.SetStyle("word-break: break-all"),
|
||||
|
||||
undefined,
|
||||
osmConnection.userDetails.map(
|
||||
(userdetails) => userdetails.csCount >= Constants.userJourney.tagsVisibleAt
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private static CreateAllPresetsPanel(
|
||||
selectedPreset: UIEventSource<PresetInfo>,
|
||||
state: SpecialVisualizationState
|
||||
): BaseUIElement {
|
||||
const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset)
|
||||
let intro: BaseUIElement = Translations.t.general.add.intro
|
||||
|
||||
let testMode: BaseUIElement = new Toggle(
|
||||
Translations.t.general.testing.SetClass("alert"),
|
||||
undefined,
|
||||
state.featureSwitchIsTesting
|
||||
)
|
||||
|
||||
return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
private static CreatePresetSelectButton(preset: PresetInfo) {
|
||||
const title = Translations.t.general.add.addNew.Subs(
|
||||
{
|
||||
category: preset.name,
|
||||
},
|
||||
preset.name["context"]
|
||||
)
|
||||
return new SubtleButton(
|
||||
preset.icon(),
|
||||
new Combine([
|
||||
title.SetClass("font-bold"),
|
||||
preset.description?.FirstSentence(),
|
||||
]).SetClass("flex flex-col")
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Generates the list with all the buttons.*/
|
||||
private static CreatePresetButtons(
|
||||
state: SpecialVisualizationState,
|
||||
selectedPreset: UIEventSource<PresetInfo>
|
||||
): BaseUIElement {
|
||||
const allButtons = []
|
||||
for (const layer of Array.from(state.layerState.filteredLayers.values())) {
|
||||
if (layer.isDisplayed.data === false) {
|
||||
// The layer is not displayed...
|
||||
if (!state.featureSwitches.featureSwitchFilter.data) {
|
||||
// ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
|
||||
continue
|
||||
}
|
||||
|
||||
if (layer.layerDef.name === undefined) {
|
||||
// this layer can never be toggled on in any case, so we skip the presets
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const presets = layer.layerDef.presets
|
||||
for (const preset of presets) {
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
|
||||
let icon: () => BaseUIElement = () =>
|
||||
layer.layerDef.mapRendering[0]
|
||||
.RenderIcon(new ImmutableStore<any>(tags), false)
|
||||
.html.SetClass("w-12 h-12 block relative")
|
||||
const presetInfo: PresetInfo = {
|
||||
layerToAddTo: layer,
|
||||
name: preset.title,
|
||||
title: preset.title,
|
||||
icon: icon,
|
||||
preciseInput: preset.preciseInput,
|
||||
...preset,
|
||||
}
|
||||
|
||||
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo)
|
||||
button.onClick(() => {
|
||||
selectedPreset.setData(presetInfo)
|
||||
})
|
||||
allButtons.push(button)
|
||||
}
|
||||
}
|
||||
return new Combine(allButtons).SetClass("flex flex-col")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue