forked from MapComplete/MapComplete
Chore: reformat all files with prettier
This commit is contained in:
parent
5757ae5dea
commit
d008dcb54d
214 changed files with 8926 additions and 8196 deletions
|
@ -9,8 +9,8 @@ import IndexText from "./BigComponents/IndexText"
|
|||
import { LoginToggle } from "./Popup/LoginButton"
|
||||
import { ImmutableStore } from "../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import {QueryParameters} from "../Logic/Web/QueryParameters";
|
||||
import {OsmConnectionFeatureSwitches} from "../Logic/State/FeatureSwitchState";
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
||||
import { OsmConnectionFeatureSwitches } from "../Logic/State/FeatureSwitchState"
|
||||
|
||||
export default class AllThemesGui {
|
||||
setup() {
|
||||
|
@ -27,9 +27,10 @@ export default class AllThemesGui {
|
|||
})
|
||||
const state = new UserRelatedState(osmConnection)
|
||||
const intro = new Combine([
|
||||
new LanguagePicker(Translations.t.index.title.SupportedLanguages(), state.language).SetClass(
|
||||
"flex absolute top-2 right-3"
|
||||
),
|
||||
new LanguagePicker(
|
||||
Translations.t.index.title.SupportedLanguages(),
|
||||
state.language
|
||||
).SetClass("flex absolute top-2 right-3"),
|
||||
new IndexText(),
|
||||
])
|
||||
new Combine([
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Wrapper around 'subtleButton' with an arrow pointing to the right
|
||||
* See also: NextButton
|
||||
*/
|
||||
import SubtleButton from "./SubtleButton.svelte";
|
||||
import {ChevronLeftIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
import {createEventDispatcher} from "svelte";
|
||||
/**
|
||||
* Wrapper around 'subtleButton' with an arrow pointing to the right
|
||||
* See also: NextButton
|
||||
*/
|
||||
import SubtleButton from "./SubtleButton.svelte"
|
||||
import { ChevronLeftIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher<{ click }>()
|
||||
export let clss = ""
|
||||
const dispatch = createEventDispatcher<{ click }>()
|
||||
export let clss = ""
|
||||
</script>
|
||||
|
||||
<SubtleButton on:click={() => dispatch("click")} options={{extraClasses:clss+ " flex items-center"}}>
|
||||
<ChevronLeftIcon class="w-12 h-12" slot="image"/>
|
||||
<slot slot="message"/>
|
||||
<SubtleButton
|
||||
on:click={() => dispatch("click")}
|
||||
options={{ extraClasses: clss + " flex items-center" }}
|
||||
>
|
||||
<ChevronLeftIcon class="w-12 h-12" slot="image" />
|
||||
<slot slot="message" />
|
||||
</SubtleButton>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource.js";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource.js"
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to bind inputs
|
||||
*/
|
||||
export let selected: UIEventSource<boolean>;
|
||||
let _c: boolean = selected.data ?? true;
|
||||
export let selected: UIEventSource<boolean>
|
||||
let _c: boolean = selected.data ?? true
|
||||
$: selected.setData(_c)
|
||||
|
||||
</script>
|
||||
|
||||
<input type="checkbox" bind:checked={_c} />
|
||||
|
|
|
@ -3,84 +3,82 @@
|
|||
* This overlay element will regularly show a hand that swipes over the underlying element.
|
||||
* This element will hide as soon as the Store 'hideSignal' receives a change (which is not undefined)
|
||||
*/
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
let mainElem: HTMLElement;
|
||||
export let hideSignal: Store<any>;
|
||||
function hide(){
|
||||
mainElem.style.visibility = "hidden";
|
||||
let mainElem: HTMLElement
|
||||
export let hideSignal: Store<any>
|
||||
function hide() {
|
||||
mainElem.style.visibility = "hidden"
|
||||
}
|
||||
if (hideSignal) {
|
||||
onDestroy(hideSignal.addCallbackD(() => {
|
||||
console.log("Received hide signal")
|
||||
hide()
|
||||
return true;
|
||||
}));
|
||||
onDestroy(
|
||||
hideSignal.addCallbackD(() => {
|
||||
console.log("Received hide signal")
|
||||
hide()
|
||||
return true
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
$: {
|
||||
mainElem?.addEventListener("click",_ => hide())
|
||||
mainElem?.addEventListener("touchstart",_ => hide())
|
||||
}
|
||||
</script>
|
||||
|
||||
$: {
|
||||
mainElem?.addEventListener("click", (_) => hide())
|
||||
mainElem?.addEventListener("touchstart", (_) => hide())
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full">
|
||||
<div id="hand-container" class="pointer-events-none">
|
||||
<img src="./assets/svg/hand.svg"/>
|
||||
<img src="./assets/svg/hand.svg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@keyframes hand-drag-animation {
|
||||
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
6% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
12% {
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
24% {
|
||||
opacity: 1;
|
||||
transform: rotate(-00deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
36% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
@keyframes hand-drag-animation {
|
||||
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
6% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
#hand-container {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
left: calc(50% + 4rem);
|
||||
top: calc(50%);
|
||||
opacity: 0.7;
|
||||
animation: hand-drag-animation 4s ease-in-out infinite;
|
||||
transform-origin: 50% 125%;
|
||||
12% {
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
24% {
|
||||
opacity: 1;
|
||||
transform: rotate(-00deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
36% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
}
|
||||
|
||||
#hand-container {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
left: calc(50% + 4rem);
|
||||
top: calc(50%);
|
||||
opacity: 0.7;
|
||||
animation: hand-drag-animation 4s ease-in-out infinite;
|
||||
transform-origin: 50% 125%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource.js";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource.js"
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to bind inputs
|
||||
*/
|
||||
export let value: UIEventSource<number>;
|
||||
let i: number = value.data;
|
||||
export let value: UIEventSource<number>
|
||||
let i: number = value.data
|
||||
$: value.setData(i)
|
||||
|
||||
</script>
|
||||
|
||||
<select bind:value={i} >
|
||||
<slot></slot>
|
||||
<select bind:value={i}>
|
||||
<slot />
|
||||
</select>
|
||||
|
|
|
@ -1,31 +1,36 @@
|
|||
<script lang="ts">
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import {XCircleIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
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 }>();
|
||||
/**
|
||||
* 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 p-4 md:p-6" style="background-color: #00000088">
|
||||
<div class="content normal-background">
|
||||
<div class="rounded-xl h-full">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="close-button">
|
||||
<!-- The close button is placed _after_ the default slot in order to always paint it on top -->
|
||||
<div class="w-8 h-8 absolute right-10 top-10 cursor-pointer" on:click={() => dispatch("close")}>
|
||||
<XCircleIcon/>
|
||||
</div>
|
||||
</slot>
|
||||
<div
|
||||
class="absolute top-0 right-0 w-screen h-screen p-4 md:p-6"
|
||||
style="background-color: #00000088"
|
||||
>
|
||||
<div class="content normal-background">
|
||||
<div class="rounded-xl h-full">
|
||||
<slot />
|
||||
</div>
|
||||
<slot name="close-button">
|
||||
<!-- The close button is placed _after_ the default slot in order to always paint it on top -->
|
||||
<div
|
||||
class="w-8 h-8 absolute right-10 top-10 cursor-pointer"
|
||||
on:click={() => dispatch("close")}
|
||||
>
|
||||
<XCircleIcon />
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.content {
|
||||
height: calc( 100vh - 2rem );
|
||||
height: calc(100vh - 2rem);
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
box-shadow: 0 0 1rem #00000088;
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
* Given an HTML string, properly shows this
|
||||
*/
|
||||
|
||||
export let src: string;
|
||||
let htmlElem: HTMLElement;
|
||||
export let src: string
|
||||
let htmlElem: HTMLElement
|
||||
$: {
|
||||
if (htmlElem) {
|
||||
htmlElem.innerHTML = src;
|
||||
htmlElem.innerHTML = src
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,5 @@
|
|||
</script>
|
||||
|
||||
{#if src !== undefined}
|
||||
<span bind:this={htmlElem} class={clss}></span>
|
||||
<span bind:this={htmlElem} class={clss} />
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
|
||||
*/
|
||||
export let condition: UIEventSource<boolean>;
|
||||
let _c = condition.data;
|
||||
onDestroy(condition.addCallback(c => {
|
||||
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,
|
||||
export let condition: UIEventSource<boolean>
|
||||
let _c = condition.data
|
||||
onDestroy(
|
||||
condition.addCallback((c) => {
|
||||
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,
|
||||
which will _unregister_ the callback if `c = true`! */
|
||||
_c = c;
|
||||
return false
|
||||
}))
|
||||
|
||||
_c = c
|
||||
return false
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if _c}
|
||||
<slot></slot>
|
||||
{:else}
|
||||
<slot name="else"></slot>
|
||||
<slot />
|
||||
{:else}
|
||||
<slot name="else" />
|
||||
{/if}
|
||||
|
|
|
@ -1,33 +1,34 @@
|
|||
<script lang="ts">
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {onDestroy} from "svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
/**
|
||||
* Functions as 'If', but uses 'display:hidden' instead.
|
||||
*/
|
||||
export let condition: UIEventSource<boolean>;
|
||||
let _c = condition.data;
|
||||
let hasBeenShownPositive = false
|
||||
let hasBeenShownNegative = false
|
||||
onDestroy(condition.addCallbackAndRun(c => {
|
||||
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,
|
||||
/**
|
||||
* Functions as 'If', but uses 'display:hidden' instead.
|
||||
*/
|
||||
export let condition: UIEventSource<boolean>
|
||||
let _c = condition.data
|
||||
let hasBeenShownPositive = false
|
||||
let hasBeenShownNegative = false
|
||||
onDestroy(
|
||||
condition.addCallbackAndRun((c) => {
|
||||
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,
|
||||
which will _unregister_ the callback if `c = true`! */
|
||||
hasBeenShownPositive = hasBeenShownPositive || c
|
||||
hasBeenShownNegative = hasBeenShownNegative || !c
|
||||
_c = c;
|
||||
return false
|
||||
}))
|
||||
|
||||
hasBeenShownPositive = hasBeenShownPositive || c
|
||||
hasBeenShownNegative = hasBeenShownNegative || !c
|
||||
_c = c
|
||||
return false
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if hasBeenShownPositive}
|
||||
<span class={_c ? "" : "hidden"}>
|
||||
<slot/>
|
||||
</span>
|
||||
<span class={_c ? "" : "hidden"}>
|
||||
<slot />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if hasBeenShownNegative}
|
||||
<span class={_c ? "hidden" : ""}>
|
||||
<slot name="else"/>
|
||||
</span>
|
||||
<span class={_c ? "hidden" : ""}>
|
||||
<slot name="else" />
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
|
||||
*/
|
||||
export let condition: UIEventSource<boolean>;
|
||||
let _c = !condition.data;
|
||||
onDestroy(condition.addCallback(c => {
|
||||
_c = !c;
|
||||
return false
|
||||
}))
|
||||
export let condition: UIEventSource<boolean>
|
||||
let _c = !condition.data
|
||||
onDestroy(
|
||||
condition.addCallback((c) => {
|
||||
_c = !c
|
||||
return false
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if _c}
|
||||
<slot></slot>
|
||||
<slot />
|
||||
{/if}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<script>
|
||||
import ToSvelte from "./ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import ToSvelte from "./ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
</script>
|
||||
|
||||
<div class="pl-2 p-1 flex">
|
||||
<div class="animate-spin self-center w-6 h-6 min-w-6">
|
||||
<ToSvelte construct={Svg.loading_svg()}></ToSvelte>
|
||||
<ToSvelte construct={Svg.loading_svg()} />
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<script lang="ts">
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import Translations from "../i18n/Translations.js";
|
||||
import Tr from "./Tr.svelte";
|
||||
import ToSvelte from "./ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Translations from "../i18n/Translations.js"
|
||||
import Tr from "./Tr.svelte"
|
||||
import ToSvelte from "./ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
|
||||
export let osmConnection: OsmConnection
|
||||
export let clss = ""
|
||||
export let osmConnection: OsmConnection
|
||||
export let clss = ""
|
||||
</script>
|
||||
|
||||
<button class={clss} on:click={() => osmConnection.AttemptLogin()}>
|
||||
<ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")}/>
|
||||
<slot name="message">
|
||||
<Tr t={Translations.t.general.loginWithOpenStreetMap}/>
|
||||
</slot>
|
||||
<ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")} />
|
||||
<slot name="message">
|
||||
<Tr t={Translations.t.general.loginWithOpenStreetMap} />
|
||||
</slot>
|
||||
</button>
|
||||
|
|
|
@ -1,47 +1,45 @@
|
|||
<script lang="ts">
|
||||
import Loading from "./Loading.svelte";
|
||||
import type { OsmServiceState } from "../../Logic/Osm/OsmConnection";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "./Tr.svelte";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import {ImmutableStore, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Loading from "./Loading.svelte"
|
||||
import type { OsmServiceState } from "../../Logic/Osm/OsmConnection"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "./Tr.svelte"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
export let state: {osmConnection: OsmConnection, featureSwitches?: { featureSwitchUserbadge?: UIEventSource<boolean>}};
|
||||
export let state: {
|
||||
osmConnection: OsmConnection
|
||||
featureSwitches?: { featureSwitchUserbadge?: UIEventSource<boolean> }
|
||||
}
|
||||
/**
|
||||
* 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 ?? new ImmutableStore(true);
|
||||
const t = Translations.t.general;
|
||||
let loadingStatus = state.osmConnection.loadingStatus
|
||||
let badge = state.featureSwitches?.featureSwitchUserbadge ?? new ImmutableStore(true)
|
||||
const t = Translations.t.general
|
||||
const offlineModes: Partial<Record<OsmServiceState, Translation>> = {
|
||||
offline: t.loginFailedOfflineMode,
|
||||
unreachable: t.loginFailedUnreachableMode,
|
||||
unknown: t.loginFailedUnreachableMode,
|
||||
readonly: t.loginFailedReadonlyMode
|
||||
};
|
||||
const apiState = state.osmConnection.apiIsOnline;
|
||||
|
||||
|
||||
readonly: t.loginFailedReadonlyMode,
|
||||
}
|
||||
const apiState = state.osmConnection.apiIsOnline
|
||||
</script>
|
||||
|
||||
{#if $badge}
|
||||
{#if !ignoreLoading && $loadingStatus === "loading"}
|
||||
<slot name="loading">
|
||||
<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">
|
||||
<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>
|
||||
<slot />
|
||||
{:else if $loadingStatus === "not-attempted"}
|
||||
<slot name="not-logged-in">
|
||||
|
||||
</slot>
|
||||
<slot name="not-logged-in" />
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
/**
|
||||
* A round button with an icon and possible a small text, which hovers above the map
|
||||
*/
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher()
|
||||
export let cls = ""
|
||||
</script>
|
||||
|
||||
|
||||
<button on:click={e => dispatch("click", e)} class={"rounded-full h-fit w-fit m-0.5 md:m-1 p-0.5 sm:p-1 pointer-events-auto "+cls} >
|
||||
<slot/>
|
||||
<button
|
||||
on:click={(e) => dispatch("click", e)}
|
||||
class={"rounded-full h-fit w-fit m-0.5 md:m-1 p-0.5 sm:p-1 pointer-events-auto " + cls}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
|
||||
/**
|
||||
* The slotted element will be shown on the right side
|
||||
*/
|
||||
const dispatch = createEventDispatcher<{ close }>();
|
||||
const dispatch = createEventDispatcher<{ close }>()
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 right-0 h-screen overflow-auto w-full md:w-6/12 lg:w-5/12 xl:w-4/12 drop-shadow-2xl" style="max-width: 100vw; max-height: 100vh">
|
||||
<div
|
||||
class="absolute top-0 right-0 h-screen overflow-auto w-full md:w-6/12 lg:w-5/12 xl:w-4/12 drop-shadow-2xl"
|
||||
style="max-width: 100vw; max-height: 100vh"
|
||||
>
|
||||
<div class="flex flex-col m-0 normal-background">
|
||||
<slot name="close-button">
|
||||
<div class="w-8 h-8 absolute right-10 top-10 cursor-pointer" on:click={() => dispatch("close")}>
|
||||
<div
|
||||
class="w-8 h-8 absolute right-10 top-10 cursor-pointer"
|
||||
on:click={() => dispatch("close")}
|
||||
>
|
||||
<XCircleIcon />
|
||||
</div>
|
||||
</slot>
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Wrapper around 'subtleButton' with an arrow pointing to the right
|
||||
* See also: BackButton
|
||||
*/
|
||||
import SubtleButton from "./SubtleButton.svelte";
|
||||
import {ChevronRightIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
import {createEventDispatcher} from "svelte";
|
||||
/**
|
||||
* Wrapper around 'subtleButton' with an arrow pointing to the right
|
||||
* See also: BackButton
|
||||
*/
|
||||
import SubtleButton from "./SubtleButton.svelte"
|
||||
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher<{ click }>()
|
||||
|
||||
export let clss : string= ""
|
||||
const dispatch = createEventDispatcher<{ click }>()
|
||||
|
||||
export let clss: string = ""
|
||||
</script>
|
||||
|
||||
<SubtleButton on:click={() => dispatch("click")} options={{extraClasses: clss+" flex items-center"}}>
|
||||
<slot name="image" slot="image"/>
|
||||
<div class="w-full flex justify-between items-center" slot="message">
|
||||
<slot/>
|
||||
<ChevronRightIcon class="w-12 h-12"/>
|
||||
</div>
|
||||
<SubtleButton
|
||||
on:click={() => dispatch("click")}
|
||||
options={{ extraClasses: clss + " flex items-center" }}
|
||||
>
|
||||
<slot name="image" slot="image" />
|
||||
<div class="w-full flex justify-between items-center" slot="message">
|
||||
<slot />
|
||||
<ChevronRightIcon class="w-12 h-12" />
|
||||
</div>
|
||||
</SubtleButton>
|
||||
|
|
|
@ -1,33 +1,30 @@
|
|||
<script lang="ts">
|
||||
|
||||
import ToSvelte from "./ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import ToSvelte from "./ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
|
||||
export let generateShareData: () => {
|
||||
export let generateShareData: () => {
|
||||
text: string
|
||||
title: string
|
||||
url: string
|
||||
}
|
||||
function share(){
|
||||
}
|
||||
function share() {
|
||||
if (!navigator.share) {
|
||||
console.log("web share not supported")
|
||||
return;
|
||||
console.log("web share not supported")
|
||||
return
|
||||
}
|
||||
navigator
|
||||
.share(generateShareData())
|
||||
.then(() => {
|
||||
console.log("Thanks for sharing!")
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`Couldn't share because of`, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
.share(generateShareData())
|
||||
.then(() => {
|
||||
console.log("Thanks for sharing!")
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`Couldn't share because of`, err.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<button on:click={share} class="secondary w-8 h-8 m-0 p-0">
|
||||
<slot name="content">
|
||||
<ToSvelte construct={Svg.share_svg().SetClass("w-7 h-7 p-1")}/>
|
||||
</slot>
|
||||
<slot name="content">
|
||||
<ToSvelte construct={Svg.share_svg().SetClass("w-7 h-7 p-1")} />
|
||||
</slot>
|
||||
</button>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Img from "./Img";
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Img from "./Img"
|
||||
|
||||
export let imageUrl: string | BaseUIElement = undefined
|
||||
export let message: string | BaseUIElement = undefined
|
||||
|
@ -10,22 +10,22 @@
|
|||
extraClasses?: string
|
||||
} = {}
|
||||
|
||||
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11");
|
||||
const dispatch = createEventDispatcher<{click}>()
|
||||
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11")
|
||||
const dispatch = createEventDispatcher<{ click }>()
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={(options.extraClasses??"") + ' secondary no-image-background'}
|
||||
class={(options.extraClasses ?? "") + " secondary no-image-background"}
|
||||
target={options?.newTab ? "_blank" : ""}
|
||||
on:click={(e) => dispatch("click", e)}
|
||||
>
|
||||
<slot name="image">
|
||||
{#if imageUrl !== undefined}
|
||||
{#if typeof imageUrl === "string"}
|
||||
<Img src={imageUrl} class={imgClasses}></Img>
|
||||
<Img src={imageUrl} class={imgClasses} />
|
||||
{/if}
|
||||
{/if}
|
||||
</slot>
|
||||
|
||||
<slot name="message"/>
|
||||
<slot name="message" />
|
||||
</button>
|
||||
|
|
|
@ -5,10 +5,10 @@ import { VariableUiElement } from "./VariableUIElement"
|
|||
import Lazy from "./Lazy"
|
||||
import Loading from "./Loading"
|
||||
import SvelteUIElement from "./SvelteUIElement"
|
||||
import SubtleLink from "./SubtleLink.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Combine from "./Combine";
|
||||
import Img from "./Img";
|
||||
import SubtleLink from "./SubtleLink.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Combine from "./Combine"
|
||||
import Img from "./Img"
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
|
@ -40,26 +40,28 @@ export class SubtleButton extends UIElement {
|
|||
}
|
||||
|
||||
protected InnerRender(): string | BaseUIElement {
|
||||
if(this.options.url !== undefined){
|
||||
return new SvelteUIElement(SubtleLink, {href: this.options.url, newTab: this.options.newTab})
|
||||
if (this.options.url !== undefined) {
|
||||
return new SvelteUIElement(SubtleLink, {
|
||||
href: this.options.url,
|
||||
newTab: this.options.newTab,
|
||||
})
|
||||
}
|
||||
|
||||
const classes = "button";
|
||||
const message = Translations.W(this.message)?.SetClass("block overflow-ellipsis no-images flex-shrink");
|
||||
let img;
|
||||
const imgClasses = "block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11")
|
||||
const classes = "button"
|
||||
const message = Translations.W(this.message)?.SetClass(
|
||||
"block overflow-ellipsis no-images flex-shrink"
|
||||
)
|
||||
let img
|
||||
const imgClasses =
|
||||
"block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11")
|
||||
if ((this.imageUrl ?? "") === "") {
|
||||
img = undefined;
|
||||
} else if (typeof (this.imageUrl) === "string") {
|
||||
img = undefined
|
||||
} else if (typeof this.imageUrl === "string") {
|
||||
img = new Img(this.imageUrl)?.SetClass(imgClasses)
|
||||
} else {
|
||||
img = this.imageUrl?.SetClass(imgClasses);
|
||||
img = this.imageUrl?.SetClass(imgClasses)
|
||||
}
|
||||
const button = new Combine([
|
||||
img,
|
||||
message
|
||||
]).SetClass("flex items-center group w-full")
|
||||
|
||||
const button = new Combine([img, message]).SetClass("flex items-center group w-full")
|
||||
|
||||
this.SetClass(classes)
|
||||
return button
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Img from "./Img";
|
||||
import { onMount } from "svelte"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Img from "./Img"
|
||||
|
||||
export let imageUrl: string | BaseUIElement = undefined
|
||||
export let href: string
|
||||
|
@ -10,10 +10,9 @@
|
|||
imgSize?: string
|
||||
// extraClasses?: string
|
||||
} = {}
|
||||
|
||||
|
||||
let imgElem: HTMLElement;
|
||||
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11");
|
||||
let imgElem: HTMLElement
|
||||
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11")
|
||||
|
||||
onMount(() => {
|
||||
// Image
|
||||
|
@ -27,24 +26,23 @@
|
|||
}
|
||||
if (img) imgElem.replaceWith(img.ConstructElement())
|
||||
}
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<a
|
||||
class={(options.extraClasses??"") + ' button text-ellipsis'}
|
||||
class={(options.extraClasses ?? "") + " button text-ellipsis"}
|
||||
{href}
|
||||
target={(newTab ? "_blank" : undefined) }
|
||||
target={newTab ? "_blank" : undefined}
|
||||
>
|
||||
<slot name="image">
|
||||
{#if imageUrl !== undefined}
|
||||
{#if typeof imageUrl === "string"}
|
||||
<Img src={imageUrl} class={imgClasses}></Img>
|
||||
{:else }
|
||||
<Img src={imageUrl} class={imgClasses} />
|
||||
{:else}
|
||||
<template bind:this={imgElem} />
|
||||
{/if}
|
||||
{/if}
|
||||
</slot>
|
||||
|
||||
<slot/>
|
||||
<slot />
|
||||
</a>
|
||||
|
|
|
@ -1,121 +1,126 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Thin wrapper around 'TabGroup' which binds the state
|
||||
*/
|
||||
/**
|
||||
* Thin wrapper around 'TabGroup' which binds the state
|
||||
*/
|
||||
|
||||
import {Tab, TabGroup, TabList, TabPanel, TabPanels} from "@rgossiaux/svelte-headlessui";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
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)
|
||||
}
|
||||
export let tab: UIEventSource<number>
|
||||
let tabElements: HTMLElement[] = []
|
||||
$: tabElements[$tab]?.click()
|
||||
$: {
|
||||
if (tabElements[tab.data]) {
|
||||
window.setTimeout(() => tabElements[tab.data].click(), 50)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="tabbedgroup w-full h-full">
|
||||
<TabGroup class="h-full w-full flex flex-col" defaultIndex={1}
|
||||
on:change={(e) =>{if(e.detail >= 0){tab.setData( e.detail); }} }>
|
||||
<div class="interactive flex items-center justify-between sticky top-0">
|
||||
<TabList class="flex flex-wrap">
|
||||
{#if $$slots.title1}
|
||||
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[0]} class="flex">
|
||||
<slot name="title0">
|
||||
Tab 0
|
||||
</slot>
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if $$slots.title1}
|
||||
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[1]} class="flex">
|
||||
<slot name="title1"/>
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if $$slots.title2}
|
||||
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[2]} class="flex">
|
||||
<slot name="title2"/>
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if $$slots.title3}
|
||||
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[3]} class="flex">
|
||||
<slot name="title3"/>
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if $$slots.title4}
|
||||
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[4]} class="flex">
|
||||
<slot name="title4"/>
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
</TabList>
|
||||
<slot name="post-tablist"/>
|
||||
</div>
|
||||
<div class="overflow-y-auto normal-background">
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</TabGroup>
|
||||
<TabGroup
|
||||
class="h-full w-full flex flex-col"
|
||||
defaultIndex={1}
|
||||
on:change={(e) => {
|
||||
if (e.detail >= 0) {
|
||||
tab.setData(e.detail)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="interactive flex items-center justify-between sticky top-0">
|
||||
<TabList class="flex flex-wrap">
|
||||
{#if $$slots.title1}
|
||||
<Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[0]} class="flex">
|
||||
<slot name="title0">Tab 0</slot>
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if $$slots.title1}
|
||||
<Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[1]} class="flex">
|
||||
<slot name="title1" />
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if $$slots.title2}
|
||||
<Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[2]} class="flex">
|
||||
<slot name="title2" />
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if $$slots.title3}
|
||||
<Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[3]} class="flex">
|
||||
<slot name="title3" />
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if $$slots.title4}
|
||||
<Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
|
||||
<div bind:this={tabElements[4]} class="flex">
|
||||
<slot name="title4" />
|
||||
</div>
|
||||
</Tab>
|
||||
{/if}
|
||||
</TabList>
|
||||
<slot name="post-tablist" />
|
||||
</div>
|
||||
<div class="overflow-y-auto normal-background">
|
||||
<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>
|
||||
</div>
|
||||
</TabGroup>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tabbedgroup {
|
||||
max-height: 100vh;
|
||||
height: 100%;
|
||||
}
|
||||
.tabbedgroup {
|
||||
max-height: 100vh;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.tab) {
|
||||
margin: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
:global(.tab) {
|
||||
margin: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
:global(.tab .flex) {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
:global(.tab .flex) {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
:global(.tab span|div) {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
display: flex;
|
||||
}
|
||||
:global(.tab span|div) {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:global(.tab-selected svg) {
|
||||
fill: var(--catch-detail-color-contrast);
|
||||
}
|
||||
:global(.tab-selected svg) {
|
||||
fill: var(--catch-detail-color-contrast);
|
||||
}
|
||||
|
||||
:global(.tab-unselected) {
|
||||
background-color: var(--background-color) !important;
|
||||
color: var(--foreground-color) !important;
|
||||
}
|
||||
:global(.tab-unselected) {
|
||||
background-color: var(--background-color) !important;
|
||||
color: var(--foreground-color) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -78,7 +78,7 @@ export default class Table extends BaseUIElement {
|
|||
for (let j = 0; j < row.length; j++) {
|
||||
try {
|
||||
let elem = row[j]
|
||||
if(elem?.ConstructElement === undefined){
|
||||
if (elem?.ConstructElement === undefined) {
|
||||
continue
|
||||
}
|
||||
const htmlElem = elem?.ConstructElement()
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
<script lang="ts">
|
||||
import BaseUIElement from "../BaseUIElement.js";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import BaseUIElement from "../BaseUIElement.js"
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
|
||||
export let construct: BaseUIElement | (() => BaseUIElement);
|
||||
let elem: HTMLElement;
|
||||
let html: HTMLElement;
|
||||
export let construct: BaseUIElement | (() => BaseUIElement)
|
||||
let elem: HTMLElement
|
||||
let html: HTMLElement
|
||||
onMount(() => {
|
||||
const uiElem = typeof construct === "function"
|
||||
? construct() : construct;
|
||||
html =uiElem?.ConstructElement();
|
||||
const uiElem = typeof construct === "function" ? construct() : construct
|
||||
html = uiElem?.ConstructElement()
|
||||
if (html !== undefined) {
|
||||
elem.replaceWith(html);
|
||||
elem.replaceWith(html)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
html?.remove();
|
||||
});
|
||||
|
||||
html?.remove()
|
||||
})
|
||||
</script>
|
||||
|
||||
<span bind:this={elem} />
|
||||
|
|
|
@ -2,36 +2,37 @@
|
|||
/**
|
||||
* Properly renders a translation
|
||||
*/
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import { onDestroy } from "svelte";
|
||||
import Locale from "../i18n/Locale";
|
||||
import { Utils } from "../../Utils";
|
||||
import FromHtml from "./FromHtml.svelte";
|
||||
import WeblateLink from "./WeblateLink.svelte";
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { onDestroy } from "svelte"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { Utils } from "../../Utils"
|
||||
import FromHtml from "./FromHtml.svelte"
|
||||
import WeblateLink from "./WeblateLink.svelte"
|
||||
|
||||
export let t: Translation;
|
||||
export let t: Translation
|
||||
export let cls: string = ""
|
||||
export let tags: Record<string, string> | undefined = undefined;
|
||||
export let tags: Record<string, string> | undefined = undefined
|
||||
// Text for the current language
|
||||
let txt: string | undefined;
|
||||
|
||||
$: onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||
const translation = t?.textFor(l);
|
||||
if (translation === undefined) {
|
||||
return;
|
||||
}
|
||||
if (tags) {
|
||||
txt = Utils.SubstituteKeys(txt, tags);
|
||||
} else {
|
||||
txt = translation;
|
||||
}
|
||||
}));
|
||||
let txt: string | undefined
|
||||
|
||||
$: onDestroy(
|
||||
Locale.language.addCallbackAndRunD((l) => {
|
||||
const translation = t?.textFor(l)
|
||||
if (translation === undefined) {
|
||||
return
|
||||
}
|
||||
if (tags) {
|
||||
txt = Utils.SubstituteKeys(txt, tags)
|
||||
} else {
|
||||
txt = translation
|
||||
}
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if t}
|
||||
<span class={cls}>
|
||||
<FromHtml src={txt}></FromHtml>
|
||||
<WeblateLink context={t.context}></WeblateLink>
|
||||
<FromHtml src={txt} />
|
||||
<WeblateLink context={t.context} />
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
@ -1,25 +1,32 @@
|
|||
<script lang="ts">
|
||||
import Locale from "../i18n/Locale";
|
||||
import LinkToWeblate from "./LinkToWeblate";
|
||||
import Locale from "../i18n/Locale"
|
||||
import LinkToWeblate from "./LinkToWeblate"
|
||||
|
||||
/**
|
||||
* Shows a small icon which will open up weblate; a contributor can translate the item for 'context' there
|
||||
*/
|
||||
export let context : string
|
||||
export let context: string
|
||||
|
||||
let linkToWeblate = Locale.showLinkToWeblate;
|
||||
let linkOnMobile = Locale.showLinkOnMobile;
|
||||
let language = Locale.language;
|
||||
let linkToWeblate = Locale.showLinkToWeblate
|
||||
let linkOnMobile = Locale.showLinkOnMobile
|
||||
let language = Locale.language
|
||||
</script>
|
||||
|
||||
|
||||
{#if !!context && context.indexOf(":") > 0}
|
||||
{#if $linkOnMobile}
|
||||
<a href={LinkToWeblate.hrefToWeblate($language, context)} target="_blank" class="mx-1 weblate-link">
|
||||
<a
|
||||
href={LinkToWeblate.hrefToWeblate($language, context)}
|
||||
target="_blank"
|
||||
class="mx-1 weblate-link"
|
||||
>
|
||||
<img src="./assets/svg/translate.svg" class="font-gray" />
|
||||
</a>
|
||||
{:else if $linkToWeblate}
|
||||
<a href={LinkToWeblate.hrefToWeblate($language, context)} class="weblate-link hidden-on-mobile mx-1" target="_blank">
|
||||
<a
|
||||
href={LinkToWeblate.hrefToWeblate($language, context)}
|
||||
class="weblate-link hidden-on-mobile mx-1"
|
||||
target="_blank"
|
||||
>
|
||||
<img src="./assets/svg/translate.svg" class="font-gray inline-block" />
|
||||
</a>
|
||||
{/if}
|
||||
|
|
|
@ -1,80 +1,88 @@
|
|||
<script lang="ts">
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {RasterLayerPolygon} from "../../Models/RasterLayers";
|
||||
import {AvailableRasterLayers} from "../../Models/RasterLayers";
|
||||
import {createEventDispatcher, onDestroy} from "svelte";
|
||||
import Svg from "../../Svg";
|
||||
import {Map as MlMap} from "maplibre-gl"
|
||||
import type {MapProperties} from "../../Models/MapProperties";
|
||||
import OverlayMap from "../Map/OverlayMap.svelte";
|
||||
import RasterLayerPicker from "../Map/RasterLayerPicker.svelte";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import Svg from "../../Svg"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import OverlayMap from "../Map/OverlayMap.svelte"
|
||||
import RasterLayerPicker from "../Map/RasterLayerPicker.svelte"
|
||||
|
||||
export let mapproperties: MapProperties
|
||||
export let normalMap: UIEventSource<MlMap>
|
||||
/**
|
||||
* The current background (raster) layer of the polygon.
|
||||
* This is undefined if a vector layer is used
|
||||
*/
|
||||
let rasterLayer: UIEventSource<RasterLayerPolygon | undefined> = mapproperties.rasterLayer
|
||||
let name = rasterLayer.data?.properties?.name
|
||||
let icon = Svg.satellite_svg()
|
||||
onDestroy(rasterLayer.addCallback(polygon => {
|
||||
name = polygon.properties?.name
|
||||
}))
|
||||
/**
|
||||
* The layers that this component can offer as a choice.
|
||||
*/
|
||||
export let availableRasterLayers: Store<RasterLayerPolygon[]>
|
||||
export let mapproperties: MapProperties
|
||||
export let normalMap: UIEventSource<MlMap>
|
||||
/**
|
||||
* The current background (raster) layer of the polygon.
|
||||
* This is undefined if a vector layer is used
|
||||
*/
|
||||
let rasterLayer: UIEventSource<RasterLayerPolygon | undefined> = mapproperties.rasterLayer
|
||||
let name = rasterLayer.data?.properties?.name
|
||||
let icon = Svg.satellite_svg()
|
||||
onDestroy(
|
||||
rasterLayer.addCallback((polygon) => {
|
||||
name = polygon.properties?.name
|
||||
})
|
||||
)
|
||||
/**
|
||||
* The layers that this component can offer as a choice.
|
||||
*/
|
||||
export let availableRasterLayers: Store<RasterLayerPolygon[]>
|
||||
|
||||
let raster0 = new UIEventSource<RasterLayerPolygon>(undefined)
|
||||
let raster0 = new UIEventSource<RasterLayerPolygon>(undefined)
|
||||
|
||||
let raster1 = new UIEventSource<RasterLayerPolygon>(undefined)
|
||||
let raster1 = new UIEventSource<RasterLayerPolygon>(undefined)
|
||||
|
||||
let currentLayer: RasterLayerPolygon
|
||||
let currentLayer: RasterLayerPolygon
|
||||
|
||||
function updatedAltLayer() {
|
||||
const available = availableRasterLayers.data
|
||||
const current = rasterLayer.data
|
||||
const defaultLayer = AvailableRasterLayers.maplibre
|
||||
const firstOther = available.find(l => l !== defaultLayer)
|
||||
const secondOther = available.find(l => l !== defaultLayer && l !== firstOther)
|
||||
raster0.setData(firstOther === current ? defaultLayer : firstOther)
|
||||
raster1.setData(secondOther === current ? defaultLayer : secondOther)
|
||||
function updatedAltLayer() {
|
||||
const available = availableRasterLayers.data
|
||||
const current = rasterLayer.data
|
||||
const defaultLayer = AvailableRasterLayers.maplibre
|
||||
const firstOther = available.find((l) => l !== defaultLayer)
|
||||
const secondOther = available.find((l) => l !== defaultLayer && l !== firstOther)
|
||||
raster0.setData(firstOther === current ? defaultLayer : firstOther)
|
||||
raster1.setData(secondOther === current ? defaultLayer : secondOther)
|
||||
}
|
||||
|
||||
updatedAltLayer()
|
||||
onDestroy(mapproperties.rasterLayer.addCallbackAndRunD(updatedAltLayer))
|
||||
onDestroy(availableRasterLayers.addCallbackAndRunD(updatedAltLayer))
|
||||
|
||||
function use(rasterLayer: UIEventSource<RasterLayerPolygon>): () => void {
|
||||
return () => {
|
||||
currentLayer = undefined
|
||||
mapproperties.rasterLayer.setData(rasterLayer.data)
|
||||
}
|
||||
}
|
||||
|
||||
updatedAltLayer()
|
||||
onDestroy(mapproperties.rasterLayer.addCallbackAndRunD(updatedAltLayer))
|
||||
onDestroy(availableRasterLayers.addCallbackAndRunD(updatedAltLayer))
|
||||
|
||||
function use(rasterLayer: UIEventSource<RasterLayerPolygon>): (() => void) {
|
||||
return () => {
|
||||
currentLayer = undefined
|
||||
mapproperties.rasterLayer.setData(rasterLayer.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const dispatch = createEventDispatcher<{ copyright_clicked }>()
|
||||
|
||||
const dispatch = createEventDispatcher<{ copyright_clicked }>()
|
||||
</script>
|
||||
|
||||
<div class="flex items-end opacity-50 hover:opacity-100">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0"
|
||||
on:click={use(raster0)}>
|
||||
<OverlayMap placedOverMap={normalMap} placedOverMapProperties={mapproperties} rasterLayer={raster0}/>
|
||||
</button>
|
||||
<button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0 " on:click={use(raster1)}>
|
||||
<OverlayMap placedOverMap={normalMap} placedOverMapProperties={mapproperties} rasterLayer={raster1}/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm flex flex-col gap-y-1 h-fit ml-1">
|
||||
|
||||
<div class="low-interaction rounded p-1 w-64">
|
||||
<RasterLayerPicker availableLayers={availableRasterLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
|
||||
</div>
|
||||
|
||||
<button class="small" on:click={() => dispatch("copyright_clicked")}>
|
||||
© OpenStreetMap
|
||||
</button>
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0" on:click={use(raster0)}>
|
||||
<OverlayMap
|
||||
placedOverMap={normalMap}
|
||||
placedOverMapProperties={mapproperties}
|
||||
rasterLayer={raster0}
|
||||
/>
|
||||
</button>
|
||||
<button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0 " on:click={use(raster1)}>
|
||||
<OverlayMap
|
||||
placedOverMap={normalMap}
|
||||
placedOverMapProperties={mapproperties}
|
||||
rasterLayer={raster1}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm flex flex-col gap-y-1 h-fit ml-1">
|
||||
<div class="low-interaction rounded p-1 w-64">
|
||||
<RasterLayerPicker
|
||||
availableLayers={availableRasterLayers}
|
||||
value={mapproperties.rasterLayer}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="small" on:click={() => dispatch("copyright_clicked")}>© OpenStreetMap</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import {Store} from "../../Logic/UIEventSource"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import licenses from "../../assets/generated/license_info.json"
|
||||
import SmallLicense from "../../Models/smallLicense"
|
||||
import {Utils} from "../../Utils"
|
||||
import { Utils } from "../../Utils"
|
||||
import Link from "../Base/Link"
|
||||
import {VariableUiElement} from "../Base/VariableUIElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import contributors from "../../assets/contributors.json"
|
||||
import translators from "../../assets/translators.json"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Title from "../Base/Title"
|
||||
import {BBox} from "../../Logic/BBox"
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Constants from "../../Models/Constants"
|
||||
import ContributorCount from "../../Logic/ContributorCount"
|
||||
import Img from "../Base/Img"
|
||||
import {TypedTranslation} from "../i18n/Translation"
|
||||
import { TypedTranslation } from "../i18n/Translation"
|
||||
import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import {RasterLayerPolygon} from "../../Models/RasterLayers";
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
|
||||
/**
|
||||
* The attribution panel in the theme menu.
|
||||
|
@ -29,7 +29,10 @@ export default class CopyrightPanel extends Combine {
|
|||
|
||||
constructor(state: {
|
||||
layout: LayoutConfig
|
||||
mapProperties: { readonly bounds: Store<BBox>, readonly rasterLayer: Store<RasterLayerPolygon> }
|
||||
mapProperties: {
|
||||
readonly bounds: Store<BBox>
|
||||
readonly rasterLayer: Store<RasterLayerPolygon>
|
||||
}
|
||||
osmConnection: OsmConnection
|
||||
dataIsLoading: Store<boolean>
|
||||
perLayer: ReadonlyMap<string, GeoIndexedStore>
|
||||
|
@ -90,27 +93,34 @@ export default class CopyrightPanel extends Combine {
|
|||
new Title(t.attributionTitle),
|
||||
t.attributionContent,
|
||||
|
||||
new VariableUiElement(state.mapProperties.rasterLayer.mapD(layer => {
|
||||
const props = layer.properties
|
||||
const attrUrl = props.attribution?.url
|
||||
const attrText = props.attribution?.text
|
||||
new VariableUiElement(
|
||||
state.mapProperties.rasterLayer.mapD((layer) => {
|
||||
const props = layer.properties
|
||||
const attrUrl = props.attribution?.url
|
||||
const attrText = props.attribution?.text
|
||||
|
||||
let bgAttr: BaseUIElement | string = undefined
|
||||
if(attrText && attrUrl){
|
||||
bgAttr = "<a href='"+attrUrl+"' target='_blank'>"+attrText+"</a>"
|
||||
}else if(attrUrl){
|
||||
bgAttr = attrUrl
|
||||
}else{
|
||||
bgAttr = attrText
|
||||
}
|
||||
if(bgAttr){
|
||||
return Translations.t.general.attribution.attributionBackgroundLayerWithCopyright.Subs({
|
||||
name: props.name,
|
||||
copyright: bgAttr
|
||||
})
|
||||
}
|
||||
return Translations.t.general.attribution.attributionBackgroundLayer.Subs(props)
|
||||
})),
|
||||
let bgAttr: BaseUIElement | string = undefined
|
||||
if (attrText && attrUrl) {
|
||||
bgAttr =
|
||||
"<a href='" + attrUrl + "' target='_blank'>" + attrText + "</a>"
|
||||
} else if (attrUrl) {
|
||||
bgAttr = attrUrl
|
||||
} else {
|
||||
bgAttr = attrText
|
||||
}
|
||||
if (bgAttr) {
|
||||
return Translations.t.general.attribution.attributionBackgroundLayerWithCopyright.Subs(
|
||||
{
|
||||
name: props.name,
|
||||
copyright: bgAttr,
|
||||
}
|
||||
)
|
||||
}
|
||||
return Translations.t.general.attribution.attributionBackgroundLayer.Subs(
|
||||
props
|
||||
)
|
||||
})
|
||||
),
|
||||
|
||||
maintainer,
|
||||
dataContributors,
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
import {UIElement} from "../UIElement"
|
||||
import { UIElement } from "../UIElement"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import {Store} from "../../Logic/UIEventSource"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
|
||||
import Img from "../Base/Img"
|
||||
import {SubtleButton} from "../Base/SubtleButton"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import Locale from "../i18n/Locale"
|
||||
import {Utils} from "../../Utils"
|
||||
import { Utils } from "../../Utils"
|
||||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
import {Translation} from "../i18n/Translation"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
|
||||
interface ExtraLinkButtonState {
|
||||
layout: { id: string; title: Translation }
|
||||
featureSwitches: { featureSwitchWelcomeMessage: Store<boolean> },
|
||||
featureSwitches: { featureSwitchWelcomeMessage: Store<boolean> }
|
||||
mapProperties: {
|
||||
location: Store<{ lon: number, lat: number }>;
|
||||
location: Store<{ lon: number; lat: number }>
|
||||
zoom: Store<number>
|
||||
}
|
||||
}
|
||||
|
@ -23,10 +23,7 @@ export default class ExtraLinkButton extends UIElement {
|
|||
private readonly _config: ExtraLinkConfig
|
||||
private readonly state: ExtraLinkButtonState
|
||||
|
||||
constructor(
|
||||
state: ExtraLinkButtonState,
|
||||
config: ExtraLinkConfig
|
||||
) {
|
||||
constructor(state: ExtraLinkButtonState, config: ExtraLinkConfig) {
|
||||
super()
|
||||
this.state = state
|
||||
this._config = config
|
||||
|
@ -45,21 +42,24 @@ export default class ExtraLinkButton extends UIElement {
|
|||
}
|
||||
|
||||
if (c.requirements?.has("no-iframe") && isIframe) {
|
||||
return undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
let link: BaseUIElement
|
||||
const theme = this.state.layout?.id ?? ""
|
||||
const basepath = window.location.host
|
||||
const href = this.state.mapProperties.location.map((loc) => {
|
||||
const subs = {
|
||||
...loc,
|
||||
theme: theme,
|
||||
basepath,
|
||||
language: Locale.language.data,
|
||||
}
|
||||
return Utils.SubstituteKeys(c.href, subs)
|
||||
}, [this.state.mapProperties.zoom])
|
||||
const href = this.state.mapProperties.location.map(
|
||||
(loc) => {
|
||||
const subs = {
|
||||
...loc,
|
||||
theme: theme,
|
||||
basepath,
|
||||
language: Locale.language.data,
|
||||
}
|
||||
return Utils.SubstituteKeys(c.href, subs)
|
||||
},
|
||||
[this.state.mapProperties.zoom]
|
||||
)
|
||||
|
||||
let img: BaseUIElement = Svg.pop_out_svg()
|
||||
if (c.icon !== undefined) {
|
||||
|
@ -81,11 +81,19 @@ export default class ExtraLinkButton extends UIElement {
|
|||
})
|
||||
|
||||
if (c.requirements?.has("no-welcome-message")) {
|
||||
link = new Toggle(undefined, link, this.state.featureSwitches.featureSwitchWelcomeMessage)
|
||||
link = new Toggle(
|
||||
undefined,
|
||||
link,
|
||||
this.state.featureSwitches.featureSwitchWelcomeMessage
|
||||
)
|
||||
}
|
||||
|
||||
if (c.requirements?.has("welcome-message")) {
|
||||
link = new Toggle(link, undefined, this.state.featureSwitches.featureSwitchWelcomeMessage)
|
||||
link = new Toggle(
|
||||
link,
|
||||
undefined,
|
||||
this.state.featureSwitches.featureSwitchWelcomeMessage
|
||||
)
|
||||
}
|
||||
|
||||
return link
|
||||
|
|
|
@ -1,107 +1,110 @@
|
|||
<script lang="ts">/**
|
||||
* 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";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Checkbox from "../Base/Checkbox.svelte";
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
||||
import type {Writable} from "svelte/store";
|
||||
import If from "../Base/If.svelte";
|
||||
import Dropdown from "../Base/Dropdown.svelte";
|
||||
import {onDestroy} from "svelte";
|
||||
import {ImmutableStore, Store} from "../../Logic/UIEventSource";
|
||||
import FilterviewWithFields from "./FilterviewWithFields.svelte";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
<script lang="ts">
|
||||
/**
|
||||
* 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"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Checkbox from "../Base/Checkbox.svelte"
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
||||
import type { Writable } from "svelte/store"
|
||||
import If from "../Base/If.svelte"
|
||||
import Dropdown from "../Base/Dropdown.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import { ImmutableStore, Store } 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 highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined);
|
||||
export let zoomlevel: Store<number> = new ImmutableStore(22);
|
||||
let layer: LayerConfig = filteredLayer.layerDef;
|
||||
let isDisplayed: Store<boolean> = filteredLayer.isDisplayed;
|
||||
export let filteredLayer: FilteredLayer
|
||||
export let highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined)
|
||||
export let zoomlevel: Store<number> = new ImmutableStore(22)
|
||||
let layer: LayerConfig = filteredLayer.layerDef
|
||||
let isDisplayed: Store<boolean> = filteredLayer.isDisplayed
|
||||
|
||||
/**
|
||||
* Gets a UIEventSource as boolean for the given option, to be used with a checkbox
|
||||
*/
|
||||
function getBooleanStateFor(option: FilterConfig): Writable<boolean> {
|
||||
const state = filteredLayer.appliedFilters.get(option.id);
|
||||
return state.sync(f => f === 0, [], (b) => b ? 0 : undefined);
|
||||
}
|
||||
/**
|
||||
* Gets a UIEventSource as boolean for the given option, to be used with a checkbox
|
||||
*/
|
||||
function getBooleanStateFor(option: FilterConfig): Writable<boolean> {
|
||||
const state = filteredLayer.appliedFilters.get(option.id)
|
||||
return state.sync(
|
||||
(f) => f === 0,
|
||||
[],
|
||||
(b) => (b ? 0 : undefined)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a UIEventSource as number for the given option, to be used with a dropdown or radiobutton
|
||||
*/
|
||||
function getStateFor(option: FilterConfig): Writable<number> {
|
||||
return filteredLayer.appliedFilters.get(option.id);
|
||||
}
|
||||
/**
|
||||
* Gets a UIEventSource as number for the given option, to be used with a dropdown or radiobutton
|
||||
*/
|
||||
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");
|
||||
}
|
||||
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 bind:this={mainElem} class="mb-1.5">
|
||||
<label class="flex gap-1 no-image-background">
|
||||
<Checkbox selected={isDisplayed}/>
|
||||
<If condition={filteredLayer.isDisplayed}>
|
||||
<ToSvelte
|
||||
construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background")}></ToSvelte>
|
||||
<ToSvelte slot="else"
|
||||
construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background opacity-50")}></ToSvelte>
|
||||
</If>
|
||||
<div bind:this={mainElem} class="mb-1.5">
|
||||
<label class="flex gap-1 no-image-background">
|
||||
<Checkbox selected={isDisplayed} />
|
||||
<If condition={filteredLayer.isDisplayed}>
|
||||
<ToSvelte
|
||||
construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background")}
|
||||
/>
|
||||
<ToSvelte
|
||||
slot="else"
|
||||
construct={() =>
|
||||
layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background opacity-50")}
|
||||
/>
|
||||
</If>
|
||||
|
||||
{filteredLayer.layerDef.name}
|
||||
{filteredLayer.layerDef.name}
|
||||
|
||||
{#if $zoomlevel < layer.minzoom}
|
||||
{#if $zoomlevel < layer.minzoom}
|
||||
<span class="alert">
|
||||
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer}/>
|
||||
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
{#if $isDisplayed && filteredLayer.layerDef.filters?.length > 0}
|
||||
<div id="subfilters" class="flex flex-col gap-y-1 ml-4">
|
||||
{#each filteredLayer.layerDef.filters as filter}
|
||||
<div>
|
||||
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with searchable fields -->
|
||||
{#if filter.options.length === 1 && filter.options[0].fields.length === 0}
|
||||
<label>
|
||||
<Checkbox selected={getBooleanStateFor(filter)} />
|
||||
{filter.options[0].question}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
</label>
|
||||
{#if filter.options.length === 1 && filter.options[0].fields.length > 0}
|
||||
<FilterviewWithFields id={filter.id} {filteredLayer} option={filter.options[0]} />
|
||||
{/if}
|
||||
|
||||
{#if $isDisplayed && filteredLayer.layerDef.filters?.length > 0}
|
||||
<div id="subfilters" class="flex flex-col gap-y-1 ml-4">
|
||||
{#each filteredLayer.layerDef.filters as filter}
|
||||
<div>
|
||||
|
||||
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with searchable fields -->
|
||||
{#if filter.options.length === 1 && filter.options[0].fields.length === 0}
|
||||
<label>
|
||||
<Checkbox selected={getBooleanStateFor(filter)}/>
|
||||
{filter.options[0].question}
|
||||
</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}
|
||||
<option value={i}>
|
||||
{ option.question}
|
||||
</option>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
{#if filter.options.length > 1}
|
||||
<Dropdown value={getStateFor(filter)}>
|
||||
{#each filter.options as option, i}
|
||||
<option value={i}>
|
||||
{option.question}
|
||||
</option>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Dropdown>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,60 +1,61 @@
|
|||
<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";
|
||||
import {Utils} from "../../Utils";
|
||||
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"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export let filteredLayer: FilteredLayer;
|
||||
export let option: FilterConfigOption;
|
||||
export let id: string;
|
||||
let parts: ({ message: string } | { subs: string })[];
|
||||
let language = Locale.language;
|
||||
$: {
|
||||
const template = option.question.textFor($language)
|
||||
parts = Utils.splitIntoSubstitutionParts(template)
|
||||
}
|
||||
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 ?? "{}");
|
||||
export let filteredLayer: FilteredLayer
|
||||
export let option: FilterConfigOption
|
||||
export let id: string
|
||||
let parts: ({ message: string } | { subs: string })[]
|
||||
let language = Locale.language
|
||||
$: {
|
||||
const template = option.question.textFor($language)
|
||||
parts = Utils.splitIntoSubstitutionParts(template)
|
||||
}
|
||||
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;
|
||||
if (v === undefined) {
|
||||
properties[key] = undefined;
|
||||
} else {
|
||||
properties[key] = v;
|
||||
}
|
||||
}
|
||||
appliedFilter?.setData(FilteredLayer.fieldsToString(properties));
|
||||
function setFields() {
|
||||
const properties: Record<string, string> = {}
|
||||
for (const key in fieldValues) {
|
||||
const v = fieldValues[key].data
|
||||
if (v === undefined) {
|
||||
properties[key] = undefined
|
||||
} else {
|
||||
properties[key] = v
|
||||
}
|
||||
}
|
||||
appliedFilter?.setData(FilteredLayer.fieldsToString(properties))
|
||||
}
|
||||
|
||||
for (const field of option.fields) {
|
||||
// A bit of cheating: the 'parts' will have '}' suffixed for fields
|
||||
const src = new UIEventSource<string>(initialState[field.name] ?? "");
|
||||
fieldTypes[field.name] = field.type;
|
||||
fieldValues[field.name] = src;
|
||||
onDestroy(src.stabilized(200).addCallback(() => {
|
||||
setFields();
|
||||
}));
|
||||
}
|
||||
|
||||
for (const field of option.fields) {
|
||||
// A bit of cheating: the 'parts' will have '}' suffixed for fields
|
||||
const src = new UIEventSource<string>(initialState[field.name] ?? "")
|
||||
fieldTypes[field.name] = field.type
|
||||
fieldValues[field.name] = src
|
||||
onDestroy(
|
||||
src.stabilized(200).addCallback(() => {
|
||||
setFields()
|
||||
})
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#each parts as part, i}
|
||||
{#if part.subs}
|
||||
<!-- This is a field! -->
|
||||
<span class="mx-1">
|
||||
<ValidatedInput value={fieldValues[part.subs]} type={fieldTypes[part.subs]}/>
|
||||
</span>
|
||||
{:else}
|
||||
{part.message}
|
||||
{/if}
|
||||
{/each}
|
||||
{#each parts as part, i}
|
||||
{#if part.subs}
|
||||
<!-- This is a field! -->
|
||||
<span class="mx-1">
|
||||
<ValidatedInput value={fieldValues[part.subs]} type={fieldTypes[part.subs]} />
|
||||
</span>
|
||||
{:else}
|
||||
{part.message}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -1,119 +1,116 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg.js"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import { Geocoding } from "../../Logic/Osm/Geocoding"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {Feature} from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Svg from "../../Svg.js";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import Hotkeys from "../Base/Hotkeys";
|
||||
import {Geocoding} from "../../Logic/Osm/Geocoding";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
import {GeoIndexedStoreForLayer} from "../../Logic/FeatureSource/Actors/GeoIndexedStore";
|
||||
import {createEventDispatcher, onDestroy} from "svelte";
|
||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
||||
export let bounds: UIEventSource<BBox>
|
||||
export let selectedElement: UIEventSource<Feature> | undefined = undefined
|
||||
export let selectedLayer: UIEventSource<LayerConfig> | undefined = undefined
|
||||
|
||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined;
|
||||
export let bounds: UIEventSource<BBox>;
|
||||
export let selectedElement: UIEventSource<Feature> | undefined = undefined;
|
||||
export let selectedLayer: UIEventSource<LayerConfig> | undefined = undefined;
|
||||
|
||||
export let clearAfterView: boolean = true
|
||||
export let clearAfterView: boolean = true
|
||||
|
||||
let searchContents: string = ""
|
||||
export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
onDestroy(triggerSearch.addCallback(_ => {
|
||||
performSearch()
|
||||
}))
|
||||
let searchContents: string = ""
|
||||
export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
onDestroy(
|
||||
triggerSearch.addCallback((_) => {
|
||||
performSearch()
|
||||
})
|
||||
)
|
||||
|
||||
let isRunning: boolean = false;
|
||||
let isRunning: boolean = false
|
||||
|
||||
let inputElement: HTMLInputElement;
|
||||
let inputElement: HTMLInputElement
|
||||
|
||||
let feedback: string = undefined;
|
||||
let feedback: string = undefined
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ctrl: "F"},
|
||||
Translations.t.hotkeyDocumentation.selectSearch,
|
||||
() => {
|
||||
inputElement?.focus();
|
||||
inputElement?.select();
|
||||
}
|
||||
);
|
||||
|
||||
const dispatch = createEventDispatcher<{ searchCompleted, searchIsValid: boolean }>()
|
||||
$: {
|
||||
if (!searchContents?.trim()) {
|
||||
dispatch("searchIsValid", false)
|
||||
}else{
|
||||
dispatch("searchIsValid", true)
|
||||
}
|
||||
Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
|
||||
inputElement?.focus()
|
||||
inputElement?.select()
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher<{ searchCompleted; searchIsValid: boolean }>()
|
||||
$: {
|
||||
if (!searchContents?.trim()) {
|
||||
dispatch("searchIsValid", false)
|
||||
} else {
|
||||
dispatch("searchIsValid", true)
|
||||
}
|
||||
}
|
||||
|
||||
async function performSearch() {
|
||||
try {
|
||||
isRunning = true
|
||||
searchContents = searchContents?.trim() ?? ""
|
||||
|
||||
async function performSearch() {
|
||||
try {
|
||||
isRunning = true;
|
||||
searchContents = searchContents?.trim() ?? "";
|
||||
|
||||
if (searchContents === "") {
|
||||
return;
|
||||
}
|
||||
const result = await Geocoding.Search(searchContents, bounds.data);
|
||||
if (result.length == 0) {
|
||||
feedback = Translations.t.general.search.nothing.txt;
|
||||
return;
|
||||
}
|
||||
const poi = result[0];
|
||||
const [lat0, lat1, lon0, lon1] = poi.boundingbox;
|
||||
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01));
|
||||
if (perLayer !== undefined) {
|
||||
const id = poi.osm_type + "/" + poi.osm_id;
|
||||
const layers = Array.from(perLayer?.values() ?? []);
|
||||
for (const layer of layers) {
|
||||
const found = layer.features.data.find(f => f.properties.id === id);
|
||||
selectedElement?.setData(found);
|
||||
selectedLayer?.setData(layer.layer.layerDef);
|
||||
|
||||
}
|
||||
}
|
||||
if(clearAfterView){
|
||||
searchContents = ""
|
||||
}
|
||||
dispatch("searchIsValid", false)
|
||||
dispatch("searchCompleted")
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
feedback = Translations.t.general.search.error.txt;
|
||||
} finally {
|
||||
isRunning = false;
|
||||
if (searchContents === "") {
|
||||
return
|
||||
}
|
||||
const result = await Geocoding.Search(searchContents, bounds.data)
|
||||
if (result.length == 0) {
|
||||
feedback = Translations.t.general.search.nothing.txt
|
||||
return
|
||||
}
|
||||
const poi = result[0]
|
||||
const [lat0, lat1, lon0, lon1] = poi.boundingbox
|
||||
bounds.set(
|
||||
new BBox([
|
||||
[lon0, lat0],
|
||||
[lon1, lat1],
|
||||
]).pad(0.01)
|
||||
)
|
||||
if (perLayer !== undefined) {
|
||||
const id = poi.osm_type + "/" + poi.osm_id
|
||||
const layers = Array.from(perLayer?.values() ?? [])
|
||||
for (const layer of layers) {
|
||||
const found = layer.features.data.find((f) => f.properties.id === id)
|
||||
selectedElement?.setData(found)
|
||||
selectedLayer?.setData(layer.layer.layerDef)
|
||||
}
|
||||
}
|
||||
if (clearAfterView) {
|
||||
searchContents = ""
|
||||
}
|
||||
dispatch("searchIsValid", false)
|
||||
dispatch("searchCompleted")
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
feedback = Translations.t.general.search.error.txt
|
||||
} finally {
|
||||
isRunning = false
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex normal-background rounded-full pl-2 justify-between">
|
||||
<form class="w-full">
|
||||
|
||||
{#if isRunning}
|
||||
<Loading>{Translations.t.general.search.searching}</Loading>
|
||||
{:else if feedback !== undefined}
|
||||
<div class="alert" on:click={() => feedback = undefined}>
|
||||
{feedback}
|
||||
</div>
|
||||
{:else }
|
||||
<input
|
||||
type="search"
|
||||
class="w-full"
|
||||
bind:this={inputElement}
|
||||
on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined}
|
||||
|
||||
bind:value={searchContents}
|
||||
placeholder={Translations.t.general.search.search}>
|
||||
{/if}
|
||||
|
||||
</form>
|
||||
<div class="w-6 h-6 self-end" on:click={performSearch}>
|
||||
<ToSvelte construct={Svg.search_svg}></ToSvelte>
|
||||
</div>
|
||||
<form class="w-full">
|
||||
{#if isRunning}
|
||||
<Loading>{Translations.t.general.search.searching}</Loading>
|
||||
{:else if feedback !== undefined}
|
||||
<div class="alert" on:click={() => (feedback = undefined)}>
|
||||
{feedback}
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
type="search"
|
||||
class="w-full"
|
||||
bind:this={inputElement}
|
||||
on:keypress={(keypr) => (keypr.key === "Enter" ? performSearch() : undefined)}
|
||||
bind:value={searchContents}
|
||||
placeholder={Translations.t.general.search.search}
|
||||
/>
|
||||
{/if}
|
||||
</form>
|
||||
<div class="w-6 h-6 self-end" on:click={performSearch}>
|
||||
<ToSvelte construct={Svg.search_svg} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,53 +1,50 @@
|
|||
<script lang="ts">
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection"
|
||||
import {UIEventSource} from "../../Logic/UIEventSource"
|
||||
import * as themeOverview from "../../assets/generated/theme_overview.json"
|
||||
import {Utils} from "../../Utils"
|
||||
import ThemesList from "./ThemesList.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import {LayoutInformation} from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import * as themeOverview from "../../assets/generated/theme_overview.json"
|
||||
import { Utils } from "../../Utils"
|
||||
import ThemesList from "./ThemesList.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
|
||||
export let search: UIEventSource<string>
|
||||
export let state: { osmConnection: OsmConnection }
|
||||
export let onMainScreen: boolean = true
|
||||
export let search: UIEventSource<string>
|
||||
export let state: { osmConnection: OsmConnection }
|
||||
export let onMainScreen: boolean = true
|
||||
|
||||
const prefix = "mapcomplete-hidden-theme-"
|
||||
const hiddenThemes: LayoutInformation[] = (themeOverview["default"] ?? themeOverview)?.filter(
|
||||
(layout) => layout.hideFromOverview
|
||||
) ?? []
|
||||
const userPreferences = state.osmConnection.preferencesHandler.preferences
|
||||
const t = Translations.t.general.morescreen
|
||||
const prefix = "mapcomplete-hidden-theme-"
|
||||
const hiddenThemes: LayoutInformation[] =
|
||||
(themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? []
|
||||
const userPreferences = state.osmConnection.preferencesHandler.preferences
|
||||
const t = Translations.t.general.morescreen
|
||||
|
||||
let knownThemesId: string[]
|
||||
$: knownThemesId = Utils.NoNull(
|
||||
Object.keys($userPreferences)
|
||||
.filter((key) => key.startsWith(prefix))
|
||||
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
|
||||
)
|
||||
$: console.log("Known theme ids:", knownThemesId)
|
||||
$: knownThemes = hiddenThemes.filter((theme) => knownThemesId.includes(theme.id))
|
||||
let knownThemesId: string[]
|
||||
$: knownThemesId = Utils.NoNull(
|
||||
Object.keys($userPreferences)
|
||||
.filter((key) => key.startsWith(prefix))
|
||||
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
|
||||
)
|
||||
$: console.log("Known theme ids:", knownThemesId)
|
||||
$: knownThemes = hiddenThemes.filter((theme) => knownThemesId.includes(theme.id))
|
||||
</script>
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
||||
<ThemesList
|
||||
hideThemes={false}
|
||||
isCustom={false}
|
||||
{onMainScreen}
|
||||
{search}
|
||||
{state}
|
||||
themes={knownThemes}
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<h3>{t.previouslyHiddenTitle.toString()}</h3>
|
||||
<p>
|
||||
{t.hiddenExplanation.Subs({
|
||||
hidden_discovered: knownThemes.length.toString(),
|
||||
total_hidden: hiddenThemes.length.toString(),
|
||||
})}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</ThemesList>
|
||||
|
||||
<ThemesList
|
||||
hideThemes={false}
|
||||
isCustom={false}
|
||||
{onMainScreen}
|
||||
{search}
|
||||
{state}
|
||||
themes={knownThemes}
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<h3>{t.previouslyHiddenTitle.toString()}</h3>
|
||||
<p>
|
||||
{t.hiddenExplanation.Subs({
|
||||
hidden_discovered: knownThemes.length.toString(),
|
||||
total_hidden: hiddenThemes.length.toString(),
|
||||
})}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</ThemesList>
|
||||
</LoginToggle>
|
||||
|
|
|
@ -2,28 +2,31 @@
|
|||
/**
|
||||
* Shows a 'floorSelector' and maps the selected floor onto a global filter
|
||||
*/
|
||||
import LayerState from "../../Logic/State/LayerState";
|
||||
import FloorSelector from "../InputElement/Helpers/FloorSelector.svelte";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import LayerState from "../../Logic/State/LayerState"
|
||||
import FloorSelector from "../InputElement/Helpers/FloorSelector.svelte"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
export let layerState: LayerState;
|
||||
export let floors: Store<string[]>;
|
||||
export let zoom: Store<number>;
|
||||
export let layerState: LayerState
|
||||
export let floors: Store<string[]>
|
||||
export let zoom: Store<number>
|
||||
const maxZoom = 16
|
||||
|
||||
let selectedFloor: UIEventSource<string> = new UIEventSource<string>(undefined);
|
||||
|
||||
selectedFloor.stabilized(5).map(floor => {
|
||||
if(floors.data === undefined || floors.data.length <= 1 || zoom.data < maxZoom){
|
||||
// Only a single floor is visible -> disable the 'level' global filter
|
||||
// OR we might have zoomed out to much ant want to show all
|
||||
layerState.setLevelFilter(undefined)
|
||||
}else{
|
||||
layerState.setLevelFilter(floor)
|
||||
}
|
||||
}, [floors, zoom])
|
||||
|
||||
let selectedFloor: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
|
||||
selectedFloor.stabilized(5).map(
|
||||
(floor) => {
|
||||
if (floors.data === undefined || floors.data.length <= 1 || zoom.data < maxZoom) {
|
||||
// Only a single floor is visible -> disable the 'level' global filter
|
||||
// OR we might have zoomed out to much ant want to show all
|
||||
layerState.setLevelFilter(undefined)
|
||||
} else {
|
||||
layerState.setLevelFilter(floor)
|
||||
}
|
||||
},
|
||||
[floors, zoom]
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if $zoom >= maxZoom}
|
||||
<FloorSelector {floors} value={selectedFloor} />
|
||||
{/if}
|
||||
<FloorSelector {floors} value={selectedFloor} />
|
||||
{/if}
|
||||
|
|
|
@ -1,31 +1,29 @@
|
|||
<script lang="ts">
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import {Store} from "../../Logic/UIEventSource";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
|
||||
/*
|
||||
/*
|
||||
A subtleButton which opens mapillary in a new tab at the current location
|
||||
*/
|
||||
|
||||
export let mapProperties: {
|
||||
readonly zoom: Store<number>,
|
||||
readonly location: Store<{ lon: number, lat: number }>
|
||||
}
|
||||
let location = mapProperties.location
|
||||
let zoom = mapProperties.zoom
|
||||
let mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${
|
||||
$location?.lat ?? 0
|
||||
}&lng=${$location?.lon ?? 0}&z=${Math.max(($zoom ?? 2) - 1, 1)}`
|
||||
|
||||
export let mapProperties: {
|
||||
readonly zoom: Store<number>
|
||||
readonly location: Store<{ lon: number; lat: number }>
|
||||
}
|
||||
let location = mapProperties.location
|
||||
let zoom = mapProperties.zoom
|
||||
let mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${$location?.lat ?? 0}&lng=${
|
||||
$location?.lon ?? 0
|
||||
}&z=${Math.max(($zoom ?? 2) - 1, 1)}`
|
||||
</script>
|
||||
|
||||
<a class="flex button items-center" href={mapillaryLink} target="_blank">
|
||||
<ToSvelte construct={() =>Svg.mapillary_black_svg().SetClass("w-12 h-12 m-2 mr-4 shrink-0")}/>
|
||||
<div class="flex flex-col">
|
||||
<Tr t={Translations.t.general.attribution.openMapillary}/>
|
||||
<Tr cls="subtle" t={ Translations.t.general.attribution.mapillaryHelp}/>
|
||||
</div>
|
||||
<ToSvelte construct={() => Svg.mapillary_black_svg().SetClass("w-12 h-12 m-2 mr-4 shrink-0")} />
|
||||
<div class="flex flex-col">
|
||||
<Tr t={Translations.t.general.attribution.openMapillary} />
|
||||
<Tr cls="subtle" t={Translations.t.general.attribution.mapillaryHelp} />
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Svg from "../../Svg"
|
||||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import LayoutConfig, {LayoutInformation} from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import {ImmutableStore, Store} from "../../Logic/UIEventSource"
|
||||
import LayoutConfig, { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import {Utils} from "../../Utils"
|
||||
import { Utils } from "../../Utils"
|
||||
import themeOverview from "../../assets/generated/theme_overview.json"
|
||||
import {TextField} from "../Input/TextField"
|
||||
import { TextField } from "../Input/TextField"
|
||||
import Locale from "../i18n/Locale"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import ThemesList from "./ThemesList.svelte"
|
||||
|
@ -29,7 +29,7 @@ export default class MoreScreen extends Combine {
|
|||
})
|
||||
search.enterPressed.addCallbackD((searchTerm) => {
|
||||
searchTerm = searchTerm.toLowerCase()
|
||||
if(!searchTerm){
|
||||
if (!searchTerm) {
|
||||
return
|
||||
}
|
||||
if (searchTerm === "personal") {
|
||||
|
|
|
@ -1,108 +1,113 @@
|
|||
<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";
|
||||
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, including when importing
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* An advanced location input, which has support to:
|
||||
* - Show more layers
|
||||
* - Snap to layers
|
||||
*
|
||||
* This one is mostly used to insert new points, including when importing
|
||||
*/
|
||||
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);
|
||||
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
|
||||
}>(undefined)
|
||||
|
||||
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
|
||||
? new UIEventSource<{ lon: number; lat: number }>(coordinate)
|
||||
: value,
|
||||
bounds: new UIEventSource<BBox>(undefined),
|
||||
allowMoving: new UIEventSource<boolean>(true),
|
||||
allowZooming: new UIEventSource<boolean>(true),
|
||||
minzoom: new UIEventSource<number>(18),
|
||||
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
|
||||
}
|
||||
|
||||
const featuresForLayer = state.perLayer.get(targetLayer.id)
|
||||
if (featuresForLayer) {
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
features: featuresForLayer,
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
let preciseLocation: UIEventSource<{ lon: number, lat: number }> = new UIEventSource<{
|
||||
lon: number;
|
||||
lat: number
|
||||
}>(undefined);
|
||||
|
||||
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 ? new UIEventSource<{ lon: number; lat: number }>(coordinate) : value,
|
||||
bounds: new UIEventSource<BBox>(undefined),
|
||||
allowMoving: new UIEventSource<boolean>(true),
|
||||
allowZooming: new UIEventSource<boolean>(true),
|
||||
minzoom: new UIEventSource<number>(18),
|
||||
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer)
|
||||
};
|
||||
|
||||
|
||||
const featuresForLayer = state.perLayer.get(targetLayer.id)
|
||||
if(featuresForLayer){
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
features: featuresForLayer
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
features: snappedLocation,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<LocationInput {map} mapProperties={initialMapProperties}
|
||||
value={preciseLocation} initialCoordinate={coordinate} maxDistanceInMeters=50 />
|
||||
<LocationInput
|
||||
{map}
|
||||
mapProperties={initialMapProperties}
|
||||
value={preciseLocation}
|
||||
initialCoordinate={coordinate}
|
||||
maxDistanceInMeters="50"
|
||||
/>
|
||||
|
|
|
@ -1,35 +1,34 @@
|
|||
<script context="module" lang="ts">
|
||||
export interface Theme {
|
||||
id: string
|
||||
icon: string
|
||||
title: any
|
||||
shortDescription: any
|
||||
definition?: any
|
||||
mustHaveLanguage?: boolean
|
||||
hideFromOverview: boolean
|
||||
keywords?: any[]
|
||||
}
|
||||
export interface Theme {
|
||||
id: string
|
||||
icon: string
|
||||
title: any
|
||||
shortDescription: any
|
||||
definition?: any
|
||||
mustHaveLanguage?: boolean
|
||||
hideFromOverview: boolean
|
||||
keywords?: any[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {UIEventSource} from "../../Logic/UIEventSource"
|
||||
import Svg from "../../Svg"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Svg from "../../Svg"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let search: UIEventSource<string>
|
||||
export let search: UIEventSource<string>
|
||||
|
||||
const t = Translations.t.general.morescreen
|
||||
const t = Translations.t.general.morescreen
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<h5>{t.noMatchingThemes.toString()}</h5>
|
||||
<div class="flex justify-center">
|
||||
|
||||
<button on:click={() => search.setData("")}>
|
||||
<ToSvelte construct={Svg.search_disable_svg().SetClass("w-6 mr-2")}/>
|
||||
<Tr slot="message" t={t.noSearch}/>
|
||||
</button>
|
||||
</div>
|
||||
<h5>{t.noMatchingThemes.toString()}</h5>
|
||||
<div class="flex justify-center">
|
||||
<button on:click={() => search.setData("")}>
|
||||
<ToSvelte construct={Svg.search_disable_svg().SetClass("w-6 mr-2")} />
|
||||
<Tr slot="message" t={t.noSearch} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import MapControlButton from "../Base/MapControlButton.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid";
|
||||
import MapControlButton from "../Base/MapControlButton.svelte";
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
|
||||
export let state: ThemeViewState
|
||||
export let state: ThemeViewState
|
||||
</script>
|
||||
|
||||
<MapControlButton on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}>
|
||||
<Square3Stack3dIcon class="w-6 h-6"/>
|
||||
<Square3Stack3dIcon class="w-6 h-6" />
|
||||
</MapControlButton>
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
<script lang="ts">
|
||||
import {Store} from "../../Logic/UIEventSource";
|
||||
import {PencilIcon} from "@babeard/svelte-heroicons/solid";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { PencilIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let mapProperties: { location: Store<{ lon: number; lat: number }>; zoom: Store<number> }
|
||||
let location = mapProperties.location
|
||||
let zoom = mapProperties.zoom
|
||||
export let objectId: undefined | string = undefined
|
||||
export let mapProperties: { location: Store<{ lon: number; lat: number }>; zoom: Store<number> }
|
||||
let location = mapProperties.location
|
||||
let zoom = mapProperties.zoom
|
||||
export let objectId: undefined | string = undefined
|
||||
|
||||
let elementSelect = ""
|
||||
if (objectId !== undefined) {
|
||||
const parts = objectId?.split("/")
|
||||
const tp = parts[0]
|
||||
if (
|
||||
parts.length === 2 &&
|
||||
!isNaN(Number(parts[1])) &&
|
||||
(tp === "node" || tp === "way" || tp === "relation")
|
||||
) {
|
||||
elementSelect = "&" + tp + "=" + parts[1]
|
||||
}
|
||||
let elementSelect = ""
|
||||
if (objectId !== undefined) {
|
||||
const parts = objectId?.split("/")
|
||||
const tp = parts[0]
|
||||
if (
|
||||
parts.length === 2 &&
|
||||
!isNaN(Number(parts[1])) &&
|
||||
(tp === "node" || tp === "way" || tp === "relation")
|
||||
) {
|
||||
elementSelect = "&" + tp + "=" + parts[1]
|
||||
}
|
||||
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${
|
||||
$zoom ?? 0
|
||||
}/${$location?.lat ?? 0}/${$location?.lon ?? 0}`
|
||||
}
|
||||
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${$zoom ?? 0}/${
|
||||
$location?.lat ?? 0
|
||||
}/${$location?.lon ?? 0}`
|
||||
</script>
|
||||
|
||||
|
||||
<a class="flex button items-center" target="_blank" href={idLink}>
|
||||
<PencilIcon class="w-12 h-12 p-2 pr-4"/>
|
||||
<Tr t={ Translations.t.general.attribution.editId}/>
|
||||
<PencilIcon class="w-12 h-12 p-2 pr-4" />
|
||||
<Tr t={Translations.t.general.attribution.editId} />
|
||||
</a>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import {Utils} from "../../Utils";
|
||||
import Constants from "../../Models/Constants";
|
||||
import Combine from "../Base/Combine"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import { Utils } from "../../Utils"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
export class OpenJosm extends Combine {
|
||||
constructor(osmConnection: OsmConnection, bounds: Store<BBox>, iconStyle?: string) {
|
||||
|
@ -32,20 +32,22 @@ export class OpenJosm extends Combine {
|
|||
)
|
||||
|
||||
const toggle = new Toggle(
|
||||
new SubtleButton(Svg.josm_logo_svg().SetStyle(iconStyle), t.editJosm).onClick(() => {
|
||||
const bbox = bounds.data
|
||||
if (bbox === undefined) {
|
||||
return
|
||||
}
|
||||
const top = bbox.getNorth()
|
||||
const bottom = bbox.getSouth()
|
||||
const right = bbox.getEast()
|
||||
const left = bbox.getWest()
|
||||
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
|
||||
Utils.download(josmLink)
|
||||
.then((answer) => josmState.setData(answer.replace(/\n/g, "").trim()))
|
||||
.catch((_) => josmState.setData("ERROR"))
|
||||
}).SetClass("w-full"),
|
||||
new SubtleButton(Svg.josm_logo_svg().SetStyle(iconStyle), t.editJosm)
|
||||
.onClick(() => {
|
||||
const bbox = bounds.data
|
||||
if (bbox === undefined) {
|
||||
return
|
||||
}
|
||||
const top = bbox.getNorth()
|
||||
const bottom = bbox.getSouth()
|
||||
const right = bbox.getEast()
|
||||
const left = bbox.getWest()
|
||||
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
|
||||
Utils.download(josmLink)
|
||||
.then((answer) => josmState.setData(answer.replace(/\n/g, "").trim()))
|
||||
.catch((_) => josmState.setData("ERROR"))
|
||||
})
|
||||
.SetClass("w-full"),
|
||||
undefined,
|
||||
osmConnection.userDetails.map(
|
||||
(ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible
|
||||
|
|
|
@ -1,42 +1,45 @@
|
|||
<script lang="ts">/**
|
||||
* The OverlayToggle shows a single toggle to enable or disable an overlay
|
||||
*/
|
||||
import Checkbox from "../Base/Checkbox.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import type { RasterLayerProperties } from "../../Models/RasterLayerProperties";
|
||||
<script lang="ts">
|
||||
/**
|
||||
* The OverlayToggle shows a single toggle to enable or disable an overlay
|
||||
*/
|
||||
import Checkbox from "../Base/Checkbox.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import type { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
|
||||
export let layerproperties : RasterLayerProperties
|
||||
export let state: {isDisplayed: UIEventSource<boolean>};
|
||||
export let zoomlevel: UIEventSource<number>;
|
||||
export let highlightedLayer: UIEventSource<string> | undefined;
|
||||
export let layerproperties: RasterLayerProperties
|
||||
export let state: { isDisplayed: UIEventSource<boolean> }
|
||||
export let zoomlevel: UIEventSource<number>
|
||||
export let highlightedLayer: UIEventSource<string> | undefined
|
||||
|
||||
let isDisplayed: boolean = state.isDisplayed.data;
|
||||
onDestroy(state.isDisplayed.addCallbackAndRunD(d => {
|
||||
isDisplayed = d;
|
||||
return false;
|
||||
}));
|
||||
let isDisplayed: boolean = state.isDisplayed.data
|
||||
onDestroy(
|
||||
state.isDisplayed.addCallbackAndRunD((d) => {
|
||||
isDisplayed = d
|
||||
return false
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
let mainElem: HTMLElement;
|
||||
$: onDestroy(
|
||||
highlightedLayer.addCallbackAndRun(highlightedLayer => {
|
||||
if (highlightedLayer === layerproperties.id) {
|
||||
mainElem?.classList?.add("glowing-shadow");
|
||||
} else {
|
||||
mainElem?.classList?.remove("glowing-shadow");
|
||||
}
|
||||
})
|
||||
);
|
||||
let mainElem: HTMLElement
|
||||
$: onDestroy(
|
||||
highlightedLayer.addCallbackAndRun((highlightedLayer) => {
|
||||
if (highlightedLayer === layerproperties.id) {
|
||||
mainElem?.classList?.add("glowing-shadow")
|
||||
} else {
|
||||
mainElem?.classList?.remove("glowing-shadow")
|
||||
}
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if layerproperties.name}
|
||||
<div bind:this={mainElem}>
|
||||
<label class="flex gap-1">
|
||||
<Checkbox selected={state.isDisplayed} />
|
||||
<Tr t={new Translation(layerproperties.name)}/>
|
||||
<Tr t={new Translation(layerproperties.name)} />
|
||||
{#if $zoomlevel < layerproperties.min_zoom}
|
||||
<span class="alert">
|
||||
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
|
||||
|
|
|
@ -1,62 +1,74 @@
|
|||
<script lang="ts">
|
||||
import type {Feature} from "geojson";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import type {SpecialVisualizationState} from "../SpecialVisualization";
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte";
|
||||
import {onDestroy} from "svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import {XCircleIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
export let layer: LayerConfig;
|
||||
export let selectedElement: Feature;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig
|
||||
export let selectedElement: Feature
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
||||
let _tags: Record<string, string>
|
||||
onDestroy(
|
||||
tags.addCallbackAndRun((tags) => {
|
||||
_tags = tags
|
||||
})
|
||||
)
|
||||
|
||||
let _tags: Record<string, string>;
|
||||
onDestroy(tags.addCallbackAndRun(tags => {
|
||||
_tags = tags;
|
||||
}));
|
||||
|
||||
let _metatags: Record<string, string>;
|
||||
onDestroy(state.userRelatedState.preferencesAsTags.addCallbackAndRun(tags => {
|
||||
_metatags = tags;
|
||||
}));
|
||||
let _metatags: Record<string, string>
|
||||
onDestroy(
|
||||
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
|
||||
_metatags = tags
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
{#if _tags._deleted === "yes"}
|
||||
<Tr t={ Translations.t.delete.isDeleted}/>
|
||||
<Tr t={Translations.t.delete.isDeleted} />
|
||||
{:else}
|
||||
<div class="flex border-b-2 border-black drop-shadow-md justify-between items-center low-interaction px-3">
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="flex border-b-2 border-black drop-shadow-md justify-between items-center low-interaction px-3"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<!-- Title element-->
|
||||
<h3>
|
||||
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} />
|
||||
</h3>
|
||||
|
||||
<!-- Title element-->
|
||||
<h3>
|
||||
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags}
|
||||
{layer}></TagRenderingAnswer>
|
||||
</h3>
|
||||
|
||||
<div class="no-weblate title-icons flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2 gap-x-0.5 p-1 links-as-button">
|
||||
{#each layer.titleIcons as titleIconConfig}
|
||||
{#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({..._metatags, ..._tags}) ?? true) && titleIconConfig.IsKnown(_tags)}
|
||||
<div class="w-8 h-8 flex items-center">
|
||||
<TagRenderingAnswer config={titleIconConfig} {tags} {selectedElement} {state}
|
||||
{layer} extraClasses="h-full justify-center"></TagRenderingAnswer>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div
|
||||
class="no-weblate title-icons flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2 gap-x-0.5 p-1 links-as-button"
|
||||
>
|
||||
{#each layer.titleIcons as titleIconConfig}
|
||||
{#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ..._metatags, ..._tags } ) ?? true) && titleIconConfig.IsKnown(_tags)}
|
||||
<div class="w-8 h-8 flex items-center">
|
||||
<TagRenderingAnswer
|
||||
config={titleIconConfig}
|
||||
{tags}
|
||||
{selectedElement}
|
||||
{state}
|
||||
{layer}
|
||||
extraClasses="h-full justify-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<XCircleIcon class="w-8 h-8 cursor-pointer" on:click={() => state.selectedElement.setData(undefined)}/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<XCircleIcon
|
||||
class="w-8 h-8 cursor-pointer"
|
||||
on:click={() => state.selectedElement.setData(undefined)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.title-icons a) {
|
||||
display: block !important;
|
||||
}
|
||||
:global(.title-icons a) {
|
||||
display: block !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,46 +1,54 @@
|
|||
<script lang="ts">
|
||||
import type {Feature} from "geojson";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import type {SpecialVisualizationState} from "../SpecialVisualization";
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import {onDestroy} from "svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
export let layer: LayerConfig;
|
||||
export let selectedElement: Feature;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let highlightedRendering: UIEventSource<string> = undefined;
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig
|
||||
export let selectedElement: Feature
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let highlightedRendering: UIEventSource<string> = undefined
|
||||
|
||||
let _tags: Record<string, string>
|
||||
onDestroy(
|
||||
tags.addCallbackAndRun((tags) => {
|
||||
_tags = tags
|
||||
})
|
||||
)
|
||||
|
||||
let _tags: Record<string, string>;
|
||||
onDestroy(tags.addCallbackAndRun(tags => {
|
||||
_tags = tags;
|
||||
}));
|
||||
|
||||
let _metatags: Record<string, string>;
|
||||
onDestroy(state.userRelatedState.preferencesAsTags.addCallbackAndRun(tags => {
|
||||
_metatags = tags;
|
||||
}));
|
||||
let _metatags: Record<string, string>
|
||||
onDestroy(
|
||||
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
|
||||
_metatags = tags
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
{#if _tags._deleted === "yes"}
|
||||
<Tr t={ Translations.t.delete.isDeleted}/>
|
||||
<button class="w-full" on:click={() => state.selectedElement.setData(undefined)}>
|
||||
<Tr t={ Translations.t.general.returnToTheMap}/>
|
||||
</button>
|
||||
<Tr t={Translations.t.delete.isDeleted} />
|
||||
<button class="w-full" on:click={() => state.selectedElement.setData(undefined)}>
|
||||
<Tr t={Translations.t.general.returnToTheMap} />
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex flex-col overflow-y-auto p-1 px-2 gap-y-2">
|
||||
{#each layer.tagRenderings as config (config.id)}
|
||||
{#if (config.condition === undefined || config.condition.matchesProperties(_tags)) && (config.metacondition === undefined || config.metacondition.matchesProperties({..._tags, ..._metatags}))}
|
||||
{#if config.IsKnown(_tags)}
|
||||
<TagRenderingEditable {tags} {config} {state} {selectedElement} {layer}
|
||||
{highlightedRendering}></TagRenderingEditable>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-col overflow-y-auto p-1 px-2 gap-y-2">
|
||||
{#each layer.tagRenderings as config (config.id)}
|
||||
{#if (config.condition === undefined || config.condition.matchesProperties(_tags)) && (config.metacondition === undefined || config.metacondition.matchesProperties( { ..._tags, ..._metatags } ))}
|
||||
{#if config.IsKnown(_tags)}
|
||||
<TagRenderingEditable
|
||||
{tags}
|
||||
{config}
|
||||
{state}
|
||||
{selectedElement}
|
||||
{layer}
|
||||
{highlightedRendering}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import {VariableUiElement} from "../Base/VariableUIElement"
|
||||
import {Translation} from "../i18n/Translation"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Svg from "../../Svg"
|
||||
import Combine from "../Base/Combine"
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import {Utils} from "../../Utils"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import Translations from "../i18n/Translations"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import {InputElement} from "../Input/InputElement"
|
||||
import {CheckBox} from "../Input/Checkboxes"
|
||||
import {SubtleButton} from "../Base/SubtleButton"
|
||||
import { InputElement } from "../Input/InputElement"
|
||||
import { CheckBox } from "../Input/Checkboxes"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import LZString from "lz-string"
|
||||
import {SpecialVisualizationState} from "../SpecialVisualization"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class ShareScreen extends Combine{
|
||||
export class ShareScreen extends Combine {
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
const layout = state?.layout
|
||||
const tr = Translations.t.general.sharescreen
|
||||
|
@ -60,8 +60,9 @@ export class ShareScreen extends Combine{
|
|||
return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
|
||||
}
|
||||
|
||||
const currentLayer: Store<{ id: string; name: string | Record<string, string> } | undefined> =
|
||||
state.mapProperties.rasterLayer.map((l) => l?.properties)
|
||||
const currentLayer: Store<
|
||||
{ id: string; name: string | Record<string, string> } | undefined
|
||||
> = state.mapProperties.rasterLayer.map((l) => l?.properties)
|
||||
const currentBackground = new VariableUiElement(
|
||||
currentLayer.map((layer) => {
|
||||
return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" })
|
||||
|
@ -90,13 +91,15 @@ export class ShareScreen extends Combine{
|
|||
(includeLayerSelection) => {
|
||||
if (includeLayerSelection) {
|
||||
return Utils.NoNull(
|
||||
Array.from( state.layerState.filteredLayers.values()).map(fLayerToParam)
|
||||
Array.from(state.layerState.filteredLayers.values()).map(fLayerToParam)
|
||||
).join("&")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
Array.from(state.layerState.filteredLayers.values()).map((flayer) => flayer.isDisplayed)
|
||||
Array.from(state.layerState.filteredLayers.values()).map(
|
||||
(flayer) => flayer.isDisplayed
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -1,87 +1,88 @@
|
|||
<script lang="ts">
|
||||
import Translations from "../i18n/Translations";
|
||||
import Svg from "../../Svg";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import Geosearch from "./Geosearch.svelte";
|
||||
import IfNot from "../Base/IfNot.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import If from "../Base/If.svelte";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {SearchIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Geosearch from "./Geosearch.svelte"
|
||||
import IfNot from "../Base/IfNot.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import If from "../Base/If.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
|
||||
/**
|
||||
* The theme introduction panel
|
||||
*/
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
let selectedElement = state.selectedElement
|
||||
let selectedLayer = state.selectedLayer
|
||||
/**
|
||||
* The theme introduction panel
|
||||
*/
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
let selectedElement = state.selectedElement
|
||||
let selectedLayer = state.selectedLayer
|
||||
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
let searchEnabled = false
|
||||
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
let searchEnabled = false
|
||||
|
||||
function jumpToCurrentLocation() {
|
||||
const glstate = state.geolocation.geolocationState
|
||||
if (glstate.currentGPSLocation.data !== undefined) {
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
|
||||
state.guistate.themeIsOpened.setData(false)
|
||||
const coor = {lon: c.longitude, lat: c.latitude}
|
||||
state.mapProperties.location.setData(coor)
|
||||
}
|
||||
if (glstate.permission.data !== "granted") {
|
||||
glstate.requestPermission()
|
||||
return
|
||||
}
|
||||
|
||||
function jumpToCurrentLocation() {
|
||||
const glstate = state.geolocation.geolocationState
|
||||
if (glstate.currentGPSLocation.data !== undefined) {
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
|
||||
state.guistate.themeIsOpened.setData(false)
|
||||
const coor = { lon: c.longitude, lat: c.latitude }
|
||||
state.mapProperties.location.setData(coor)
|
||||
}
|
||||
|
||||
if (glstate.permission.data !== "granted") {
|
||||
glstate.requestPermission()
|
||||
return
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tr t={layout.description}></Tr>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.general}/>
|
||||
<Tr t={layout.description} />
|
||||
<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 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>
|
||||
<Tr t={layout.descriptionTail} />
|
||||
<NextButton clss="primary w-full" on:click={() => state.guistate.themeIsOpened.setData(false)}>
|
||||
<div class="flex justify-center w-full text-2xl">
|
||||
<Tr t={Translations.t.general.openTheMap}/>
|
||||
</div>
|
||||
<div class="flex justify-center w-full text-2xl">
|
||||
<Tr t={Translations.t.general.openTheMap} />
|
||||
</div>
|
||||
</NextButton>
|
||||
|
||||
<div class="flex w-full flex-wrap sm:flex-nowrap">
|
||||
<IfNot condition={state.geolocation.geolocationState.permission.map(p => p === "denied")}>
|
||||
<button class="flex w-full gap-x-2 items-center" on:click={jumpToCurrentLocation}>
|
||||
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")}/>
|
||||
<Tr t={Translations.t.general.openTheMapAtGeolocation}/>
|
||||
</button>
|
||||
</IfNot>
|
||||
|
||||
<div class="flex gap-x-2 items-center w-full border rounded .button p-2 m-1 low-interaction">
|
||||
<div class="w-full">
|
||||
<Geosearch bounds={state.mapProperties.bounds}
|
||||
on:searchCompleted={() => state.guistate.themeIsOpened.setData(false)}
|
||||
on:searchIsValid={isValid => {searchEnabled= isValid}}
|
||||
perLayer={state.perLayer}
|
||||
{selectedElement}
|
||||
{selectedLayer}
|
||||
{triggerSearch}>
|
||||
</Geosearch>
|
||||
</div>
|
||||
<button class={"flex gap-x-2 justify-between items-center "+(searchEnabled ? "" : "disabled")}
|
||||
on:click={() => triggerSearch.ping()}>
|
||||
<Tr t={Translations.t.general.search.searchShort}/>
|
||||
<SearchIcon class="w-6 h-6"></SearchIcon>
|
||||
</button>
|
||||
<IfNot condition={state.geolocation.geolocationState.permission.map((p) => p === "denied")}>
|
||||
<button class="flex w-full gap-x-2 items-center" on:click={jumpToCurrentLocation}>
|
||||
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")} />
|
||||
<Tr t={Translations.t.general.openTheMapAtGeolocation} />
|
||||
</button>
|
||||
</IfNot>
|
||||
|
||||
<div class="flex gap-x-2 items-center w-full border rounded .button p-2 m-1 low-interaction">
|
||||
<div class="w-full">
|
||||
<Geosearch
|
||||
bounds={state.mapProperties.bounds}
|
||||
on:searchCompleted={() => state.guistate.themeIsOpened.setData(false)}
|
||||
on:searchIsValid={(isValid) => {
|
||||
searchEnabled = isValid
|
||||
}}
|
||||
perLayer={state.perLayer}
|
||||
{selectedElement}
|
||||
{selectedLayer}
|
||||
{triggerSearch}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class={"flex gap-x-2 justify-between items-center " + (searchEnabled ? "" : "disabled")}
|
||||
on:click={() => triggerSearch.ping()}
|
||||
>
|
||||
<Tr t={Translations.t.general.search.searchShort} />
|
||||
<SearchIcon class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script lang="ts">
|
||||
import NoThemeResultButton from "./NoThemeResultButton.svelte"
|
||||
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection"
|
||||
import {UIEventSource} from "../../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import ThemeButton from "./ThemeButton.svelte"
|
||||
import {LayoutInformation} from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import MoreScreen from "./MoreScreen"
|
||||
|
||||
export let search: UIEventSource<string>
|
||||
|
@ -30,7 +30,6 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
|
||||
{#each filteredThemes as theme (theme.id)}
|
||||
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
|
||||
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
|
||||
|
|
|
@ -1,37 +1,37 @@
|
|||
<script lang="ts">
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection"
|
||||
import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import {Utils} from "../../Utils"
|
||||
import ThemesList from "./ThemesList.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import ThemesList from "./ThemesList.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
|
||||
export let search: UIEventSource<string>
|
||||
export let state: UserRelatedState & {
|
||||
osmConnection: OsmConnection
|
||||
}
|
||||
export let onMainScreen: boolean = true
|
||||
export let search: UIEventSource<string>
|
||||
export let state: UserRelatedState & {
|
||||
osmConnection: OsmConnection
|
||||
}
|
||||
export let onMainScreen: boolean = true
|
||||
|
||||
const t = Translations.t.general
|
||||
const currentIds: Store<string[]> = state.installedUserThemes
|
||||
const stableIds = Stores.ListStabilized<string>(currentIds)
|
||||
let customThemes
|
||||
$: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id)))
|
||||
$: console.log("Custom themes are", customThemes)
|
||||
const t = Translations.t.general
|
||||
const currentIds: Store<string[]> = state.installedUserThemes
|
||||
const stableIds = Stores.ListStabilized<string>(currentIds)
|
||||
let customThemes
|
||||
$: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id)))
|
||||
$: console.log("Custom themes are", customThemes)
|
||||
</script>
|
||||
|
||||
{#if customThemes.length > 0}
|
||||
<ThemesList
|
||||
{search}
|
||||
{state}
|
||||
{onMainScreen}
|
||||
themes={customThemes}
|
||||
isCustom={true}
|
||||
hideThemes={false}
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<!-- TODO: Change string to exclude html -->
|
||||
{@html t.customThemeIntro.toString()}
|
||||
</svelte:fragment>
|
||||
</ThemesList>
|
||||
<ThemesList
|
||||
{search}
|
||||
{state}
|
||||
{onMainScreen}
|
||||
themes={customThemes}
|
||||
isCustom={true}
|
||||
hideThemes={false}
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<!-- TODO: Change string to exclude html -->
|
||||
{@html t.customThemeIntro.toString()}
|
||||
</svelte:fragment>
|
||||
</ThemesList>
|
||||
{/if}
|
||||
|
|
|
@ -1,50 +1,53 @@
|
|||
<script lang="ts">
|
||||
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { PencilAltIcon, UserCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { onDestroy } from "svelte";
|
||||
import Showdown from "showdown";
|
||||
import FromHtml from "../Base/FromHtml.svelte";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations.js";
|
||||
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { PencilAltIcon, UserCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { onDestroy } from "svelte"
|
||||
import Showdown from "showdown"
|
||||
import FromHtml from "../Base/FromHtml.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations.js"
|
||||
|
||||
/**
|
||||
* This panel shows information about the logged-in user, showing account name, profile pick, description and an edit-button
|
||||
*/
|
||||
export let osmConnection: OsmConnection;
|
||||
let userdetails: UIEventSource<UserDetails> = osmConnection.userDetails;
|
||||
let description: string;
|
||||
onDestroy(userdetails.addCallbackAndRunD(userdetails => {
|
||||
description = new Showdown.Converter()
|
||||
.makeHtml(userdetails.description)
|
||||
?.replace(/>/g, ">")
|
||||
?.replace(/</g, "<");
|
||||
|
||||
}));
|
||||
export let osmConnection: OsmConnection
|
||||
let userdetails: UIEventSource<UserDetails> = osmConnection.userDetails
|
||||
let description: string
|
||||
onDestroy(
|
||||
userdetails.addCallbackAndRunD((userdetails) => {
|
||||
description = new Showdown.Converter()
|
||||
.makeHtml(userdetails.description)
|
||||
?.replace(/>/g, ">")
|
||||
?.replace(/</g, "<")
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="flex border border-gray-600 border-dashed m-1 p-1 rounded-md link-underline">
|
||||
{#if $userdetails.img}
|
||||
<img src={$userdetails.img} class="rounded-full w-12 h-12 m-4">
|
||||
<img src={$userdetails.img} class="rounded-full w-12 h-12 m-4" />
|
||||
{:else}
|
||||
<UserCircleIcon class="w-12 h-12" />
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<h3>{$userdetails.name}</h3>
|
||||
{#if description}
|
||||
<FromHtml src={description}/>
|
||||
<a href={osmConnection.Backend() + "/profile/edit"} target="_blank" class="link-no-underline flex items-center self-end">
|
||||
<FromHtml src={description} />
|
||||
<a
|
||||
href={osmConnection.Backend() + "/profile/edit"}
|
||||
target="_blank"
|
||||
class="link-no-underline flex items-center self-end"
|
||||
>
|
||||
<PencilAltIcon slot="image" class="p-2 w-8 h-8" />
|
||||
<Tr slot="message" t={Translations.t.userinfo.editDescription} />
|
||||
</a>
|
||||
|
||||
{:else}
|
||||
<Tr t={Translations.t. userinfo.noDescription} />
|
||||
<Tr t={Translations.t.userinfo.noDescription} />
|
||||
<a href={osmConnection.Backend() + "/profile/edit"} target="_blank" class="flex items-center">
|
||||
<PencilAltIcon slot="image" class="p-2 w-8 h-8" />
|
||||
<Tr slot="message" t={Translations.t.userinfo.noDescriptionCallToAction} />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,33 +8,33 @@
|
|||
*
|
||||
* This component is _not_ responsible for the rest of the flow, e.g. the confirm button
|
||||
*/
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import split_point from "../../assets/layers/split_point/split_point.json";
|
||||
import split_road from "../../assets/layers/split_road/split_road.json";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import type { MapProperties } from "../../Models/MapProperties";
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor";
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte";
|
||||
import { OsmWay } from "../../Logic/Osm/OsmObject";
|
||||
import ShowDataLayer from "../Map/ShowDataLayer";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||
import { BBox } from "../../Logic/BBox";
|
||||
import type { Feature, LineString, Point } from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import split_point from "../../assets/layers/split_point/split_point.json"
|
||||
import split_road from "../../assets/layers/split_road/split_road.json"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
import { OsmWay } from "../../Logic/Osm/OsmObject"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import type { Feature, LineString, Point } from "geojson"
|
||||
|
||||
const splitpoint_style = new LayerConfig(
|
||||
<LayerConfigJson>split_point,
|
||||
"(BUILTIN) SplitRoadWizard.ts",
|
||||
true
|
||||
) as const;
|
||||
) as const
|
||||
|
||||
const splitroad_style = new LayerConfig(
|
||||
<LayerConfigJson>split_road,
|
||||
"(BUILTIN) SplitRoadWizard.ts",
|
||||
true
|
||||
) as const;
|
||||
) as const
|
||||
|
||||
/**
|
||||
* The way to focus on
|
||||
|
@ -45,60 +45,62 @@
|
|||
* A default is given
|
||||
*/
|
||||
export let layer: LayerConfig = splitroad_style
|
||||
/**
|
||||
* Optional: use these properties to set e.g. background layer
|
||||
*/
|
||||
export let mapProperties: undefined | Partial<MapProperties> = undefined;
|
||||
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let adaptor = new MapLibreAdaptor(map, mapProperties);
|
||||
|
||||
const wayGeojson: Feature<LineString> = GeoOperations.forceLineString( osmWay.asGeoJson())
|
||||
/**
|
||||
* Optional: use these properties to set e.g. background layer
|
||||
*/
|
||||
export let mapProperties: undefined | Partial<MapProperties> = undefined
|
||||
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let adaptor = new MapLibreAdaptor(map, mapProperties)
|
||||
|
||||
const wayGeojson: Feature<LineString> = GeoOperations.forceLineString(osmWay.asGeoJson())
|
||||
adaptor.location.setData(GeoOperations.centerpointCoordinatesObj(wayGeojson))
|
||||
adaptor.bounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
features: new StaticFeatureSource([wayGeojson]),
|
||||
drawMarkers: false,
|
||||
layer: layer
|
||||
layer: layer,
|
||||
})
|
||||
|
||||
export let splitPoints: UIEventSource< Feature<
|
||||
Point,
|
||||
{
|
||||
id: number
|
||||
index: number
|
||||
dist: number
|
||||
location: number
|
||||
}
|
||||
>[]> = new UIEventSource([])
|
||||
|
||||
export let splitPoints: UIEventSource<
|
||||
Feature<
|
||||
Point,
|
||||
{
|
||||
id: number
|
||||
index: number
|
||||
dist: number
|
||||
location: number
|
||||
}
|
||||
>[]
|
||||
> = new UIEventSource([])
|
||||
const splitPointsFS = new StaticFeatureSource(splitPoints)
|
||||
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: splitpoint_style,
|
||||
features: splitPointsFS,
|
||||
onClick: (clickedFeature: Feature) => {
|
||||
console.log("Clicked feature is", clickedFeature, splitPoints.data)
|
||||
const i = splitPoints.data.findIndex(f => f === clickedFeature)
|
||||
if(i < 0){
|
||||
const i = splitPoints.data.findIndex((f) => f === clickedFeature)
|
||||
if (i < 0) {
|
||||
return
|
||||
}
|
||||
splitPoints.data.splice(i, 1)
|
||||
splitPoints.ping()
|
||||
}
|
||||
},
|
||||
})
|
||||
let id = 0
|
||||
adaptor.lastClickLocation.addCallbackD(({lon, lat}) => {
|
||||
adaptor.lastClickLocation.addCallbackD(({ lon, lat }) => {
|
||||
const projected = GeoOperations.nearestPoint(wayGeojson, [lon, lat])
|
||||
|
||||
|
||||
projected.properties["id"] = id
|
||||
id++
|
||||
splitPoints.data.push(<any> projected)
|
||||
splitPoints.data.push(<any>projected)
|
||||
splitPoints.ping()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full">
|
||||
<MaplibreMap {map}></MaplibreMap>
|
||||
<MaplibreMap {map} />
|
||||
</div>
|
||||
|
|
|
@ -1,98 +1,95 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { ArrowDownTrayIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { FeatureCollection } from "geojson"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import DownloadHelper from "./DownloadHelper"
|
||||
import { Utils } from "../../Utils"
|
||||
import type { PriviligedLayerType } from "../../Models/Constants"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
import type {SpecialVisualizationState} from "../SpecialVisualization";
|
||||
import {ArrowDownTrayIcon} from "@babeard/svelte-heroicons/mini";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import type {FeatureCollection} from "geojson";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import DownloadHelper from "./DownloadHelper";
|
||||
import {Utils} from "../../Utils";
|
||||
import type {PriviligedLayerType} from "../../Models/Constants";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let extension: string
|
||||
export let mimetype: string
|
||||
export let construct: (
|
||||
geojsonCleaned: FeatureCollection,
|
||||
title: string,
|
||||
status?: UIEventSource<string>
|
||||
) => (Blob | string) | Promise<void>
|
||||
export let mainText: Translation
|
||||
export let helperText: Translation
|
||||
export let metaIsIncluded: boolean
|
||||
let downloadHelper: DownloadHelper = new DownloadHelper(state)
|
||||
|
||||
export let extension: string
|
||||
export let mimetype: string
|
||||
export let construct: (geojsonCleaned: FeatureCollection, title: string, status?: UIEventSource<string>) => (Blob | string) | Promise<void>
|
||||
export let mainText: Translation
|
||||
export let helperText: Translation
|
||||
export let metaIsIncluded: boolean
|
||||
let downloadHelper: DownloadHelper = new DownloadHelper(state)
|
||||
const t = Translations.t.general.download
|
||||
|
||||
const t = Translations.t.general.download
|
||||
let isExporting = false
|
||||
let isError = false
|
||||
|
||||
let isExporting = false
|
||||
let isError = false
|
||||
let status: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
|
||||
let status: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
async function clicked() {
|
||||
isExporting = true
|
||||
const gpsLayer = state.layerState.filteredLayers.get(<PriviligedLayerType>"gps_location")
|
||||
state.lastClickObject.features.setData([])
|
||||
|
||||
async function clicked() {
|
||||
isExporting = true
|
||||
const gpsLayer = state.layerState.filteredLayers.get(
|
||||
<PriviligedLayerType>"gps_location"
|
||||
)
|
||||
state.lastClickObject.features.setData([])
|
||||
|
||||
const gpsIsDisplayed = gpsLayer.isDisplayed.data
|
||||
try {
|
||||
gpsLayer.isDisplayed.setData(false)
|
||||
const geojson: FeatureCollection = downloadHelper.getCleanGeoJson(metaIsIncluded)
|
||||
const name = state.layout.id
|
||||
|
||||
const title = `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.${extension}`
|
||||
const promise = construct(geojson, title, status)
|
||||
let data: Blob | string
|
||||
if (typeof promise === "string") {
|
||||
data = promise
|
||||
} else if (typeof promise["then"] === "function") {
|
||||
data = await <Promise<Blob | string>>promise
|
||||
} else {
|
||||
data = <Blob>promise
|
||||
}
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
console.log("Got data", data)
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
data,
|
||||
title,
|
||||
{
|
||||
mimetype,
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
isError = true
|
||||
console.error(e)
|
||||
} finally {
|
||||
isExporting = false
|
||||
gpsLayer.isDisplayed.setData(gpsIsDisplayed)
|
||||
}
|
||||
const gpsIsDisplayed = gpsLayer.isDisplayed.data
|
||||
try {
|
||||
gpsLayer.isDisplayed.setData(false)
|
||||
const geojson: FeatureCollection = downloadHelper.getCleanGeoJson(metaIsIncluded)
|
||||
const name = state.layout.id
|
||||
|
||||
const title = `MapComplete_${name}_export_${new Date()
|
||||
.toISOString()
|
||||
.substr(0, 19)}.${extension}`
|
||||
const promise = construct(geojson, title, status)
|
||||
let data: Blob | string
|
||||
if (typeof promise === "string") {
|
||||
data = promise
|
||||
} else if (typeof promise["then"] === "function") {
|
||||
data = await (<Promise<Blob | string>>promise)
|
||||
} else {
|
||||
data = <Blob>promise
|
||||
}
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
console.log("Got data", data)
|
||||
Utils.offerContentsAsDownloadableFile(data, title, {
|
||||
mimetype,
|
||||
})
|
||||
} catch (e) {
|
||||
isError = true
|
||||
console.error(e)
|
||||
} finally {
|
||||
isExporting = false
|
||||
gpsLayer.isDisplayed.setData(gpsIsDisplayed)
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isError}
|
||||
<Tr cls="alert" t={Translations.t.general.error}/>
|
||||
<Tr cls="alert" t={Translations.t.general.error} />
|
||||
{:else if isExporting}
|
||||
<Loading>
|
||||
{#if $status}
|
||||
{$status}
|
||||
{:else}
|
||||
<Tr t={t.exporting}/>
|
||||
{/if}
|
||||
</Loading>
|
||||
<Loading>
|
||||
{#if $status}
|
||||
{$status}
|
||||
{:else}
|
||||
<Tr t={t.exporting} />
|
||||
{/if}
|
||||
</Loading>
|
||||
{:else}
|
||||
<button class="flex w-full" on:click={clicked}>
|
||||
<slot name="image">
|
||||
<ArrowDownTrayIcon class="w-12 h-12 mr-2 shrink-0"/>
|
||||
</slot>
|
||||
<span class="flex flex-col items-start">
|
||||
<Tr t={mainText}/>
|
||||
<Tr t={helperText} cls="subtle"/>
|
||||
</span>
|
||||
</button>
|
||||
<button class="flex w-full" on:click={clicked}>
|
||||
<slot name="image">
|
||||
<ArrowDownTrayIcon class="w-12 h-12 mr-2 shrink-0" />
|
||||
</slot>
|
||||
<span class="flex flex-col items-start">
|
||||
<Tr t={mainText} />
|
||||
<Tr t={helperText} cls="subtle" />
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {SpecialVisualizationState} from "../SpecialVisualization";
|
||||
import {Feature, FeatureCollection} from "geojson";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {Utils} from "../../Utils";
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Feature, FeatureCollection } from "geojson"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger"
|
||||
import geojson2svg from "geojson2svg"
|
||||
|
||||
|
@ -10,11 +10,10 @@ import geojson2svg from "geojson2svg"
|
|||
* Exposes the download-functionality
|
||||
*/
|
||||
export default class DownloadHelper {
|
||||
private readonly _state: SpecialVisualizationState;
|
||||
private readonly _state: SpecialVisualizationState
|
||||
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
this._state = state;
|
||||
|
||||
this._state = state
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,8 +22,8 @@ export default class DownloadHelper {
|
|||
private static cleanFeature(f: Feature): Feature {
|
||||
f = {
|
||||
type: f.type,
|
||||
geometry: {...f.geometry},
|
||||
properties: {...f.properties},
|
||||
geometry: { ...f.geometry },
|
||||
properties: { ...f.properties },
|
||||
}
|
||||
|
||||
for (const key in f.properties) {
|
||||
|
@ -46,9 +45,7 @@ export default class DownloadHelper {
|
|||
return f
|
||||
}
|
||||
|
||||
public getCleanGeoJson(
|
||||
includeMetaData: boolean
|
||||
): FeatureCollection {
|
||||
public getCleanGeoJson(includeMetaData: boolean): FeatureCollection {
|
||||
const featuresPerLayer = this.getCleanGeoJsonPerLayer(includeMetaData)
|
||||
const features = [].concat(...Array.from(featuresPerLayer.values()))
|
||||
return {
|
||||
|
@ -79,15 +76,13 @@ export default class DownloadHelper {
|
|||
* perLayer.set("testlayer", features)
|
||||
* new DownloadHelper(<any> {perLayer}).asSvg().replace(/\n/g, "") // => `<svg width="1000px" height="1000px" viewBox="0 0 1000 1000"> <g id="testlayer" inkscape:groupmode="layer" inkscape:label="testlayer"> <path d="M0,27.77777777777778 1000,472.22222222222223" style="fill:none;stroke-width:1" stroke="#ff0000"/> </g></svg>`
|
||||
*/
|
||||
public asSvg(
|
||||
options?: {
|
||||
layers?: LayerConfig[]
|
||||
width?: 1000 | number
|
||||
height?: 1000 | number
|
||||
mapExtent?: BBox
|
||||
unit?: "px" | "mm" | string
|
||||
}
|
||||
) {
|
||||
public asSvg(options?: {
|
||||
layers?: LayerConfig[]
|
||||
width?: 1000 | number
|
||||
height?: 1000 | number
|
||||
mapExtent?: BBox
|
||||
unit?: "px" | "mm" | string
|
||||
}) {
|
||||
const perLayer = this._state.perLayer
|
||||
options = options ?? {}
|
||||
const width = options.width ?? 1000
|
||||
|
@ -96,7 +91,7 @@ export default class DownloadHelper {
|
|||
throw "Invalid width of height, they should be > 0"
|
||||
}
|
||||
const unit = options.unit ?? "px"
|
||||
const mapExtent = {left: -180, bottom: -90, right: 180, top: 90}
|
||||
const mapExtent = { left: -180, bottom: -90, right: 180, top: 90 }
|
||||
if (options.mapExtent !== undefined) {
|
||||
const bbox = options.mapExtent
|
||||
mapExtent.left = bbox.minLon
|
||||
|
@ -104,7 +99,7 @@ export default class DownloadHelper {
|
|||
mapExtent.bottom = bbox.minLat
|
||||
mapExtent.top = bbox.maxLat
|
||||
}
|
||||
console.log("Generateing svg, extent:", {mapExtent, width, height})
|
||||
console.log("Generateing svg, extent:", { mapExtent, width, height })
|
||||
const elements: string[] = []
|
||||
|
||||
for (const layer of Array.from(perLayer.keys())) {
|
||||
|
@ -117,7 +112,7 @@ export default class DownloadHelper {
|
|||
const rendering = layerDef?.lineRendering[0]
|
||||
|
||||
const converter = geojson2svg({
|
||||
viewportSize: {width, height},
|
||||
viewportSize: { width, height },
|
||||
mapExtent,
|
||||
output: "svg",
|
||||
attributes: [
|
||||
|
@ -140,7 +135,7 @@ export default class DownloadHelper {
|
|||
feature.properties.stroke = Utils.colorAsHex(Utils.color(stroke))
|
||||
}
|
||||
|
||||
const groupPaths: string[] = converter.convert({type: "FeatureCollection", features})
|
||||
const groupPaths: string[] = converter.convert({ type: "FeatureCollection", features })
|
||||
const group =
|
||||
` <g id="${layer}" inkscape:groupmode="layer" inkscape:label="${layer}">\n` +
|
||||
groupPaths.map((p) => " " + p).join("\n") +
|
||||
|
@ -154,9 +149,7 @@ export default class DownloadHelper {
|
|||
return header + "\n" + elements.join("\n") + "\n</svg>"
|
||||
}
|
||||
|
||||
public getCleanGeoJsonPerLayer(
|
||||
includeMetaData: boolean
|
||||
): Map<string, Feature[]> {
|
||||
public getCleanGeoJsonPerLayer(includeMetaData: boolean): Map<string, Feature[]> {
|
||||
const state = this._state
|
||||
const featuresPerLayer = new Map<string, any[]>()
|
||||
const neededLayers = state.layout.layers.filter((l) => l.source !== null).map((l) => l.id)
|
||||
|
@ -188,15 +181,20 @@ export default class DownloadHelper {
|
|||
createImage(key: string, width: string, height: string): HTMLImageElement {
|
||||
const img = document.createElement("img")
|
||||
const sources = {
|
||||
"layouticon":this._state.layout.icon
|
||||
layouticon: this._state.layout.icon,
|
||||
}
|
||||
img.src = sources[key]
|
||||
if(!img.src){
|
||||
throw "Invalid key for 'createImage': "+key+"; try one of: "+Object.keys(sources).join(", ")
|
||||
if (!img.src) {
|
||||
throw (
|
||||
"Invalid key for 'createImage': " +
|
||||
key +
|
||||
"; try one of: " +
|
||||
Object.keys(sources).join(", ")
|
||||
)
|
||||
}
|
||||
img.style.width = width
|
||||
img.style.height = height
|
||||
console.log("Fetching an image with src", img.src)
|
||||
return img;
|
||||
return img
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,98 +1,96 @@
|
|||
<script lang="ts">
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import DownloadHelper from "./DownloadHelper"
|
||||
import DownloadButton from "./DownloadButton.svelte"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { SvgToPdf } from "../../Utils/svgToPdf"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import DownloadPdf from "./DownloadPdf.svelte"
|
||||
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import DownloadHelper from "./DownloadHelper";
|
||||
import DownloadButton from "./DownloadButton.svelte";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {SvgToPdf} from "../../Utils/svgToPdf";
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import DownloadPdf from "./DownloadPdf.svelte";
|
||||
export let state: ThemeViewState
|
||||
let isLoading = state.dataIsLoading
|
||||
|
||||
export let state: ThemeViewState
|
||||
let isLoading = state.dataIsLoading
|
||||
const t = Translations.t.general.download
|
||||
|
||||
const t = Translations.t.general.download
|
||||
|
||||
const downloadHelper = new DownloadHelper(state)
|
||||
|
||||
let metaIsIncluded = false
|
||||
const name = state.layout.id
|
||||
|
||||
|
||||
function offerSvg(): string {
|
||||
const maindiv = document.getElementById("maindiv")
|
||||
const layers = state.layout.layers.filter((l) => l.source !== null)
|
||||
return downloadHelper.asSvg({
|
||||
layers,
|
||||
mapExtent: state.mapProperties.bounds.data,
|
||||
width: maindiv.offsetWidth,
|
||||
height: maindiv.offsetHeight,
|
||||
})
|
||||
}
|
||||
const downloadHelper = new DownloadHelper(state)
|
||||
|
||||
let metaIsIncluded = false
|
||||
const name = state.layout.id
|
||||
|
||||
function offerSvg(): string {
|
||||
const maindiv = document.getElementById("maindiv")
|
||||
const layers = state.layout.layers.filter((l) => l.source !== null)
|
||||
return downloadHelper.asSvg({
|
||||
layers,
|
||||
mapExtent: state.mapProperties.bounds.data,
|
||||
width: maindiv.offsetWidth,
|
||||
height: maindiv.offsetHeight,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{#if $isLoading}
|
||||
<Loading/>
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="w-full flex flex-col" />
|
||||
<h3>
|
||||
<Tr t={t.title} />
|
||||
</h3>
|
||||
|
||||
<div class="w-full flex flex-col"></div>
|
||||
<h3>
|
||||
<Tr t={t.title}/>
|
||||
</h3>
|
||||
<DownloadButton
|
||||
{state}
|
||||
extension="geojson"
|
||||
mimetype="application/vnd.geo+json"
|
||||
construct={(geojson) => JSON.stringify(geojson)}
|
||||
mainText={t.downloadGeojson}
|
||||
helperText={t.downloadGeoJsonHelper}
|
||||
{metaIsIncluded}
|
||||
/>
|
||||
|
||||
<DownloadButton {state}
|
||||
extension="geojson"
|
||||
mimetype="application/vnd.geo+json"
|
||||
construct={(geojson) => JSON.stringify(geojson)}
|
||||
mainText={t.downloadGeojson}
|
||||
helperText={t.downloadGeoJsonHelper}
|
||||
{metaIsIncluded}/>
|
||||
<DownloadButton
|
||||
{state}
|
||||
extension="csv"
|
||||
mimetype="text/csv"
|
||||
construct={(geojson) => GeoOperations.toCSV(geojson)}
|
||||
mainText={t.downloadCSV}
|
||||
helperText={t.downloadCSVHelper}
|
||||
{metaIsIncluded}
|
||||
/>
|
||||
|
||||
<DownloadButton {state}
|
||||
extension="csv"
|
||||
mimetype="text/csv"
|
||||
construct={(geojson) => GeoOperations.toCSV(geojson)}
|
||||
mainText={t.downloadCSV}
|
||||
helperText={t.downloadCSVHelper}
|
||||
{metaIsIncluded}/>
|
||||
<label class="mb-8 mt-2">
|
||||
<input type="checkbox" bind:value={metaIsIncluded} />
|
||||
<Tr t={t.includeMetaData} />
|
||||
</label>
|
||||
|
||||
<DownloadButton
|
||||
{state}
|
||||
{metaIsIncluded}
|
||||
extension="svg"
|
||||
mimetype="image/svg+xml"
|
||||
mainText={t.downloadAsSvg}
|
||||
helperText={t.downloadAsSvgHelper}
|
||||
construct={offerSvg}
|
||||
/>
|
||||
|
||||
<label class="mb-8 mt-2">
|
||||
<input type="checkbox" bind:value={metaIsIncluded}>
|
||||
<Tr t={t.includeMetaData}/>
|
||||
</label>
|
||||
<DownloadButton
|
||||
{state}
|
||||
{metaIsIncluded}
|
||||
extension="png"
|
||||
mimetype="image/png"
|
||||
mainText={t.downloadAsPng}
|
||||
helperText={t.downloadAsPngHelper}
|
||||
construct={(_) => state.mapProperties.exportAsPng(4)}
|
||||
/>
|
||||
|
||||
<DownloadButton {state} {metaIsIncluded}
|
||||
extension="svg"
|
||||
mimetype="image/svg+xml"
|
||||
mainText={t.downloadAsSvg}
|
||||
helperText={t.downloadAsSvgHelper}
|
||||
construct={offerSvg}
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
{#each Object.keys(SvgToPdf.templates) as key}
|
||||
{#if SvgToPdf.templates[key].isPublic}
|
||||
<DownloadPdf {state} templateName={key} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<DownloadButton {state} {metaIsIncluded}
|
||||
extension="png"
|
||||
mimetype="image/png"
|
||||
mainText={t.downloadAsPng}
|
||||
helperText={t.downloadAsPngHelper}
|
||||
construct={_ => state.mapProperties.exportAsPng(4)}
|
||||
/>
|
||||
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#each Object.keys(SvgToPdf.templates) as key}
|
||||
{#if SvgToPdf.templates[key].isPublic}
|
||||
<DownloadPdf {state} templateName={key}/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
<Tr cls="link-underline" t={t.licenseInfo}/>
|
||||
<Tr cls="link-underline" t={t.licenseInfo} />
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,63 +1,67 @@
|
|||
<script lang="ts">
|
||||
import DownloadButton from "./DownloadButton.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { SvgToPdf } from "../../Utils/svgToPdf"
|
||||
import type { PdfTemplateInfo } from "../../Utils/svgToPdf"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { Utils } from "../../Utils"
|
||||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
import Constants from "../../Models/Constants"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import DownloadHelper from "./DownloadHelper"
|
||||
|
||||
import DownloadButton from "./DownloadButton.svelte";
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import {SvgToPdf} from "../../Utils/svgToPdf";
|
||||
import type {PdfTemplateInfo} from "../../Utils/svgToPdf";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {Utils} from "../../Utils";
|
||||
import {AvailableRasterLayers} from "../../Models/RasterLayers";
|
||||
import Constants from "../../Models/Constants";
|
||||
import Locale from "../i18n/Locale";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import DownloadHelper from "./DownloadHelper";
|
||||
export let templateName: string
|
||||
export let state: ThemeViewState
|
||||
const template: PdfTemplateInfo = SvgToPdf.templates[templateName]
|
||||
console.log("template", template)
|
||||
let mainText: Translation =
|
||||
typeof template.description === "string"
|
||||
? new Translation(template.description)
|
||||
: template.description
|
||||
let t = Translations.t.general.download
|
||||
const downloadHelper = new DownloadHelper(state)
|
||||
|
||||
export let templateName: string
|
||||
export let state: ThemeViewState
|
||||
const template: PdfTemplateInfo = SvgToPdf.templates[templateName]
|
||||
console.log("template", template)
|
||||
let mainText: Translation = typeof template.description === "string" ? new Translation(template.description) : template.description
|
||||
let t = Translations.t.general.download
|
||||
const downloadHelper = new DownloadHelper(state)
|
||||
async function constructPdf(_, title: string, status: UIEventSource<string>) {
|
||||
title =
|
||||
title.substring(0, title.length - 4) + "_" + template.format + "_" + template.orientation
|
||||
const templateUrls = SvgToPdf.templates[templateName].pages
|
||||
const templates: string[] = await Promise.all(templateUrls.map((url) => Utils.download(url)))
|
||||
console.log("Templates are", templates)
|
||||
const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.maplibre
|
||||
const creator = new SvgToPdf(title, templates, {
|
||||
state,
|
||||
freeComponentId: "belowmap",
|
||||
createImage: (key: string, width: string, height: string) =>
|
||||
downloadHelper.createImage(key, width, height),
|
||||
textSubstitutions: <Record<string, string>>{
|
||||
"layout.title": state.layout.title,
|
||||
layoutid: state.layout.id,
|
||||
title: state.layout.title,
|
||||
layoutImg: state.layout.icon,
|
||||
version: Constants.vNumber,
|
||||
date: new Date().toISOString().substring(0, 16),
|
||||
background: new Translation(bg.properties.name).txt,
|
||||
},
|
||||
})
|
||||
|
||||
async function constructPdf(_, title: string, status: UIEventSource<string>) {
|
||||
title=title.substring(0, title.length - 4)+"_"+template.format+"_"+template.orientation
|
||||
const templateUrls = SvgToPdf.templates[templateName].pages
|
||||
const templates: string[] = await Promise.all(templateUrls.map(url => Utils.download(url)))
|
||||
console.log("Templates are", templates)
|
||||
const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.maplibre
|
||||
const creator = new SvgToPdf(title, templates, {
|
||||
state,
|
||||
freeComponentId: "belowmap",
|
||||
createImage: (key: string, width: string, height: string) => downloadHelper.createImage(key, width, height),
|
||||
textSubstitutions: <Record<string, string>>{
|
||||
"layout.title": state.layout.title,
|
||||
layoutid: state.layout.id,
|
||||
title: state.layout.title,
|
||||
layoutImg: state.layout.icon,
|
||||
version: Constants.vNumber,
|
||||
date: new Date().toISOString().substring(0, 16),
|
||||
background: new Translation(bg.properties.name).txt
|
||||
}
|
||||
})
|
||||
|
||||
const unsub = creator.status.addCallbackAndRunD(s => {
|
||||
console.log("SVG creator status:", s)
|
||||
status?.setData(s);
|
||||
})
|
||||
await creator.ExportPdf(Locale.language.data)
|
||||
unsub()
|
||||
return undefined
|
||||
}
|
||||
const unsub = creator.status.addCallbackAndRunD((s) => {
|
||||
console.log("SVG creator status:", s)
|
||||
status?.setData(s)
|
||||
})
|
||||
await creator.ExportPdf(Locale.language.data)
|
||||
unsub()
|
||||
return undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<DownloadButton construct={constructPdf}
|
||||
extension="pdf"
|
||||
helperText={t.downloadAsPdfHelper}
|
||||
metaIsIncluded={false}
|
||||
{mainText}
|
||||
mimetype="application/pdf"
|
||||
{state}
|
||||
<DownloadButton
|
||||
construct={constructPdf}
|
||||
extension="pdf"
|
||||
helperText={t.downloadAsPdfHelper}
|
||||
metaIsIncluded={false}
|
||||
{mainText}
|
||||
mimetype="application/pdf"
|
||||
{state}
|
||||
/>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import {Tag} from "../../Logic/Tags/Tag"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import FileSelectorButton from "../Input/FileSelectorButton"
|
||||
import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||
import {VariableUiElement} from "../Base/VariableUIElement"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Loading from "../Base/Loading"
|
||||
import {LoginToggle} from "../Popup/LoginButton"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import Constants from "../../Models/Constants"
|
||||
import {SpecialVisualizationState} from "../SpecialVisualization"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
|
||||
|
@ -80,9 +80,9 @@ export class ImageUploadFlow extends Toggle {
|
|||
const fileSelector = new FileSelectorButton(label, {
|
||||
acceptType: "image/*",
|
||||
allowMultiple: true,
|
||||
labelClasses: "rounded-full border-2 border-black font-bold"
|
||||
labelClasses: "rounded-full border-2 border-black font-bold",
|
||||
})
|
||||
/* fileSelector.SetClass(
|
||||
/* fileSelector.SetClass(
|
||||
"p-2 border-4 border-detail rounded-full font-bold h-full align-middle w-full flex justify-center"
|
||||
)
|
||||
.SetStyle(" border-color: var(--foreground-color);")*/
|
||||
|
@ -144,7 +144,7 @@ export class ImageUploadFlow extends Toggle {
|
|||
return new Loading(t.uploadingPicture).SetClass("alert")
|
||||
} else {
|
||||
return new Loading(
|
||||
t.uploadingMultiple.Subs({count: "" + l})
|
||||
t.uploadingMultiple.Subs({ count: "" + l })
|
||||
).SetClass("alert")
|
||||
}
|
||||
})
|
||||
|
@ -168,7 +168,7 @@ export class ImageUploadFlow extends Toggle {
|
|||
if (l == 1) {
|
||||
return t.uploadDone.Clone().SetClass("thanks block")
|
||||
}
|
||||
return t.uploadMultipleDone.Subs({count: l}).SetClass("thanks block")
|
||||
return t.uploadMultipleDone.Subs({ count: l }).SetClass("thanks block")
|
||||
})
|
||||
),
|
||||
|
||||
|
@ -177,7 +177,7 @@ export class ImageUploadFlow extends Toggle {
|
|||
Translations.t.image.respectPrivacy,
|
||||
new VariableUiElement(
|
||||
licenseStore.map((license) =>
|
||||
Translations.t.image.currentLicense.Subs({license})
|
||||
Translations.t.image.currentLicense.Subs({ license })
|
||||
)
|
||||
)
|
||||
.onClick(() => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {InputElement} from "./InputElement"
|
||||
import {UIEventSource} from "../../Logic/UIEventSource"
|
||||
import {Utils} from "../../Utils"
|
||||
import { InputElement } from "./InputElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import InputElementMap from "./InputElementMap"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
|
|
@ -11,20 +11,20 @@ export default class FileSelectorButton extends InputElement<FileList> {
|
|||
private readonly _label: BaseUIElement
|
||||
private readonly _acceptType: string
|
||||
private readonly allowMultiple: boolean
|
||||
private readonly _labelClasses: string;
|
||||
private readonly _labelClasses: string
|
||||
|
||||
constructor(
|
||||
label: BaseUIElement,
|
||||
options?: {
|
||||
acceptType: "image/*" | string
|
||||
allowMultiple: true | boolean,
|
||||
allowMultiple: true | boolean
|
||||
labelClasses?: string
|
||||
}
|
||||
) {
|
||||
super()
|
||||
this._label = label
|
||||
this._acceptType = options?.acceptType ?? "image/*"
|
||||
this._labelClasses= options?.labelClasses ?? ""
|
||||
this._labelClasses = options?.labelClasses ?? ""
|
||||
this.SetClass("block cursor-pointer")
|
||||
label.SetClass("cursor-pointer")
|
||||
this.allowMultiple = options?.allowMultiple ?? true
|
||||
|
@ -43,7 +43,7 @@ export default class FileSelectorButton extends InputElement<FileList> {
|
|||
const el = document.createElement("form")
|
||||
const label = document.createElement("label")
|
||||
label.appendChild(this._label.ConstructElement())
|
||||
label.classList.add(...this._labelClasses.split(" ").filter(t => t !== ""))
|
||||
label.classList.add(...this._labelClasses.split(" ").filter((t) => t !== ""))
|
||||
el.appendChild(label)
|
||||
|
||||
const actualInputElement = document.createElement("input")
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
/**
|
||||
* Simple wrapper around the HTML-color field.
|
||||
*/
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
|
||||
export let value: UIEventSource<undefined | string>;
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
|
||||
export let value: UIEventSource<undefined | string>
|
||||
</script>
|
||||
|
||||
|
||||
<input bind:value={$value} type="color">
|
||||
<input bind:value={$value} type="color" />
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
/**
|
||||
* Simple wrapper around the HTML-date field.
|
||||
*/
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
|
||||
export let value: UIEventSource<undefined | string>;
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
|
||||
export let value: UIEventSource<undefined | string>
|
||||
</script>
|
||||
|
||||
|
||||
<input bind:value={$value} type="date">
|
||||
<input bind:value={$value} type="date" />
|
||||
|
|
|
@ -1,69 +1,68 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { MapProperties } from "../../../Models/MapProperties";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor";
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import Svg from "../../../Svg.js";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import type { MapProperties } from "../../../Models/MapProperties"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor"
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import Svg from "../../../Svg.js"
|
||||
|
||||
/**
|
||||
* A visualisation to pick a direction on a map background.
|
||||
*/
|
||||
export let value: UIEventSource<undefined | string>;
|
||||
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> };
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let mla = new MapLibreAdaptor(map, mapProperties);
|
||||
export let value: UIEventSource<undefined | string>
|
||||
export let mapProperties: Partial<MapProperties> & {
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
}
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let mla = new MapLibreAdaptor(map, mapProperties)
|
||||
mla.allowMoving.setData(false)
|
||||
mla.allowZooming.setData(false)
|
||||
let directionElem: HTMLElement | undefined;
|
||||
$: value.addCallbackAndRunD(degrees => {
|
||||
let directionElem: HTMLElement | undefined
|
||||
$: value.addCallbackAndRunD((degrees) => {
|
||||
if (directionElem === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
directionElem.style.rotate = degrees + "deg";
|
||||
});
|
||||
directionElem.style.rotate = degrees + "deg"
|
||||
})
|
||||
|
||||
let mainElem : HTMLElement
|
||||
let mainElem: HTMLElement
|
||||
function onPosChange(x: number, y: number) {
|
||||
const rect = mainElem.getBoundingClientRect();
|
||||
const dx = -(rect.left + rect.right) / 2 + x;
|
||||
const dy = (rect.top + rect.bottom) / 2 - y;
|
||||
const angle = (180 * Math.atan2(dy, dx)) / Math.PI;
|
||||
const angleGeo = Math.floor((450 - angle) % 360);
|
||||
value.setData(""+angleGeo);
|
||||
const rect = mainElem.getBoundingClientRect()
|
||||
const dx = -(rect.left + rect.right) / 2 + x
|
||||
const dy = (rect.top + rect.bottom) / 2 - y
|
||||
const angle = (180 * Math.atan2(dy, dx)) / Math.PI
|
||||
const angleGeo = Math.floor((450 - angle) % 360)
|
||||
value.setData("" + angleGeo)
|
||||
}
|
||||
|
||||
let isDown = false;
|
||||
let isDown = false
|
||||
</script>
|
||||
|
||||
<div bind:this={mainElem} class="relative w-48 h-48 cursor-pointer overflow-hidden"
|
||||
on:click={e => onPosChange(e.x, e.y)}
|
||||
on:mousedown={e => {
|
||||
isDown = true
|
||||
onPosChange(e.clientX, e.clientY)
|
||||
} }
|
||||
on:mousemove={e => {
|
||||
if(isDown){
|
||||
<div
|
||||
bind:this={mainElem}
|
||||
class="relative w-48 h-48 cursor-pointer overflow-hidden"
|
||||
on:click={(e) => onPosChange(e.x, e.y)}
|
||||
on:mousedown={(e) => {
|
||||
isDown = true
|
||||
onPosChange(e.clientX, e.clientY)
|
||||
}}
|
||||
on:mousemove={(e) => {
|
||||
if (isDown) {
|
||||
onPosChange(e.clientX, e.clientY)
|
||||
|
||||
}}}
|
||||
|
||||
on:mouseup={() => {
|
||||
isDown = false
|
||||
} }
|
||||
on:touchmove={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}
|
||||
|
||||
|
||||
on:touchstart={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}>
|
||||
}
|
||||
}}
|
||||
on:mouseup={() => {
|
||||
isDown = false
|
||||
}}
|
||||
on:touchmove={(e) => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}
|
||||
on:touchstart={(e) => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}
|
||||
>
|
||||
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
|
||||
<MaplibreMap {map} attribution={false}></MaplibreMap>
|
||||
<MaplibreMap {map} attribution={false} />
|
||||
</div>
|
||||
|
||||
<div bind:this={directionElem} class="absolute w-full h-full top-0 left-0">
|
||||
|
||||
<ToSvelte construct={ Svg.direction_stroke_svg}>
|
||||
|
||||
</ToSvelte>
|
||||
<ToSvelte construct={Svg.direction_stroke_svg} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,139 +1,150 @@
|
|||
<script lang="ts">
|
||||
import { Store, Stores, UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import { Store, Stores, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
|
||||
/**
|
||||
* Given the available floors, shows an elevator to pick a single one
|
||||
*
|
||||
*
|
||||
* This is but the input element, the logic of handling the filter is in 'LevelSelector'
|
||||
*/
|
||||
export let floors: Store<string[]>;
|
||||
export let value: UIEventSource<string>;
|
||||
export let floors: Store<string[]>
|
||||
export let value: UIEventSource<string>
|
||||
|
||||
const HEIGHT = 40;
|
||||
const HEIGHT = 40
|
||||
|
||||
let initialIndex = Math.max(0, floors?.data?.findIndex(f => f === value?.data) ?? 0);
|
||||
let index: UIEventSource<number> = new UIEventSource<number>(initialIndex);
|
||||
let forceIndex: number | undefined = undefined;
|
||||
let top = Math.max(0, initialIndex) * HEIGHT;
|
||||
let elevator: HTMLImageElement;
|
||||
let initialIndex = Math.max(0, floors?.data?.findIndex((f) => f === value?.data) ?? 0)
|
||||
let index: UIEventSource<number> = new UIEventSource<number>(initialIndex)
|
||||
let forceIndex: number | undefined = undefined
|
||||
let top = Math.max(0, initialIndex) * HEIGHT
|
||||
let elevator: HTMLImageElement
|
||||
|
||||
let mouseDown = false;
|
||||
let mouseDown = false
|
||||
|
||||
let container: HTMLElement;
|
||||
let container: HTMLElement
|
||||
|
||||
$:{
|
||||
$: {
|
||||
if (top > 0 || forceIndex !== undefined) {
|
||||
index.setData(closestFloorIndex());
|
||||
value.setData(floors.data[forceIndex ?? closestFloorIndex()]);
|
||||
index.setData(closestFloorIndex())
|
||||
value.setData(floors.data[forceIndex ?? closestFloorIndex()])
|
||||
}
|
||||
}
|
||||
|
||||
function unclick() {
|
||||
mouseDown = false;
|
||||
mouseDown = false
|
||||
}
|
||||
|
||||
function click() {
|
||||
mouseDown = true;
|
||||
mouseDown = true
|
||||
}
|
||||
|
||||
function closestFloorIndex() {
|
||||
return Math.min(floors.data.length - 1, Math.max(0, Math.round(top / HEIGHT)));
|
||||
return Math.min(floors.data.length - 1, Math.max(0, Math.round(top / HEIGHT)))
|
||||
}
|
||||
|
||||
function onMove(e: { movementY: number }) {
|
||||
if (mouseDown) {
|
||||
forceIndex = undefined;
|
||||
const containerY = container.clientTop;
|
||||
const containerMax = containerY + (floors.data.length - 1) * HEIGHT;
|
||||
top = Math.min(Math.max(0, top + e.movementY), containerMax);
|
||||
forceIndex = undefined
|
||||
const containerY = container.clientTop
|
||||
const containerMax = containerY + (floors.data.length - 1) * HEIGHT
|
||||
top = Math.min(Math.max(0, top + e.movementY), containerMax)
|
||||
}
|
||||
}
|
||||
|
||||
let momentum = 0;
|
||||
let momentum = 0
|
||||
|
||||
function stabilize() {
|
||||
// Automatically move the elevator to the closes floor
|
||||
if (mouseDown) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const target = (forceIndex ?? index.data) * HEIGHT;
|
||||
let diff = target - top;
|
||||
const target = (forceIndex ?? index.data) * HEIGHT
|
||||
let diff = target - top
|
||||
if (diff > 1) {
|
||||
diff /= 3;
|
||||
diff /= 3
|
||||
}
|
||||
const sign = Math.sign(diff);
|
||||
momentum = momentum + sign;
|
||||
let diffR = Math.min(Math.abs(momentum), forceIndex !== undefined ? 9 : 3, Math.abs(diff));
|
||||
momentum = Math.sign(momentum) * Math.min(diffR, Math.abs(momentum));
|
||||
top += sign * diffR;
|
||||
const sign = Math.sign(diff)
|
||||
momentum = momentum + sign
|
||||
let diffR = Math.min(Math.abs(momentum), forceIndex !== undefined ? 9 : 3, Math.abs(diff))
|
||||
momentum = Math.sign(momentum) * Math.min(diffR, Math.abs(momentum))
|
||||
top += sign * diffR
|
||||
if (index.data === forceIndex) {
|
||||
forceIndex = undefined;
|
||||
forceIndex = undefined
|
||||
}
|
||||
top = Math.max(top, 0)
|
||||
}
|
||||
|
||||
Stores.Chronic(50).addCallback(_ => stabilize());
|
||||
floors.addCallback(floors => {
|
||||
forceIndex = floors.findIndex(s => s === value.data)
|
||||
Stores.Chronic(50).addCallback((_) => stabilize())
|
||||
floors.addCallback((floors) => {
|
||||
forceIndex = floors.findIndex((s) => s === value.data)
|
||||
})
|
||||
|
||||
let image: HTMLImageElement;
|
||||
$:{
|
||||
let image: HTMLImageElement
|
||||
$: {
|
||||
if (image) {
|
||||
let lastY = 0;
|
||||
let lastY = 0
|
||||
image.ontouchstart = (e: TouchEvent) => {
|
||||
mouseDown = true;
|
||||
lastY = e.changedTouches[0].clientY;
|
||||
};
|
||||
image.ontouchmove = e => {
|
||||
const y = e.changedTouches[0].clientY;
|
||||
mouseDown = true
|
||||
lastY = e.changedTouches[0].clientY
|
||||
}
|
||||
image.ontouchmove = (e) => {
|
||||
const y = e.changedTouches[0].clientY
|
||||
console.log(y)
|
||||
const movementY = y - lastY;
|
||||
lastY = y;
|
||||
onMove({ movementY });
|
||||
};
|
||||
image.ontouchend = unclick;
|
||||
const movementY = y - lastY
|
||||
lastY = y
|
||||
onMove({ movementY })
|
||||
}
|
||||
image.ontouchend = unclick
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="relative"
|
||||
style={`height: calc(${HEIGHT}px * ${$floors.length}); width: 96px`}>
|
||||
<div
|
||||
bind:this={container}
|
||||
class="relative"
|
||||
style={`height: calc(${HEIGHT}px * ${$floors.length}); width: 96px`}
|
||||
>
|
||||
<div class="h-full absolute w-min right-0">
|
||||
{#each $floors as floor, i}
|
||||
<button style={`height: ${HEIGHT}px; width: ${HEIGHT}px`}
|
||||
class={"m-0 border-2 border-gray-300 flex content-box justify-center items-center "+(i === (forceIndex ?? $index) ? "selected": "" )
|
||||
}
|
||||
on:click={() => {forceIndex = i}}
|
||||
> {floor}</button>
|
||||
<button
|
||||
style={`height: ${HEIGHT}px; width: ${HEIGHT}px`}
|
||||
class={"m-0 border-2 border-gray-300 flex content-box justify-center items-center " +
|
||||
(i === (forceIndex ?? $index) ? "selected" : "")}
|
||||
on:click={() => {
|
||||
forceIndex = i
|
||||
}}
|
||||
>
|
||||
{floor}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div style={`width: ${HEIGHT}px`}>
|
||||
<img bind:this={image} class="draggable" draggable="false" on:mousedown={click} src="./assets/svg/elevator.svg"
|
||||
style={" top: "+top+"px;"} />
|
||||
<img
|
||||
bind:this={image}
|
||||
class="draggable"
|
||||
draggable="false"
|
||||
on:mousedown={click}
|
||||
src="./assets/svg/elevator.svg"
|
||||
style={" top: " + top + "px;"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:window on:mousemove={onMove} on:mouseup={unclick} />
|
||||
|
||||
<style>
|
||||
.draggable {
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
position: absolute;
|
||||
user-drag: none;
|
||||
|
||||
.draggable {
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
position: absolute;
|
||||
user-drag: none;
|
||||
|
||||
height: 72px;
|
||||
margin-top: -15px;
|
||||
margin-bottom: -15px;
|
||||
margin-left: -18px;
|
||||
-webkit-user-drag: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
height: 72px;
|
||||
margin-top: -15px;
|
||||
margin-bottom: -15px;
|
||||
margin-left: -18px;
|
||||
-webkit-user-drag: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,82 +1,91 @@
|
|||
<script lang="ts">
|
||||
import {Store, UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import type {MapProperties} from "../../../Models/MapProperties";
|
||||
import {Map as MlMap} from "maplibre-gl";
|
||||
import {MapLibreAdaptor} from "../../Map/MapLibreAdaptor";
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte";
|
||||
import DragInvitation from "../../Base/DragInvitation.svelte";
|
||||
import {GeoOperations} from "../../../Logic/GeoOperations";
|
||||
import ShowDataLayer from "../../Map/ShowDataLayer";
|
||||
import * as boundsdisplay from "../../../assets/layers/range/range.json"
|
||||
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import * as turf from "@turf/turf"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import {onDestroy} from "svelte";
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import type { MapProperties } from "../../../Models/MapProperties"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor"
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte"
|
||||
import DragInvitation from "../../Base/DragInvitation.svelte"
|
||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||
import ShowDataLayer from "../../Map/ShowDataLayer"
|
||||
import * as boundsdisplay from "../../../assets/layers/range/range.json"
|
||||
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import * as turf from "@turf/turf"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
/**
|
||||
* A visualisation to pick a location on a map background
|
||||
*/
|
||||
export let value: UIEventSource<{ lon: number, lat: number }>;
|
||||
export let initialCoordinate : {lon: number, lat :number}
|
||||
initialCoordinate = initialCoordinate ?? value.data
|
||||
export let maxDistanceInMeters: number = undefined
|
||||
export let mapProperties: Partial<MapProperties> & {
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
} = undefined;
|
||||
/**
|
||||
* Called when setup is done, can be used to add more layers to the map
|
||||
*/
|
||||
export let onCreated: (value: Store<{
|
||||
lon: number,
|
||||
lat: number
|
||||
}>, map: Store<MlMap>, mapProperties: MapProperties) => void = undefined
|
||||
/**
|
||||
* A visualisation to pick a location on a map background
|
||||
*/
|
||||
export let value: UIEventSource<{ lon: number; lat: number }>
|
||||
export let initialCoordinate: { lon: number; lat: number }
|
||||
initialCoordinate = initialCoordinate ?? value.data
|
||||
export let maxDistanceInMeters: number = undefined
|
||||
export let mapProperties: Partial<MapProperties> & {
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
} = undefined
|
||||
/**
|
||||
* Called when setup is done, can be used to add more layers to the map
|
||||
*/
|
||||
export let onCreated: (
|
||||
value: Store<{
|
||||
lon: number
|
||||
lat: number
|
||||
}>,
|
||||
map: Store<MlMap>,
|
||||
mapProperties: MapProperties
|
||||
) => void = undefined
|
||||
|
||||
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let mla = new MapLibreAdaptor(map, mapProperties);
|
||||
mapProperties.location.syncWith(value)
|
||||
if (onCreated) {
|
||||
onCreated(value, map, mla)
|
||||
}
|
||||
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let mla = new MapLibreAdaptor(map, mapProperties)
|
||||
mapProperties.location.syncWith(value)
|
||||
if (onCreated) {
|
||||
onCreated(value, map, mla)
|
||||
}
|
||||
|
||||
let rangeIsShown = false
|
||||
if (maxDistanceInMeters) {
|
||||
onDestroy(mla.location.addCallbackD(newLocation => {
|
||||
const l = [newLocation.lon, newLocation.lat]
|
||||
const c: [number, number] = [initialCoordinate.lon, initialCoordinate.lat]
|
||||
const d = GeoOperations.distanceBetween(l, c)
|
||||
let rangeIsShown = false
|
||||
if (maxDistanceInMeters) {
|
||||
onDestroy(
|
||||
mla.location.addCallbackD((newLocation) => {
|
||||
const l = [newLocation.lon, newLocation.lat]
|
||||
const c: [number, number] = [initialCoordinate.lon, initialCoordinate.lat]
|
||||
const d = GeoOperations.distanceBetween(l, c)
|
||||
console.log("distance is", d, l, c)
|
||||
if (d <= maxDistanceInMeters) {
|
||||
return
|
||||
}
|
||||
// This is too far away - let's move back
|
||||
const correctLocation = GeoOperations.along(c, l, maxDistanceInMeters - 10)
|
||||
window.setTimeout(() => {
|
||||
mla.location.setData({lon: correctLocation[0], lat: correctLocation[1]})
|
||||
}, 25)
|
||||
if (d <= maxDistanceInMeters) {
|
||||
return
|
||||
}
|
||||
// This is too far away - let's move back
|
||||
const correctLocation = GeoOperations.along(c, l, maxDistanceInMeters - 10)
|
||||
window.setTimeout(() => {
|
||||
mla.location.setData({ lon: correctLocation[0], lat: correctLocation[1] })
|
||||
}, 25)
|
||||
|
||||
if (!rangeIsShown) {
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: new LayerConfig(boundsdisplay),
|
||||
features: new StaticFeatureSource(
|
||||
[turf.circle(c, maxDistanceInMeters, {units: "meters", properties: {"range":"yes", id: "0"}}, )]
|
||||
)
|
||||
})
|
||||
rangeIsShown = true
|
||||
}
|
||||
}))
|
||||
}
|
||||
if (!rangeIsShown) {
|
||||
new ShowDataLayer(map, {
|
||||
layer: new LayerConfig(boundsdisplay),
|
||||
features: new StaticFeatureSource([
|
||||
turf.circle(c, maxDistanceInMeters, {
|
||||
units: "meters",
|
||||
properties: { range: "yes", id: "0" },
|
||||
}),
|
||||
]),
|
||||
})
|
||||
rangeIsShown = true
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<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}/>
|
||||
</div>
|
||||
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
|
||||
<MaplibreMap {map} />
|
||||
</div>
|
||||
|
||||
<div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50 flex items-center">
|
||||
<img class="h-full max-h-24" src="./assets/svg/move-arrows.svg"/>
|
||||
</div>
|
||||
|
||||
<DragInvitation hideSignal={mla.location.stabilized(3000)}></DragInvitation>
|
||||
<div
|
||||
class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50 flex items-center"
|
||||
>
|
||||
<img class="h-full max-h-24" src="./assets/svg/move-arrows.svg" />
|
||||
</div>
|
||||
|
||||
<DragInvitation hideSignal={mla.location.stabilized(3000)} />
|
||||
</div>
|
||||
|
|
|
@ -1,32 +1,33 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Constructs an input helper element for the given type.
|
||||
* Note that all values are stringified
|
||||
*/
|
||||
/**
|
||||
* Constructs an input helper element for the given type.
|
||||
* Note that all values are stringified
|
||||
*/
|
||||
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {ValidatorType} from "./Validators";
|
||||
import InputHelpers from "./InputHelpers";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import type {Feature} from "geojson";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { ValidatorType } from "./Validators"
|
||||
import InputHelpers from "./InputHelpers"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
|
||||
export let type: ValidatorType;
|
||||
export let value: UIEventSource<string>;
|
||||
|
||||
export let feature: Feature;
|
||||
export let args: (string | number | boolean)[] = undefined;
|
||||
|
||||
let properties = {feature, args: args ?? []};
|
||||
let construct = new UIEventSource<(value, extraProperties) => BaseUIElement>(undefined)
|
||||
$: {
|
||||
construct.setData(InputHelpers.AvailableInputHelpers[type])
|
||||
}
|
||||
export let type: ValidatorType
|
||||
export let value: UIEventSource<string>
|
||||
|
||||
export let feature: Feature
|
||||
export let args: (string | number | boolean)[] = undefined
|
||||
|
||||
let properties = { feature, args: args ?? [] }
|
||||
let construct = new UIEventSource<(value, extraProperties) => BaseUIElement>(undefined)
|
||||
$: {
|
||||
construct.setData(InputHelpers.AvailableInputHelpers[type])
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if construct !== undefined}
|
||||
<ToSvelte construct={() => new VariableUiElement(construct.mapD(construct => construct(value, properties)))}/>
|
||||
<ToSvelte
|
||||
construct={() =>
|
||||
new VariableUiElement(construct.mapD((construct) => construct(value, properties)))}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -1,109 +1,116 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { ValidatorType } from "./Validators"
|
||||
import Validators from "./Validators"
|
||||
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import { Validator } from "./Validator"
|
||||
import { Unit } from "../../Models/Unit"
|
||||
import UnitInput from "../Popup/UnitInput.svelte"
|
||||
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {ValidatorType} from "./Validators";
|
||||
import Validators from "./Validators";
|
||||
import {ExclamationIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {createEventDispatcher, onDestroy} from "svelte";
|
||||
import {Validator} from "./Validator";
|
||||
import {Unit} from "../../Models/Unit";
|
||||
import UnitInput from "../Popup/UnitInput.svelte";
|
||||
export let type: ValidatorType
|
||||
export let feedback: UIEventSource<Translation> | undefined = undefined
|
||||
export let getCountry: () => string | undefined
|
||||
export let placeholder: string | Translation | undefined
|
||||
export let unit: Unit = undefined
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
/**
|
||||
* Internal state bound to the input element.
|
||||
*
|
||||
* This is only copied to 'value' when appropriate so that no invalid values leak outside;
|
||||
* Additionally, the unit is added when copying
|
||||
*/
|
||||
let _value = new UIEventSource(value.data ?? "")
|
||||
|
||||
export let type: ValidatorType;
|
||||
export let feedback: UIEventSource<Translation> | undefined = undefined;
|
||||
export let getCountry: () => string | undefined
|
||||
export let placeholder: string | Translation | undefined
|
||||
export let unit: Unit = undefined
|
||||
let validator: Validator = Validators.get(type ?? "string")
|
||||
let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
||||
|
||||
export let value: UIEventSource<string>;
|
||||
/**
|
||||
* Internal state bound to the input element.
|
||||
*
|
||||
* This is only copied to 'value' when appropriate so that no invalid values leak outside;
|
||||
* Additionally, the unit is added when copying
|
||||
*/
|
||||
let _value = new UIEventSource(value.data ?? "");
|
||||
|
||||
let validator: Validator = Validators.get(type ?? "string")
|
||||
let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
||||
|
||||
function initValueAndDenom() {
|
||||
if (unit && value.data) {
|
||||
const [v, denom] = unit?.findDenomination(value.data, getCountry)
|
||||
if (denom) {
|
||||
_value.setData(v)
|
||||
selectedUnit.setData(denom.canonical)
|
||||
} else {
|
||||
_value.setData(value.data ?? "")
|
||||
}
|
||||
} else {
|
||||
_value.setData(value.data ?? "")
|
||||
}
|
||||
function initValueAndDenom() {
|
||||
if (unit && value.data) {
|
||||
const [v, denom] = unit?.findDenomination(value.data, getCountry)
|
||||
if (denom) {
|
||||
_value.setData(v)
|
||||
selectedUnit.setData(denom.canonical)
|
||||
} else {
|
||||
_value.setData(value.data ?? "")
|
||||
}
|
||||
} else {
|
||||
_value.setData(value.data ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
initValueAndDenom()
|
||||
|
||||
$: {
|
||||
// The type changed -> reset some values
|
||||
validator = Validators.get(type ?? "string")
|
||||
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
||||
feedback = feedback?.setData(validator?.getFeedback(_value.data, getCountry))
|
||||
|
||||
initValueAndDenom()
|
||||
}
|
||||
|
||||
$: {
|
||||
// The type changed -> reset some values
|
||||
validator = Validators.get(type ?? "string")
|
||||
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
||||
feedback = feedback?.setData(validator?.getFeedback(_value.data, getCountry));
|
||||
|
||||
initValueAndDenom()
|
||||
function setValues() {
|
||||
// Update the value stores
|
||||
const v = _value.data
|
||||
if (!validator.isValid(v, getCountry) || v === "") {
|
||||
value.setData(undefined)
|
||||
feedback?.setData(validator.getFeedback(v, getCountry))
|
||||
return
|
||||
}
|
||||
|
||||
function setValues() {
|
||||
// Update the value stores
|
||||
const v = _value.data
|
||||
if (!validator.isValid(v, getCountry) || v === "") {
|
||||
value.setData(undefined);
|
||||
feedback?.setData(validator.getFeedback(v, getCountry));
|
||||
return
|
||||
}
|
||||
|
||||
if (unit && isNaN(Number(v))) {
|
||||
value.setData(undefined);
|
||||
return
|
||||
}
|
||||
|
||||
feedback?.setData(undefined);
|
||||
value.setData(v + (selectedUnit.data ?? ""));
|
||||
if (unit && isNaN(Number(v))) {
|
||||
value.setData(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
onDestroy(_value.addCallbackAndRun(_ => setValues()))
|
||||
onDestroy(selectedUnit.addCallback(_ => setValues()))
|
||||
if (validator === undefined) {
|
||||
throw "Not a valid type for a validator:" + type;
|
||||
}
|
||||
|
||||
const isValid = _value.map(v => validator.isValid(v, getCountry));
|
||||
|
||||
let htmlElem: HTMLInputElement;
|
||||
|
||||
let dispatch = createEventDispatcher<{ selected }>();
|
||||
$: {
|
||||
if (htmlElem !== undefined) {
|
||||
htmlElem.onfocus = () => dispatch("selected");
|
||||
}
|
||||
feedback?.setData(undefined)
|
||||
value.setData(v + (selectedUnit.data ?? ""))
|
||||
}
|
||||
|
||||
onDestroy(_value.addCallbackAndRun((_) => setValues()))
|
||||
onDestroy(selectedUnit.addCallback((_) => setValues()))
|
||||
if (validator === undefined) {
|
||||
throw "Not a valid type for a validator:" + type
|
||||
}
|
||||
|
||||
const isValid = _value.map((v) => validator.isValid(v, getCountry))
|
||||
|
||||
let htmlElem: HTMLInputElement
|
||||
|
||||
let dispatch = createEventDispatcher<{ selected }>()
|
||||
$: {
|
||||
if (htmlElem !== undefined) {
|
||||
htmlElem.onfocus = () => dispatch("selected")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if validator.textArea}
|
||||
<textarea class="w-full" bind:value={$_value} inputmode={validator.inputmode ?? "text"}
|
||||
placeholder={_placeholder}></textarea>
|
||||
{:else }
|
||||
<textarea
|
||||
class="w-full"
|
||||
bind:value={$_value}
|
||||
inputmode={validator.inputmode ?? "text"}
|
||||
placeholder={_placeholder}
|
||||
/>
|
||||
{:else}
|
||||
<span class="inline-flex">
|
||||
<input bind:this={htmlElem} bind:value={$_value} class="w-full" inputmode={validator.inputmode ?? "text"}
|
||||
placeholder={_placeholder}>
|
||||
{#if !$isValid}
|
||||
<ExclamationIcon class="h-6 w-6 -ml-6"></ExclamationIcon>
|
||||
<input
|
||||
bind:this={htmlElem}
|
||||
bind:value={$_value}
|
||||
class="w-full"
|
||||
inputmode={validator.inputmode ?? "text"}
|
||||
placeholder={_placeholder}
|
||||
/>
|
||||
{#if !$isValid}
|
||||
<ExclamationIcon class="h-6 w-6 -ml-6" />
|
||||
{/if}
|
||||
|
||||
|
||||
{#if unit !== undefined}
|
||||
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value}/>
|
||||
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
@ -48,7 +48,7 @@ export abstract class Validator {
|
|||
* Returns 'undefined' if the element is valid
|
||||
*/
|
||||
public getFeedback(s: string, _?: () => string): Translation | undefined {
|
||||
if(this.isValid(s)){
|
||||
if (this.isValid(s)) {
|
||||
return undefined
|
||||
}
|
||||
const tr = Translations.t.validation[this.name]
|
||||
|
@ -57,7 +57,7 @@ export abstract class Validator {
|
|||
}
|
||||
}
|
||||
|
||||
public getPlaceholder(){
|
||||
public getPlaceholder() {
|
||||
return Translations.t.validation[this.name].description
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Translation} from "../../i18n/Translation.js"
|
||||
import { Translation } from "../../i18n/Translation.js"
|
||||
import Translations from "../../i18n/Translations.js"
|
||||
import * as emailValidatorLibrary from "email-validator"
|
||||
import {Validator} from "../Validator"
|
||||
import { Validator } from "../Validator"
|
||||
|
||||
export default class EmailValidator extends Validator {
|
||||
constructor() {
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import {parsePhoneNumberFromString} from "libphonenumber-js"
|
||||
import {Validator} from "../Validator"
|
||||
import {Translation} from "../../i18n/Translation";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import { parsePhoneNumberFromString } from "libphonenumber-js"
|
||||
import { Validator } from "../Validator"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
|
||||
export default class PhoneValidator extends Validator {
|
||||
constructor() {
|
||||
super("phone", "A phone number", "tel")
|
||||
}
|
||||
|
||||
|
||||
getFeedback(s: string, requestCountry?: () => string): Translation {
|
||||
if(this.isValid(s, requestCountry)){
|
||||
if (this.isValid(s, requestCountry)) {
|
||||
return undefined
|
||||
}
|
||||
const tr = Translations.t.validation.phone
|
||||
const generic = tr.feedback
|
||||
if(requestCountry){
|
||||
const country = requestCountry()
|
||||
if(country){
|
||||
return tr.feedbackCountry.Subs({country})
|
||||
if (requestCountry) {
|
||||
const country = requestCountry()
|
||||
if (country) {
|
||||
return tr.feedbackCountry.Subs({ country })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +43,7 @@ export default class PhoneValidator extends Validator {
|
|||
str = str.substring("tel:".length)
|
||||
}
|
||||
let countryCode = undefined
|
||||
if(country){
|
||||
if (country) {
|
||||
countryCode = country()
|
||||
}
|
||||
return parsePhoneNumberFromString(
|
||||
|
|
|
@ -2,6 +2,11 @@ import { Validator } from "../Validator"
|
|||
|
||||
export default class TextValidator extends Validator {
|
||||
constructor() {
|
||||
super("text", "A longer piece of text. Uses an textArea instead of a textField", "text", true)
|
||||
super(
|
||||
"text",
|
||||
"A longer piece of text. Uses an textArea instead of a textField",
|
||||
"text",
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Translation } from "./i18n/Translation"
|
|||
import Lazy from "./Base/Lazy"
|
||||
import Toggle from "./Input/Toggle"
|
||||
import LanguageUtils from "../Utils/LanguageUtils"
|
||||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
|
||||
export default class LanguagePicker extends Toggle {
|
||||
constructor(languages: string[], assignTo: UIEventSource<string>) {
|
||||
|
@ -16,13 +16,18 @@ export default class LanguagePicker extends Toggle {
|
|||
super(undefined, undefined, undefined)
|
||||
} else {
|
||||
const normalPicker = LanguagePicker.dropdownFor(languages, assignTo ?? Locale.language)
|
||||
const fullPicker = new Lazy(() => LanguagePicker.dropdownFor(allLanguages, assignTo ?? Locale.language))
|
||||
const fullPicker = new Lazy(() =>
|
||||
LanguagePicker.dropdownFor(allLanguages, assignTo ?? Locale.language)
|
||||
)
|
||||
super(fullPicker, normalPicker, Locale.showLinkToWeblate)
|
||||
const allLanguages: string[] = LanguageUtils.usedLanguagesSorted
|
||||
}
|
||||
}
|
||||
|
||||
private static dropdownFor(languages: string[], assignTo: UIEventSource<string>): BaseUIElement {
|
||||
private static dropdownFor(
|
||||
languages: string[],
|
||||
assignTo: UIEventSource<string>
|
||||
): BaseUIElement {
|
||||
return new DropDown(
|
||||
undefined,
|
||||
languages
|
||||
|
@ -30,7 +35,7 @@ export default class LanguagePicker extends Toggle {
|
|||
.map((lang) => {
|
||||
return { value: lang, shown: LanguagePicker.hybrid(lang) }
|
||||
}),
|
||||
assignTo
|
||||
assignTo
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import type {Map as MLMap} from "maplibre-gl"
|
||||
import {Map as MlMap, SourceSpecification} from "maplibre-gl"
|
||||
import {RasterLayerPolygon} from "../../Models/RasterLayers"
|
||||
import {Utils} from "../../Utils"
|
||||
import {BBox} from "../../Logic/BBox"
|
||||
import {ExportableMap, MapProperties} from "../../Models/MapProperties"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MLMap } from "maplibre-gl"
|
||||
import { Map as MlMap, SourceSpecification } from "maplibre-gl"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { ExportableMap, MapProperties } from "../../Models/MapProperties"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "./MaplibreMap.svelte"
|
||||
import {RasterLayerProperties} from "../../Models/RasterLayerProperties"
|
||||
import * as htmltoimage from 'html-to-image';
|
||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
import * as htmltoimage from "html-to-image"
|
||||
|
||||
/**
|
||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||
|
@ -53,7 +53,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
if (this.location.data) {
|
||||
// The MapLibre adaptor updates the element in the location and then pings them
|
||||
// Often, code setting this up doesn't expect the object they pass in to be changed, so we create a copy
|
||||
this.location.setData({...this.location.data})
|
||||
this.location.setData({ ...this.location.data })
|
||||
}
|
||||
this.zoom = state?.zoom ?? new UIEventSource(1)
|
||||
this.minzoom = state?.minzoom ?? new UIEventSource(0)
|
||||
|
@ -86,7 +86,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
console.log(e)
|
||||
const lon = e.lngLat.lng
|
||||
const lat = e.lngLat.lat
|
||||
lastClickLocation.setData({lon, lat})
|
||||
lastClickLocation.setData({ lon, lat })
|
||||
}
|
||||
|
||||
maplibreMap.addCallbackAndRunD((map) => {
|
||||
|
@ -170,16 +170,25 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
}
|
||||
}
|
||||
|
||||
public static setDpi(drawOn: HTMLCanvasElement, ctx: CanvasRenderingContext2D, dpiFactor: number) {
|
||||
public static setDpi(
|
||||
drawOn: HTMLCanvasElement,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
dpiFactor: number
|
||||
) {
|
||||
drawOn.style.width = drawOn.style.width || drawOn.width + "px"
|
||||
drawOn.style.height = drawOn.style.height || drawOn.height + "px"
|
||||
|
||||
|
||||
// Resize canvas and scale future draws.
|
||||
drawOn.width = Math.ceil(drawOn.width * dpiFactor)
|
||||
drawOn.height = Math.ceil(drawOn.height * dpiFactor)
|
||||
ctx.scale(dpiFactor, dpiFactor)
|
||||
console.log("Resizing canvas with setDPI:", drawOn.width, drawOn.height, drawOn.style.width, drawOn.style.height)
|
||||
console.log(
|
||||
"Resizing canvas with setDPI:",
|
||||
drawOn.width,
|
||||
drawOn.height,
|
||||
drawOn.style.width,
|
||||
drawOn.style.height
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -230,11 +239,21 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
console.log("Getting markers")
|
||||
// MapLibreAdaptor.setDpi(drawOn, ctx, 1)
|
||||
const markers = await this.drawMarkers(dpiFactor)
|
||||
console.log("Drawing markers (" + markers.width + "*" + markers.height + ") onto drawOn (" + drawOn.width + "*" + drawOn.height + ")")
|
||||
console.log(
|
||||
"Drawing markers (" +
|
||||
markers.width +
|
||||
"*" +
|
||||
markers.height +
|
||||
") onto drawOn (" +
|
||||
drawOn.width +
|
||||
"*" +
|
||||
drawOn.height +
|
||||
")"
|
||||
)
|
||||
ctx.drawImage(markers, 0, 0, drawOn.width, drawOn.height)
|
||||
ctx.scale(dpiFactor, dpiFactor)
|
||||
this._maplibreMap.data?.resize()
|
||||
return await new Promise<Blob>(resolve => drawOn.toBlob(blob => resolve(blob)))
|
||||
return await new Promise<Blob>((resolve) => drawOn.toBlob((blob) => resolve(blob)))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -270,7 +289,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
}
|
||||
const width = map.getCanvas().clientWidth
|
||||
const height = map.getCanvas().clientHeight
|
||||
console.log("Canvas size markers:", map.getCanvas().width, map.getCanvas().height, "canvasClientRect:", width, height)
|
||||
console.log(
|
||||
"Canvas size markers:",
|
||||
map.getCanvas().width,
|
||||
map.getCanvas().height,
|
||||
"canvasClientRect:",
|
||||
width,
|
||||
height
|
||||
)
|
||||
map.getCanvas().style.display = "none"
|
||||
const img = await htmltoimage.toCanvas(map.getCanvasContainer(), {
|
||||
pixelRatio: dpiFactor,
|
||||
|
@ -288,12 +314,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
if (!map) {
|
||||
return
|
||||
}
|
||||
const {lng, lat} = map.getCenter()
|
||||
const { lng, lat } = map.getCenter()
|
||||
if (lng === 0 && lat === 0) {
|
||||
return
|
||||
}
|
||||
if (this.location.data === undefined) {
|
||||
this.location.setData({lon: lng, lat})
|
||||
this.location.setData({ lon: lng, lat })
|
||||
} else if (!isSetup) {
|
||||
const dt = this.location.data
|
||||
dt.lon = map.getCenter().lng
|
||||
|
@ -329,7 +355,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
const center = map.getCenter()
|
||||
if (center.lng !== loc.lon || center.lat !== loc.lat) {
|
||||
map.setCenter({lng: loc.lon, lat: loc.lat})
|
||||
map.setCenter({ lng: loc.lon, lat: loc.lat })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -379,7 +405,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
return
|
||||
}
|
||||
|
||||
if(background.type === "vector"){
|
||||
if (background.type === "vector") {
|
||||
console.log("Background layer is vector")
|
||||
map.setStyle(background.url)
|
||||
return
|
||||
|
@ -389,12 +415,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
map.resize()
|
||||
|
||||
let addLayerBeforeId = "aeroway_fill"// this is the first non-landuse item in the stylesheet, we add the raster layer before the roads but above the landuse
|
||||
let addLayerBeforeId = "aeroway_fill" // this is the first non-landuse item in the stylesheet, we add the raster layer before the roads but above the landuse
|
||||
if (background.category === "osmbasedmap" || background.category === "map") {
|
||||
// The background layer is already an OSM-based map or another map, so we don't want anything from the baselayer
|
||||
let layers = map.getStyle().layers
|
||||
// THe last index of the maptiler layers
|
||||
let lastIndex = layers.findIndex(layer => layer.id === "housenumber")
|
||||
let lastIndex = layers.findIndex((layer) => layer.id === "housenumber")
|
||||
addLayerBeforeId = layers[lastIndex + 1]?.id ?? "housenumber"
|
||||
}
|
||||
|
||||
|
@ -404,7 +430,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
type: "raster",
|
||||
source: background.id,
|
||||
paint: {},
|
||||
}, addLayerBeforeId
|
||||
},
|
||||
addLayerBeforeId
|
||||
)
|
||||
await this.awaitStyleIsLoaded()
|
||||
this.removeCurrentLayer(map)
|
||||
|
|
|
@ -4,42 +4,46 @@
|
|||
*
|
||||
* As it replaces the old 'MinimapObj' onto MapLibre and the existing codebase, this is sometimes a bit awkward
|
||||
*/
|
||||
import { onMount } from "svelte";
|
||||
import { Map } from "@onsvisual/svelte-maps";
|
||||
import type { Map as MaplibreMap } from "maplibre-gl";
|
||||
import type { Writable } from "svelte/store";
|
||||
import {AvailableRasterLayers} from "../../Models/RasterLayers";
|
||||
|
||||
import { onMount } from "svelte"
|
||||
import { Map } from "@onsvisual/svelte-maps"
|
||||
import type { Map as MaplibreMap } from "maplibre-gl"
|
||||
import type { Writable } from "svelte/store"
|
||||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
|
||||
/**
|
||||
* Beware: this map will _only_ be set by this component
|
||||
* It should thus be treated as a 'store' by external parties
|
||||
*/
|
||||
export let map: Writable<MaplibreMap>
|
||||
export let map: Writable<MaplibreMap>
|
||||
|
||||
export let attribution = false
|
||||
let center = {};
|
||||
let center = {}
|
||||
|
||||
onMount(() => {
|
||||
$map.on("load", function() {
|
||||
$map.resize();
|
||||
});
|
||||
});
|
||||
const styleUrl = AvailableRasterLayers.maplibre.properties.url;
|
||||
$map.on("load", function () {
|
||||
$map.resize()
|
||||
})
|
||||
})
|
||||
const styleUrl = AvailableRasterLayers.maplibre.properties.url
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<Map bind:center={center}
|
||||
bind:map={$map}
|
||||
{attribution}
|
||||
css="./maplibre-gl.css"
|
||||
|
||||
id="map" location={{lng: 0, lat: 0, zoom: 0}} maxzoom=24 style={styleUrl} />
|
||||
<Map
|
||||
bind:center
|
||||
bind:map={$map}
|
||||
{attribution}
|
||||
css="./maplibre-gl.css"
|
||||
id="map"
|
||||
location={{ lng: 0, lat: 0, zoom: 0 }}
|
||||
maxzoom="24"
|
||||
style={styleUrl}
|
||||
/>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,69 +1,69 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* The overlay map is a bit a weird map:
|
||||
* it is a HTML-component which is intended to be placed _over_ another map.
|
||||
* It will align itself in order to seamlessly show the same location; but possibly in a different style
|
||||
*/
|
||||
import MaplibreMap from "./MaplibreMap.svelte";
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Map as MlMap} from "maplibre-gl";
|
||||
import {MapLibreAdaptor} from "./MapLibreAdaptor";
|
||||
import type {MapProperties} from "../../Models/MapProperties";
|
||||
import {onDestroy} from "svelte";
|
||||
import type {RasterLayerPolygon} from "../../Models/RasterLayers";
|
||||
/**
|
||||
* The overlay map is a bit a weird map:
|
||||
* it is a HTML-component which is intended to be placed _over_ another map.
|
||||
* It will align itself in order to seamlessly show the same location; but possibly in a different style
|
||||
*/
|
||||
import MaplibreMap from "./MaplibreMap.svelte"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { MapLibreAdaptor } from "./MapLibreAdaptor"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import { onDestroy } from "svelte"
|
||||
import type { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
|
||||
export let placedOverMapProperties: MapProperties
|
||||
export let placedOverMap: UIEventSource<MlMap>
|
||||
|
||||
export let placedOverMapProperties: MapProperties
|
||||
export let placedOverMap: UIEventSource<MlMap>
|
||||
export let rasterLayer: UIEventSource<RasterLayerPolygon>
|
||||
|
||||
export let rasterLayer: UIEventSource<RasterLayerPolygon>
|
||||
export let visible: Store<boolean> = undefined
|
||||
let altmap: UIEventSource<MlMap> = new UIEventSource(undefined)
|
||||
let altproperties = new MapLibreAdaptor(altmap, {
|
||||
rasterLayer,
|
||||
zoom: UIEventSource.feedFrom(placedOverMapProperties.zoom),
|
||||
})
|
||||
altproperties.allowMoving.setData(false)
|
||||
altproperties.allowZooming.setData(false)
|
||||
|
||||
export let visible: Store<boolean> = undefined
|
||||
let altmap: UIEventSource<MlMap> = new UIEventSource(undefined)
|
||||
let altproperties = new MapLibreAdaptor(altmap, {
|
||||
rasterLayer,
|
||||
zoom: UIEventSource.feedFrom(placedOverMapProperties.zoom)
|
||||
})
|
||||
altproperties.allowMoving.setData(false)
|
||||
altproperties.allowZooming.setData(false)
|
||||
function pixelCenterOf(map: UIEventSource<MlMap>): [number, number] {
|
||||
const rect = map?.data?.getCanvas()?.getBoundingClientRect()
|
||||
if (!rect) {
|
||||
return undefined
|
||||
}
|
||||
const x = (rect.left + rect.right) / 2
|
||||
const y = (rect.top + rect.bottom) / 2
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
function pixelCenterOf(map: UIEventSource<MlMap>): [number, number] {
|
||||
const rect = map?.data?.getCanvas()?.getBoundingClientRect()
|
||||
if (!rect) {
|
||||
return undefined
|
||||
function updateLocation() {
|
||||
if (!placedOverMap.data || !altmap.data) {
|
||||
return
|
||||
}
|
||||
altmap.data.resize()
|
||||
const { lon, lat } = placedOverMapProperties.location.data
|
||||
const altMapCenter = pixelCenterOf(altmap)
|
||||
const c = placedOverMap.data.unproject(altMapCenter)
|
||||
altproperties.location.setData({ lon: c.lng, lat: c.lat })
|
||||
}
|
||||
|
||||
onDestroy(placedOverMapProperties.location.addCallbackAndRunD(updateLocation))
|
||||
updateLocation()
|
||||
window.setTimeout(updateLocation, 150)
|
||||
window.setTimeout(updateLocation, 500)
|
||||
|
||||
if (visible) {
|
||||
onDestroy(
|
||||
visible?.addCallbackAndRunD((v) => {
|
||||
if (!v) {
|
||||
return
|
||||
}
|
||||
const x = (rect.left + rect.right) / 2
|
||||
const y = (rect.top + rect.bottom) / 2
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
function updateLocation() {
|
||||
if (!placedOverMap.data || !altmap.data) {
|
||||
return
|
||||
}
|
||||
altmap.data.resize()
|
||||
const {lon, lat} = placedOverMapProperties.location.data
|
||||
const altMapCenter = pixelCenterOf(altmap)
|
||||
const c = placedOverMap.data.unproject(altMapCenter)
|
||||
altproperties.location.setData({lon: c.lng, lat: c.lat})
|
||||
}
|
||||
|
||||
onDestroy(placedOverMapProperties.location.addCallbackAndRunD(updateLocation))
|
||||
updateLocation()
|
||||
window.setTimeout(updateLocation, 150)
|
||||
window.setTimeout(updateLocation, 500)
|
||||
|
||||
if (visible) {
|
||||
onDestroy(visible?.addCallbackAndRunD(v => {
|
||||
if (!v) {
|
||||
return
|
||||
}
|
||||
updateLocation()
|
||||
window.setTimeout(updateLocation, 150)
|
||||
window.setTimeout(updateLocation, 500)
|
||||
}))
|
||||
}
|
||||
|
||||
updateLocation()
|
||||
window.setTimeout(updateLocation, 150)
|
||||
window.setTimeout(updateLocation, 500)
|
||||
})
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<MaplibreMap map={altmap}/>
|
||||
<MaplibreMap map={altmap} />
|
||||
|
|
|
@ -1,73 +1,94 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* The RasterLayerOverview shows the available 4 categories of maps with a RasterLayerPicker
|
||||
*/
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {RasterLayerPolygon} from "../../Models/RasterLayers";
|
||||
import type {MapProperties} from "../../Models/MapProperties";
|
||||
import {Map as MlMap} from "maplibre-gl";
|
||||
import RasterLayerPicker from "./RasterLayerPicker.svelte";
|
||||
import type {EliCategory} from "../../Models/RasterLayerProperties";
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
/**
|
||||
* The RasterLayerOverview shows the available 4 categories of maps with a RasterLayerPicker
|
||||
*/
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import RasterLayerPicker from "./RasterLayerPicker.svelte"
|
||||
import type { EliCategory } from "../../Models/RasterLayerProperties"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let availableLayers: Store<RasterLayerPolygon[]>
|
||||
export let mapproperties: MapProperties
|
||||
export let userstate: UserRelatedState
|
||||
export let map: Store<MlMap>
|
||||
/**
|
||||
* Used to toggle the background layers on/off
|
||||
*/
|
||||
export let visible: UIEventSource<boolean> = undefined
|
||||
export let availableLayers: Store<RasterLayerPolygon[]>
|
||||
export let mapproperties: MapProperties
|
||||
export let userstate: UserRelatedState
|
||||
export let map: Store<MlMap>
|
||||
/**
|
||||
* Used to toggle the background layers on/off
|
||||
*/
|
||||
export let visible: UIEventSource<boolean> = undefined
|
||||
|
||||
type CategoryType = "photo" | "map" | "other" | "osmbasedmap"
|
||||
const categories: Record<CategoryType, EliCategory[]> = {
|
||||
"photo": ["photo", "historicphoto"],
|
||||
"map": ["map", "historicmap"],
|
||||
"other": ["other", "elevation"],
|
||||
"osmbasedmap": ["osmbasedmap"]
|
||||
}
|
||||
type CategoryType = "photo" | "map" | "other" | "osmbasedmap"
|
||||
const categories: Record<CategoryType, EliCategory[]> = {
|
||||
photo: ["photo", "historicphoto"],
|
||||
map: ["map", "historicmap"],
|
||||
other: ["other", "elevation"],
|
||||
osmbasedmap: ["osmbasedmap"],
|
||||
}
|
||||
|
||||
function availableForCategory(type: CategoryType): Store<RasterLayerPolygon[]> {
|
||||
const keywords = categories[type]
|
||||
return availableLayers.mapD(available => available.filter(layer =>
|
||||
keywords.indexOf(<EliCategory>layer.properties.category) >= 0
|
||||
))
|
||||
}
|
||||
function availableForCategory(type: CategoryType): Store<RasterLayerPolygon[]> {
|
||||
const keywords = categories[type]
|
||||
return availableLayers.mapD((available) =>
|
||||
available.filter((layer) => keywords.indexOf(<EliCategory>layer.properties.category) >= 0)
|
||||
)
|
||||
}
|
||||
|
||||
const mapLayers = availableForCategory("map")
|
||||
const osmbasedmapLayers = availableForCategory("osmbasedmap")
|
||||
const photoLayers = availableForCategory("photo")
|
||||
const otherLayers = availableForCategory("other")
|
||||
const mapLayers = availableForCategory("map")
|
||||
const osmbasedmapLayers = availableForCategory("osmbasedmap")
|
||||
const photoLayers = availableForCategory("photo")
|
||||
const otherLayers = availableForCategory("other")
|
||||
|
||||
function onApply() {
|
||||
visible.setData(false)
|
||||
}
|
||||
|
||||
function getPref(type: CategoryType): undefined | UIEventSource<string> {
|
||||
return userstate?.osmConnection?.GetPreference("preferred-layer-" + type)
|
||||
}
|
||||
function onApply() {
|
||||
visible.setData(false)
|
||||
}
|
||||
|
||||
function getPref(type: CategoryType): undefined | UIEventSource<string> {
|
||||
return userstate?.osmConnection?.GetPreference("preferred-layer-" + type)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="h-full flex flex-col">
|
||||
<slot name="title">
|
||||
<h2>
|
||||
<Tr t={Translations.t.general.backgroundMap}/>
|
||||
</h2>
|
||||
</slot>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 h-full w-full">
|
||||
<RasterLayerPicker availableLayers={photoLayers} favourite={getPref("photo")} {map} {mapproperties}
|
||||
on:appliedLayer={onApply} {visible}/>
|
||||
<RasterLayerPicker availableLayers={mapLayers} favourite={getPref("map")} {map} {mapproperties}
|
||||
on:appliedLayer={onApply} {visible}/>
|
||||
<RasterLayerPicker availableLayers={osmbasedmapLayers} favourite={getPref("osmbasedmap")}
|
||||
{map} {mapproperties} on:appliedLayer={onApply} {visible}/>
|
||||
<RasterLayerPicker availableLayers={otherLayers} favourite={getPref("other")} {map} {mapproperties}
|
||||
on:appliedLayer={onApply} {visible}/>
|
||||
</div>
|
||||
<slot name="title">
|
||||
<h2>
|
||||
<Tr t={Translations.t.general.backgroundMap} />
|
||||
</h2>
|
||||
</slot>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 h-full w-full">
|
||||
<RasterLayerPicker
|
||||
availableLayers={photoLayers}
|
||||
favourite={getPref("photo")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
<RasterLayerPicker
|
||||
availableLayers={mapLayers}
|
||||
favourite={getPref("map")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
<RasterLayerPicker
|
||||
availableLayers={osmbasedmapLayers}
|
||||
favourite={getPref("osmbasedmap")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
<RasterLayerPicker
|
||||
availableLayers={otherLayers}
|
||||
favourite={getPref("other")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,77 +1,92 @@
|
|||
<script lang="ts">
|
||||
import type {RasterLayerPolygon} from "../../Models/RasterLayers";
|
||||
import OverlayMap from "./OverlayMap.svelte";
|
||||
import type {MapProperties} from "../../Models/MapProperties";
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Map as MlMap} from "maplibre-gl"
|
||||
import {createEventDispatcher, onDestroy} from "svelte";
|
||||
import type { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import OverlayMap from "./OverlayMap.svelte"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
|
||||
/***
|
||||
* Chooses a background-layer out of available options
|
||||
*/
|
||||
export let availableLayers: Store<RasterLayerPolygon[]>
|
||||
export let mapproperties: MapProperties
|
||||
export let map: Store<MlMap>
|
||||
/***
|
||||
* Chooses a background-layer out of available options
|
||||
*/
|
||||
export let availableLayers: Store<RasterLayerPolygon[]>
|
||||
export let mapproperties: MapProperties
|
||||
export let map: Store<MlMap>
|
||||
|
||||
export let visible: Store<boolean> = undefined
|
||||
|
||||
let dispatch = createEventDispatcher<{appliedLayer}>()
|
||||
|
||||
export let favourite : UIEventSource<string> | undefined = undefined
|
||||
|
||||
export let visible: Store<boolean> = undefined
|
||||
|
||||
let rasterLayer = new UIEventSource<RasterLayerPolygon>(availableLayers.data?.[0])
|
||||
let hasLayers = true
|
||||
onDestroy(availableLayers.addCallbackAndRun(layers => {
|
||||
if (layers === undefined || layers.length === 0) {
|
||||
hasLayers = false
|
||||
return
|
||||
let dispatch = createEventDispatcher<{ appliedLayer }>()
|
||||
|
||||
export let favourite: UIEventSource<string> | undefined = undefined
|
||||
|
||||
let rasterLayer = new UIEventSource<RasterLayerPolygon>(availableLayers.data?.[0])
|
||||
let hasLayers = true
|
||||
onDestroy(
|
||||
availableLayers.addCallbackAndRun((layers) => {
|
||||
if (layers === undefined || layers.length === 0) {
|
||||
hasLayers = false
|
||||
return
|
||||
}
|
||||
hasLayers = true
|
||||
rasterLayer.setData(layers[0])
|
||||
})
|
||||
)
|
||||
|
||||
if (favourite) {
|
||||
onDestroy(
|
||||
favourite.addCallbackAndRunD((favourite) => {
|
||||
const fav = availableLayers.data?.find((l) => l.properties.id === favourite)
|
||||
if (!fav) {
|
||||
return
|
||||
}
|
||||
hasLayers = true
|
||||
rasterLayer.setData(layers[0])
|
||||
}))
|
||||
|
||||
if(favourite){
|
||||
onDestroy(favourite.addCallbackAndRunD(favourite => {
|
||||
const fav = availableLayers.data?.find(l => l.properties.id === favourite)
|
||||
if(!fav){
|
||||
return
|
||||
}
|
||||
rasterLayer.setData(fav)
|
||||
}))
|
||||
|
||||
onDestroy(rasterLayer.addCallbackAndRunD(selected => {
|
||||
favourite?.setData(selected.properties.id)
|
||||
}))
|
||||
}
|
||||
rasterLayer.setData(fav)
|
||||
})
|
||||
)
|
||||
|
||||
let rasterLayerOnMap = UIEventSource.feedFrom(rasterLayer)
|
||||
|
||||
if (visible) {
|
||||
onDestroy(visible?.addCallbackAndRunD(visible => {
|
||||
if (visible) {
|
||||
rasterLayerOnMap.setData(rasterLayer.data ?? availableLayers.data[0])
|
||||
} else {
|
||||
rasterLayerOnMap.setData(undefined)
|
||||
}
|
||||
}))
|
||||
}
|
||||
onDestroy(
|
||||
rasterLayer.addCallbackAndRunD((selected) => {
|
||||
favourite?.setData(selected.properties.id)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
let rasterLayerOnMap = UIEventSource.feedFrom(rasterLayer)
|
||||
|
||||
if (visible) {
|
||||
onDestroy(
|
||||
visible?.addCallbackAndRunD((visible) => {
|
||||
if (visible) {
|
||||
rasterLayerOnMap.setData(rasterLayer.data ?? availableLayers.data[0])
|
||||
} else {
|
||||
rasterLayerOnMap.setData(undefined)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if hasLayers}
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<button on:click={() => {mapproperties.rasterLayer.setData(rasterLayer.data);
|
||||
dispatch("appliedLayer")
|
||||
}} class="w-full h-full m-0 p-0">
|
||||
<OverlayMap rasterLayer={rasterLayerOnMap} placedOverMap={map} placedOverMapProperties={mapproperties}
|
||||
{visible}/>
|
||||
</button>
|
||||
<select bind:value={$rasterLayer} class="w-full">
|
||||
{#each $availableLayers as availableLayer }
|
||||
<option value={availableLayer}>
|
||||
{availableLayer.properties.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<button
|
||||
on:click={() => {
|
||||
mapproperties.rasterLayer.setData(rasterLayer.data)
|
||||
dispatch("appliedLayer")
|
||||
}}
|
||||
class="w-full h-full m-0 p-0"
|
||||
>
|
||||
<OverlayMap
|
||||
rasterLayer={rasterLayerOnMap}
|
||||
placedOverMap={map}
|
||||
placedOverMapProperties={mapproperties}
|
||||
{visible}
|
||||
/>
|
||||
</button>
|
||||
<select bind:value={$rasterLayer} class="w-full">
|
||||
{#each $availableLayers as availableLayer}
|
||||
<option value={availableLayer}>
|
||||
{availableLayer.properties.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import type {Map as MlMap} from "maplibre-gl"
|
||||
import {GeoJSONSource, Marker} from "maplibre-gl"
|
||||
import {ShowDataLayerOptions} from "./ShowDataLayerOptions"
|
||||
import {GeoOperations} from "../../Logic/GeoOperations"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MlMap } from "maplibre-gl"
|
||||
import { GeoJSONSource, Marker } from "maplibre-gl"
|
||||
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
|
||||
import {OsmTags} from "../../Models/OsmFeature"
|
||||
import {FeatureSource, FeatureSourceForLayer} from "../../Logic/FeatureSource/FeatureSource"
|
||||
import {BBox} from "../../Logic/BBox"
|
||||
import {Feature, Point} from "geojson"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { Feature, Point } from "geojson"
|
||||
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
|
||||
import {Utils} from "../../Utils"
|
||||
import { Utils } from "../../Utils"
|
||||
import * as range_layer from "../../assets/layers/range/range.json"
|
||||
import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
||||
|
@ -143,7 +143,7 @@ class PointRenderingLayer {
|
|||
} else {
|
||||
store = new ImmutableStore(<OsmTags>feature.properties)
|
||||
}
|
||||
const {html, iconAnchor} = this._config.RenderIcon(store, true)
|
||||
const { html, iconAnchor } = this._config.RenderIcon(store, true)
|
||||
html.SetClass("marker")
|
||||
if (this._onClick !== undefined) {
|
||||
html.SetClass("cursor-pointer")
|
||||
|
@ -177,7 +177,7 @@ class PointRenderingLayer {
|
|||
if (newloc[0] === oldLoc.lng && newloc[1] === oldLoc.lat) {
|
||||
return
|
||||
}
|
||||
marker.setLngLat({lon: newloc[0], lat: newloc[1]})
|
||||
marker.setLngLat({ lon: newloc[0], lat: newloc[1] })
|
||||
})
|
||||
}
|
||||
return marker
|
||||
|
@ -332,7 +332,7 @@ class LineRenderingLayer {
|
|||
map.on("click", polylayer, (e) => {
|
||||
console.log("Got polylayer click:", e)
|
||||
// polygon-layer-listener
|
||||
if(e.originalEvent["consumed"]){
|
||||
if (e.originalEvent["consumed"]) {
|
||||
// This is a polygon beneath a marker, we can ignore it
|
||||
return
|
||||
}
|
||||
|
@ -382,7 +382,7 @@ class LineRenderingLayer {
|
|||
}
|
||||
if (this._fetchStore === undefined) {
|
||||
map.setFeatureState(
|
||||
{source: this._layername, id},
|
||||
{ source: this._layername, id },
|
||||
this.calculatePropsFor(feature.properties)
|
||||
)
|
||||
} else {
|
||||
|
@ -390,7 +390,7 @@ class LineRenderingLayer {
|
|||
this._listenerInstalledOn.add(id)
|
||||
tags.addCallbackAndRunD((properties) => {
|
||||
map.setFeatureState(
|
||||
{source: this._layername, id},
|
||||
{ source: this._layername, id },
|
||||
this.calculatePropsFor(properties)
|
||||
)
|
||||
})
|
||||
|
@ -462,9 +462,7 @@ export default class ShowDataLayer {
|
|||
})
|
||||
}
|
||||
|
||||
public destruct() {
|
||||
|
||||
}
|
||||
public destruct() {}
|
||||
|
||||
private zoomToCurrentFeatures(map: MlMap) {
|
||||
if (this._options.zoomToFeatures) {
|
||||
|
@ -472,20 +470,22 @@ export default class ShowDataLayer {
|
|||
const bbox = BBox.bboxAroundAll(features.map(BBox.get))
|
||||
map.resize()
|
||||
map.fitBounds(bbox.toLngLat(), {
|
||||
padding: {top: 10, bottom: 10, left: 10, right: 10},
|
||||
animate: false
|
||||
padding: { top: 10, bottom: 10, left: 10, right: 10 },
|
||||
animate: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private initDrawFeatures(map: MlMap) {
|
||||
let {features, doShowLayer, fetchStore, selectedElement, selectedLayer} = this._options
|
||||
let { features, doShowLayer, fetchStore, selectedElement, selectedLayer } = this._options
|
||||
const onClick =
|
||||
this._options.onClick ??
|
||||
(this._options.layer.title === undefined ? undefined : ((feature: Feature) => {
|
||||
selectedElement?.setData(feature)
|
||||
selectedLayer?.setData(this._options.layer)
|
||||
}))
|
||||
(this._options.layer.title === undefined
|
||||
? undefined
|
||||
: (feature: Feature) => {
|
||||
selectedElement?.setData(feature)
|
||||
selectedLayer?.setData(this._options.layer)
|
||||
})
|
||||
if (this._options.drawLines !== false) {
|
||||
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
|
||||
const lineRenderingConfig = this._options.layer.lineRendering[i]
|
||||
|
|
|
@ -70,7 +70,7 @@ export default class OpeningHoursInput extends InputElement<string> {
|
|||
if (OH.ParsePHRule(rule) !== null) {
|
||||
continue
|
||||
}
|
||||
if(leftOvers.indexOf(rule) >= 0){
|
||||
if (leftOvers.indexOf(rule) >= 0) {
|
||||
continue
|
||||
}
|
||||
leftOvers.push(rule)
|
||||
|
|
|
@ -10,7 +10,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
|
|||
import Table from "../Base/Table"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Loading from "../Base/Loading";
|
||||
import Loading from "../Base/Loading"
|
||||
|
||||
export default class OpeningHoursVisualization extends Toggle {
|
||||
private static readonly weekdays: Translation[] = [
|
||||
|
@ -30,7 +30,7 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
prefix = "",
|
||||
postfix = ""
|
||||
) {
|
||||
const country = tags.map(tags => tags._country)
|
||||
const country = tags.map((tags) => tags._country)
|
||||
const ohTable = new VariableUiElement(
|
||||
tags
|
||||
.map((tags) => {
|
||||
|
@ -43,32 +43,35 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
}
|
||||
return value
|
||||
}) // This mapping will absorb all other changes to tags in order to prevent regeneration
|
||||
.map((ohtext) => {
|
||||
if (ohtext === undefined) {
|
||||
return new FixedUiElement(
|
||||
"No opening hours defined with key " + key
|
||||
).SetClass("alert")
|
||||
}
|
||||
try {
|
||||
return OpeningHoursVisualization.CreateFullVisualisation(
|
||||
OH.CreateOhObject(<any>tags.data, ohtext)
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn(e, e.stack)
|
||||
return new Combine([
|
||||
Translations.t.general.opening_hours.error_loading,
|
||||
new Toggle(
|
||||
new FixedUiElement(e).SetClass("subtle"),
|
||||
undefined,
|
||||
state?.osmConnection?.userDetails.map(
|
||||
(userdetails) =>
|
||||
userdetails.csCount >=
|
||||
Constants.userJourney.tagsVisibleAndWikiLinked
|
||||
)
|
||||
),
|
||||
])
|
||||
}
|
||||
}, [country])
|
||||
.map(
|
||||
(ohtext) => {
|
||||
if (ohtext === undefined) {
|
||||
return new FixedUiElement(
|
||||
"No opening hours defined with key " + key
|
||||
).SetClass("alert")
|
||||
}
|
||||
try {
|
||||
return OpeningHoursVisualization.CreateFullVisualisation(
|
||||
OH.CreateOhObject(<any>tags.data, ohtext)
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn(e, e.stack)
|
||||
return new Combine([
|
||||
Translations.t.general.opening_hours.error_loading,
|
||||
new Toggle(
|
||||
new FixedUiElement(e).SetClass("subtle"),
|
||||
undefined,
|
||||
state?.osmConnection?.userDetails.map(
|
||||
(userdetails) =>
|
||||
userdetails.csCount >=
|
||||
Constants.userJourney.tagsVisibleAndWikiLinked
|
||||
)
|
||||
),
|
||||
])
|
||||
}
|
||||
},
|
||||
[country]
|
||||
)
|
||||
)
|
||||
|
||||
super(
|
||||
|
|
|
@ -1,296 +1,339 @@
|
|||
<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 {OsmWay} from "../../../Logic/Osm/OsmObject";
|
||||
import {Tag} from "../../../Logic/Tags/Tag";
|
||||
import type {WayId} from "../../../Models/OsmFeature";
|
||||
import Loading from "../../Base/Loading.svelte";
|
||||
import type {GlobalFilter} from "../../../Models/GlobalFilter";
|
||||
import {onDestroy} from "svelte";
|
||||
import NextButton from "../../Base/NextButton.svelte";
|
||||
import BackButton from "../../Base/BackButton.svelte";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import Svg from "../../../Svg";
|
||||
import MapControlButton from "../../Base/MapControlButton.svelte";
|
||||
import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid";
|
||||
/**
|
||||
* 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 { OsmWay } from "../../../Logic/Osm/OsmObject"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import type { WayId } from "../../../Models/OsmFeature"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import type { GlobalFilter } from "../../../Models/GlobalFilter"
|
||||
import { onDestroy } from "svelte"
|
||||
import NextButton from "../../Base/NextButton.svelte"
|
||||
import BackButton from "../../Base/BackButton.svelte"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import Svg from "../../../Svg"
|
||||
import MapControlButton from "../../Base/MapControlButton.svelte"
|
||||
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
|
||||
|
||||
export let coordinate: { lon: number, lat: number };
|
||||
export let state: SpecialVisualizationState;
|
||||
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 checkedOfGlobalFilters: number = 0
|
||||
let confirmedCategory = false;
|
||||
$: if (selectedPreset === undefined) {
|
||||
confirmedCategory = false;
|
||||
creating = false;
|
||||
checkedOfGlobalFilters = 0
|
||||
let selectedPreset: {
|
||||
preset: PresetConfig
|
||||
layer: LayerConfig
|
||||
icon: string
|
||||
tags: Record<string, string>
|
||||
} = undefined
|
||||
let checkedOfGlobalFilters: number = 0
|
||||
let confirmedCategory = false
|
||||
$: if (selectedPreset === undefined) {
|
||||
confirmedCategory = false
|
||||
creating = false
|
||||
checkedOfGlobalFilters = 0
|
||||
}
|
||||
|
||||
let flayer: FilteredLayer = undefined
|
||||
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined
|
||||
let layerHasFilters: Store<boolean> | undefined = undefined
|
||||
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters
|
||||
let _globalFilter: GlobalFilter[] = []
|
||||
onDestroy(
|
||||
globalFilter.addCallbackAndRun((globalFilter) => {
|
||||
console.log("Global filters are", globalFilter)
|
||||
_globalFilter = globalFilter ?? []
|
||||
})
|
||||
)
|
||||
$: {
|
||||
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
|
||||
|
||||
const isLoading = state.dataIsLoading
|
||||
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.concat(
|
||||
..._globalFilter.map((f) => f?.onNewPoint?.tags ?? [])
|
||||
)
|
||||
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags)
|
||||
|
||||
let snapToWay: undefined | OsmWay = undefined
|
||||
if (snapTo !== undefined) {
|
||||
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0)
|
||||
if (downloaded !== "deleted") {
|
||||
snapToWay = downloaded
|
||||
}
|
||||
}
|
||||
|
||||
let flayer: FilteredLayer = undefined;
|
||||
let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined;
|
||||
let layerHasFilters: Store<boolean> | undefined = undefined;
|
||||
let globalFilter: UIEventSource<GlobalFilter[]> = state.layerState.globalFilters;
|
||||
let _globalFilter: GlobalFilter[] = [];
|
||||
onDestroy(globalFilter.addCallbackAndRun(globalFilter => {
|
||||
console.log("Global filters are", globalFilter);
|
||||
_globalFilter = globalFilter ?? [];
|
||||
}));
|
||||
$:{
|
||||
flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id);
|
||||
layerIsDisplayed = flayer?.isDisplayed;
|
||||
layerHasFilters = flayer?.hasFilter;
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
|
||||
theme: state.layout?.id ?? "unkown",
|
||||
changeType: "create",
|
||||
snapOnto: snapToWay,
|
||||
})
|
||||
await state.changes.applyAction(newElementAction)
|
||||
state.newFeatures.features.ping()
|
||||
// The 'changes' should have created a new point, which added this into the 'featureProperties'
|
||||
const newId = newElementAction.newElementId
|
||||
console.log("Applied pending changes, fetching store for", newId)
|
||||
const tagsStore = state.featureProperties.getStore(newId)
|
||||
{
|
||||
// Set some metainfo
|
||||
const properties = tagsStore.data
|
||||
if (snapTo) {
|
||||
// metatags (starting with underscore) are not uploaded, so we can safely mark this
|
||||
delete properties["_referencing_ways"]
|
||||
properties["_referencing_ways"] = `["${snapTo}"]`
|
||||
}
|
||||
properties["_backend"] = state.osmConnection.Backend()
|
||||
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 t = Translations.t.general.add;
|
||||
|
||||
const zoom = state.mapProperties.zoom;
|
||||
|
||||
const isLoading = state.dataIsLoading;
|
||||
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.concat(..._globalFilter.map(f => f?.onNewPoint?.tags ?? []));
|
||||
console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags);
|
||||
|
||||
let snapToWay: undefined | OsmWay = undefined;
|
||||
if (snapTo !== undefined) {
|
||||
const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0);
|
||||
if (downloaded !== "deleted") {
|
||||
snapToWay = downloaded;
|
||||
}
|
||||
}
|
||||
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon,
|
||||
{
|
||||
theme: state.layout?.id ?? "unkown",
|
||||
changeType: "create",
|
||||
snapOnto: snapToWay
|
||||
});
|
||||
await state.changes.applyAction(newElementAction);
|
||||
state.newFeatures.features.ping();
|
||||
// The 'changes' should have created a new point, which added this into the 'featureProperties'
|
||||
const newId = newElementAction.newElementId;
|
||||
console.log("Applied pending changes, fetching store for", newId);
|
||||
const tagsStore = state.featureProperties.getStore(newId);
|
||||
{
|
||||
// Set some metainfo
|
||||
const properties = tagsStore.data;
|
||||
if (snapTo) {
|
||||
// metatags (starting with underscore) are not uploaded, so we can safely mark this
|
||||
delete properties["_referencing_ways"];
|
||||
properties["_referencing_ways"] = `["${snapTo}"]`;
|
||||
}
|
||||
properties["_backend"] = state.osmConnection.Backend();
|
||||
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.selectedLayer.setData(selectedPreset.layer);
|
||||
state.selectedElement.setData(feature);
|
||||
tagsStore.ping();
|
||||
|
||||
}
|
||||
|
||||
const feature = state.indexedFeatures.featuresById.data.get(newId)
|
||||
abort()
|
||||
state.selectedLayer.setData(selectedPreset.layer)
|
||||
state.selectedElement.setData(feature)
|
||||
tagsStore.ping()
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<!-- This component is basically one big if/then/else flow checking for many conditions and edge cases that (in some cases) have to be handled;
|
||||
<!-- This component is basically one big if/then/else flow checking for many conditions and edge cases that (in some cases) have to be handled;
|
||||
1. the first (and outermost) is of course: are we logged in?
|
||||
2. What do we want to add?
|
||||
3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) -->
|
||||
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
|
||||
<Tr slot="message" t={Translations.t.general.add.pleaseLogin}/>
|
||||
</LoginButton>
|
||||
{#if $isLoading}
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.add.stillLoading}/>
|
||||
</Loading>
|
||||
</div>
|
||||
{:else 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>
|
||||
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
|
||||
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
|
||||
</LoginButton>
|
||||
{#if $isLoading}
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.add.stillLoading} />
|
||||
</Loading>
|
||||
</div>
|
||||
{:else if $zoom < Constants.minZoomLevelToAddNewPoint}
|
||||
<div class="alert">
|
||||
<Tr t={Translations.t.general.add.zoomInFurther} />
|
||||
</div>
|
||||
{:else if selectedPreset === undefined}
|
||||
<!-- First, select the correct preset -->
|
||||
<PresetList
|
||||
{state}
|
||||
on:select={(event) => {
|
||||
selectedPreset = event.detail
|
||||
}}
|
||||
/>
|
||||
{: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>
|
||||
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
<button
|
||||
class="flex w-full gap-x-1"
|
||||
on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(selectedPreset.layer)
|
||||
}}
|
||||
>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
|
||||
<Tr t={Translations.t.general.add.openLayerControl} />
|
||||
</button>
|
||||
|
||||
{: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>
|
||||
<button
|
||||
class="flex w-full gap-x-1 primary"
|
||||
on:click={() => {
|
||||
layerIsDisplayed.setData(true)
|
||||
abort()
|
||||
}}
|
||||
>
|
||||
<EyeIcon class="w-12" />
|
||||
<Tr t={Translations.t.general.add.enableLayer.Subs({ name: selectedPreset.layer.name })} />
|
||||
</button>
|
||||
</div>
|
||||
{:else if $layerHasFilters}
|
||||
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8" />
|
||||
<Tr t={Translations.t.general.add.disableFiltersExplanation} />
|
||||
</div>
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
<button
|
||||
class="flex w-full gap-x-1 primary"
|
||||
on:click={() => {
|
||||
abort()
|
||||
state.layerState.filteredLayers.get(selectedPreset.layer.id).disableAllFilters()
|
||||
}}
|
||||
>
|
||||
<EyeOffIcon class="w-12" />
|
||||
<Tr t={Translations.t.general.add.disableFilters} />
|
||||
</button>
|
||||
<button
|
||||
class="flex w-full gap-x-1"
|
||||
on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(selectedPreset.layer)
|
||||
}}
|
||||
>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
|
||||
<Tr t={Translations.t.general.add.openLayerControl} />
|
||||
</button>
|
||||
</div>
|
||||
{:else if !confirmedCategory}
|
||||
<!-- Second, confirm the category -->
|
||||
<h2 class="mr-12">
|
||||
<Tr
|
||||
t={Translations.t.general.add.confirmTitle.Subs({ title: selectedPreset.preset.title })}
|
||||
/>
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
<Tr t={Translations.t.general.add.confirmIntro} />
|
||||
|
||||
<button class="flex w-full gap-x-1"
|
||||
on:click={() => {abort();state.guistate.openFilterView(selectedPreset.layer)}}>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
|
||||
<Tr t={Translations.t.general.add.openLayerControl}/>
|
||||
</button>
|
||||
|
||||
<button class="flex w-full gap-x-1 primary" on:click={() => {layerIsDisplayed.setData(true);abort()}}>
|
||||
<EyeIcon class="w-12"/>
|
||||
<Tr t={Translations.t.general.add.enableLayer.Subs({name: selectedPreset.layer.name})}/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{:else if $layerHasFilters}
|
||||
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8"/>
|
||||
<Tr t={Translations.t.general.add.disableFiltersExplanation}/>
|
||||
</div>
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
<button class="flex w-full gap-x-1 primary"
|
||||
on:click={() => {abort(); state.layerState.filteredLayers.get(selectedPreset.layer.id).disableAllFilters()} }>
|
||||
<EyeOffIcon class="w-12"/>
|
||||
<Tr t={Translations.t.general.add.disableFilters}/>
|
||||
</button>
|
||||
<button class="flex w-full gap-x-1"
|
||||
on:click={() => {abort();state.guistate.openFilterView(selectedPreset.layer)}}>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
|
||||
<Tr t={Translations.t.general.add.openLayerControl}/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{:else if !confirmedCategory }
|
||||
<!-- Second, confirm the category -->
|
||||
<h2 class="mr-12">
|
||||
<Tr t={Translations.t.general.add.confirmTitle.Subs({title: selectedPreset.preset.title})}/>
|
||||
</h2>
|
||||
|
||||
<Tr t={Translations.t.general.add.confirmIntro}/>
|
||||
|
||||
|
||||
{#if selectedPreset.preset.description}
|
||||
<Tr t={selectedPreset.preset.description}/>
|
||||
{/if}
|
||||
|
||||
{#if selectedPreset.preset.exampleImages}
|
||||
<h3>
|
||||
{#if selectedPreset.preset.exampleImages.length === 1}
|
||||
<Tr t={Translations.t.general.example}/>
|
||||
{:else}
|
||||
<Tr t={Translations.t.general.examples }/>
|
||||
{/if}
|
||||
</h3>
|
||||
<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})} {state}
|
||||
tags={new And(selectedPreset.preset.tags)}></TagHint>
|
||||
|
||||
<div class="flex w-full flex-wrap-reverse md:flex-nowrap">
|
||||
|
||||
<BackButton on:click={() => selectedPreset = undefined} clss="w-full">
|
||||
<Tr t={t.backToSelect}/>
|
||||
</BackButton>
|
||||
|
||||
<NextButton on:click={() => confirmedCategory = true} clss="primary w-full">
|
||||
<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 class="w-full">
|
||||
<Tr t={selectedPreset.text}></Tr>
|
||||
</div>
|
||||
</NextButton>
|
||||
</div>
|
||||
|
||||
{:else if _globalFilter?.length > 0 && _globalFilter?.length > checkedOfGlobalFilters}
|
||||
<Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12"/>
|
||||
<SubtleButton on:click={() => {checkedOfGlobalFilters = checkedOfGlobalFilters + 1}}>
|
||||
<img slot="image" src={_globalFilter[checkedOfGlobalFilters].onNewPoint?.icon ?? "./assets/svg/confirm.svg"}
|
||||
class="w-12 h-12">
|
||||
<Tr slot="message"
|
||||
t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({preset: selectedPreset.preset})}/>
|
||||
</SubtleButton>
|
||||
<SubtleButton on:click={() => {globalFilter.setData([]); abort()}}>
|
||||
<img slot="image" src="./assets/svg/close.svg" class="w-8 h-8"/>
|
||||
<Tr slot="message" t={Translations.t.general.cancel}/>
|
||||
</SubtleButton>
|
||||
{:else if !creating}
|
||||
<div class="relative w-full p-1">
|
||||
<div class="w-full h-96 max-h-screen rounded-xl overflow-hidden">
|
||||
<NewPointLocationInput value={preciseCoordinate} snappedTo={snappedToObject} {state} {coordinate}
|
||||
targetLayer={selectedPreset.layer}
|
||||
snapToLayers={selectedPreset.preset.preciseInput.snapToLayers}/>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 p-4">
|
||||
<MapControlButton on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}>
|
||||
<Square3Stack3dIcon class="w-6 h-6"/>
|
||||
</MapControlButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
|
||||
<BackButton on:click={() => selectedPreset = undefined} clss="w-full">
|
||||
<Tr t={t.backToSelect}/>
|
||||
</BackButton>
|
||||
|
||||
<NextButton on:click={confirm} clss="primary w-full">
|
||||
<div class="w-full flex justify-end gap-x-2">
|
||||
<Tr t={Translations.t.general.add.confirmLocation}/>
|
||||
</div>
|
||||
</NextButton>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<Loading>Creating point...</Loading>
|
||||
{#if selectedPreset.preset.description}
|
||||
<Tr t={selectedPreset.preset.description} />
|
||||
{/if}
|
||||
|
||||
{#if selectedPreset.preset.exampleImages}
|
||||
<h3>
|
||||
{#if selectedPreset.preset.exampleImages.length === 1}
|
||||
<Tr t={Translations.t.general.example} />
|
||||
{:else}
|
||||
<Tr t={Translations.t.general.examples} />
|
||||
{/if}
|
||||
</h3>
|
||||
<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 })}
|
||||
{state}
|
||||
tags={new And(selectedPreset.preset.tags)}
|
||||
/>
|
||||
|
||||
<div class="flex w-full flex-wrap-reverse md:flex-nowrap">
|
||||
<BackButton on:click={() => (selectedPreset = undefined)} clss="w-full">
|
||||
<Tr t={t.backToSelect} />
|
||||
</BackButton>
|
||||
|
||||
<NextButton on:click={() => (confirmedCategory = true)} clss="primary w-full">
|
||||
<div slot="image" class="relative">
|
||||
<FromHtml src={selectedPreset.icon} />
|
||||
<img class="absolute bottom-0 right-0 w-4 h-4" src="./assets/svg/confirm.svg" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<Tr t={selectedPreset.text} />
|
||||
</div>
|
||||
</NextButton>
|
||||
</div>
|
||||
{:else if _globalFilter?.length > 0 && _globalFilter?.length > checkedOfGlobalFilters}
|
||||
<Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12" />
|
||||
<SubtleButton
|
||||
on:click={() => {
|
||||
checkedOfGlobalFilters = checkedOfGlobalFilters + 1
|
||||
}}
|
||||
>
|
||||
<img
|
||||
slot="image"
|
||||
src={_globalFilter[checkedOfGlobalFilters].onNewPoint?.icon ?? "./assets/svg/confirm.svg"}
|
||||
class="w-12 h-12"
|
||||
/>
|
||||
<Tr
|
||||
slot="message"
|
||||
t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({
|
||||
preset: selectedPreset.preset,
|
||||
})}
|
||||
/>
|
||||
</SubtleButton>
|
||||
<SubtleButton
|
||||
on:click={() => {
|
||||
globalFilter.setData([])
|
||||
abort()
|
||||
}}
|
||||
>
|
||||
<img slot="image" src="./assets/svg/close.svg" class="w-8 h-8" />
|
||||
<Tr slot="message" t={Translations.t.general.cancel} />
|
||||
</SubtleButton>
|
||||
{:else if !creating}
|
||||
<div class="relative w-full p-1">
|
||||
<div class="w-full h-96 max-h-screen rounded-xl overflow-hidden">
|
||||
<NewPointLocationInput
|
||||
value={preciseCoordinate}
|
||||
snappedTo={snappedToObject}
|
||||
{state}
|
||||
{coordinate}
|
||||
targetLayer={selectedPreset.layer}
|
||||
snapToLayers={selectedPreset.preset.preciseInput.snapToLayers}
|
||||
/>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 p-4">
|
||||
<MapControlButton
|
||||
on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}
|
||||
>
|
||||
<Square3Stack3dIcon class="w-6 h-6" />
|
||||
</MapControlButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
<BackButton on:click={() => (selectedPreset = undefined)} clss="w-full">
|
||||
<Tr t={t.backToSelect} />
|
||||
</BackButton>
|
||||
|
||||
<NextButton on:click={confirm} clss="primary w-full">
|
||||
<div class="w-full flex justify-end gap-x-2">
|
||||
<Tr t={Translations.t.general.add.confirmLocation} />
|
||||
</div>
|
||||
</NextButton>
|
||||
</div>
|
||||
{:else}
|
||||
<Loading>Creating point...</Loading>
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
|
|
|
@ -1,92 +1,92 @@
|
|||
<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 {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";
|
||||
import NextButton from "../../Base/NextButton.svelte";
|
||||
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 { 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"
|
||||
import NextButton from "../../Base/NextButton.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>
|
||||
}[] = [];
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
select: { preset: PresetConfig, layer: LayerConfig, icon: string, tags: Record<string, string> }
|
||||
}>();
|
||||
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 class="flex flex-col w-full">
|
||||
<h2 class="mr-12"> <!-- The title gets a big right margin to give place to the 'close'-button, see https://github.com/pietervdvn/MapComplete/issues/1445 -->
|
||||
<Tr t={Translations.t.general.add.intro}/>
|
||||
</h2>
|
||||
|
||||
{#each presets as preset}
|
||||
<NextButton on:click={() => dispatch("select", preset)}>
|
||||
<FromHtml slot="image" src={preset.icon}></FromHtml>
|
||||
<div class="flex flex-col">
|
||||
<b class="w-fit">
|
||||
<Tr t={preset.text}/>
|
||||
</b>
|
||||
{#if preset.description}
|
||||
<Tr t={preset.description} cls="font-normal"/>
|
||||
{/if}
|
||||
</div>
|
||||
<h2 class="mr-12">
|
||||
<!-- The title gets a big right margin to give place to the 'close'-button, see https://github.com/pietervdvn/MapComplete/issues/1445 -->
|
||||
<Tr t={Translations.t.general.add.intro} />
|
||||
</h2>
|
||||
|
||||
</NextButton>
|
||||
{/each}
|
||||
{#each presets as preset}
|
||||
<NextButton on:click={() => dispatch("select", preset)}>
|
||||
<FromHtml slot="image" src={preset.icon} />
|
||||
<div class="flex flex-col">
|
||||
<b class="w-fit">
|
||||
<Tr t={preset.text} />
|
||||
</b>
|
||||
{#if preset.description}
|
||||
<Tr t={preset.description} cls="font-normal" />
|
||||
{/if}
|
||||
</div>
|
||||
</NextButton>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -69,7 +69,7 @@ export class AddNoteCommentViz implements SpecialVisualization {
|
|||
)
|
||||
).onClick(async () => {
|
||||
const id = tags.data[args[1] ?? "id"]
|
||||
await state.osmConnection.closeNote(id, txt.data)
|
||||
await state.osmConnection.closeNote(id, txt.data)
|
||||
tags.data["closed_at"] = new Date().toISOString()
|
||||
tags.ping()
|
||||
})
|
||||
|
|
|
@ -1,63 +1,65 @@
|
|||
<script lang="ts">
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Table from "../Base/Table";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import SimpleMetaTaggers from "../../Logic/SimpleMetaTagger";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {onDestroy} from "svelte";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Lazy from "../Base/Lazy";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Table from "../Base/Table"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import SimpleMetaTaggers from "../../Logic/SimpleMetaTagger"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { onDestroy } from "svelte"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
|
||||
//Svelte props
|
||||
export let tags: UIEventSource<any>;
|
||||
export let state: any;
|
||||
export let tags: UIEventSource<any>
|
||||
export let state: any
|
||||
|
||||
const calculatedTags = [].concat(
|
||||
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])
|
||||
);
|
||||
)
|
||||
|
||||
const allTags = tags.map((tags) => {
|
||||
const parts: (string | BaseUIElement)[][] = [];
|
||||
const parts: (string | BaseUIElement)[][] = []
|
||||
for (const key in tags) {
|
||||
let v = tags[key];
|
||||
let v = tags[key]
|
||||
if (v === "") {
|
||||
v = "<b>empty string</b>";
|
||||
v = "<b>empty string</b>"
|
||||
}
|
||||
parts.push([key, v ?? "<b>undefined</b>"]);
|
||||
parts.push([key, v ?? "<b>undefined</b>"])
|
||||
}
|
||||
|
||||
for (const key of calculatedTags) {
|
||||
const value = tags[key];
|
||||
const value = tags[key]
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
let type = "";
|
||||
let type = ""
|
||||
if (typeof value !== "string") {
|
||||
type = " <i>" + typeof value + "</i>";
|
||||
type = " <i>" + typeof value + "</i>"
|
||||
}
|
||||
parts.push(["<i>" + key + "</i>", value]);
|
||||
parts.push(["<i>" + key + "</i>", value])
|
||||
}
|
||||
|
||||
for (const metatag of SimpleMetaTaggers.metatags.filter(mt => mt.isLazy)) {
|
||||
const title = "<i>" + metatag.keys.join(";") + "</i> (lazy)";
|
||||
for (const metatag of SimpleMetaTaggers.metatags.filter((mt) => mt.isLazy)) {
|
||||
const title = "<i>" + metatag.keys.join(";") + "</i> (lazy)"
|
||||
const toggleState = new UIEventSource(false)
|
||||
const toggle: BaseUIElement = new Toggle(
|
||||
new Lazy(() => new FixedUiElement(metatag.keys.map(key => tags[key]).join(";"))),
|
||||
new FixedUiElement("Evaluate").onClick(() => toggleState.setData(true)) ,
|
||||
new Lazy(() => new FixedUiElement(metatag.keys.map((key) => tags[key]).join(";"))),
|
||||
new FixedUiElement("Evaluate").onClick(() => toggleState.setData(true)),
|
||||
toggleState
|
||||
);
|
||||
parts.push([title, toggle]);
|
||||
)
|
||||
parts.push([title, toggle])
|
||||
}
|
||||
|
||||
return parts;
|
||||
});
|
||||
return parts
|
||||
})
|
||||
|
||||
let _allTags = [];
|
||||
onDestroy(allTags.addCallbackAndRunD(allTags => {
|
||||
_allTags = allTags;
|
||||
}));
|
||||
const tagsTable = new Table(["Key", "Value"], _allTags).SetClass("zebra-table break-all");
|
||||
let _allTags = []
|
||||
onDestroy(
|
||||
allTags.addCallbackAndRunD((allTags) => {
|
||||
_allTags = allTags
|
||||
})
|
||||
)
|
||||
const tagsTable = new Table(["Key", "Value"], _allTags).SetClass("zebra-table break-all")
|
||||
</script>
|
||||
|
||||
<section>
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import {Stores, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import {SubtleButton} from "../Base/SubtleButton"
|
||||
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Img from "../Base/Img"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Combine from "../Base/Combine"
|
||||
import Link from "../Base/Link"
|
||||
import {Utils} from "../../Utils"
|
||||
import { Utils } from "../../Utils"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import {VariableUiElement} from "../Base/VariableUIElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Loading from "../Base/Loading"
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Translations from "../i18n/Translations"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import {Changes} from "../../Logic/Osm/Changes"
|
||||
import {UIElement} from "../UIElement"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { UIElement } from "../UIElement"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import List from "../Base/List"
|
||||
import {SpecialVisualization, SpecialVisualizationState} from "../SpecialVisualization"
|
||||
import {IndexedFeatureSource} from "../../Logic/FeatureSource/FeatureSource"
|
||||
import {MapLibreAdaptor} from "../Map/MapLibreAdaptor"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
import SpecialVisualizations from "../SpecialVisualizations"
|
||||
import {Feature} from "geojson";
|
||||
import { Feature } from "geojson"
|
||||
|
||||
export interface AutoAction extends SpecialVisualization {
|
||||
supportsAutoAction: boolean
|
||||
|
@ -111,7 +111,7 @@ class ApplyButton extends UIElement {
|
|||
mla.allowZooming.setData(false)
|
||||
mla.allowMoving.setData(false)
|
||||
|
||||
const previewMap = new SvelteUIElement(MaplibreMap, {map: mlmap}).SetClass("h-48")
|
||||
const previewMap = new SvelteUIElement(MaplibreMap, { map: mlmap }).SetClass("h-48")
|
||||
|
||||
const features = this.target_feature_ids.map((id) =>
|
||||
this.state.indexedFeatures.featuresById.data.get(id)
|
||||
|
@ -132,9 +132,18 @@ class ApplyButton extends UIElement {
|
|||
return new FixedUiElement("All done!").SetClass("thanks")
|
||||
}
|
||||
if (st === "running") {
|
||||
return new Loading(new VariableUiElement(this.appliedNumberOfFeatures.map(appliedTo => {
|
||||
return "Applying changes, currently at " + appliedTo + "/" + this.target_feature_ids.length
|
||||
})))
|
||||
return new Loading(
|
||||
new VariableUiElement(
|
||||
this.appliedNumberOfFeatures.map((appliedTo) => {
|
||||
return (
|
||||
"Applying changes, currently at " +
|
||||
appliedTo +
|
||||
"/" +
|
||||
this.target_feature_ids.length
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
const error = st.error
|
||||
return new Combine([
|
||||
|
@ -153,7 +162,7 @@ class ApplyButton extends UIElement {
|
|||
console.log("Applying auto-action on " + this.target_feature_ids.length + " features")
|
||||
|
||||
for (let i = 0; i < this.target_feature_ids.length; i++) {
|
||||
const targetFeatureId = this.target_feature_ids[i];
|
||||
const targetFeatureId = this.target_feature_ids[i]
|
||||
const feature = this.state.indexedFeatures.featuresById.data.get(targetFeatureId)
|
||||
const featureTags = this.state.featureProperties.getStore(targetFeatureId)
|
||||
const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
|
||||
|
@ -164,8 +173,8 @@ class ApplyButton extends UIElement {
|
|||
if (specialRenderings.length == 0) {
|
||||
console.warn(
|
||||
"AutoApply: feature " +
|
||||
targetFeatureId +
|
||||
" got a rendering without supported auto actions:",
|
||||
targetFeatureId +
|
||||
" got a rendering without supported auto actions:",
|
||||
rendering
|
||||
)
|
||||
}
|
||||
|
@ -175,9 +184,14 @@ class ApplyButton extends UIElement {
|
|||
continue
|
||||
}
|
||||
const action = <AutoAction>specialRendering.func
|
||||
await action.applyActionOn(feature, this.state, featureTags, specialRendering.args)
|
||||
await action.applyActionOn(
|
||||
feature,
|
||||
this.state,
|
||||
featureTags,
|
||||
specialRendering.args
|
||||
)
|
||||
}
|
||||
if( i % 50 === 0){
|
||||
if (i % 50 === 0) {
|
||||
await this.state.changes.flushChanges("Auto button: intermediate save")
|
||||
}
|
||||
this.appliedNumberOfFeatures.setData(i + 1)
|
||||
|
@ -187,7 +201,7 @@ class ApplyButton extends UIElement {
|
|||
this.buttonState.setData("done")
|
||||
} catch (e) {
|
||||
console.error("Error while running autoApply: ", e)
|
||||
this.buttonState.setData({error: e})
|
||||
this.buttonState.setData({ error: e })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +256,7 @@ export default class AutoApplyButton implements SpecialVisualization {
|
|||
"To effectively use this button, you'll need some ingredients:",
|
||||
new List([
|
||||
"A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " +
|
||||
supportedActions.join(", "),
|
||||
supportedActions.join(", "),
|
||||
"A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ",
|
||||
new Link("current_view", "./BuiltinLayers.md#current_view"),
|
||||
"Then, use a calculated tag on the host feature to determine the overlapping object ids",
|
||||
|
@ -262,7 +276,7 @@ export default class AutoApplyButton implements SpecialVisualization {
|
|||
!(
|
||||
state.featureSwitchIsTesting.data ||
|
||||
state.osmConnection._oauth_config.url ===
|
||||
OsmConnection.oauth_configs["osm-test"].url
|
||||
OsmConnection.oauth_configs["osm-test"].url
|
||||
)
|
||||
) {
|
||||
const t = Translations.t.general.add.import
|
||||
|
@ -289,11 +303,11 @@ export default class AutoApplyButton implements SpecialVisualization {
|
|||
const to_parse = new UIEventSource<string[]>(undefined)
|
||||
// Very ugly hack: read the value every 500ms
|
||||
Stores.Chronic(500, () => to_parse.data === undefined).addCallback(() => {
|
||||
let applicable = <string | string[]> tagSource.data[argument[1]]
|
||||
if(typeof applicable === "string"){
|
||||
let applicable = <string | string[]>tagSource.data[argument[1]]
|
||||
if (typeof applicable === "string") {
|
||||
applicable = JSON.parse(applicable)
|
||||
}
|
||||
to_parse.setData(<string[]> applicable)
|
||||
to_parse.setData(<string[]>applicable)
|
||||
})
|
||||
|
||||
const loading = new Loading("Gathering which elements support auto-apply... ")
|
||||
|
|
|
@ -57,7 +57,7 @@ export class CloseNoteButton implements SpecialVisualization {
|
|||
comment: string
|
||||
minZoom: string
|
||||
zoomButton: string
|
||||
} = <any> Utils.ParseVisArgs(this.args, args)
|
||||
} = <any>Utils.ParseVisArgs(this.args, args)
|
||||
|
||||
let icon = Svg.checkmark_svg()
|
||||
if (params.icon !== "checkmark.svg" && (args[2] ?? "") !== "") {
|
||||
|
|
|
@ -2,47 +2,47 @@
|
|||
/**
|
||||
* 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";
|
||||
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;
|
||||
export let coordinate: { lon: number; lat: number }
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
let comment: UIEventSource<string> = LocalStorageSource.Get("note-text");
|
||||
let created = false;
|
||||
let comment: UIEventSource<string> = LocalStorageSource.Get("note-text")
|
||||
let created = false
|
||||
|
||||
let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note");
|
||||
let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note")
|
||||
|
||||
let hasFilter = notelayer?.hasFilter
|
||||
let isDisplayed = notelayer?.isDisplayed
|
||||
|
||||
let hasFilter = notelayer?.hasFilter;
|
||||
let isDisplayed = notelayer?.isDisplayed;
|
||||
|
||||
function enableNoteLayer() {
|
||||
state.guistate.closeAll();
|
||||
isDisplayed.setData(true);
|
||||
state.guistate.closeAll()
|
||||
isDisplayed.setData(true)
|
||||
}
|
||||
|
||||
async function uploadNote() {
|
||||
let txt = comment.data;
|
||||
let txt = comment.data
|
||||
if (txt === undefined || txt === "") {
|
||||
return;
|
||||
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 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]
|
||||
coordinates: [loc.lon, loc.lat],
|
||||
},
|
||||
properties: {
|
||||
id: "" + id.id,
|
||||
|
@ -53,20 +53,20 @@
|
|||
text: txt,
|
||||
html: txt,
|
||||
user: state.osmConnection?.userDetails?.data?.name,
|
||||
uid: state.osmConnection?.userDetails?.data?.uid
|
||||
}
|
||||
])
|
||||
}
|
||||
};
|
||||
uid: state.osmConnection?.userDetails?.data?.uid,
|
||||
},
|
||||
]),
|
||||
},
|
||||
}
|
||||
// Normally, the 'Changes' will generate the new element. The 'notes' are an exception to this
|
||||
state.newFeatures.features.data.push(feature);
|
||||
state.newFeatures.features.ping();
|
||||
state.selectedElement?.setData(feature);
|
||||
comment.setData("");
|
||||
created = true;
|
||||
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
|
||||
|
@ -77,29 +77,28 @@
|
|||
</div>
|
||||
{:else}
|
||||
<h3>
|
||||
<Tr t={Translations.t.notes.createNoteTitle}></Tr>
|
||||
<Tr t={Translations.t.notes.createNoteTitle} />
|
||||
</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>
|
||||
<Tr t={Translations.t.notes.noteLayerHasFilters} />
|
||||
</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>
|
||||
<img slot="image" src="./assets/svg/filter.svg" class="w-8 h-8 mr-4" />
|
||||
<Tr slot="message" t={Translations.t.notes.disableAllNoteFilters} />
|
||||
</SubtleButton>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Tr t={Translations.t.notes.createNoteIntro}></Tr>
|
||||
<Tr t={Translations.t.notes.createNoteIntro} />
|
||||
<div class="border rounded-sm border-grey-500">
|
||||
<div class="w-full p-1">
|
||||
<ValidatedInput type="text" value={comment}></ValidatedInput>
|
||||
<ValidatedInput type="text" value={comment} />
|
||||
</div>
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
@ -111,30 +110,26 @@
|
|||
|
||||
{#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>
|
||||
<img slot="image" src="./assets/svg/addSmall.svg" class="w-8 h-8 mr-4" />
|
||||
<Tr slot="message" t={Translations.t.notes.createNote} />
|
||||
</SubtleButton>
|
||||
{:else}
|
||||
<div class="alert">
|
||||
<Tr t={ Translations.t.notes.textNeeded}></Tr>
|
||||
<Tr t={Translations.t.notes.textNeeded} />
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
<div class="alert">
|
||||
<Tr t={Translations.t.notes.noteLayerNotEnabled}></Tr>
|
||||
<Tr t={Translations.t.notes.noteLayerNotEnabled} />
|
||||
</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>
|
||||
<img slot="image" src="./assets/svg/layers.svg" class="w-8 h-8 mr-4" />
|
||||
<Tr slot="message" t={Translations.t.notes.noteLayerDoEnable} />
|
||||
</SubtleButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import {Translation} from "../../i18n/Translation";
|
||||
import OsmObjectDownloader from "../../../Logic/Osm/OsmObjectDownloader";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {OsmId} from "../../../Models/OsmFeature";
|
||||
import {OsmConnection} from "../../../Logic/Osm/OsmConnection";
|
||||
import {SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import Constants from "../../../Models/Constants";
|
||||
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import OsmObjectDownloader from "../../../Logic/Osm/OsmObjectDownloader"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { OsmId } from "../../../Models/OsmFeature"
|
||||
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
|
||||
import { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Constants from "../../../Models/Constants"
|
||||
|
||||
export class DeleteFlowState {
|
||||
public readonly canBeDeleted: UIEventSource<boolean | undefined> = new UIEventSource<boolean | undefined>(undefined)
|
||||
public readonly canBeDeletedReason: UIEventSource<Translation | undefined> = new UIEventSource<Translation>(undefined)
|
||||
public readonly canBeDeleted: UIEventSource<boolean | undefined> = new UIEventSource<
|
||||
boolean | undefined
|
||||
>(undefined)
|
||||
public readonly canBeDeletedReason: UIEventSource<Translation | undefined> =
|
||||
new UIEventSource<Translation>(undefined)
|
||||
private readonly objectDownloader: OsmObjectDownloader
|
||||
private readonly _id: OsmId
|
||||
private readonly _allowDeletionAtChangesetCount: number
|
||||
|
@ -176,9 +178,10 @@ export class DeleteFlowState {
|
|||
} else {
|
||||
// alright, this point can be safely deleted!
|
||||
this.canBeDeleted.setData(true)
|
||||
this.canBeDeletedReason.setData(allByMyself.data ? t.onlyEditedByLoggedInUser : t.safeDelete)
|
||||
this.canBeDeletedReason.setData(
|
||||
allByMyself.data ? t.onlyEditedByLoggedInUser : t.safeDelete
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,155 +1,156 @@
|
|||
<script lang="ts">
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import { InformationCircleIcon, TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import type { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
||||
import DeleteConfig from "../../../Models/ThemeConfig/DeleteConfig"
|
||||
import TagRenderingQuestion from "../TagRendering/TagRenderingQuestion.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction"
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import { DeleteFlowState } from "./DeleteFlowState"
|
||||
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||
import type {SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import {InformationCircleIcon, TrashIcon} from "@babeard/svelte-heroicons/mini";
|
||||
import type {OsmId, OsmTags} from "../../../Models/OsmFeature";
|
||||
import DeleteConfig from "../../../Models/ThemeConfig/DeleteConfig";
|
||||
import TagRenderingQuestion from "../TagRendering/TagRenderingQuestion.svelte";
|
||||
import type {Feature} from "geojson";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
|
||||
import {XCircleIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction";
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
|
||||
import Loading from "../../Base/Loading.svelte";
|
||||
import {DeleteFlowState} from "./DeleteFlowState";
|
||||
export let state: SpecialVisualizationState
|
||||
export let deleteConfig: DeleteConfig
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let deleteConfig: DeleteConfig
|
||||
export let tags: UIEventSource<OsmTags>
|
||||
|
||||
export let tags: UIEventSource<OsmTags>
|
||||
let featureId: OsmId = <OsmId>tags.data.id
|
||||
|
||||
let featureId: OsmId = <OsmId> tags.data.id
|
||||
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
|
||||
const deleteAbility = new DeleteFlowState(
|
||||
const deleteAbility = new DeleteFlowState(featureId, state, deleteConfig.neededChangesets)
|
||||
|
||||
const canBeDeleted: UIEventSource<boolean | undefined> = deleteAbility.canBeDeleted
|
||||
const canBeDeletedReason = deleteAbility.canBeDeletedReason
|
||||
|
||||
const hasSoftDeletion = deleteConfig.softDeletionTags !== undefined
|
||||
let currentState: "start" | "confirm" | "applying" | "deleted" = "start"
|
||||
$: {
|
||||
console.log("Current state is", currentState, $canBeDeleted, canBeDeletedReason)
|
||||
deleteAbility.CheckDeleteability(true)
|
||||
}
|
||||
|
||||
const t = Translations.t.delete
|
||||
|
||||
let selectedTags: TagsFilter
|
||||
let changedProperties = undefined
|
||||
$: changedProperties = TagUtils.changeAsProperties(selectedTags?.asChange(tags?.data ?? {}) ?? [])
|
||||
let isHardDelete = undefined
|
||||
$: isHardDelete = changedProperties[DeleteConfig.deleteReasonKey] !== undefined
|
||||
|
||||
async function onDelete() {
|
||||
currentState = "applying"
|
||||
let actionToTake: OsmChangeAction
|
||||
const changedProperties = TagUtils.changeAsProperties(selectedTags.asChange(tags?.data ?? {}))
|
||||
const deleteReason = changedProperties[DeleteConfig.deleteReasonKey]
|
||||
console.log("Deleting! Hard?:", canBeDeleted.data, deleteReason)
|
||||
if (deleteReason) {
|
||||
// This is a proper, hard deletion
|
||||
actionToTake = new DeleteAction(
|
||||
featureId,
|
||||
state,
|
||||
deleteConfig.neededChangesets
|
||||
)
|
||||
|
||||
const canBeDeleted: UIEventSource<boolean | undefined> = deleteAbility.canBeDeleted
|
||||
const canBeDeletedReason = deleteAbility.canBeDeletedReason
|
||||
|
||||
const hasSoftDeletion = deleteConfig.softDeletionTags !== undefined
|
||||
let currentState: "start" | "confirm" | "applying" | "deleted" = ("start")
|
||||
$: {
|
||||
console.log("Current state is", currentState, $canBeDeleted, canBeDeletedReason)
|
||||
deleteAbility.CheckDeleteability(true)
|
||||
}
|
||||
|
||||
const t = Translations.t.delete
|
||||
|
||||
let selectedTags: TagsFilter
|
||||
let changedProperties = undefined
|
||||
$: changedProperties = TagUtils.changeAsProperties(selectedTags?.asChange(tags?.data ?? {}) ?? [])
|
||||
let isHardDelete = undefined
|
||||
$: isHardDelete = changedProperties[DeleteConfig.deleteReasonKey] !== undefined
|
||||
|
||||
|
||||
async function onDelete() {
|
||||
currentState = "applying"
|
||||
let actionToTake: OsmChangeAction
|
||||
const changedProperties = TagUtils.changeAsProperties(selectedTags.asChange(tags?.data ?? {}))
|
||||
const deleteReason = changedProperties[DeleteConfig.deleteReasonKey]
|
||||
console.log("Deleting! Hard?:", canBeDeleted.data, deleteReason)
|
||||
if (deleteReason) {
|
||||
// This is a proper, hard deletion
|
||||
actionToTake = new DeleteAction(
|
||||
featureId,
|
||||
deleteConfig.softDeletionTags,
|
||||
{
|
||||
theme: state?.layout?.id ?? "unknown",
|
||||
specialMotivation: deleteReason,
|
||||
},
|
||||
canBeDeleted.data
|
||||
)
|
||||
} else {
|
||||
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping
|
||||
actionToTake = new ChangeTagAction(featureId,
|
||||
selectedTags,
|
||||
tags.data,
|
||||
{
|
||||
theme: state?.layout?.id ?? "unkown",
|
||||
changeType: "special-delete",
|
||||
})
|
||||
}
|
||||
|
||||
await state.changes?.applyAction(actionToTake)
|
||||
tags.data["_deleted"] = "yes"
|
||||
tags.ping()
|
||||
currentState = "deleted"
|
||||
deleteConfig.softDeletionTags,
|
||||
{
|
||||
theme: state?.layout?.id ?? "unknown",
|
||||
specialMotivation: deleteReason,
|
||||
},
|
||||
canBeDeleted.data
|
||||
)
|
||||
} else {
|
||||
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping
|
||||
actionToTake = new ChangeTagAction(featureId, selectedTags, tags.data, {
|
||||
theme: state?.layout?.id ?? "unkown",
|
||||
changeType: "special-delete",
|
||||
})
|
||||
}
|
||||
|
||||
await state.changes?.applyAction(actionToTake)
|
||||
tags.data["_deleted"] = "yes"
|
||||
tags.ping()
|
||||
currentState = "deleted"
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $canBeDeleted === false && !hasSoftDeletion}
|
||||
<div class="flex low-interaction">
|
||||
<InformationCircleIcon class="w-6 h-6"/>
|
||||
<Tr t={$canBeDeletedReason}/>
|
||||
<Tr class="subtle" t={t.useSomethingElse}/>
|
||||
</div>
|
||||
<div class="flex low-interaction">
|
||||
<InformationCircleIcon class="w-6 h-6" />
|
||||
<Tr t={$canBeDeletedReason} />
|
||||
<Tr class="subtle" t={t.useSomethingElse} />
|
||||
</div>
|
||||
{:else}
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
{#if currentState === "start"}
|
||||
<button class="flex" on:click={() => {currentState = "confirm"}}>
|
||||
<TrashIcon class="w-6 h-6"/>
|
||||
<Tr t={t.delete}/>
|
||||
</button>
|
||||
{:else if currentState === "confirm"}
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
{#if currentState === "start"}
|
||||
<button
|
||||
class="flex"
|
||||
on:click={() => {
|
||||
currentState = "confirm"
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="w-6 h-6" />
|
||||
<Tr t={t.delete} />
|
||||
</button>
|
||||
{:else if currentState === "confirm"}
|
||||
<TagRenderingQuestion
|
||||
bind:selectedTags
|
||||
{tags}
|
||||
config={deleteConfig.constructTagRendering()}
|
||||
{state}
|
||||
selectedElement={feature}
|
||||
{layer}
|
||||
>
|
||||
<button
|
||||
slot="save-button"
|
||||
on:click={onDelete}
|
||||
class={(selectedTags === undefined ? "disabled" : "") + " flex primary bg-red-600"}
|
||||
>
|
||||
<TrashIcon
|
||||
class={"w-6 h-6 rounded-full p-1 ml-1 mr-2 " +
|
||||
(selectedTags !== undefined ? "bg-red-600" : "")}
|
||||
/>
|
||||
<Tr t={t.delete} />
|
||||
</button>
|
||||
<button slot="cancel" on:click={() => (currentState = "start")}>
|
||||
<Tr t={t.cancel} />
|
||||
</button>
|
||||
<XCircleIcon
|
||||
slot="upper-right"
|
||||
class="w-8 h-8 cursor-pointer"
|
||||
on:click={() => {
|
||||
currentState = "start"
|
||||
}}
|
||||
/>
|
||||
|
||||
<TagRenderingQuestion
|
||||
bind:selectedTags={selectedTags}
|
||||
{tags} config={deleteConfig.constructTagRendering()}
|
||||
{state} selectedElement={feature}
|
||||
{layer}>
|
||||
|
||||
<button slot="save-button" on:click={onDelete}
|
||||
class={(selectedTags === undefined ? "disabled" : "")+ " flex primary bg-red-600"}>
|
||||
<TrashIcon
|
||||
class={"w-6 h-6 rounded-full p-1 ml-1 mr-2 "+(selectedTags !== undefined ? "bg-red-600" : "")}/>
|
||||
<Tr t={t.delete}/>
|
||||
</button>
|
||||
<button slot="cancel" on:click={() => currentState = "start"}>
|
||||
<Tr t={t.cancel}/>
|
||||
</button>
|
||||
<XCircleIcon slot="upper-right" class="w-8 h-8 cursor-pointer"
|
||||
on:click={() => {currentState = "start"}}/>
|
||||
|
||||
<div slot="under-buttons">
|
||||
{#if selectedTags !== undefined}
|
||||
{#if canBeDeleted && isHardDelete}
|
||||
<!-- This is a hard delete - explain that this is a hard delete...-->
|
||||
<Tr t={t.explanations.hardDelete}/>
|
||||
{:else}
|
||||
<!-- This is a soft deletion: we explain _why_ the deletion is soft -->
|
||||
<Tr t={t.explanations.softDelete.Subs({reason: $canBeDeletedReason})}/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</TagRenderingQuestion>
|
||||
|
||||
{:else if currentState === "applying"}
|
||||
<Loading/>
|
||||
{:else}
|
||||
<!-- currentState === 'deleted' -->
|
||||
|
||||
<div class="flex low-interaction">
|
||||
<TrashIcon class="w-6 h-6"/>
|
||||
<Tr t={t.isDeleted}/>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
</LoginToggle>
|
||||
<div slot="under-buttons">
|
||||
{#if selectedTags !== undefined}
|
||||
{#if canBeDeleted && isHardDelete}
|
||||
<!-- This is a hard delete - explain that this is a hard delete...-->
|
||||
<Tr t={t.explanations.hardDelete} />
|
||||
{:else}
|
||||
<!-- This is a soft deletion: we explain _why_ the deletion is soft -->
|
||||
<Tr t={t.explanations.softDelete.Subs({ reason: $canBeDeletedReason })} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</TagRenderingQuestion>
|
||||
{:else if currentState === "applying"}
|
||||
<Loading />
|
||||
{:else}
|
||||
<!-- currentState === 'deleted' -->
|
||||
|
||||
<div class="flex low-interaction">
|
||||
<TrashIcon class="w-6 h-6" />
|
||||
<Tr t={t.isDeleted} />
|
||||
</div>
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
{/if}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import {SubtleButton} from "../Base/SubtleButton"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import Combine from "../Base/Combine"
|
||||
import {GeoOperations} from "../../Logic/GeoOperations"
|
||||
import {Utils} from "../../Utils"
|
||||
import {SpecialVisualization, SpecialVisualizationState} from "../SpecialVisualization"
|
||||
import {UIEventSource} from "../../Logic/UIEventSource"
|
||||
import {Feature, LineString} from "geojson"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { Utils } from "../../Utils"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Feature, LineString } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
|
||||
export class ExportAsGpxViz implements SpecialVisualization {
|
||||
|
|
|
@ -1,50 +1,58 @@
|
|||
import {SpecialVisualization, SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {Feature, Geometry, LineString, Polygon} from "geojson";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import BaseUIElement from "../../BaseUIElement";
|
||||
import {ImportFlowArguments, ImportFlowUtils} from "./ImportFlow";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import {Utils} from "../../../Utils";
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement";
|
||||
import WayImportFlow from "./WayImportFlow.svelte";
|
||||
import ConflateImportFlowState from "./ConflateImportFlowState";
|
||||
import {AutoAction} from "../AutoApplyButton";
|
||||
import {IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
|
||||
import {Changes} from "../../../Logic/Osm/Changes";
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {OsmConnection} from "../../../Logic/Osm/OsmConnection";
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { Feature, Geometry, LineString, Polygon } from "geojson"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import { ImportFlowArguments, ImportFlowUtils } from "./ImportFlow"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { Utils } from "../../../Utils"
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement"
|
||||
import WayImportFlow from "./WayImportFlow.svelte"
|
||||
import ConflateImportFlowState from "./ConflateImportFlowState"
|
||||
import { AutoAction } from "../AutoApplyButton"
|
||||
import { IndexedFeatureSource } from "../../../Logic/FeatureSource/FeatureSource"
|
||||
import { Changes } from "../../../Logic/Osm/Changes"
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
||||
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
|
||||
|
||||
export interface ConflateFlowArguments extends ImportFlowArguments {
|
||||
way_to_conflate: string
|
||||
point_move_mode?: "move_osm" | undefined;
|
||||
point_move_mode?: "move_osm" | undefined
|
||||
max_snap_distance?: string
|
||||
snap_onto_layers?: string,
|
||||
snap_onto_layers?: string
|
||||
}
|
||||
|
||||
export default class ConflateImportButtonViz implements SpecialVisualization, AutoAction {
|
||||
supportsAutoAction: boolean = true;
|
||||
public readonly funcName: string = "conflate_button";
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
|
||||
supportsAutoAction: boolean = true
|
||||
public readonly funcName: string = "conflate_button"
|
||||
public readonly args: {
|
||||
name: string
|
||||
defaultValue?: string
|
||||
doc: string
|
||||
required?: boolean
|
||||
}[] = [
|
||||
...ImportFlowUtils.generalArguments,
|
||||
{
|
||||
name: "way_to_conflate",
|
||||
doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag",
|
||||
},
|
||||
|
||||
|
||||
];
|
||||
readonly docs: string = "This button will modify the geometry of an existing OSM way to match the specified geometry. This can conflate OSM-ways with LineStrings and Polygons (only simple polygons with one single ring). An attempt is made to move points with special values to a decent new location (e.g. entrances)" + ImportFlowUtils.documentationGeneral
|
||||
]
|
||||
readonly docs: string =
|
||||
"This button will modify the geometry of an existing OSM way to match the specified geometry. This can conflate OSM-ways with LineStrings and Polygons (only simple polygons with one single ring). An attempt is made to move points with special values to a decent new location (e.g. entrances)" +
|
||||
ImportFlowUtils.documentationGeneral
|
||||
public readonly needsNodeDatabase = true
|
||||
|
||||
|
||||
async applyActionOn(feature: Feature<Geometry, { [name: string]: any; }>, state: {
|
||||
osmConnection: OsmConnection,
|
||||
layout: LayoutConfig;
|
||||
changes: Changes;
|
||||
indexedFeatures: IndexedFeatureSource;
|
||||
}, tagSource: UIEventSource<any>, argument: string[]): Promise<void> {
|
||||
|
||||
async applyActionOn(
|
||||
feature: Feature<Geometry, { [name: string]: any }>,
|
||||
state: {
|
||||
osmConnection: OsmConnection
|
||||
layout: LayoutConfig
|
||||
changes: Changes
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
},
|
||||
tagSource: UIEventSource<any>,
|
||||
argument: string[]
|
||||
): Promise<void> {
|
||||
{
|
||||
// Small safety check to prevent duplicate imports
|
||||
const id = tagSource.data.id
|
||||
|
@ -61,15 +69,27 @@ export default class ConflateImportButtonViz implements SpecialVisualization, Au
|
|||
const args: ConflateFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
|
||||
const idOfWayToReplaceGeometry = tagSource.data[args.way_to_conflate]
|
||||
const action = ConflateImportFlowState.createAction(<Feature<LineString | Polygon>>feature, args, state, idOfWayToReplaceGeometry, tagsToApply)
|
||||
const action = ConflateImportFlowState.createAction(
|
||||
<Feature<LineString | Polygon>>feature,
|
||||
args,
|
||||
state,
|
||||
idOfWayToReplaceGeometry,
|
||||
tagsToApply
|
||||
)
|
||||
tagSource.data["_imported"] = "yes"
|
||||
tagSource.ping()
|
||||
await state.changes.applyAction(action)
|
||||
}
|
||||
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
|
||||
|
||||
const canBeImported = feature.geometry.type === "LineString" ||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
const canBeImported =
|
||||
feature.geometry.type === "LineString" ||
|
||||
(feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
||||
if (!canBeImported) {
|
||||
return Translations.t.general.add.import.wrongTypeToConflate.SetClass("alert")
|
||||
|
@ -77,9 +97,16 @@ export default class ConflateImportButtonViz implements SpecialVisualization, Au
|
|||
const args: ConflateFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
|
||||
const idOfWayToReplaceGeometry = tagSource.data[args.way_to_conflate]
|
||||
const importFlow = new ConflateImportFlowState(state, <Feature<LineString | Polygon>>feature, args, tagsToApply, tagSource, idOfWayToReplaceGeometry)
|
||||
const importFlow = new ConflateImportFlowState(
|
||||
state,
|
||||
<Feature<LineString | Polygon>>feature,
|
||||
args,
|
||||
tagsToApply,
|
||||
tagSource,
|
||||
idOfWayToReplaceGeometry
|
||||
)
|
||||
return new SvelteUIElement(WayImportFlow, {
|
||||
importFlow
|
||||
importFlow,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +1,48 @@
|
|||
import ImportFlow from "./ImportFlow";
|
||||
import {ConflateFlowArguments} from "./ConflateImportButtonViz";
|
||||
import {SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import {Feature, LineString, Polygon} from "geojson";
|
||||
import {Store, UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {Tag} from "../../../Logic/Tags/Tag";
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import ReplaceGeometryAction from "../../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||
import {GeoOperations} from "../../../Logic/GeoOperations";
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import {MergePointConfig} from "../../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Changes} from "../../../Logic/Osm/Changes";
|
||||
import {FeatureSource, IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import {OsmConnection} from "../../../Logic/Osm/OsmConnection";
|
||||
import ImportFlow from "./ImportFlow"
|
||||
import { ConflateFlowArguments } from "./ConflateImportButtonViz"
|
||||
import { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import { Feature, LineString, Polygon } from "geojson"
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import ReplaceGeometryAction from "../../../Logic/Osm/Actions/ReplaceGeometryAction"
|
||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import { MergePointConfig } from "../../../Logic/Osm/Actions/CreateWayWithPointReuseAction"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Changes } from "../../../Logic/Osm/Changes"
|
||||
import { FeatureSource, IndexedFeatureSource } from "../../../Logic/FeatureSource/FeatureSource"
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
|
||||
|
||||
export default class ConflateImportFlowState extends ImportFlow<ConflateFlowArguments> {
|
||||
|
||||
public readonly originalFeature: Feature
|
||||
private readonly action: OsmChangeAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string };
|
||||
constructor(state: SpecialVisualizationState, originalFeature: Feature<LineString | Polygon>, args: ConflateFlowArguments, tagsToApply: Store<Tag[]>, originalFeatureTags: UIEventSource<Record<string, string>>, idOfFeatureToReplaceGeometry: string) {
|
||||
private readonly action: OsmChangeAction & {
|
||||
getPreview?(): Promise<FeatureSource>
|
||||
newElementId?: string
|
||||
}
|
||||
constructor(
|
||||
state: SpecialVisualizationState,
|
||||
originalFeature: Feature<LineString | Polygon>,
|
||||
args: ConflateFlowArguments,
|
||||
tagsToApply: Store<Tag[]>,
|
||||
originalFeatureTags: UIEventSource<Record<string, string>>,
|
||||
idOfFeatureToReplaceGeometry: string
|
||||
) {
|
||||
super(state, args, tagsToApply, originalFeatureTags)
|
||||
this.originalFeature = originalFeature
|
||||
this.action = ConflateImportFlowState.createAction(originalFeature, args, state, idOfFeatureToReplaceGeometry, tagsToApply)
|
||||
|
||||
this.action = ConflateImportFlowState.createAction(
|
||||
originalFeature,
|
||||
args,
|
||||
state,
|
||||
idOfFeatureToReplaceGeometry,
|
||||
tagsToApply
|
||||
)
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public GetPreview(): Promise<FeatureSource>{
|
||||
public GetPreview(): Promise<FeatureSource> {
|
||||
return this.action.getPreview()
|
||||
}
|
||||
|
||||
|
@ -43,18 +57,19 @@ export default class ConflateImportFlowState extends ImportFlow<ConflateFlowArgu
|
|||
this.state.selectedElement.setData(this.state.indexedFeatures.featuresById.data.get(newId))
|
||||
}
|
||||
|
||||
public static createAction(feature: Feature<LineString | Polygon>,
|
||||
args: ConflateFlowArguments,
|
||||
state: {
|
||||
osmConnection: OsmConnection,
|
||||
layout: LayoutConfig;
|
||||
changes: Changes;
|
||||
indexedFeatures: IndexedFeatureSource,
|
||||
fullNodeDatabase?: FullNodeDatabaseSource
|
||||
},
|
||||
idOfFeatureToReplaceGeometry,
|
||||
tagsToApply: Store<Tag[]>
|
||||
): OsmChangeAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string } {
|
||||
public static createAction(
|
||||
feature: Feature<LineString | Polygon>,
|
||||
args: ConflateFlowArguments,
|
||||
state: {
|
||||
osmConnection: OsmConnection
|
||||
layout: LayoutConfig
|
||||
changes: Changes
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
fullNodeDatabase?: FullNodeDatabaseSource
|
||||
},
|
||||
idOfFeatureToReplaceGeometry,
|
||||
tagsToApply: Store<Tag[]>
|
||||
): OsmChangeAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string } {
|
||||
const nodesMustMatch = args.snap_onto_layers
|
||||
?.split(";")
|
||||
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
|
||||
|
@ -69,7 +84,6 @@ export default class ConflateImportFlowState extends ImportFlow<ConflateFlowArgu
|
|||
mergeConfigs.push(mergeConfig)
|
||||
}
|
||||
|
||||
|
||||
return new ReplaceGeometryAction(
|
||||
state,
|
||||
GeoOperations.removeOvernoding(feature),
|
||||
|
|
|
@ -1,146 +1,170 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
|
||||
* They show some default components
|
||||
*/
|
||||
import ImportFlow from "./ImportFlow";
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||
import BackButton from "../../Base/BackButton.svelte";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import NextButton from "../../Base/NextButton.svelte";
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import Loading from "../../Base/Loading.svelte";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
import TagHint from "../TagHint.svelte";
|
||||
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
|
||||
import {Store} from "../../../Logic/UIEventSource";
|
||||
import Svg from "../../../Svg";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import {EyeIcon, EyeOffIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
/**
|
||||
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
|
||||
* They show some default components
|
||||
*/
|
||||
import ImportFlow from "./ImportFlow"
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||
import BackButton from "../../Base/BackButton.svelte"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import NextButton from "../../Base/NextButton.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
import TagHint from "../TagHint.svelte"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import { Store } from "../../../Logic/UIEventSource"
|
||||
import Svg from "../../../Svg"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
|
||||
export let importFlow: ImportFlow
|
||||
let state = importFlow.state
|
||||
export let importFlow: ImportFlow
|
||||
let state = importFlow.state
|
||||
|
||||
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
|
||||
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
|
||||
|
||||
const isLoading = state.dataIsLoading
|
||||
const dispatch = createEventDispatcher<{ confirm }>()
|
||||
const canBeImported = importFlow.canBeImported()
|
||||
const tags: Store<TagsFilter> = importFlow.tagsToApply.map(tags => new And(tags))
|
||||
const isLoading = state.dataIsLoading
|
||||
const dispatch = createEventDispatcher<{ confirm }>()
|
||||
const canBeImported = importFlow.canBeImported()
|
||||
const tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags))
|
||||
|
||||
const isDisplayed = importFlow.targetLayer.isDisplayed
|
||||
const hasFilter = importFlow.targetLayer.hasFilter
|
||||
|
||||
function abort() {
|
||||
state.selectedElement.setData(undefined)
|
||||
state.selectedLayer.setData(undefined)
|
||||
}
|
||||
const isDisplayed = importFlow.targetLayer.isDisplayed
|
||||
const hasFilter = importFlow.targetLayer.hasFilter
|
||||
|
||||
function abort() {
|
||||
state.selectedElement.setData(undefined)
|
||||
state.selectedLayer.setData(undefined)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
||||
|
||||
{#if $canBeImported !== true && $canBeImported !== undefined}
|
||||
<Tr cls="alert w-full flex justify-center" t={$canBeImported.error}/>
|
||||
{#if $canBeImported.extraHelp}
|
||||
<Tr t={$canBeImported.extraHelp}/>
|
||||
{/if}
|
||||
{:else if !$isDisplayed}
|
||||
<!-- 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: importFlow.targetLayer.layerDef.name })
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
|
||||
<button class="flex w-full gap-x-1"
|
||||
on:click={() => {abort();state.guistate.openFilterView(importFlow.targetLayer.layerDef)}}>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
|
||||
<Tr t={Translations.t.general.add.openLayerControl}/>
|
||||
</button>
|
||||
|
||||
<button class="flex w-full gap-x-1 primary" on:click={() => {isDisplayed.setData(true);abort()}}>
|
||||
<EyeIcon class="w-12"/>
|
||||
<Tr t={Translations.t.general.add.enableLayer.Subs({name: importFlow.targetLayer.layerDef.name})}/>
|
||||
</button>
|
||||
</div>
|
||||
{:else if $hasFilter}
|
||||
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8"/>
|
||||
<Tr t={Translations.t.general.add.disableFiltersExplanation}/>
|
||||
</div>
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
<button class="flex w-full gap-x-1 primary"
|
||||
on:click={() => {abort(); importFlow.targetLayer.disableAllFilters()}}>
|
||||
<EyeOffIcon class="w-12"/>
|
||||
<Tr t={Translations.t.general.add.disableFilters}/>
|
||||
</button>
|
||||
<button class="flex w-full gap-x-1"
|
||||
on:click={() => {abort();state.guistate.openFilterView(importFlow.targetLayer.layerDef)}}>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
|
||||
<Tr t={Translations.t.general.add.openLayerControl}/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{:else if $isLoading}
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.add.stillLoading}/>
|
||||
</Loading>
|
||||
|
||||
{:else if currentFlowStep === "start"}
|
||||
<NextButton clss="primary w-full" on:click={() => currentFlowStep = "confirm"}>
|
||||
<slot name="start-flow-text">
|
||||
{#if importFlow?.args?.icon}
|
||||
<img class="w-8 h-8" src={importFlow.args.icon}/>
|
||||
{/if}
|
||||
{importFlow.args.text}
|
||||
</slot>
|
||||
</NextButton>
|
||||
{:else if currentFlowStep === "confirm"}
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<div class="w-full h-full">
|
||||
<slot name="map"/>
|
||||
</div>
|
||||
<div class="flex flex-col-reverse md:flex-row">
|
||||
<BackButton clss="w-full" on:click={() => currentFlowStep = "start"}>
|
||||
<Tr t={Translations.t.general.back}/>
|
||||
</BackButton>
|
||||
<NextButton clss="primary w-full" on:click={() => {
|
||||
currentFlowStep = "imported"
|
||||
dispatch("confirm")
|
||||
}}>
|
||||
<span slot="image" class="w-8 h-8 pr-4">
|
||||
{#if importFlow.args.icon}
|
||||
<img src={importFlow.args.icon}>
|
||||
{:else}
|
||||
<ToSvelte construct={Svg.confirm_svg().SetClass("w-8 h-8 pr-4")}/>
|
||||
{/if}
|
||||
</span>
|
||||
<slot name="confirm-text">
|
||||
{importFlow.args.text}
|
||||
</slot>
|
||||
</NextButton>
|
||||
</div>
|
||||
|
||||
<div class="subtle">
|
||||
<TagHint embedIn={str => Translations.t.general.add.import.importTags.Subs({tags: str})} {state}
|
||||
tags={$tags}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{:else if currentFlowStep === "importing"}
|
||||
<Loading/>
|
||||
{:else if currentFlowStep === "imported"}
|
||||
<div class="thanks w-full p-4">
|
||||
<Tr t={Translations.t.general.add.import.hasBeenImported}/>
|
||||
</div>
|
||||
{#if $canBeImported !== true && $canBeImported !== undefined}
|
||||
<Tr cls="alert w-full flex justify-center" t={$canBeImported.error} />
|
||||
{#if $canBeImported.extraHelp}
|
||||
<Tr t={$canBeImported.extraHelp} />
|
||||
{/if}
|
||||
{:else if !$isDisplayed}
|
||||
<!-- 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: importFlow.targetLayer.layerDef.name,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
<button
|
||||
class="flex w-full gap-x-1"
|
||||
on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(importFlow.targetLayer.layerDef)
|
||||
}}
|
||||
>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
|
||||
<Tr t={Translations.t.general.add.openLayerControl} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex w-full gap-x-1 primary"
|
||||
on:click={() => {
|
||||
isDisplayed.setData(true)
|
||||
abort()
|
||||
}}
|
||||
>
|
||||
<EyeIcon class="w-12" />
|
||||
<Tr
|
||||
t={Translations.t.general.add.enableLayer.Subs({
|
||||
name: importFlow.targetLayer.layerDef.name,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{:else if $hasFilter}
|
||||
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8" />
|
||||
<Tr t={Translations.t.general.add.disableFiltersExplanation} />
|
||||
</div>
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
<button
|
||||
class="flex w-full gap-x-1 primary"
|
||||
on:click={() => {
|
||||
abort()
|
||||
importFlow.targetLayer.disableAllFilters()
|
||||
}}
|
||||
>
|
||||
<EyeOffIcon class="w-12" />
|
||||
<Tr t={Translations.t.general.add.disableFilters} />
|
||||
</button>
|
||||
<button
|
||||
class="flex w-full gap-x-1"
|
||||
on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(importFlow.targetLayer.layerDef)
|
||||
}}
|
||||
>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
|
||||
<Tr t={Translations.t.general.add.openLayerControl} />
|
||||
</button>
|
||||
</div>
|
||||
{:else if $isLoading}
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.add.stillLoading} />
|
||||
</Loading>
|
||||
{:else if currentFlowStep === "start"}
|
||||
<NextButton clss="primary w-full" on:click={() => (currentFlowStep = "confirm")}>
|
||||
<slot name="start-flow-text">
|
||||
{#if importFlow?.args?.icon}
|
||||
<img class="w-8 h-8" src={importFlow.args.icon} />
|
||||
{/if}
|
||||
{importFlow.args.text}
|
||||
</slot>
|
||||
</NextButton>
|
||||
{:else if currentFlowStep === "confirm"}
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<div class="w-full h-full">
|
||||
<slot name="map" />
|
||||
</div>
|
||||
<div class="flex flex-col-reverse md:flex-row">
|
||||
<BackButton clss="w-full" on:click={() => (currentFlowStep = "start")}>
|
||||
<Tr t={Translations.t.general.back} />
|
||||
</BackButton>
|
||||
<NextButton
|
||||
clss="primary w-full"
|
||||
on:click={() => {
|
||||
currentFlowStep = "imported"
|
||||
dispatch("confirm")
|
||||
}}
|
||||
>
|
||||
<span slot="image" class="w-8 h-8 pr-4">
|
||||
{#if importFlow.args.icon}
|
||||
<img src={importFlow.args.icon} />
|
||||
{:else}
|
||||
<ToSvelte construct={Svg.confirm_svg().SetClass("w-8 h-8 pr-4")} />
|
||||
{/if}
|
||||
</span>
|
||||
<slot name="confirm-text">
|
||||
{importFlow.args.text}
|
||||
</slot>
|
||||
</NextButton>
|
||||
</div>
|
||||
|
||||
<div class="subtle">
|
||||
<TagHint
|
||||
embedIn={(str) => Translations.t.general.add.import.importTags.Subs({ tags: str })}
|
||||
{state}
|
||||
tags={$tags}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if currentFlowStep === "importing"}
|
||||
<Loading />
|
||||
{:else if currentFlowStep === "imported"}
|
||||
<div class="thanks w-full p-4">
|
||||
<Tr t={Translations.t.general.add.import.hasBeenImported} />
|
||||
</div>
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import {SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {ImmutableStore, Store, UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {Tag} from "../../../Logic/Tags/Tag";
|
||||
import TagApplyButton from "../TagApplyButton";
|
||||
import {PointImportFlowArguments} from "./PointImportFlowState";
|
||||
import {Translation} from "../../i18n/Translation";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import {OsmConnection} from "../../../Logic/Osm/OsmConnection";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import {LayerConfigJson} from "../../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import conflation_json from "../../../assets/layers/conflation/conflation.json";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
import { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import TagApplyButton from "../TagApplyButton"
|
||||
import { PointImportFlowArguments } from "./PointImportFlowState"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import conflation_json from "../../../assets/layers/conflation/conflation.json"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
|
||||
export interface ImportFlowArguments {
|
||||
readonly text: string
|
||||
readonly text: string
|
||||
readonly tags: string
|
||||
readonly targetLayer: string
|
||||
readonly icon?: string
|
||||
|
@ -40,11 +40,12 @@ ${Utils.Special_visualizations_tagsToApplyHelpText}
|
|||
${Utils.special_visualizations_importRequirementDocs}
|
||||
`
|
||||
|
||||
public static generalArguments = [{
|
||||
name: "targetLayer",
|
||||
doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements",
|
||||
required: true,
|
||||
},
|
||||
public static generalArguments = [
|
||||
{
|
||||
name: "targetLayer",
|
||||
doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
doc: "The tags to add onto the new object - see specification above. If this is a key (a single word occuring in the properties of the object), the corresponding value is taken and expanded instead",
|
||||
|
@ -59,7 +60,8 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
name: "icon",
|
||||
doc: "A nice icon to show in the button",
|
||||
defaultValue: "./assets/svg/addSmall.svg",
|
||||
},]
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Given the tagsstore of the point which represents the challenge, creates a new store with tags that should be applied onto the newly created point,
|
||||
|
@ -82,17 +84,17 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
const items: string = originalFeatureTags.data[tags]
|
||||
console.debug(
|
||||
"The import button is using tags from properties[" +
|
||||
tags +
|
||||
"] of this object, namely ",
|
||||
tags +
|
||||
"] of this object, namely ",
|
||||
items
|
||||
)
|
||||
|
||||
if(items.startsWith("{")){
|
||||
if (items.startsWith("{")) {
|
||||
// This is probably a JSON
|
||||
const properties: Record<string, string> = JSON.parse(items)
|
||||
const keys = Object.keys(properties)
|
||||
const tags = keys.map(k => new Tag(k, properties[k]))
|
||||
return new ImmutableStore((tags))
|
||||
const tags = keys.map((k) => new Tag(k, properties[k]))
|
||||
return new ImmutableStore(tags)
|
||||
}
|
||||
|
||||
newTags = TagApplyButton.generateTagsToApply(items, originalFeatureTags)
|
||||
|
@ -110,24 +112,32 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
* @param argsRaw
|
||||
*/
|
||||
public static getLayerDependencies(argsRaw: string[], argSpec?) {
|
||||
const args: ImportFlowArguments = <any>Utils.ParseVisArgs(argSpec ?? ImportFlowUtils.generalArguments, argsRaw)
|
||||
const args: ImportFlowArguments = <any>(
|
||||
Utils.ParseVisArgs(argSpec ?? ImportFlowUtils.generalArguments, argsRaw)
|
||||
)
|
||||
return [args.targetLayer]
|
||||
}
|
||||
|
||||
public static getLayerDependenciesWithSnapOnto(argSpec: {
|
||||
name: string,
|
||||
defaultValue?: string
|
||||
}[], argsRaw: string[]): string[] {
|
||||
public static getLayerDependenciesWithSnapOnto(
|
||||
argSpec: {
|
||||
name: string
|
||||
defaultValue?: string
|
||||
}[],
|
||||
argsRaw: string[]
|
||||
): string[] {
|
||||
const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec)
|
||||
const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw)
|
||||
const snapOntoLayers = argsParsed.snap_onto_layers?.split(";")?.map(l => l.trim()) ?? []
|
||||
const snapOntoLayers = argsParsed.snap_onto_layers?.split(";")?.map((l) => l.trim()) ?? []
|
||||
deps.push(...snapOntoLayers)
|
||||
return deps
|
||||
}
|
||||
|
||||
public static buildTagSpec(args: ImportFlowArguments, tagSource: Store<Record<string, string>>): Store<string> {
|
||||
public static buildTagSpec(
|
||||
args: ImportFlowArguments,
|
||||
tagSource: Store<Record<string, string>>
|
||||
): Store<string> {
|
||||
let tagSpec = args.tags
|
||||
return tagSource.mapD(tags => {
|
||||
return tagSource.mapD((tags) => {
|
||||
if (
|
||||
tagSpec.indexOf(" ") < 0 &&
|
||||
tagSpec.indexOf(";") < 0 &&
|
||||
|
@ -137,8 +147,8 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
tagSpec = tags[args.tags]
|
||||
console.debug(
|
||||
"The import button is using tags from properties[" +
|
||||
args.tags +
|
||||
"] of this object, namely ",
|
||||
args.tags +
|
||||
"] of this object, namely ",
|
||||
tagSpec
|
||||
)
|
||||
}
|
||||
|
@ -147,70 +157,72 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The ImportFlow dictates some aspects of the import flow, e.g. what type of map should be shown and, in the case of a preview map, what layers that should be added.
|
||||
*
|
||||
* This class works together closely with ImportFlow.svelte
|
||||
*/
|
||||
export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
||||
|
||||
public readonly state: SpecialVisualizationState;
|
||||
public readonly args: ArgT;
|
||||
public readonly targetLayer: FilteredLayer;
|
||||
public readonly state: SpecialVisualizationState
|
||||
public readonly args: ArgT
|
||||
public readonly targetLayer: FilteredLayer
|
||||
public readonly tagsToApply: Store<Tag[]>
|
||||
protected readonly _originalFeatureTags: UIEventSource<Record<string, string>>;
|
||||
protected readonly _originalFeatureTags: UIEventSource<Record<string, string>>
|
||||
|
||||
constructor(state: SpecialVisualizationState, args: ArgT, tagsToApply: Store<Tag[]>, originalTags: UIEventSource<Record<string, string>>) {
|
||||
this.state = state;
|
||||
this.args = args;
|
||||
this.tagsToApply = tagsToApply;
|
||||
this._originalFeatureTags = originalTags;
|
||||
constructor(
|
||||
state: SpecialVisualizationState,
|
||||
args: ArgT,
|
||||
tagsToApply: Store<Tag[]>,
|
||||
originalTags: UIEventSource<Record<string, string>>
|
||||
) {
|
||||
this.state = state
|
||||
this.args = args
|
||||
this.tagsToApply = tagsToApply
|
||||
this._originalFeatureTags = originalTags
|
||||
this.targetLayer = state.layerState.filteredLayers.get(args.targetLayer)
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a store that contains either 'true' or gives a translation with the reason why it cannot be imported
|
||||
*/
|
||||
public canBeImported(): Store<true | { error: Translation, extraHelp?: Translation }> {
|
||||
public canBeImported(): Store<true | { error: Translation; extraHelp?: Translation }> {
|
||||
const state = this.state
|
||||
return state.featureSwitchIsTesting.map(isTesting => {
|
||||
const t = Translations.t.general.add.import
|
||||
return state.featureSwitchIsTesting.map(
|
||||
(isTesting) => {
|
||||
const t = Translations.t.general.add.import
|
||||
|
||||
if(this._originalFeatureTags.data["_imported"] === "yes"){
|
||||
return {error: t.hasBeenImported}
|
||||
}
|
||||
|
||||
const usesTestUrl = this.state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url
|
||||
if (!state.layout.official && !(isTesting || usesTestUrl)) {
|
||||
// Unofficial theme - imports not allowed
|
||||
return {
|
||||
error: t.officialThemesOnly,
|
||||
extraHelp: t.howToTest
|
||||
if (this._originalFeatureTags.data["_imported"] === "yes") {
|
||||
return { error: t.hasBeenImported }
|
||||
}
|
||||
}
|
||||
|
||||
if (this.targetLayer === undefined) {
|
||||
const e = `Target layer not defined: error in import button for theme: ${this.state.layout.id}: layer ${this.args.targetLayer} not found`
|
||||
console.error(e)
|
||||
return {error: new Translation({"*": e})}
|
||||
}
|
||||
const usesTestUrl =
|
||||
this.state.osmConnection._oauth_config.url ===
|
||||
OsmConnection.oauth_configs["osm-test"].url
|
||||
if (!state.layout.official && !(isTesting || usesTestUrl)) {
|
||||
// Unofficial theme - imports not allowed
|
||||
return {
|
||||
error: t.officialThemesOnly,
|
||||
extraHelp: t.howToTest,
|
||||
}
|
||||
}
|
||||
|
||||
if (state.mapProperties.zoom.data < 18) {
|
||||
return {error: t.zoomInMore}
|
||||
}
|
||||
if (this.targetLayer === undefined) {
|
||||
const e = `Target layer not defined: error in import button for theme: ${this.state.layout.id}: layer ${this.args.targetLayer} not found`
|
||||
console.error(e)
|
||||
return { error: new Translation({ "*": e }) }
|
||||
}
|
||||
|
||||
if (state.mapProperties.zoom.data < 18) {
|
||||
return { error: t.zoomInMore }
|
||||
}
|
||||
|
||||
if(state.dataIsLoading.data){
|
||||
return {error: Translations.t.general.add.stillLoading}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}, [state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags])
|
||||
|
||||
if (state.dataIsLoading.data) {
|
||||
return { error: Translations.t.general.add.stillLoading }
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
[state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import {Feature, Point} from "geojson";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {SpecialVisualization, SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import BaseUIElement from "../../BaseUIElement";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement";
|
||||
import PointImportFlow from "./PointImportFlow.svelte";
|
||||
import {PointImportFlowArguments, PointImportFlowState} from "./PointImportFlowState";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {ImportFlowUtils} from "./ImportFlow";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import { Feature, Point } from "geojson"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement"
|
||||
import PointImportFlow from "./PointImportFlow.svelte"
|
||||
import { PointImportFlowArguments, PointImportFlowState } from "./PointImportFlowState"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { ImportFlowUtils } from "./ImportFlow"
|
||||
import Translations from "../../i18n/Translations"
|
||||
|
||||
/**
|
||||
* The wrapper to make the special visualisation for the PointImportFlow
|
||||
*/
|
||||
export class PointImportButtonViz implements SpecialVisualization {
|
||||
|
||||
public readonly funcName: string
|
||||
public readonly docs: string | BaseUIElement
|
||||
public readonly example?: string
|
||||
|
@ -22,43 +21,53 @@ export class PointImportButtonViz implements SpecialVisualization {
|
|||
|
||||
constructor() {
|
||||
this.funcName = "import_button"
|
||||
this.docs = "This button will copy the point from an external dataset into OpenStreetMap" + ImportFlowUtils.documentationGeneral
|
||||
this.args =
|
||||
[...ImportFlowUtils.generalArguments,
|
||||
{
|
||||
name: "snap_onto_layers",
|
||||
doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list",
|
||||
},
|
||||
{
|
||||
name: "max_snap_distance",
|
||||
doc: "The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete",
|
||||
defaultValue: "5",
|
||||
},
|
||||
{
|
||||
name: "note_id",
|
||||
doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'",
|
||||
},
|
||||
{
|
||||
name: "maproulette_id",
|
||||
doc: "The property name of the maproulette_id - this is probably `mr_taskId`. If given, the maproulette challenge will be marked as fixed. Only use this if part of a maproulette-layer.",
|
||||
},
|
||||
]
|
||||
this.docs =
|
||||
"This button will copy the point from an external dataset into OpenStreetMap" +
|
||||
ImportFlowUtils.documentationGeneral
|
||||
this.args = [
|
||||
...ImportFlowUtils.generalArguments,
|
||||
{
|
||||
name: "snap_onto_layers",
|
||||
doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list",
|
||||
},
|
||||
{
|
||||
name: "max_snap_distance",
|
||||
doc: "The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete",
|
||||
defaultValue: "5",
|
||||
},
|
||||
{
|
||||
name: "note_id",
|
||||
doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'",
|
||||
},
|
||||
{
|
||||
name: "maproulette_id",
|
||||
doc: "The property name of the maproulette_id - this is probably `mr_taskId`. If given, the maproulette challenge will be marked as fixed. Only use this if part of a maproulette-layer.",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
if (feature.geometry.type !== "Point") {
|
||||
return Translations.t.general.add.import.wrongType.SetClass("alert")
|
||||
}
|
||||
const baseArgs: PointImportFlowArguments = <any> Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource , baseArgs)
|
||||
const importFlow = new PointImportFlowState(state, <Feature<Point>> feature, baseArgs, tagsToApply, tagSource)
|
||||
|
||||
return new SvelteUIElement(
|
||||
PointImportFlow, {
|
||||
importFlow
|
||||
}
|
||||
const baseArgs: PointImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, baseArgs)
|
||||
const importFlow = new PointImportFlowState(
|
||||
state,
|
||||
<Feature<Point>>feature,
|
||||
baseArgs,
|
||||
tagsToApply,
|
||||
tagSource
|
||||
)
|
||||
|
||||
return new SvelteUIElement(PointImportFlow, {
|
||||
importFlow,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,62 +1,62 @@
|
|||
<script lang="ts">
|
||||
import ImportFlow from "./ImportFlow.svelte"
|
||||
import { PointImportFlowState } from "./PointImportFlowState"
|
||||
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import MapControlButton from "../../Base/MapControlButton.svelte"
|
||||
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
|
||||
|
||||
import ImportFlow from "./ImportFlow.svelte";
|
||||
import {PointImportFlowState} from "./PointImportFlowState";
|
||||
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import MapControlButton from "../../Base/MapControlButton.svelte";
|
||||
import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid";
|
||||
export let importFlow: PointImportFlowState
|
||||
|
||||
export let importFlow: PointImportFlowState
|
||||
const state = importFlow.state
|
||||
|
||||
const state = importFlow.state;
|
||||
const args = importFlow.args
|
||||
|
||||
const args = importFlow.args
|
||||
// The following variables are used for the map
|
||||
const targetLayer: LayerConfig = state.layout.layers.find((l) => l.id === args.targetLayer)
|
||||
const snapToLayers: string[] | undefined =
|
||||
args.snap_onto_layers?.split(",")?.map((l) => l.trim()) ?? []
|
||||
const maxSnapDistance: number = Number(args.max_snap_distance ?? 25) ?? 25
|
||||
|
||||
// The following variables are used for the map
|
||||
const targetLayer: LayerConfig = state.layout.layers.find(l => l.id === args.targetLayer)
|
||||
const snapToLayers: string[] | undefined = args.snap_onto_layers?.split(",")?.map(l => l.trim()) ?? []
|
||||
const maxSnapDistance: number = Number(args.max_snap_distance ?? 25) ?? 25;
|
||||
const snappedTo: UIEventSource<string | undefined> = new UIEventSource<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const snappedTo: UIEventSource<string | undefined> = new UIEventSource<string | undefined>(undefined);
|
||||
|
||||
const startCoordinate = {
|
||||
lon: importFlow.startCoordinate[0],
|
||||
lat: importFlow.startCoordinate[1]
|
||||
}
|
||||
const value: UIEventSource<{ lon: number, lat: number }> = new UIEventSource<{ lon: number; lat: number }>(
|
||||
startCoordinate
|
||||
);
|
||||
|
||||
|
||||
async function onConfirm(): Promise<void> {
|
||||
const importedId = await importFlow.onConfirm(
|
||||
value.data,
|
||||
snappedTo.data
|
||||
)
|
||||
state.selectedLayer.setData(targetLayer)
|
||||
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(importedId))
|
||||
}
|
||||
const startCoordinate = {
|
||||
lon: importFlow.startCoordinate[0],
|
||||
lat: importFlow.startCoordinate[1],
|
||||
}
|
||||
const value: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
|
||||
lon: number
|
||||
lat: number
|
||||
}>(startCoordinate)
|
||||
|
||||
async function onConfirm(): Promise<void> {
|
||||
const importedId = await importFlow.onConfirm(value.data, snappedTo.data)
|
||||
state.selectedLayer.setData(targetLayer)
|
||||
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(importedId))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<ImportFlow {importFlow} on:confirm={onConfirm }>
|
||||
<div class="relative" slot="map">
|
||||
<div class="h-32">
|
||||
<NewPointLocationInput coordinate={startCoordinate}
|
||||
{maxSnapDistance}
|
||||
{snapToLayers}
|
||||
{snappedTo}
|
||||
{state}
|
||||
{targetLayer}
|
||||
{value}
|
||||
/>
|
||||
</div>
|
||||
<MapControlButton on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)} cls="absolute bottom-0">
|
||||
<Square3Stack3dIcon class="w-6 h-6"/>
|
||||
</MapControlButton>
|
||||
<ImportFlow {importFlow} on:confirm={onConfirm}>
|
||||
<div class="relative" slot="map">
|
||||
<div class="h-32">
|
||||
<NewPointLocationInput
|
||||
coordinate={startCoordinate}
|
||||
{maxSnapDistance}
|
||||
{snapToLayers}
|
||||
{snappedTo}
|
||||
{state}
|
||||
{targetLayer}
|
||||
{value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MapControlButton
|
||||
on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}
|
||||
cls="absolute bottom-0"
|
||||
>
|
||||
<Square3Stack3dIcon class="w-6 h-6" />
|
||||
</MapControlButton>
|
||||
</div>
|
||||
</ImportFlow>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import ImportFlow, {ImportFlowArguments} from "./ImportFlow";
|
||||
import {SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import {Store, UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {OsmObject, OsmWay} from "../../../Logic/Osm/OsmObject";
|
||||
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
import {Feature, Point} from "geojson";
|
||||
import Maproulette from "../../../Logic/Maproulette";
|
||||
import {GeoOperations} from "../../../Logic/GeoOperations";
|
||||
import {Tag} from "../../../Logic/Tags/Tag";
|
||||
import ImportFlow, { ImportFlowArguments } from "./ImportFlow"
|
||||
import { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { OsmObject, OsmWay } from "../../../Logic/Osm/OsmObject"
|
||||
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
|
||||
import { Feature, Point } from "geojson"
|
||||
import Maproulette from "../../../Logic/Maproulette"
|
||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
|
||||
export interface PointImportFlowArguments extends ImportFlowArguments {
|
||||
max_snap_distance?: string
|
||||
|
@ -19,11 +19,17 @@ export interface PointImportFlowArguments extends ImportFlowArguments {
|
|||
|
||||
export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
||||
public readonly startCoordinate: [number, number]
|
||||
private readonly _originalFeature: Feature<Point>;
|
||||
private readonly _originalFeature: Feature<Point>
|
||||
|
||||
constructor(state: SpecialVisualizationState, originalFeature: Feature<Point>, args: PointImportFlowArguments, tagsToApply: Store<Tag[]>, originalFeatureTags: UIEventSource<Record<string, string>>) {
|
||||
super(state, args, tagsToApply, originalFeatureTags);
|
||||
this._originalFeature = originalFeature;
|
||||
constructor(
|
||||
state: SpecialVisualizationState,
|
||||
originalFeature: Feature<Point>,
|
||||
args: PointImportFlowArguments,
|
||||
tagsToApply: Store<Tag[]>,
|
||||
originalFeatureTags: UIEventSource<Record<string, string>>
|
||||
) {
|
||||
super(state, args, tagsToApply, originalFeatureTags)
|
||||
this._originalFeature = originalFeature
|
||||
this.startCoordinate = GeoOperations.centerpointCoordinates(originalFeature)
|
||||
}
|
||||
|
||||
|
@ -79,8 +85,8 @@ export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
|||
if (this.state.featureSwitchIsTesting.data) {
|
||||
console.log(
|
||||
"Not marking maproulette task " +
|
||||
maproulette_id +
|
||||
" as fixed, because we are in testing mode"
|
||||
maproulette_id +
|
||||
" as fixed, because we are in testing mode"
|
||||
)
|
||||
} else {
|
||||
console.log("Marking maproulette task as fixed")
|
||||
|
@ -92,5 +98,4 @@ export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
|||
this.state.mapProperties.location.setData(location)
|
||||
return newElementAction.newElementId
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,30 +1,29 @@
|
|||
import {SpecialVisualization, SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import {AutoAction} from "../AutoApplyButton";
|
||||
import {Feature, LineString, Polygon} from "geojson";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../../BaseUIElement";
|
||||
import {ImportFlowUtils} from "./ImportFlow";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement";
|
||||
import {FixedUiElement} from "../../Base/FixedUiElement";
|
||||
import WayImportFlow from "./WayImportFlow.svelte";
|
||||
import WayImportFlowState, {WayImportFlowArguments} from "./WayImportFlowState";
|
||||
import {Utils} from "../../../Utils";
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Changes} from "../../../Logic/Osm/Changes";
|
||||
import {IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import { AutoAction } from "../AutoApplyButton"
|
||||
import { Feature, LineString, Polygon } from "geojson"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import { ImportFlowUtils } from "./ImportFlow"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement"
|
||||
import { FixedUiElement } from "../../Base/FixedUiElement"
|
||||
import WayImportFlow from "./WayImportFlow.svelte"
|
||||
import WayImportFlowState, { WayImportFlowArguments } from "./WayImportFlowState"
|
||||
import { Utils } from "../../../Utils"
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Changes } from "../../../Logic/Osm/Changes"
|
||||
import { IndexedFeatureSource } from "../../../Logic/FeatureSource/FeatureSource"
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
|
||||
/**
|
||||
* Wrapper around 'WayImportFlow' to make it a special visualisation
|
||||
*/
|
||||
export default class WayImportButtonViz implements AutoAction, SpecialVisualization {
|
||||
|
||||
|
||||
public readonly funcName: string= "import_way_button"
|
||||
public readonly docs: string = "This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" + ImportFlowUtils.documentationGeneral
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string }[]= [
|
||||
public readonly funcName: string = "import_way_button"
|
||||
public readonly docs: string =
|
||||
"This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" +
|
||||
ImportFlowUtils.documentationGeneral
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string }[] = [
|
||||
...ImportFlowUtils.generalArguments,
|
||||
{
|
||||
name: "snap_to_point_if",
|
||||
|
@ -58,7 +57,13 @@ export default class WayImportButtonViz implements AutoAction, SpecialVisualizat
|
|||
public readonly supportsAutoAction = true
|
||||
public readonly needsNodeDatabase = true
|
||||
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, _: LayerConfig): BaseUIElement {
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
_: LayerConfig
|
||||
): BaseUIElement {
|
||||
const geometry = feature.geometry
|
||||
if (!(geometry.type == "LineString" || geometry.type === "Polygon")) {
|
||||
console.error("Invalid type to import", geometry.type)
|
||||
|
@ -66,18 +71,29 @@ export default class WayImportButtonViz implements AutoAction, SpecialVisualizat
|
|||
}
|
||||
const args: WayImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
|
||||
const importFlow = new WayImportFlowState(state, <Feature<LineString | Polygon>>feature, args, tagsToApply, tagSource)
|
||||
const importFlow = new WayImportFlowState(
|
||||
state,
|
||||
<Feature<LineString | Polygon>>feature,
|
||||
args,
|
||||
tagsToApply,
|
||||
tagSource
|
||||
)
|
||||
return new SvelteUIElement(WayImportFlow, {
|
||||
importFlow
|
||||
importFlow,
|
||||
})
|
||||
}
|
||||
|
||||
public async applyActionOn(feature: Feature, state: {
|
||||
layout: LayoutConfig;
|
||||
changes: Changes;
|
||||
indexedFeatures: IndexedFeatureSource,
|
||||
fullNodeDatabase: FullNodeDatabaseSource
|
||||
}, tagSource: UIEventSource<any>, argument: string[]): Promise<void> {
|
||||
public async applyActionOn(
|
||||
feature: Feature,
|
||||
state: {
|
||||
layout: LayoutConfig
|
||||
changes: Changes
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
fullNodeDatabase: FullNodeDatabaseSource
|
||||
},
|
||||
tagSource: UIEventSource<any>,
|
||||
argument: string[]
|
||||
): Promise<void> {
|
||||
{
|
||||
// Small safety check to prevent duplicate imports
|
||||
const id = tagSource.data.id
|
||||
|
@ -87,20 +103,26 @@ export default class WayImportButtonViz implements AutoAction, SpecialVisualizat
|
|||
ImportFlowUtils.importedIds.add(id)
|
||||
}
|
||||
|
||||
if(feature.geometry.type !== "LineString" && feature.geometry.type !== "Polygon"){
|
||||
if (feature.geometry.type !== "LineString" && feature.geometry.type !== "Polygon") {
|
||||
return
|
||||
}
|
||||
|
||||
const args: WayImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
|
||||
const mergeConfigs = WayImportFlowState.GetMergeConfig(args)
|
||||
const action = WayImportFlowState.CreateAction(<Feature<LineString | Polygon >>feature, args, state, tagsToApply, mergeConfigs)
|
||||
const action = WayImportFlowState.CreateAction(
|
||||
<Feature<LineString | Polygon>>feature,
|
||||
args,
|
||||
state,
|
||||
tagsToApply,
|
||||
mergeConfigs
|
||||
)
|
||||
tagSource.data["_imported"] = "yes"
|
||||
tagSource.ping()
|
||||
await state.changes.applyAction(action)
|
||||
}
|
||||
|
||||
getLayerDependencies(args: string[]){
|
||||
getLayerDependencies(args: string[]) {
|
||||
return ImportFlowUtils.getLayerDependenciesWithSnapOnto(this.args, args)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,61 +1,60 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Can be used for both WayImportFlow and ConflateImportFlow
|
||||
*/
|
||||
import WayImportFlowState from "./WayImportFlowState";
|
||||
import ImportFlow from "./ImportFlow.svelte";
|
||||
import MapControlButton from "../../Base/MapControlButton.svelte";
|
||||
import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {Map as MlMap} from "maplibre-gl"
|
||||
import {MapLibreAdaptor} from "../../Map/MapLibreAdaptor";
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte";
|
||||
import ShowDataLayer from "../../Map/ShowDataLayer";
|
||||
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import {ImportFlowUtils} from "./ImportFlow";
|
||||
import {GeoOperations} from "../../../Logic/GeoOperations";
|
||||
import ConflateImportFlowState from "./ConflateImportFlowState";
|
||||
export let importFlow: WayImportFlowState | ConflateImportFlowState
|
||||
|
||||
const state = importFlow.state
|
||||
const map = new UIEventSource<MlMap>(undefined)
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(importFlow.originalFeature)
|
||||
const mla = new MapLibreAdaptor(map, {
|
||||
allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
rasterLayer : state.mapProperties.rasterLayer,
|
||||
location: new UIEventSource<{lon: number; lat: number}>({lon, lat}),
|
||||
zoom: new UIEventSource<number>(18)
|
||||
})
|
||||
/**
|
||||
* Can be used for both WayImportFlow and ConflateImportFlow
|
||||
*/
|
||||
import WayImportFlowState from "./WayImportFlowState"
|
||||
import ImportFlow from "./ImportFlow.svelte"
|
||||
import MapControlButton from "../../Base/MapControlButton.svelte"
|
||||
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor"
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte"
|
||||
import ShowDataLayer from "../../Map/ShowDataLayer"
|
||||
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { ImportFlowUtils } from "./ImportFlow"
|
||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||
import ConflateImportFlowState from "./ConflateImportFlowState"
|
||||
export let importFlow: WayImportFlowState | ConflateImportFlowState
|
||||
|
||||
// Show all relevant data - including (eventually) the way of which the geometry will be replaced
|
||||
ShowDataLayer.showMultipleLayers(
|
||||
map,
|
||||
new StaticFeatureSource([importFlow.originalFeature]),
|
||||
state.layout.layers,
|
||||
{zoomToFeatures: false}
|
||||
)
|
||||
|
||||
importFlow.GetPreview().then(features => {
|
||||
new ShowDataLayer(map, {
|
||||
zoomToFeatures: false,
|
||||
features,
|
||||
layer: ImportFlowUtils.conflationLayer,
|
||||
})
|
||||
const state = importFlow.state
|
||||
const map = new UIEventSource<MlMap>(undefined)
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(importFlow.originalFeature)
|
||||
const mla = new MapLibreAdaptor(map, {
|
||||
allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
rasterLayer: state.mapProperties.rasterLayer,
|
||||
location: new UIEventSource<{ lon: number; lat: number }>({ lon, lat }),
|
||||
zoom: new UIEventSource<number>(18),
|
||||
})
|
||||
|
||||
// Show all relevant data - including (eventually) the way of which the geometry will be replaced
|
||||
ShowDataLayer.showMultipleLayers(
|
||||
map,
|
||||
new StaticFeatureSource([importFlow.originalFeature]),
|
||||
state.layout.layers,
|
||||
{ zoomToFeatures: false }
|
||||
)
|
||||
|
||||
importFlow.GetPreview().then((features) => {
|
||||
new ShowDataLayer(map, {
|
||||
zoomToFeatures: false,
|
||||
features,
|
||||
layer: ImportFlowUtils.conflationLayer,
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<ImportFlow {importFlow} on:confirm={() => importFlow.onConfirm()}>
|
||||
<div slot="map" class="relative">
|
||||
|
||||
<div class="h-32">
|
||||
<MaplibreMap {map}/>
|
||||
</div>
|
||||
<MapControlButton on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)} cls="absolute bottom-0">
|
||||
<Square3Stack3dIcon class="w-6 h-6"/>
|
||||
</MapControlButton>
|
||||
<div slot="map" class="relative">
|
||||
<div class="h-32">
|
||||
<MaplibreMap {map} />
|
||||
</div>
|
||||
<MapControlButton
|
||||
on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}
|
||||
cls="absolute bottom-0"
|
||||
>
|
||||
<Square3Stack3dIcon class="w-6 h-6" />
|
||||
</MapControlButton>
|
||||
</div>
|
||||
</ImportFlow>
|
||||
|
|
|
@ -1,48 +1,60 @@
|
|||
import ImportFlow, {ImportFlowArguments} from "./ImportFlow";
|
||||
import {SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import {Feature, LineString, Polygon} from "geojson";
|
||||
import {Store, UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {Tag} from "../../../Logic/Tags/Tag";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
import ImportFlow, { ImportFlowArguments } from "./ImportFlow"
|
||||
import { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import { Feature, LineString, Polygon } from "geojson"
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
import CreateWayWithPointReuseAction, {
|
||||
MergePointConfig
|
||||
} from "../../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import {OsmCreateAction, PreviewableAction} from "../../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import {FeatureSource, IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
|
||||
import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Changes} from "../../../Logic/Osm/Changes";
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
MergePointConfig,
|
||||
} from "../../../Logic/Osm/Actions/CreateWayWithPointReuseAction"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import { OsmCreateAction, PreviewableAction } from "../../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import { FeatureSource, IndexedFeatureSource } from "../../../Logic/FeatureSource/FeatureSource"
|
||||
import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Changes } from "../../../Logic/Osm/Changes"
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
|
||||
export interface WayImportFlowArguments extends ImportFlowArguments {
|
||||
export interface WayImportFlowArguments extends ImportFlowArguments {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string,
|
||||
snap_to_layer_max_distance: string,
|
||||
max_move_distance: string,
|
||||
move_osm_point_if,
|
||||
snap_onto_layers: string
|
||||
snap_to_layer_max_distance: string
|
||||
max_move_distance: string
|
||||
move_osm_point_if
|
||||
snap_to_point_if
|
||||
}
|
||||
|
||||
export default class WayImportFlowState extends ImportFlow<WayImportFlowArguments> {
|
||||
public readonly originalFeature: Feature<LineString | Polygon>;
|
||||
public readonly originalFeature: Feature<LineString | Polygon>
|
||||
|
||||
private readonly action: OsmCreateAction & { getPreview?(): Promise<FeatureSource>; }
|
||||
private readonly action: OsmCreateAction & { getPreview?(): Promise<FeatureSource> }
|
||||
|
||||
constructor(state: SpecialVisualizationState, originalFeature: Feature<LineString | Polygon>, args: WayImportFlowArguments, tagsToApply: Store<Tag[]>, originalFeatureTags: UIEventSource<Record<string, string>>) {
|
||||
super(state, args, tagsToApply, originalFeatureTags);
|
||||
this.originalFeature = originalFeature;
|
||||
constructor(
|
||||
state: SpecialVisualizationState,
|
||||
originalFeature: Feature<LineString | Polygon>,
|
||||
args: WayImportFlowArguments,
|
||||
tagsToApply: Store<Tag[]>,
|
||||
originalFeatureTags: UIEventSource<Record<string, string>>
|
||||
) {
|
||||
super(state, args, tagsToApply, originalFeatureTags)
|
||||
this.originalFeature = originalFeature
|
||||
const mergeConfigs = WayImportFlowState.GetMergeConfig(args)
|
||||
this.action = WayImportFlowState.CreateAction(originalFeature, args, state, tagsToApply, mergeConfigs)
|
||||
this.action = WayImportFlowState.CreateAction(
|
||||
originalFeature,
|
||||
args,
|
||||
state,
|
||||
tagsToApply,
|
||||
mergeConfigs
|
||||
)
|
||||
}
|
||||
|
||||
public static CreateAction(
|
||||
feature: Feature<LineString | Polygon>,
|
||||
args: WayImportFlowArguments,
|
||||
state: {
|
||||
layout: LayoutConfig;
|
||||
changes: Changes;
|
||||
indexedFeatures: IndexedFeatureSource,
|
||||
layout: LayoutConfig
|
||||
changes: Changes
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
fullNodeDatabase?: FullNodeDatabaseSource
|
||||
},
|
||||
tagsToApply: Store<Tag[]>,
|
||||
|
@ -124,5 +136,4 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
|
|||
}
|
||||
return this.action.getPreview()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {GeoOperations} from "../../Logic/GeoOperations"
|
||||
import {ImmutableStore, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import {SpecialVisualization, SpecialVisualizationState} from "../SpecialVisualization"
|
||||
import {Feature} from "geojson"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Feature } from "geojson"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement";
|
||||
import MapillaryLink from "../BigComponents/MapillaryLink.svelte";
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MapillaryLink from "../BigComponents/MapillaryLink.svelte"
|
||||
|
||||
export class MapillaryLinkVis implements SpecialVisualization {
|
||||
funcName = "mapillary_link"
|
||||
|
@ -31,9 +31,9 @@ export class MapillaryLinkVis implements SpecialVisualization {
|
|||
return new SvelteUIElement(MapillaryLink, {
|
||||
mapProperties: {
|
||||
lat,
|
||||
lon
|
||||
lon,
|
||||
},
|
||||
zoom: new ImmutableStore(zoom)
|
||||
zoom: new ImmutableStore(zoom),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
import {SubtleButton} from "../Base/SubtleButton"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import {UIEventSource} from "../../Logic/UIEventSource"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import {VariableUiElement} from "../Base/VariableUIElement"
|
||||
import {Translation} from "../i18n/Translation"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import {GeoOperations} from "../../Logic/GeoOperations"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction"
|
||||
import MoveConfig from "../../Models/ThemeConfig/MoveConfig"
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import {And} from "../../Logic/Tags/And"
|
||||
import {Tag} from "../../Logic/Tags/Tag"
|
||||
import {LoginToggle} from "./LoginButton"
|
||||
import {SpecialVisualizationState} from "../SpecialVisualization"
|
||||
import {Feature, Point} from "geojson"
|
||||
import {OsmTags} from "../../Models/OsmFeature"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { LoginToggle } from "./LoginButton"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Feature, Point } from "geojson"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import {MapProperties} from "../../Models/MapProperties"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
|
||||
import Geosearch from "../BigComponents/Geosearch.svelte"
|
||||
import Constants from "../../Models/Constants"
|
||||
import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte";
|
||||
import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
|
||||
interface MoveReason {
|
||||
text: Translation | string
|
||||
|
@ -102,9 +102,11 @@ export default class MoveWizard extends Toggle {
|
|||
})
|
||||
}
|
||||
|
||||
const moveAgainButton = new SubtleButton(Svg.move_svg(), t.inviteToMoveAgain).onClick(() => {
|
||||
currentStep.setData("reason")
|
||||
})
|
||||
const moveAgainButton = new SubtleButton(Svg.move_svg(), t.inviteToMoveAgain).onClick(
|
||||
() => {
|
||||
currentStep.setData("reason")
|
||||
}
|
||||
)
|
||||
|
||||
const selectReason = new Combine(
|
||||
reasons.map((r) =>
|
||||
|
@ -128,26 +130,27 @@ export default class MoveWizard extends Toggle {
|
|||
const mapProperties: Partial<MapProperties> = {
|
||||
minzoom: new UIEventSource(reason.minZoom),
|
||||
zoom: new UIEventSource(reason?.startZoom ?? 16),
|
||||
location: new UIEventSource({lon, lat}),
|
||||
location: new UIEventSource({ lon, lat }),
|
||||
bounds: new UIEventSource(undefined),
|
||||
rasterLayer: state.mapProperties.rasterLayer
|
||||
rasterLayer: state.mapProperties.rasterLayer,
|
||||
}
|
||||
const value = new UIEventSource<{ lon: number; lat: number }>(undefined)
|
||||
const locationInput =
|
||||
new Combine([
|
||||
|
||||
new SvelteUIElement(LocationInput, {
|
||||
mapProperties,
|
||||
value,
|
||||
}).SetClass("w-full h-full"),
|
||||
new SvelteUIElement(OpenBackgroundSelectorButton, {state}).SetClass("absolute bottom-0 left-0")
|
||||
|
||||
]).SetClass("relative w-full h-full")
|
||||
|
||||
const locationInput = new Combine([
|
||||
new SvelteUIElement(LocationInput, {
|
||||
mapProperties,
|
||||
value,
|
||||
}).SetClass("w-full h-full"),
|
||||
new SvelteUIElement(OpenBackgroundSelectorButton, { state }).SetClass(
|
||||
"absolute bottom-0 left-0"
|
||||
),
|
||||
]).SetClass("relative w-full h-full")
|
||||
|
||||
let searchPanel: BaseUIElement = undefined
|
||||
if (reason.includeSearch) {
|
||||
searchPanel = new SvelteUIElement(Geosearch, {bounds: mapProperties.bounds, clearAfterView: false})
|
||||
searchPanel = new SvelteUIElement(Geosearch, {
|
||||
bounds: mapProperties.bounds,
|
||||
clearAfterView: false,
|
||||
})
|
||||
}
|
||||
|
||||
locationInput.SetStyle("height: 17.5rem")
|
||||
|
|
|
@ -12,7 +12,7 @@ import Combine from "../Base/Combine"
|
|||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class PlantNetDetectionViz implements SpecialVisualization {
|
||||
funcName = "plantnet_detection"
|
||||
|
@ -27,7 +27,11 @@ export class PlantNetDetectionViz implements SpecialVisualization {
|
|||
},
|
||||
]
|
||||
|
||||
public constr(state: SpecialVisualizationState, tags: UIEventSource<Record<string, string>>, args: string[]) {
|
||||
public constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
) {
|
||||
let imagePrefixes: string[] = undefined
|
||||
if (args.length > 0) {
|
||||
imagePrefixes = [].concat(...args.map((a) => a.split(",")))
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {Store} from "../../Logic/UIEventSource"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import {LoginToggle} from "./LoginButton";
|
||||
import { LoginToggle } from "./LoginButton"
|
||||
|
||||
export class EditButton extends Toggle {
|
||||
constructor(osmConnection: OsmConnection, onClick: () => void) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue