Chore: reformat all files with prettier

This commit is contained in:
Pieter Vander Vennet 2023-06-14 20:39:36 +02:00
parent 5757ae5dea
commit d008dcb54d
214 changed files with 8926 additions and 8196 deletions

View file

@ -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([

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
/>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(/&gt;/g, ">")
?.replace(/&lt;/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(/&gt;/g, ">")
?.replace(/&lt;/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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(() => {

View file

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

View file

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

View file

@ -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" />

View file

@ -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" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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] ?? "") !== "") {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(",")))

View file

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