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
|
@ -11,18 +11,17 @@
|
|||
let mainElem: HTMLElement;
|
||||
export let hideSignal: Store<any>;
|
||||
function hide(){
|
||||
console.trace("Hiding...")
|
||||
mainElem.style.visibility = "hidden";
|
||||
}
|
||||
if (hideSignal) {
|
||||
onDestroy(hideSignal.addCallbackD(() => {
|
||||
console.trace("Hiding invitation")
|
||||
console.log("Received hide signal")
|
||||
hide()
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
$: {
|
||||
console.log("Binding listeners on", mainElem)
|
||||
mainElem?.addEventListener("click",_ => hide())
|
||||
mainElem?.addEventListener("touchstart",_ => hide())
|
||||
}
|
||||
|
@ -30,8 +29,8 @@ $: {
|
|||
|
||||
|
||||
<div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full">
|
||||
<div id="hand-container">
|
||||
<ToSvelte construct={Svg.hand_ui}></ToSvelte>
|
||||
<div id="hand-container" class="pointer-events-none">
|
||||
<img src="./assets/svg/hand.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
|
||||
/**
|
||||
* The slotted element will be shown on top, with a lower-opacity border
|
||||
*/
|
||||
const dispatch = createEventDispatcher<{ close }>();
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 right-0 w-screen h-screen overflow-auto" style="background-color: #00000088">
|
||||
<div class="flex flex-col m-4 sm:m-6 md:m-8 p-4 sm:p-6 md:m-8 normal-background rounded normal-background">
|
||||
<slot name="close-button">
|
||||
<div class="w-8 h-8 absolute right-10 top-10 cursor-pointer" on:click={() => dispatch("close")}>
|
||||
<XCircleIcon />
|
||||
</div>
|
||||
</slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
|
15
UI/Base/LoginButton.svelte
Normal file
15
UI/Base/LoginButton.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
|
||||
import SubtleButton from "./SubtleButton.svelte";
|
||||
import Translations from "../i18n/Translations.js";
|
||||
import Tr from "./Tr.svelte";
|
||||
|
||||
export let osmConnection: OsmConnection
|
||||
</script>
|
||||
|
||||
<SubtleButton on:click={() => osmConnection.AttemptLogin()}>
|
||||
<img slot="image" src="./assets/svg/login.svg" class="w-8"/>
|
||||
<slot name="message" slot="message">
|
||||
<Tr t={Translations.t.general.loginWithOpenStreetMap}/>
|
||||
</slot>
|
||||
</SubtleButton>
|
45
UI/Base/LoginToggle.svelte
Normal file
45
UI/Base/LoginToggle.svelte
Normal file
|
@ -0,0 +1,45 @@
|
|||
<script lang="ts">
|
||||
import Loading from "./Loading.svelte";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { OsmServiceState } from "../../Logic/Osm/OsmConnection";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "./Tr.svelte";
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
/**
|
||||
* If set, 'loading' will act as if we are already logged in.
|
||||
*/
|
||||
export let ignoreLoading: boolean = false
|
||||
let loadingStatus = state.osmConnection.loadingStatus;
|
||||
let badge = state.featureSwitches.featureSwitchUserbadge;
|
||||
const t = Translations.t.general;
|
||||
const offlineModes: Partial<Record<OsmServiceState, Translation>> = {
|
||||
offline: t.loginFailedOfflineMode,
|
||||
unreachable: t.loginFailedUnreachableMode,
|
||||
readonly: t.loginFailedReadonlyMode
|
||||
};
|
||||
const apiState = state.osmConnection.apiIsOnline;
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if $badge}
|
||||
{#if !ignoreLoading && $loadingStatus === "loading"}
|
||||
<slot name="loading">
|
||||
<Loading></Loading>
|
||||
</slot>
|
||||
{:else if $loadingStatus === "error"}
|
||||
<div class="flex items-center alert max-w-64">
|
||||
<img src="./assets/svg/invalid.svg" class="w-8 h-8 m-2 shrink-0">
|
||||
<Tr t={offlineModes[$apiState]} />
|
||||
</div>
|
||||
|
||||
{:else if $loadingStatus === "logged-in"}
|
||||
<slot></slot>
|
||||
{:else if $loadingStatus === "not-attempted"}
|
||||
<slot name="not-logged-in">
|
||||
|
||||
</slot>
|
||||
{/if}
|
||||
{/if}
|
|
@ -8,6 +8,6 @@
|
|||
</script>
|
||||
|
||||
|
||||
<div on:click={e => dispatch("click", e)} class="subtle-background rounded-full min-w-10 w-fit h-10 m-0.5 md:m-1 p-1">
|
||||
<div on:click={e => dispatch("click", e)} class="subtle-background rounded-full min-w-10 w-fit h-10 m-0.5 md:m-1 p-1 cursor-pointer">
|
||||
<slot class="m-4"></slot>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Img from "./Img";
|
||||
|
@ -24,7 +24,7 @@
|
|||
let imgElem: HTMLElement;
|
||||
let msgElem: HTMLElement;
|
||||
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11");
|
||||
|
||||
const dispatch = createEventDispatcher<{click}>()
|
||||
onMount(() => {
|
||||
// Image
|
||||
if (imgElem && imageUrl) {
|
||||
|
@ -47,15 +47,16 @@
|
|||
</script>
|
||||
|
||||
<svelte:element
|
||||
class={(options.extraClasses??"") + 'flex hover:shadow-xl transition-[color,background-color,box-shadow] hover:bg-unsubtle'}
|
||||
class={(options.extraClasses??"") + 'flex hover:shadow-xl transition-[color,background-color,box-shadow] hover:bg-unsubtle cursor-pointer'}
|
||||
href={$href}
|
||||
target={options?.newTab ? "_blank" : ""}
|
||||
this={href === undefined ? "span" : "a"}
|
||||
on:click={(e) => dispatch("click", e)}
|
||||
>
|
||||
<slot name="image">
|
||||
{#if imageUrl !== undefined}
|
||||
{#if typeof imageUrl === "string"}
|
||||
<Img src={imageUrl} class={imgClasses+ " bg-red border border-black"}></Img>
|
||||
<Img src={imageUrl} class={imgClasses}></Img>
|
||||
{:else }
|
||||
<template bind:this={imgElem} />
|
||||
{/if}
|
||||
|
|
|
@ -20,11 +20,15 @@ export default class SvelteUIElement<
|
|||
}): SvelteComponentTyped<Props, Events, Slots>
|
||||
}
|
||||
private readonly _props: Props
|
||||
private readonly _events: Events
|
||||
private readonly _slots: Slots
|
||||
|
||||
constructor(svelteElement, props: Props) {
|
||||
constructor(svelteElement, props: Props, events?: Events, slots?: Slots) {
|
||||
super()
|
||||
this._svelteComponent = svelteElement
|
||||
this._props = props
|
||||
this._events = events
|
||||
this._slots = slots
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
|
@ -32,6 +36,8 @@ export default class SvelteUIElement<
|
|||
new this._svelteComponent({
|
||||
target: el,
|
||||
props: this._props,
|
||||
events: this._events,
|
||||
slots: this._slots,
|
||||
})
|
||||
return el
|
||||
}
|
||||
|
|
68
UI/Base/TabbedGroup.svelte
Normal file
68
UI/Base/TabbedGroup.svelte
Normal file
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Thin wrapper around 'TabGroup' which binds the state
|
||||
*/
|
||||
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
|
||||
export let tab: UIEventSource<number>;
|
||||
let tabElements: HTMLElement[] = [];
|
||||
$: tabElements[$tab]?.click();
|
||||
$: {
|
||||
if (tabElements[tab.data]) {
|
||||
window.setTimeout(() => tabElements[tab.data].click(), 50)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<TabGroup defaultIndex={1} on:change={(e) =>{if(e.detail >= 0){tab.setData( e.detail); }} }>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div bind:this={tabElements[0]} class="flex">
|
||||
<slot name="title0">
|
||||
Tab 0
|
||||
</slot>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div bind:this={tabElements[1]} class="flex">
|
||||
<slot name="title1" />
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div bind:this={tabElements[2]} class="flex">
|
||||
<slot name="title2" />
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div bind:this={tabElements[3]} class="flex">
|
||||
<slot name="title3" />
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div bind:this={tabElements[4]} class="flex">
|
||||
<slot name="title4" />
|
||||
</div>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels defaultIndex={$tab}>
|
||||
<TabPanel>
|
||||
<slot name="content0">
|
||||
<div>Empty</div>
|
||||
</slot>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<slot name="content1" />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<slot name="content2" />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<slot name="content3" />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<slot name="content4" />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import StrayClickHandler from "../Logic/Actors/StrayClickHandler"
|
|||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import NewNoteUi from "./Popup/NewNoteUi"
|
||||
import Combine from "./Base/Combine"
|
||||
import AddNewMarker from "./BigComponents/AddNewMarker"
|
||||
import FilteredLayer from "../Models/FilteredLayer"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
|
@ -108,13 +107,6 @@ export default class DefaultGUI {
|
|||
newPointDialogIsShown
|
||||
)
|
||||
|
||||
addNewPoint.isShown.addCallback((isShown) => {
|
||||
if (!isShown) {
|
||||
// Clear the 'last-click'-location when the dialog is closed - this causes the popup and the marker to be removed
|
||||
state.LastClickLocation.setData(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
let noteMarker = undefined
|
||||
if (!hasPresets && addNewNoteDialog !== undefined) {
|
||||
noteMarker = new Combine([
|
||||
|
@ -126,15 +118,6 @@ export default class DefaultGUI {
|
|||
.SetClass("block relative h-full")
|
||||
.SetStyle("left: calc( 50% - 15px )") // This is a bit hacky, yes I know!
|
||||
}
|
||||
|
||||
StrayClickHandler.construct(
|
||||
state,
|
||||
addNewPoint,
|
||||
hasPresets ? new AddNewMarker(state.filteredLayers) : noteMarker
|
||||
)
|
||||
state.LastClickLocation.addCallbackAndRunD((_) => {
|
||||
ScrollableFullScreen.collapse()
|
||||
})
|
||||
}
|
||||
|
||||
if (noteLayer !== undefined) {
|
||||
|
@ -208,22 +191,6 @@ export default class DefaultGUI {
|
|||
self.InitWelcomeMessage()
|
||||
)
|
||||
|
||||
const communityIndex = Toggle.If(state.featureSwitchCommunityIndex, () => {
|
||||
const communityIndexControl = new MapControlButton(Svg.community_svg())
|
||||
const communityIndex = new ScrollableFullScreen(
|
||||
() => Translations.t.communityIndex.title,
|
||||
() => new SvelteUIElement(CommunityIndexView, { ...state }),
|
||||
"community_index"
|
||||
)
|
||||
communityIndexControl.onClick(() => {
|
||||
communityIndex.Activate()
|
||||
})
|
||||
return communityIndexControl
|
||||
})
|
||||
|
||||
const testingBadge = Toggle.If(state.featureSwitchIsTesting, () =>
|
||||
new FixedUiElement("TESTING").SetClass("alert m-2 border-2 border-black")
|
||||
)
|
||||
new ScrollableFullScreen(
|
||||
() => Translations.t.general.attribution.attributionTitle,
|
||||
() => new CopyrightPanel(state),
|
||||
|
@ -233,14 +200,7 @@ export default class DefaultGUI {
|
|||
const copyright = new MapControlButton(Svg.copyright_svg()).onClick(() =>
|
||||
guiState.copyrightViewIsOpened.setData(true)
|
||||
)
|
||||
new Combine([
|
||||
welcomeMessageMapControl,
|
||||
userInfoMapControl,
|
||||
copyright,
|
||||
communityIndex,
|
||||
extraLink,
|
||||
testingBadge,
|
||||
])
|
||||
new Combine([welcomeMessageMapControl, userInfoMapControl, copyright, extraLink])
|
||||
.SetClass("flex flex-col")
|
||||
.AttachTo("top-left")
|
||||
|
||||
|
@ -264,32 +224,11 @@ export default class DefaultGUI {
|
|||
}
|
||||
|
||||
private InitWelcomeMessage(): BaseUIElement {
|
||||
const isOpened = this.guiState.welcomeMessageIsOpened
|
||||
new FullWelcomePaneWithTabs(
|
||||
isOpened,
|
||||
return new FullWelcomePaneWithTabs(
|
||||
new UIEventSource<boolean>(false),
|
||||
this.guiState.welcomeMessageOpenedTab,
|
||||
this.state,
|
||||
this.guiState
|
||||
)
|
||||
|
||||
// ?-Button on Desktop, opens panel with close-X.
|
||||
const help = new MapControlButton(Svg.help_svg())
|
||||
help.onClick(() => isOpened.setData(true))
|
||||
|
||||
const openedTime = new Date().getTime()
|
||||
this.state.locationControl.addCallback(() => {
|
||||
if (new Date().getTime() - openedTime < 15 * 1000) {
|
||||
// Don't autoclose the first 15 secs when the map is moving
|
||||
return
|
||||
}
|
||||
isOpened.setData(false)
|
||||
return true // Unregister this caller - we only autoclose once
|
||||
})
|
||||
|
||||
this.state.selectedElement.addCallbackAndRunD((_) => {
|
||||
isOpened.setData(false)
|
||||
})
|
||||
|
||||
return help.SetClass("pointer-events-auto")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,35 @@
|
|||
import { BBox } from "../../Logic/BBox"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Combine from "../Base/Combine"
|
||||
import Title from "../Base/Title"
|
||||
import { Overpass } from "../../Logic/Osm/Overpass"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Constants from "../../Models/Constants"
|
||||
import RelationsTracker from "../../Logic/Osm/RelationsTracker"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import Loading from "../Base/Loading"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import { Utils } from "../../Utils"
|
||||
import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage"
|
||||
import Minimap from "../Base/Minimap"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
||||
import Loc from "../../Models/Loc"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import ValidatedTextField from "../Input/ValidatedTextField"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
import import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox"
|
||||
import { ImportUtils } from "./ImportUtils"
|
||||
import Translations from "../i18n/Translations"
|
||||
import currentview from "../../assets/layers/current_view/current_view.json"
|
||||
import { CheckBox } from "../Input/Checkboxes"
|
||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
||||
import { Feature, FeatureCollection, Point } from "geojson"
|
||||
import DivContainer from "../Base/DivContainer"
|
||||
import { BBox } from "../../Logic/BBox";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import Combine from "../Base/Combine";
|
||||
import Title from "../Base/Title";
|
||||
import { Overpass } from "../../Logic/Osm/Overpass";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import Constants from "../../Models/Constants";
|
||||
import RelationsTracker from "../../Logic/Osm/RelationsTracker";
|
||||
import { VariableUiElement } from "../Base/VariableUIElement";
|
||||
import { FlowStep } from "./FlowStep";
|
||||
import Loading from "../Base/Loading";
|
||||
import { SubtleButton } from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import { Utils } from "../../Utils";
|
||||
import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage";
|
||||
import Minimap from "../Base/Minimap";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import Loc from "../../Models/Loc";
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource";
|
||||
import import_candidate from "../../assets/layers/import_candidate/import_candidate.json";
|
||||
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||
import { ImportUtils } from "./ImportUtils";
|
||||
import Translations from "../i18n/Translations";
|
||||
import currentview from "../../assets/layers/current_view/current_view.json";
|
||||
import { CheckBox } from "../Input/Checkboxes";
|
||||
import { Feature, FeatureCollection, Point } from "geojson";
|
||||
import DivContainer from "../Base/DivContainer";
|
||||
|
||||
/**
|
||||
* Given the data to import, the bbox and the layer, will query overpass for similar items
|
||||
|
@ -323,13 +322,7 @@ export default class ConflationChecker
|
|||
),
|
||||
t.setRangeToZero,
|
||||
matchedFeaturesMap,
|
||||
new Combine([
|
||||
new BackgroundMapSwitch(
|
||||
{ backgroundLayer: background, locationControl: matchedFeaturesMap.location },
|
||||
background
|
||||
),
|
||||
showOsmLayer,
|
||||
]).SetClass("flex"),
|
||||
showOsmLayer,
|
||||
]).SetClass("flex flex-col")
|
||||
super([
|
||||
new Title(t.title),
|
||||
|
|
|
@ -17,7 +17,6 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
|||
import Title from "../Base/Title"
|
||||
import CheckBoxes from "../Input/Checkboxes"
|
||||
import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
|
||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
||||
import { Feature, Point } from "geojson"
|
||||
import DivContainer from "../Base/DivContainer"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
|
@ -112,13 +111,7 @@ export class MapPreview
|
|||
const currentBounds = new UIEventSource<BBox>(undefined)
|
||||
const { ui, mapproperties, map } = MapLibreAdaptor.construct()
|
||||
|
||||
const layerControl = new BackgroundMapSwitch(
|
||||
{
|
||||
backgroundLayer: background,
|
||||
locationControl: location,
|
||||
},
|
||||
background
|
||||
)
|
||||
|
||||
ui.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
layerPicker.GetValue().addCallbackAndRunD((layerToShow) => {
|
||||
|
@ -160,7 +153,6 @@ export class MapPreview
|
|||
mismatchIndicator,
|
||||
ui,
|
||||
new DivContainer("fullscreen"),
|
||||
layerControl,
|
||||
confirm,
|
||||
])
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { InputElement } from "./InputElement"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { InputElement } from "./InputElement";
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine";
|
||||
import Svg from "../../Svg";
|
||||
import Loc from "../../Models/Loc";
|
||||
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers";
|
||||
|
||||
/**
|
||||
* Selects a length after clicking on the minimap, in meters
|
||||
|
@ -38,7 +37,7 @@ export default class LengthInput extends InputElement<string> {
|
|||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
let map: BaseUIElement & MinimapObj = undefined
|
||||
let map: BaseUIElement = undefined
|
||||
let layerControl: BaseUIElement = undefined
|
||||
map = Minimap.createMiniMap({
|
||||
background: this.background,
|
||||
|
@ -50,16 +49,6 @@ export default class LengthInput extends InputElement<string> {
|
|||
},
|
||||
})
|
||||
|
||||
layerControl = new BackgroundMapSwitch(
|
||||
{
|
||||
locationControl: this._location,
|
||||
backgroundLayer: this.background,
|
||||
},
|
||||
this.background,
|
||||
{
|
||||
allowedCategories: ["map", "photo"],
|
||||
}
|
||||
)
|
||||
const crosshair = new Combine([
|
||||
Svg.length_crosshair_svg().SetStyle(
|
||||
`position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`
|
||||
|
@ -70,9 +59,6 @@ export default class LengthInput extends InputElement<string> {
|
|||
|
||||
const element = new Combine([
|
||||
crosshair,
|
||||
layerControl?.SetStyle(
|
||||
"position: absolute; bottom: 0.25rem; left: 0.25rem; z-index: 1000"
|
||||
),
|
||||
map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"),
|
||||
])
|
||||
.SetClass("relative block bg-white border border-black rounded-xl overflow-hidden")
|
||||
|
|
|
@ -12,31 +12,29 @@
|
|||
* A visualisation to pick a direction on a map background
|
||||
*/
|
||||
export let value: UIEventSource<{lon: number, lat: number}>;
|
||||
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> };
|
||||
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> } = undefined;
|
||||
/**
|
||||
* Called when setup is done, cna be used to add layrs to the map
|
||||
*/
|
||||
export let onCreated : (value: Store<{lon: number, lat: number}> , map: Store<MlMap>, mapProperties: MapProperties ) => void
|
||||
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let mla = new MapLibreAdaptor(map, mapProperties);
|
||||
mla.allowMoving.setData(true)
|
||||
mla.allowZooming.setData(true)
|
||||
|
||||
|
||||
if(onCreated){
|
||||
onCreated(value, map, mla)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative h-32 cursor-pointer overflow-hidden">
|
||||
<div class="relative h-full min-h-32 cursor-pointer overflow-hidden">
|
||||
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
|
||||
<MaplibreMap {map} attribution={false}></MaplibreMap>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50">
|
||||
<ToSvelte construct={() => Svg.move_arrows_svg().SetClass("h-full")}></ToSvelte>
|
||||
<div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50 flex items-center">
|
||||
<img src="./assets/svg/move-arrows.svg" class="h-full max-h-24"/>
|
||||
</div>
|
||||
|
||||
<DragInvitation></DragInvitation>
|
||||
<DragInvitation hideSignal={mla.location.stabilized(3000)}></DragInvitation>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -33,10 +33,10 @@
|
|||
|
||||
let dispatch = createEventDispatcher<{ selected }>();
|
||||
$: {
|
||||
console.log(htmlElem)
|
||||
console.log(htmlElem);
|
||||
if (htmlElem !== undefined) {
|
||||
htmlElem.onfocus = () => {
|
||||
console.log("Dispatching selected event")
|
||||
console.log("Dispatching selected event");
|
||||
return dispatch("selected");
|
||||
};
|
||||
}
|
||||
|
@ -44,12 +44,12 @@
|
|||
</script>
|
||||
|
||||
{#if validator.textArea}
|
||||
<textarea bind:value={$_value} inputmode={validator.inputmode ?? "text"}></textarea>
|
||||
<textarea class="w-full" bind:value={$_value} inputmode={validator.inputmode ?? "text"}></textarea>
|
||||
{:else }
|
||||
<div class="flex">
|
||||
<span class="flex">
|
||||
<input bind:this={htmlElem} bind:value={$_value} inputmode={validator.inputmode ?? "text"}>
|
||||
{#if !$isValid}
|
||||
<ExclamationIcon class="h-6 w-6 -ml-6"></ExclamationIcon>
|
||||
{/if}
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
@ -35,8 +35,8 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
readonly allowMoving: UIEventSource<true | boolean | undefined>
|
||||
readonly allowZooming: UIEventSource<true | boolean | undefined>
|
||||
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
|
||||
readonly minzoom: UIEventSource<number>
|
||||
private readonly _maplibreMap: Store<MLMap>
|
||||
private readonly _bounds: UIEventSource<BBox>
|
||||
/**
|
||||
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
||||
* @private
|
||||
|
@ -48,9 +48,10 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
|
||||
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })
|
||||
this.zoom = state?.zoom ?? new UIEventSource(1)
|
||||
this.minzoom = state?.minzoom ?? new UIEventSource(0)
|
||||
this.zoom.addCallbackAndRunD((z) => {
|
||||
if (z < 0) {
|
||||
this.zoom.setData(0)
|
||||
if (z < this.minzoom.data) {
|
||||
this.zoom.setData(this.minzoom.data)
|
||||
}
|
||||
if (z > 24) {
|
||||
this.zoom.setData(24)
|
||||
|
@ -59,8 +60,7 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined)
|
||||
this.allowMoving = state?.allowMoving ?? new UIEventSource(true)
|
||||
this.allowZooming = state?.allowZooming ?? new UIEventSource(true)
|
||||
this._bounds = new UIEventSource(undefined)
|
||||
this.bounds = this._bounds
|
||||
this.bounds = state?.bounds ?? new UIEventSource(undefined)
|
||||
this.rasterLayer =
|
||||
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
|
||||
|
||||
|
@ -69,32 +69,28 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
const self = this
|
||||
maplibreMap.addCallbackAndRunD((map) => {
|
||||
map.on("load", () => {
|
||||
this.updateStores()
|
||||
self.setBackground()
|
||||
self.MoveMapToCurrentLoc(self.location.data)
|
||||
self.SetZoom(self.zoom.data)
|
||||
self.setMaxBounds(self.maxbounds.data)
|
||||
self.setAllowMoving(self.allowMoving.data)
|
||||
self.setAllowZooming(self.allowZooming.data)
|
||||
self.setMinzoom(self.minzoom.data)
|
||||
})
|
||||
self.MoveMapToCurrentLoc(self.location.data)
|
||||
self.SetZoom(self.zoom.data)
|
||||
self.setMaxBounds(self.maxbounds.data)
|
||||
self.setAllowMoving(self.allowMoving.data)
|
||||
self.setAllowZooming(self.allowZooming.data)
|
||||
map.on("moveend", () => {
|
||||
const dt = this.location.data
|
||||
dt.lon = map.getCenter().lng
|
||||
dt.lat = map.getCenter().lat
|
||||
this.location.ping()
|
||||
this.zoom.setData(Math.round(map.getZoom() * 10) / 10)
|
||||
const bounds = map.getBounds()
|
||||
const bbox = new BBox([
|
||||
[bounds.getEast(), bounds.getNorth()],
|
||||
[bounds.getWest(), bounds.getSouth()],
|
||||
])
|
||||
self._bounds.setData(bbox)
|
||||
})
|
||||
self.setMinzoom(self.minzoom.data)
|
||||
this.updateStores()
|
||||
map.on("moveend", () => this.updateStores())
|
||||
map.on("click", (e) => {
|
||||
if (e.originalEvent["consumed"]) {
|
||||
// Workaround, 'ShowPointLayer' sets this flag
|
||||
return
|
||||
}
|
||||
const lon = e.lngLat.lng
|
||||
const lat = e.lngLat.lat
|
||||
lastClickLocation.setData({ lon, lat })
|
||||
|
@ -117,6 +113,23 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
|
||||
}
|
||||
|
||||
private updateStores() {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
return
|
||||
}
|
||||
const dt = this.location.data
|
||||
dt.lon = map.getCenter().lng
|
||||
dt.lat = map.getCenter().lat
|
||||
this.location.ping()
|
||||
this.zoom.setData(Math.round(map.getZoom() * 10) / 10)
|
||||
const bounds = map.getBounds()
|
||||
const bbox = new BBox([
|
||||
[bounds.getEast(), bounds.getNorth()],
|
||||
[bounds.getWest(), bounds.getSouth()],
|
||||
])
|
||||
this.bounds.setData(bbox)
|
||||
}
|
||||
/**
|
||||
* Convenience constructor
|
||||
*/
|
||||
|
@ -191,7 +204,7 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
if (map === undefined) {
|
||||
return
|
||||
}
|
||||
while (!map.isStyleLoaded()) {
|
||||
while (!map?.isStyleLoaded()) {
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
}
|
||||
|
@ -265,9 +278,9 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
return
|
||||
}
|
||||
if (bbox) {
|
||||
map.setMaxBounds(bbox.toLngLat())
|
||||
map?.setMaxBounds(bbox.toLngLat())
|
||||
} else {
|
||||
map.setMaxBounds(null)
|
||||
map?.setMaxBounds(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -287,6 +300,14 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
}
|
||||
}
|
||||
|
||||
private setMinzoom(minzoom: number) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
return
|
||||
}
|
||||
map.setMinZoom(minzoom)
|
||||
}
|
||||
|
||||
private setAllowZooming(allow: true | boolean | undefined) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { GeoOperations } from "../../Logic/GeoOperations"
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { Feature } from "geojson"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
|
@ -124,8 +124,11 @@ class PointRenderingLayer {
|
|||
|
||||
if (this._onClick) {
|
||||
const self = this
|
||||
el.addEventListener("click", function () {
|
||||
el.addEventListener("click", function (ev) {
|
||||
self._onClick(feature)
|
||||
ev.preventDefault()
|
||||
// Workaround to signal the MapLibreAdaptor to ignore this click
|
||||
ev["consumed"] = true
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -164,6 +167,7 @@ class LineRenderingLayer {
|
|||
private readonly _layername: string
|
||||
private readonly _listenerInstalledOn: Set<string> = new Set<string>()
|
||||
|
||||
private static missingIdTriggered = false
|
||||
constructor(
|
||||
map: MlMap,
|
||||
features: FeatureSource,
|
||||
|
@ -281,11 +285,14 @@ class LineRenderingLayer {
|
|||
const feature = features[i]
|
||||
const id = feature.properties.id ?? feature.id
|
||||
if (id === undefined) {
|
||||
console.trace(
|
||||
"Got a feature without ID; this causes rendering bugs:",
|
||||
feature,
|
||||
"from"
|
||||
)
|
||||
if (!LineRenderingLayer.missingIdTriggered) {
|
||||
console.trace(
|
||||
"Got a feature without ID; this causes rendering bugs:",
|
||||
feature,
|
||||
"from"
|
||||
)
|
||||
LineRenderingLayer.missingIdTriggered = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (this._listenerInstalledOn.has(id)) {
|
||||
|
@ -334,7 +341,7 @@ export default class ShowDataLayer {
|
|||
options?: Partial<ShowDataLayerOptions>
|
||||
) {
|
||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||
layers.map((l) => new FilteredLayer(l)),
|
||||
layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
|
||||
new StaticFeatureSource(features)
|
||||
)
|
||||
perLayer.forEach((fs) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Feature } from "geojson"
|
||||
|
|
|
@ -8,8 +8,7 @@ import Combine from "../Base/Combine"
|
|||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import SimpleAddUI, { PresetInfo } from "../BigComponents/SimpleAddUI"
|
||||
import Img from "../Base/Img"
|
||||
import { PresetInfo } from "../BigComponents/SimpleAddUI"
|
||||
import Title from "../Base/Title"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
|
@ -115,10 +114,6 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
)
|
||||
.SetClass("font-bold break-words")
|
||||
.onClick(() => {
|
||||
console.log(
|
||||
"The confirmLocationPanel - precise input yielded ",
|
||||
preciseInput?.GetValue()?.data
|
||||
)
|
||||
const globalFilterTagsToAdd: Tag[][] = state.globalFilters.data
|
||||
.filter((gf) => gf.onNewPoint !== undefined)
|
||||
.map((gf) => gf.onNewPoint.tags)
|
||||
|
@ -131,30 +126,13 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
)
|
||||
})
|
||||
|
||||
const warn = Translations.t.general.add.warnVisibleForEveryone
|
||||
.Clone()
|
||||
.SetClass("alert w-full block")
|
||||
if (preciseInput !== undefined) {
|
||||
confirmButton = new Combine([preciseInput, warn, confirmButton])
|
||||
confirmButton = new Combine([preciseInput, confirmButton])
|
||||
} else {
|
||||
confirmButton = new Combine([warn, confirmButton])
|
||||
confirmButton = new Combine([confirmButton])
|
||||
}
|
||||
|
||||
const openLayerControl = new SubtleButton(
|
||||
Svg.layers_ui(),
|
||||
new Combine([
|
||||
Translations.t.general.add.layerNotEnabled
|
||||
.Subs({ layer: preset.layerToAddTo.layerDef.name })
|
||||
.SetClass("alert"),
|
||||
Translations.t.general.add.openLayerControl,
|
||||
])
|
||||
).onClick(() => filterViewIsOpened.setData(true))
|
||||
|
||||
let openLayerOrConfirm = new Toggle(
|
||||
confirmButton,
|
||||
openLayerControl,
|
||||
preset.layerToAddTo.isDisplayed
|
||||
)
|
||||
let openLayerOrConfirm = confirmButton
|
||||
|
||||
const disableFilter = new SubtleButton(
|
||||
new Combine([
|
||||
|
@ -200,21 +178,8 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
)
|
||||
}
|
||||
|
||||
const hasActiveFilter = preset.layerToAddTo.appliedFilters.map((appliedFilters) => {
|
||||
const activeFilters = Array.from(appliedFilters.values()).filter(
|
||||
(f) => f?.currentFilter !== undefined
|
||||
)
|
||||
return activeFilters.length === 0
|
||||
})
|
||||
|
||||
// If at least one filter is active which _might_ hide a newly added item, this blocks the preset and requests the filter to be disabled
|
||||
const disableFiltersOrConfirm = new Toggle(
|
||||
openLayerOrConfirm,
|
||||
disableFilter,
|
||||
hasActiveFilter
|
||||
)
|
||||
|
||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection)
|
||||
const disableFiltersOrConfirm = new Toggle(openLayerOrConfirm, disableFilter)
|
||||
|
||||
const cancelButton = new SubtleButton(
|
||||
options?.cancelIcon ?? Svg.close_ui(),
|
||||
|
@ -223,18 +188,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
|
||||
let examples: BaseUIElement = undefined
|
||||
if (preset.exampleImages !== undefined && preset.exampleImages.length > 0) {
|
||||
examples = new Combine([
|
||||
new Title(
|
||||
preset.exampleImages.length == 1
|
||||
? Translations.t.general.example
|
||||
: Translations.t.general.examples
|
||||
),
|
||||
new Combine(
|
||||
preset.exampleImages.map((img) =>
|
||||
new Img(img).SetClass("h-64 m-1 w-auto rounded-lg")
|
||||
)
|
||||
).SetClass("flex flex-wrap items-stretch"),
|
||||
])
|
||||
examples = new Combine([new Title()])
|
||||
}
|
||||
|
||||
super([
|
||||
|
@ -247,7 +201,6 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
cancelButton,
|
||||
preset.description,
|
||||
examples,
|
||||
tagInfo,
|
||||
])
|
||||
|
||||
this.SetClass("flex flex-col")
|
||||
|
|
239
UI/Popup/AddNewPoint/AddNewPoint.svelte
Normal file
239
UI/Popup/AddNewPoint/AddNewPoint.svelte
Normal file
|
@ -0,0 +1,239 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* This component ties together all the steps that are needed to create a new point.
|
||||
* There are many subcomponents which help with that
|
||||
*/
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import PresetList from "./PresetList.svelte";
|
||||
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import SubtleButton from "../../Base/SubtleButton.svelte";
|
||||
import FromHtml from "../../Base/FromHtml.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import TagHint from "../TagHint.svelte";
|
||||
import { And } from "../../../Logic/Tags/And.js";
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||
import Constants from "../../../Models/Constants.js";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import LoginButton from "../../Base/LoginButton.svelte";
|
||||
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte";
|
||||
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
import { OsmObject } from "../../../Logic/Osm/OsmObject";
|
||||
import { Tag } from "../../../Logic/Tags/Tag";
|
||||
import type { WayId } from "../../../Models/OsmFeature";
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils";
|
||||
import Loading from "../../Base/Loading.svelte";
|
||||
|
||||
export let coordinate: { lon: number, lat: number };
|
||||
export let state: SpecialVisualizationState;
|
||||
|
||||
let selectedPreset: { preset: PresetConfig, layer: LayerConfig, icon: string, tags: Record<string, string> } = undefined;
|
||||
|
||||
let confirmedCategory = false;
|
||||
$: if (selectedPreset === undefined) {
|
||||
confirmedCategory = false;
|
||||
creating = false
|
||||
}
|
||||
|
||||
let flayer: FilteredLayer = undefined;
|
||||
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined;
|
||||
let layerHasFilters: Store<boolean> | undefined = undefined;
|
||||
|
||||
$:{
|
||||
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id);
|
||||
layerIsDisplayed = flayer?.isDisplayed;
|
||||
layerHasFilters = flayer?.hasFilter;
|
||||
}
|
||||
const t = Translations.t.general.add;
|
||||
|
||||
const zoom = state.mapProperties.zoom;
|
||||
|
||||
let preciseCoordinate: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(undefined);
|
||||
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined);
|
||||
|
||||
let creating = false;
|
||||
|
||||
/**
|
||||
* Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters.
|
||||
* Will delete the lastclick-location
|
||||
*/
|
||||
function abort() {
|
||||
state.selectedElement.setData(undefined);
|
||||
// When aborted, we force the contributors to place the pin _again_
|
||||
// This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map
|
||||
state.lastClickObject.features.setData([]);
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
creating = true;
|
||||
const location: { lon: number; lat: number } = preciseCoordinate.data;
|
||||
const snapTo: WayId | undefined = <WayId>snappedToObject.data;
|
||||
const tags: Tag[] = selectedPreset.preset.tags;
|
||||
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags);
|
||||
|
||||
const snapToWay = snapTo === undefined ? undefined : await OsmObject.DownloadObjectAsync(snapTo, 0);
|
||||
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
|
||||
theme: state.layout?.id ?? "unkown",
|
||||
changeType: "create",
|
||||
snapOnto: snapToWay
|
||||
});
|
||||
await state.changes.applyAction(newElementAction);
|
||||
const newId = newElementAction.newElementId;
|
||||
state.newFeatures.features.data.push({
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: newId,
|
||||
...TagUtils.KVtoProperties(tags)
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [location.lon, location.lat]
|
||||
}
|
||||
});
|
||||
state.newFeatures.features.ping();
|
||||
console.log("New features:", state.newFeatures.features.data )
|
||||
{
|
||||
// Set some metainfo
|
||||
const tagsStore = state.featureProperties.getStore(newId);
|
||||
const properties = tagsStore.data;
|
||||
if (snapTo) {
|
||||
// metatags (starting with underscore) are not uploaded, so we can safely mark this
|
||||
properties["_referencing_ways"] = `["${snapTo}"]`;
|
||||
}
|
||||
properties["_last_edit:timestamp"] = new Date().toISOString();
|
||||
const userdetails = state.osmConnection.userDetails.data;
|
||||
properties["_last_edit:contributor"] = userdetails.name;
|
||||
properties["_last_edit:uid"] = "" + userdetails.uid;
|
||||
tagsStore.ping();
|
||||
}
|
||||
const feature = state.indexedFeatures.featuresById.data.get(newId);
|
||||
abort();
|
||||
state.selectedElement.setData(feature);
|
||||
state.selectedLayer.setData(selectedPreset.layer);
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
|
||||
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
|
||||
</LoginButton>
|
||||
|
||||
{#if $zoom < Constants.minZoomLevelToAddNewPoint}
|
||||
<div class="alert">
|
||||
<Tr t={Translations.t.general.add.zoomInFurther}></Tr>
|
||||
</div>
|
||||
{:else if selectedPreset === undefined}
|
||||
<!-- First, select the correct preset -->
|
||||
<PresetList {state} on:select={event => {selectedPreset = event.detail}}></PresetList>
|
||||
|
||||
|
||||
{:else if !$layerIsDisplayed}
|
||||
<!-- Check that the layer is enabled, so that we don't add a duplicate -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8" />
|
||||
<Tr t={Translations.t.general.add.layerNotEnabled
|
||||
.Subs({ layer: selectedPreset.layer.name })
|
||||
} />
|
||||
</div>
|
||||
|
||||
<SubtleButton on:click={() => {
|
||||
layerIsDisplayed.setData(true)
|
||||
abort()
|
||||
}}>
|
||||
<EyeIcon slot="image" class="w-8" />
|
||||
<Tr slot="message" t={Translations.t.general.add.enableLayer.Subs({name: selectedPreset.layer.name})} />
|
||||
</SubtleButton>
|
||||
<SubtleButton on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(selectedPreset.layer) } }>
|
||||
<img src="./assets/svg/layers.svg" slot="image" class="w-6">
|
||||
<Tr slot="message" t={Translations.t.general.add.openLayerControl}></Tr>
|
||||
</SubtleButton>
|
||||
|
||||
|
||||
{:else if $layerHasFilters}
|
||||
<!-- Some filters are enabled. The feature to add might already be mapped, but hiddne -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8" />
|
||||
<Tr t={Translations.t.general.add.disableFiltersExplanation} />
|
||||
</div>
|
||||
|
||||
<SubtleButton on:click={() => {
|
||||
abort()
|
||||
const flayer = state.layerState.filteredLayers.get(selectedPreset.layer.id)
|
||||
flayer.disableAllFilters()
|
||||
}
|
||||
}>
|
||||
<EyeOffIcon class="w-8" />
|
||||
<Tr slot="message" t={Translations.t.general.add.disableFilters}></Tr>
|
||||
</SubtleButton>
|
||||
|
||||
|
||||
<SubtleButton on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(selectedPreset.layer)
|
||||
}
|
||||
}>
|
||||
<img src="./assets/svg/layers.svg" slot="image" class="w-6">
|
||||
<Tr slot="message" t={Translations.t.general.add.openLayerControl}></Tr>
|
||||
</SubtleButton>
|
||||
|
||||
{:else if !confirmedCategory }
|
||||
<!-- Second, confirm the category -->
|
||||
<Tr t={Translations.t.general.add.confirmIntro.Subs({title: selectedPreset.preset.title})}></Tr>
|
||||
|
||||
|
||||
{#if selectedPreset.preset.description}
|
||||
<Tr t={selectedPreset.preset.description} />
|
||||
{/if}
|
||||
|
||||
{#if selectedPreset.preset.exampleImages}
|
||||
<h4>
|
||||
{#if selectedPreset.preset.exampleImages.length == 1}
|
||||
<Tr t={Translations.t.general.example} />
|
||||
{:else}
|
||||
<Tr t={Translations.t.general.examples } />
|
||||
{/if}
|
||||
</h4>
|
||||
<span class="flex flex-wrap items-stretch">
|
||||
{#each selectedPreset.preset.exampleImages as src}
|
||||
<img {src} class="h-64 m-1 w-auto rounded-lg">
|
||||
{/each}
|
||||
</span>
|
||||
{/if}
|
||||
<TagHint embedIn={tags => t.presetInfo.Subs({tags})} osmConnection={state.osmConnection}
|
||||
tags={new And(selectedPreset.preset.tags)}></TagHint>
|
||||
|
||||
|
||||
<SubtleButton on:click={() => confirmedCategory = true}>
|
||||
<div slot="image" class="relative">
|
||||
<FromHtml src={selectedPreset.icon}></FromHtml>
|
||||
<img class="absolute bottom-0 right-0 w-4 h-4" src="./assets/svg/confirm.svg">
|
||||
</div>
|
||||
<div slot="message">
|
||||
<Tr t={selectedPreset.text}></Tr>
|
||||
</div>
|
||||
</SubtleButton>
|
||||
<SubtleButton on:click={() => selectedPreset = undefined}>
|
||||
<img src="./assets/svg/back.svg" class="w-8 h-8" slot="image">
|
||||
<div slot="message">
|
||||
<Tr t={t.backToSelect} />
|
||||
</div>
|
||||
</SubtleButton>
|
||||
{:else if !creating}
|
||||
<NewPointLocationInput value={preciseCoordinate} snappedTo={snappedToObject} {state} {coordinate}
|
||||
targetLayer={selectedPreset.layer}
|
||||
snapToLayers={selectedPreset.preset.preciseInput.snapToLayers}></NewPointLocationInput>
|
||||
<SubtleButton on:click={confirm}>
|
||||
<span slot="message">Confirm location</span>
|
||||
</SubtleButton>
|
||||
{:else}
|
||||
<Loading>Creating point...</Loading>
|
||||
{/if}
|
||||
</LoginToggle>
|
88
UI/Popup/AddNewPoint/PresetList.svelte
Normal file
88
UI/Popup/AddNewPoint/PresetList.svelte
Normal file
|
@ -0,0 +1,88 @@
|
|||
<script lang="ts">
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import SubtleButton from "../../Base/SubtleButton.svelte";
|
||||
import { Translation } from "../../i18n/Translation";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import { ImmutableStore } from "../../../Logic/UIEventSource";
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import FromHtml from "../../Base/FromHtml.svelte";
|
||||
|
||||
/**
|
||||
* This component lists all the presets and allows the user to select one
|
||||
*/
|
||||
export let state: SpecialVisualizationState;
|
||||
let layout: LayoutConfig = state.layout;
|
||||
let presets: {
|
||||
preset: PresetConfig,
|
||||
layer: LayerConfig,
|
||||
text: Translation,
|
||||
icon: string,
|
||||
tags: Record<string, string>
|
||||
}[] = [];
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
const flayer = state.layerState.filteredLayers.get(layer.id);
|
||||
if (flayer.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.name === undefined) {
|
||||
// this layer can never be toggled on in any case, so we skip the presets
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const preset of layer.presets) {
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
|
||||
const icon: string =
|
||||
layer.mapRendering[0]
|
||||
.RenderIcon(new ImmutableStore<any>(tags), false)
|
||||
.html.SetClass("w-12 h-12 block relative")
|
||||
.ConstructElement().innerHTML;
|
||||
|
||||
const description = preset.description?.FirstSentence();
|
||||
|
||||
const simplified = {
|
||||
preset,
|
||||
layer,
|
||||
icon,
|
||||
description,
|
||||
tags,
|
||||
text: Translations.t.general.add.addNew.Subs({ category: preset.title }, preset.title["context"])
|
||||
};
|
||||
presets.push(simplified);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: {preset: PresetConfig, layer: LayerConfig, icon: string, tags: Record<string, string>} }>();
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Tr t={Translations.t.general.add.intro} />
|
||||
{#each presets as preset}
|
||||
<SubtleButton on:click={() => dispatch("select", preset)}>
|
||||
<FromHtml slot="image" src={preset.icon}></FromHtml>
|
||||
<div slot="message">
|
||||
|
||||
<b>
|
||||
<Tr t={preset.text} />
|
||||
</b>
|
||||
{#if preset.description}
|
||||
<Tr t={preset.description}/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</SubtleButton>
|
||||
{/each}
|
||||
</div>
|
139
UI/Popup/CreateNewNote.svelte
Normal file
139
UI/Popup/CreateNewNote.svelte
Normal file
|
@ -0,0 +1,139 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* UIcomponent to create a new note at the given location
|
||||
*/
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource";
|
||||
import ValidatedInput from "../InputElement/ValidatedInput.svelte";
|
||||
import SubtleButton from "../Base/SubtleButton.svelte";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations.js";
|
||||
import type { Feature, Point } from "geojson";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
|
||||
export let coordinate: { lon: number, lat: number };
|
||||
export let state: SpecialVisualizationState;
|
||||
|
||||
let comment: UIEventSource<string> = LocalStorageSource.Get("note-text");
|
||||
let created = false;
|
||||
|
||||
let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note");
|
||||
|
||||
let hasFilter = notelayer?.hasFilter;
|
||||
let isDisplayed = notelayer?.isDisplayed;
|
||||
|
||||
function enableNoteLayer() {
|
||||
state.guistate.closeAll();
|
||||
isDisplayed.setData(true);
|
||||
}
|
||||
|
||||
async function uploadNote() {
|
||||
let txt = comment.data;
|
||||
if (txt === undefined || txt === "") {
|
||||
return;
|
||||
}
|
||||
const loc = coordinate;
|
||||
txt += "\n\n #MapComplete #" + state?.layout?.id;
|
||||
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt);
|
||||
console.log("Created a note, got id",id)
|
||||
const feature = <Feature<Point>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [loc.lon, loc.lat]
|
||||
},
|
||||
properties: {
|
||||
id: "" + id.id,
|
||||
date_created: new Date().toISOString(),
|
||||
_first_comment: txt,
|
||||
comments: JSON.stringify([
|
||||
{
|
||||
text: txt,
|
||||
html: txt,
|
||||
user: state.osmConnection?.userDetails?.data?.name,
|
||||
uid: state.osmConnection?.userDetails?.data?.uid
|
||||
}
|
||||
])
|
||||
}
|
||||
};
|
||||
state.newFeatures.features.data.push(feature);
|
||||
state.newFeatures.features.ping();
|
||||
state.selectedElement?.setData(feature);
|
||||
comment.setData("");
|
||||
created = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
{#if notelayer === undefined}
|
||||
<div class="alert">
|
||||
This theme does not include the layer 'note'. As a result, no nodes can be created
|
||||
</div>
|
||||
{:else if created}
|
||||
<div class="thanks">
|
||||
<Tr t={Translations.t.notes.isCreated} />
|
||||
</div>
|
||||
{:else}
|
||||
<h3>
|
||||
<Tr t={Translations.t.notes.createNoteTitle}></Tr>
|
||||
</h3>
|
||||
|
||||
{#if $isDisplayed}
|
||||
<!-- The layer is displayed, so we can add a note without worrying for duplicates -->
|
||||
{#if $hasFilter}
|
||||
<div class="flex flex-col">
|
||||
|
||||
<!-- ...but a filter is set ...-->
|
||||
<div class="alert">
|
||||
<Tr t={ Translations.t.notes.noteLayerHasFilters}></Tr>
|
||||
</div>
|
||||
<SubtleButton on:click={() => notelayer.disableAllFilters()}>
|
||||
<img slot="image" src="./assets/svg/filter.svg" class="w-8 h-8 mr-4">
|
||||
<Tr slot="message" t={Translations.t.notes.disableAllNoteFilters}></Tr>
|
||||
</SubtleButton>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Tr t={Translations.t.notes.createNoteIntro}></Tr>
|
||||
<div class="border rounded-sm border-grey-500">
|
||||
<div class="w-full p-1">
|
||||
<ValidatedInput type="text" value={comment}></ValidatedInput>
|
||||
</div>
|
||||
|
||||
<LoginToggle {state}>
|
||||
<span slot="loading"><!--empty: don't show a loading message--></span>
|
||||
<div slot="not-logged-in" class="alert">
|
||||
<Tr t={Translations.t.notes.warnAnonymous} />
|
||||
</div>
|
||||
</LoginToggle>
|
||||
|
||||
{#if $comment.length >= 3}
|
||||
<SubtleButton on:click={uploadNote}>
|
||||
<img slot="image" src="./assets/svg/addSmall.svg" class="w-8 h-8 mr-4">
|
||||
<Tr slot="message" t={ Translations.t.notes.createNote}></Tr>
|
||||
</SubtleButton>
|
||||
{:else}
|
||||
<div class="alert">
|
||||
<Tr t={ Translations.t.notes.textNeeded}></Tr>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
<div class="alert">
|
||||
<Tr t={Translations.t.notes.noteLayerNotEnabled}></Tr>
|
||||
</div>
|
||||
<SubtleButton on:click={enableNoteLayer}>
|
||||
<img slot="image" src="./assets/svg/layers.svg" class="w-8 h-8 mr-4">
|
||||
<Tr slot="message" t={Translations.t.notes.noteLayerDoEnable}></Tr>
|
||||
</SubtleButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{/if}
|
|
@ -127,7 +127,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
const allRenderings: BaseUIElement[] = [
|
||||
new VariableUiElement(
|
||||
tags
|
||||
.map((data) => data[Tag.newlyCreated.key])
|
||||
.map((data) => data["_newly_created"])
|
||||
.map((isCreated) => {
|
||||
if (isCreated === undefined) {
|
||||
return undefined
|
||||
|
|
|
@ -20,7 +20,7 @@ import CreateWayWithPointReuseAction, {
|
|||
MergePointConfig,
|
||||
} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"
|
||||
import OsmChangeAction, { OsmCreateAction } from "../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
||||
import { PresetInfo } from "../BigComponents/SimpleAddUI"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
|
|
|
@ -36,6 +36,9 @@ export class MinimapViz implements SpecialVisualization {
|
|||
keys.splice(0, 1)
|
||||
const featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map(
|
||||
(featuresById) => {
|
||||
if (featuresById === undefined) {
|
||||
return []
|
||||
}
|
||||
const properties = tagSource.data
|
||||
const features: Feature[] = []
|
||||
for (const key of keys) {
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Title from "../Base/Title"
|
||||
import ValidatedTextField from "../Input/ValidatedTextField"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
|
||||
export default class NewNoteUi extends Toggle {
|
||||
constructor(
|
||||
noteLayer: FilteredLayer,
|
||||
isShown: UIEventSource<boolean>,
|
||||
state: {
|
||||
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
|
||||
osmConnection: OsmConnection
|
||||
layoutToUse: LayoutConfig
|
||||
featurePipeline: FeaturePipeline
|
||||
selectedElement: UIEventSource<any>
|
||||
}
|
||||
) {
|
||||
const t = Translations.t.notes
|
||||
const isCreated = new UIEventSource(false)
|
||||
state.LastClickLocation.addCallbackAndRun((_) => isCreated.setData(false)) // Reset 'isCreated' on every click
|
||||
const text = ValidatedTextField.ForType("text").ConstructInputElement({
|
||||
value: LocalStorageSource.Get("note-text"),
|
||||
})
|
||||
text.SetClass("border rounded-sm border-grey-500")
|
||||
|
||||
const postNote = new SubtleButton(Svg.addSmall_svg().SetClass("max-h-7"), t.createNote)
|
||||
postNote.OnClickWithLoading(t.creating, async () => {
|
||||
let txt = text.GetValue().data
|
||||
if (txt === undefined || txt === "") {
|
||||
return
|
||||
}
|
||||
txt += "\n\n #MapComplete #" + state?.layoutToUse?.id
|
||||
const loc = state.LastClickLocation.data
|
||||
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt)
|
||||
const feature = {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [loc.lon, loc.lat],
|
||||
},
|
||||
properties: {
|
||||
id: "" + id.id,
|
||||
date_created: new Date().toISOString(),
|
||||
_first_comment: txt,
|
||||
comments: JSON.stringify([
|
||||
{
|
||||
text: txt,
|
||||
html: txt,
|
||||
user: state.osmConnection?.userDetails?.data?.name,
|
||||
uid: state.osmConnection?.userDetails?.data?.uid,
|
||||
},
|
||||
]),
|
||||
},
|
||||
}
|
||||
state?.featurePipeline?.InjectNewPoint(feature)
|
||||
state.selectedElement?.setData(feature)
|
||||
Hash.hash.setData(feature.properties.id)
|
||||
text.GetValue().setData("")
|
||||
isCreated.setData(true)
|
||||
})
|
||||
const createNoteDialog = new Combine([
|
||||
new Title(t.createNoteTitle),
|
||||
t.createNoteIntro,
|
||||
text,
|
||||
new Combine([
|
||||
new Toggle(
|
||||
undefined,
|
||||
t.warnAnonymous.SetClass("block alert"),
|
||||
state?.osmConnection?.isLoggedIn
|
||||
),
|
||||
new Toggle(
|
||||
postNote,
|
||||
t.textNeeded.SetClass("block alert"),
|
||||
text.GetValue().map((txt) => txt?.length > 3)
|
||||
),
|
||||
]).SetClass("flex justify-end items-center"),
|
||||
]).SetClass("flex flex-col border-2 border-black rounded-xl p-4")
|
||||
|
||||
const newNoteUi = new Toggle(
|
||||
new Toggle(t.isCreated.SetClass("thanks"), createNoteDialog, isCreated),
|
||||
undefined,
|
||||
new UIEventSource<boolean>(true)
|
||||
)
|
||||
|
||||
super(
|
||||
new Toggle(
|
||||
new Combine([
|
||||
t.noteLayerHasFilters.SetClass("alert"),
|
||||
new SubtleButton(Svg.filter_svg(), t.disableAllNoteFilters).onClick(() => {
|
||||
const filters = noteLayer.appliedFilters.data
|
||||
for (const key of Array.from(filters.keys())) {
|
||||
filters.set(key, undefined)
|
||||
}
|
||||
noteLayer.appliedFilters.ping()
|
||||
isShown.setData(false)
|
||||
}),
|
||||
]).SetClass("flex flex-col"),
|
||||
newNoteUi,
|
||||
noteLayer.appliedFilters.map((filters) => {
|
||||
console.log("Applied filters for notes are: ", filters)
|
||||
return Array.from(filters.values()).some((v) => v?.currentFilter !== undefined)
|
||||
})
|
||||
),
|
||||
new Combine([
|
||||
t.noteLayerNotEnabled.SetClass("alert"),
|
||||
new SubtleButton(Svg.layers_svg(), t.noteLayerDoEnable).onClick(() => {
|
||||
noteLayer.isDisplayed.setData(true)
|
||||
isShown.setData(false)
|
||||
}),
|
||||
]).SetClass("flex flex-col"),
|
||||
noteLayer.isDisplayed
|
||||
)
|
||||
}
|
||||
}
|
35
UI/Popup/TagHint.svelte
Normal file
35
UI/Popup/TagHint.svelte
Normal file
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter";
|
||||
import FromHtml from "../Base/FromHtml.svelte";
|
||||
import Constants from "../../Models/Constants.js";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
/**
|
||||
* A 'TagHint' will show the given tags in a human readable form.
|
||||
* Depending on the options, it'll link through to the wiki or might be completely hidden
|
||||
*/
|
||||
export let osmConnection: OsmConnection;
|
||||
/**
|
||||
* If given, this function will be called to embed the given tags hint into this translation
|
||||
*/
|
||||
export let embedIn: (() => Translation) | undefined = undefined;
|
||||
const userDetails = osmConnection.userDetails;
|
||||
export let tags: TagsFilter;
|
||||
let linkToWiki = false;
|
||||
onDestroy(osmConnection.userDetails.addCallbackAndRunD(userdetails => {
|
||||
linkToWiki = userdetails.csCount > Constants.userJourney.tagsVisibleAndWikiLinked;
|
||||
}));
|
||||
let tagsExplanation = "";
|
||||
$: tagsExplanation = tags?.asHumanString(linkToWiki, false, {});
|
||||
</script>
|
||||
|
||||
{#if $userDetails.loggedIn}
|
||||
{#if embedIn === undefined}
|
||||
<FromHtml src={tagsExplanation} />
|
||||
{:else}
|
||||
<Tr t={embedIn(tagsExplanation)} />
|
||||
{/if}
|
||||
{/if}
|
|
@ -18,17 +18,23 @@
|
|||
export let state: SpecialVisualizationState;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let feature: Feature;
|
||||
export let layer: LayerConfig
|
||||
export let layer: LayerConfig;
|
||||
|
||||
let txt: string;
|
||||
onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||
$: onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||
txt = t.textFor(l);
|
||||
}));
|
||||
let specs: RenderingSpecification[] = SpecialVisualizations.constructSpecification(txt);
|
||||
let specs: RenderingSpecification[] = [];
|
||||
$: {
|
||||
if (txt !== undefined) {
|
||||
specs = SpecialVisualizations.constructSpecification(txt);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each specs as specpart}
|
||||
{#if typeof specpart === "string"}
|
||||
<FromHtml src= {Utils.SubstituteKeys(specpart, $tags)}></FromHtml>
|
||||
<FromHtml src={Utils.SubstituteKeys(specpart, $tags)}></FromHtml>
|
||||
{:else if $tags !== undefined }
|
||||
<ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature, layer)}></ToSvelte>
|
||||
{/if}
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
export let state: SpecialVisualizationState;
|
||||
export let selectedElement: Feature;
|
||||
export let config: TagRenderingConfig;
|
||||
if(config === undefined){
|
||||
throw "Config is undefined in tagRenderingAnswer"
|
||||
}
|
||||
export let layer: LayerConfig
|
||||
let trs: { then: Translation; icon?: string; iconClass?: string }[];
|
||||
$: trs = Utils.NoNull(config?.GetRenderValues(_tags));
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import SpecialTranslation from "./SpecialTranslation.svelte";
|
||||
import TagHint from "../TagHint.svelte";
|
||||
|
||||
export let config: TagRenderingConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
|
@ -87,7 +88,9 @@
|
|||
<div class="border border-black subtle-background flex flex-col">
|
||||
<If condition={state.featureSwitchIsTesting}>
|
||||
<div class="flex justify-between">
|
||||
<SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
|
||||
<span>
|
||||
<SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
|
||||
</span>
|
||||
<span class="alert">{config.id}</span>
|
||||
</div>
|
||||
<SpecialTranslation slot="else" t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
|
||||
|
@ -149,8 +152,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<FromHtml src={selectedTags?.asHumanString(true, true, {})} />
|
||||
|
||||
<TagHint osmConnection={state.osmConnection} tags={selectedTags}></TagHint>
|
||||
<div>
|
||||
<!-- TagRenderingQuestion-buttons -->
|
||||
<slot name="cancel"></slot>
|
||||
|
|
|
@ -2,7 +2,11 @@ import { Store, UIEventSource } from "../Logic/UIEventSource"
|
|||
import BaseUIElement from "./BaseUIElement"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
||||
import {
|
||||
FeatureSource,
|
||||
IndexedFeatureSource,
|
||||
WritableFeatureSource,
|
||||
} from "../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../Logic/Osm/Changes"
|
||||
import { MapProperties } from "../Models/MapProperties"
|
||||
|
@ -13,12 +17,14 @@ import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
|||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import SimpleFeatureSource from "../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { MenuState } from "../Models/MenuState"
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
*/
|
||||
export interface SpecialVisualizationState {
|
||||
readonly guistate: DefaultGuiState
|
||||
readonly guistate: MenuState
|
||||
readonly layout: LayoutConfig
|
||||
readonly featureSwitches: FeatureSwitchState
|
||||
|
||||
|
@ -27,6 +33,12 @@ export interface SpecialVisualizationState {
|
|||
|
||||
readonly indexedFeatures: IndexedFeatureSource
|
||||
|
||||
/**
|
||||
* Some features will create a new element that should be displayed.
|
||||
* These can be injected by appending them to this featuresource (and pinging it)
|
||||
*/
|
||||
readonly newFeatures: WritableFeatureSource
|
||||
|
||||
readonly historicalUserLocations: WritableFeatureSource
|
||||
|
||||
readonly osmConnection: OsmConnection
|
||||
|
@ -39,6 +51,10 @@ export interface SpecialVisualizationState {
|
|||
readonly mapProperties: MapProperties
|
||||
|
||||
readonly selectedElement: UIEventSource<Feature>
|
||||
/**
|
||||
* Works together with 'selectedElement' to indicate what properties should be displayed
|
||||
*/
|
||||
readonly selectedLayer: UIEventSource<LayerConfig>
|
||||
|
||||
/**
|
||||
* If data is currently being fetched from external sources
|
||||
|
@ -54,6 +70,7 @@ export interface SpecialVisualizationState {
|
|||
readonly mangroveIdentity: MangroveIdentity
|
||||
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
||||
}
|
||||
readonly lastClickObject: WritableFeatureSource
|
||||
}
|
||||
|
||||
export interface SpecialVisualization {
|
||||
|
|
|
@ -57,6 +57,11 @@ import SvelteUIElement from "./Base/SvelteUIElement"
|
|||
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||
import QuestionViz from "./Popup/QuestionViz"
|
||||
import SimpleAddUI from "./BigComponents/SimpleAddUI"
|
||||
import { Feature } from "geojson"
|
||||
import { GeoOperations } from "../Logic/GeoOperations"
|
||||
import CreateNewNote from "./Popup/CreateNewNote.svelte"
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
|
||||
|
||||
export default class SpecialVisualizations {
|
||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
||||
|
@ -84,7 +89,10 @@ export default class SpecialVisualizations {
|
|||
}
|
||||
|
||||
if (template["type"] !== undefined) {
|
||||
console.trace("Got a non-expanded template while constructing the specification")
|
||||
console.trace(
|
||||
"Got a non-expanded template while constructing the specification:",
|
||||
template
|
||||
)
|
||||
throw "Got a non-expanded template while constructing the specification"
|
||||
}
|
||||
const allKnownSpecials = extraMappings.concat(SpecialVisualizations.specialVisualizations)
|
||||
|
@ -230,6 +238,26 @@ export default class SpecialVisualizations {
|
|||
]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public static renderExampleOfSpecial(
|
||||
state: SpecialVisualizationState,
|
||||
s: SpecialVisualization
|
||||
): BaseUIElement {
|
||||
const examples =
|
||||
s.structuredExamples === undefined
|
||||
? []
|
||||
: s.structuredExamples().map((e) => {
|
||||
return s.constr(
|
||||
state,
|
||||
new UIEventSource<Record<string, string>>(e.feature.properties),
|
||||
e.args,
|
||||
e.feature,
|
||||
undefined
|
||||
)
|
||||
})
|
||||
return new Combine([new Title(s.funcName), s.docs, ...examples])
|
||||
}
|
||||
|
||||
private static initList(): SpecialVisualization[] {
|
||||
const specialVisualizations: SpecialVisualization[] = [
|
||||
new QuestionViz(),
|
||||
|
@ -237,11 +265,14 @@ export default class SpecialVisualizations {
|
|||
funcName: "add_new_point",
|
||||
docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`",
|
||||
args: [],
|
||||
constr(state: SpecialVisualizationState): BaseUIElement {
|
||||
return new SimpleAddUI(state)
|
||||
constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement {
|
||||
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
return new SvelteUIElement(AddNewPoint, {
|
||||
state,
|
||||
coordinate: { lon, lat },
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
new HistogramViz(),
|
||||
new StealViz(),
|
||||
new MinimapViz(),
|
||||
|
@ -250,6 +281,20 @@ export default class SpecialVisualizations {
|
|||
new MultiApplyViz(),
|
||||
new ExportAsGpxViz(),
|
||||
new AddNoteCommentViz(),
|
||||
{
|
||||
funcName: "open_note",
|
||||
args: [],
|
||||
docs: "Creates a new map note on the given location. This options is placed in the 'last_click'-popup automatically if the 'notes'-layer is enabled",
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
return new SvelteUIElement(CreateNewNote, { state, coordinate: { lon, lat } })
|
||||
},
|
||||
},
|
||||
new CloseNoteButton(),
|
||||
new PlantNetDetectionViz(),
|
||||
|
||||
|
@ -680,9 +725,7 @@ export default class SpecialVisualizations {
|
|||
if (title === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new SubstitutedTranslation(title, tagsSource, state).RemoveClass(
|
||||
"w-full"
|
||||
)
|
||||
return new SubstitutedTranslation(title, tagsSource, state)
|
||||
})
|
||||
),
|
||||
},
|
||||
|
@ -960,24 +1003,4 @@ export default class SpecialVisualizations {
|
|||
|
||||
return specialVisualizations
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public static renderExampleOfSpecial(
|
||||
state: SpecialVisualizationState,
|
||||
s: SpecialVisualization
|
||||
): BaseUIElement {
|
||||
const examples =
|
||||
s.structuredExamples === undefined
|
||||
? []
|
||||
: s.structuredExamples().map((e) => {
|
||||
return s.constr(
|
||||
state,
|
||||
new UIEventSource<Record<string, string>>(e.feature.properties),
|
||||
e.args,
|
||||
e.feature,
|
||||
undefined
|
||||
)
|
||||
})
|
||||
return new Combine([new Title(s.funcName), s.docs, ...examples])
|
||||
}
|
||||
}
|
||||
|
|
18
UI/Test.svelte
Normal file
18
UI/Test.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
// Testing grounds
|
||||
import { UIEventSource } from "../Logic/UIEventSource";
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte";
|
||||
|
||||
let tab = new UIEventSource(1)
|
||||
console.log("Tab control", tab)
|
||||
|
||||
</script>
|
||||
|
||||
<TabbedGroup {tab}>
|
||||
<div slot="title0">Title 0</div>
|
||||
<div slot="content0">Content 0 loaded</div>
|
||||
|
||||
<div slot="title1">Title 1</div>
|
||||
<div slot="content1">Content 1</div>
|
||||
|
||||
</TabbedGroup>
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
|
@ -19,10 +19,14 @@
|
|||
import Geosearch from "./BigComponents/Geosearch.svelte";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
||||
import Translations from "./i18n/Translations";
|
||||
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { CogIcon, MenuIcon, EyeIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import Tr from "./Base/Tr.svelte";
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
|
||||
import FloatOver from "./Base/FloatOver.svelte";
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.js";
|
||||
import { Utils } from "../Utils.js";
|
||||
import Constants from "../Models/Constants";
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte";
|
||||
|
||||
export let layout: LayoutConfig;
|
||||
const state = new ThemeViewState(layout);
|
||||
|
@ -47,8 +51,8 @@
|
|||
</div>
|
||||
|
||||
<div class="absolute top-0 left-0 mt-2 ml-2">
|
||||
<MapControlButton on:click={() => state.guistate.welcomeMessageIsOpened.setData(true)}>
|
||||
<div class="flex mr-2 items-center">
|
||||
<MapControlButton on:click={() => state.guistate.themeIsOpened.setData(true)}>
|
||||
<div class="flex mr-2 items-center cursor-pointer">
|
||||
<img class="w-8 h-8 block mr-2" src={layout.icon}>
|
||||
<b>
|
||||
<Tr t={layout.title}></Tr>
|
||||
|
@ -56,7 +60,7 @@
|
|||
</div>
|
||||
</MapControlButton>
|
||||
<MapControlButton on:click={() =>state.guistate.menuIsOpened.setData(true)}>
|
||||
<MenuIcon class="w-8 h-8"></MenuIcon>
|
||||
<MenuIcon class="w-8 h-8 cursor-pointer"></MenuIcon>
|
||||
</MapControlButton>
|
||||
<If condition={state.featureSwitchIsTesting}>
|
||||
<span class="alert">
|
||||
|
@ -86,107 +90,118 @@
|
|||
|
||||
<div class="absolute top-0 right-0 mt-4 mr-4">
|
||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||
<Geosearch bounds={state.mapProperties.bounds} {selectedElement} {selectedLayer}></Geosearch>
|
||||
<Geosearch bounds={state.mapProperties.bounds} {selectedElement} {selectedLayer} {state}></Geosearch>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
|
||||
<If condition={state.guistate.welcomeMessageIsOpened}>
|
||||
<!-- Theme page -->
|
||||
<FloatOver>
|
||||
<div on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>Close</div>
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<Tr t={layout.title} />
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<Tr t={Translations.t.general.menu.filter} />
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel class="flex flex-col">
|
||||
<Tr t={layout.description}></Tr>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.general} />
|
||||
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
||||
<If condition={state.featureSwitches.featureSwitchAddNew}>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.addNew} />
|
||||
</If>
|
||||
{/if}
|
||||
|
||||
<!--toTheMap,
|
||||
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
|
||||
-->
|
||||
<Tr t={layout.descriptionTail}></Tr>
|
||||
<div class="m-x-8">
|
||||
<button class="subtle-background rounded w-full p-4"
|
||||
on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>
|
||||
<Tr t={Translations.t.general.openTheMap} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div class="flex flex-col">
|
||||
<!-- Filter panel -- TODO move to actual location-->
|
||||
{#each layout.layers as layer}
|
||||
<Filterview filteredLayer={state.layerState.filteredLayers.get(layer.id)}></Filterview>
|
||||
{/each}
|
||||
|
||||
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>Content 3</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</FloatOver>
|
||||
</If>
|
||||
|
||||
|
||||
<If condition={state.guistate.menuIsOpened}>
|
||||
<!-- Menu page -->
|
||||
<FloatOver>
|
||||
<div on:click={() => state.guistate.menuIsOpened.setData(false)}>Close</div>
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>About MapComplete</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Settings</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div class="flex">
|
||||
<div class="w-6">
|
||||
<ToSvelte construct={Svg.community_ui}></ToSvelte>
|
||||
</div>
|
||||
Get in touch with others
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Privacy</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel class="flex flex-col">
|
||||
About MC
|
||||
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel>User settings</TabPanel>
|
||||
<TabPanel>
|
||||
<CommunityIndexView location={state.mapProperties.location}></CommunityIndexView>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel>Privacy</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</FloatOver>
|
||||
</If>
|
||||
|
||||
{#if $selectedElement !== undefined && $selectedLayer !== undefined}
|
||||
<FloatOver>
|
||||
<FloatOver on:close={() => {selectedElement.setData(undefined)}}>
|
||||
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
||||
tags={$selectedElementTags} state={state}></SelectedElementView>
|
||||
</FloatOver>
|
||||
|
||||
{/if}
|
||||
|
||||
<If condition={state.guistate.themeIsOpened}>
|
||||
<!-- Theme page -->
|
||||
<FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}>
|
||||
<TabbedGroup tab={state.guistate.themeViewTabIndex}>
|
||||
<Tr slot="title0" t={layout.title} />
|
||||
|
||||
<div slot="content0">
|
||||
|
||||
<Tr t={layout.description}></Tr>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.general} />
|
||||
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
||||
<If condition={state.featureSwitches.featureSwitchAddNew}>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.addNew} />
|
||||
</If>
|
||||
{/if}
|
||||
|
||||
<!--toTheMap,
|
||||
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
|
||||
-->
|
||||
<Tr t={layout.descriptionTail}></Tr>
|
||||
<div class="m-x-8">
|
||||
<button class="subtle-background rounded w-full p-4"
|
||||
on:click={() => state.guistate.themeIsOpened.setData(false)}>
|
||||
<Tr t={Translations.t.general.openTheMap} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div slot="title1" class="flex">
|
||||
<If condition={state.featureSwitches.featureSwitchFilter}>
|
||||
<img class="w-4 h-4" src="./assets/svg/filter.svg">
|
||||
<Tr t={Translations.t.general.menu.filter} />
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div slot="content1" class="flex flex-col">
|
||||
{#each layout.layers as layer}
|
||||
<Filterview zoomlevel={state.mapProperties.zoom} filteredLayer={state.layerState.filteredLayers.get(layer.id)} highlightedLayer={state.guistate.highlightedLayerInFilters}></Filterview>
|
||||
{/each}
|
||||
<If condition={state.featureSwitches.featureSwitchBackgroundSelection}>
|
||||
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
|
||||
</If>
|
||||
</div>
|
||||
</TabbedGroup>
|
||||
</FloatOver>
|
||||
</If>
|
||||
|
||||
|
||||
<If condition={state.guistate.menuIsOpened}>
|
||||
<!-- Menu page -->
|
||||
<FloatOver on:close={() => state.guistate.menuIsOpened.setData(false)}>
|
||||
<TabGroup on:change={(e) => {state.guistate.menuViewTabIndex.setData(e.detail)} }>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div class="flex">
|
||||
<Tr t={Translations.t.general.aboutMapcompleteTitle}></Tr>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div class="flex">
|
||||
<CogIcon class="w-6 h-6"/>
|
||||
Settings
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div class="flex">
|
||||
<img class="w-6" src="./assets/svg/community.svg">
|
||||
Get in touch with others
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<div class="flex">
|
||||
<EyeIcon class="w-6"/>
|
||||
<Tr t={Translations.t.privacy.title}></Tr>
|
||||
</div>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels >
|
||||
<TabPanel class="flex flex-col">
|
||||
<Tr t={Translations.t.general.aboutMapcomplete.Subs({
|
||||
osmcha_link: Utils.OsmChaLinkFor(7),
|
||||
})}></Tr>
|
||||
|
||||
{Constants.vNumber}
|
||||
</TabPanel>
|
||||
<TabPanel>User settings</TabPanel>
|
||||
<TabPanel>
|
||||
<CommunityIndexView location={state.mapProperties.location}></CommunityIndexView>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ToSvelte construct={() => new PrivacyPolicy()}></ToSvelte>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</FloatOver>
|
||||
</If>
|
||||
|
||||
|
||||
<style>
|
||||
/* WARNING: This is just for demonstration.
|
||||
Using :global() in this way can be risky. */
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue