refactoring: fix basic flow to add a new point

This commit is contained in:
Pieter Vander Vennet 2023-04-06 01:33:08 +02:00
parent 52a0810ea9
commit 0241f89d3d
109 changed files with 1931 additions and 1446 deletions

View file

@ -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")
}
}

View file

@ -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")
}
}

View file

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

View file

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

View 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>

View file

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

View file

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

View file

@ -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")
}

View 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>

View file

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

View file

@ -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")
}
}