forked from MapComplete/MapComplete
Merge master
This commit is contained in:
commit
890816d2dd
424 changed files with 40595 additions and 3354 deletions
|
@ -141,7 +141,7 @@
|
|||
>
|
||||
<Center class=" h-6 w-6" />
|
||||
</div>
|
||||
{:else}
|
||||
{:else if !!$label}
|
||||
<div
|
||||
class={twMerge("soft relative rounded-full border border-black", size)}
|
||||
on:click={() => focusMap()}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
import { twMerge } from "tailwind-merge"
|
||||
import Loading from "../../assets/svg/Loading.svelte"
|
||||
|
||||
export let cls: string = undefined
|
||||
export let cls: string = "flex p-1 pl-2"
|
||||
</script>
|
||||
|
||||
<div class={twMerge("flex p-1 pl-2", cls)}>
|
||||
<div class={cls}>
|
||||
<div class="min-w-6 h-6 w-6 shrink-0 animate-spin self-center">
|
||||
<Loading />
|
||||
</div>
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
import { twJoin } from "tailwind-merge"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
|
||||
/**
|
||||
* A round button with an icon and possible a small text, which hovers above the map
|
||||
*/
|
||||
const dispatch = createEventDispatcher()
|
||||
export let cls = "m-0.5 p-0.5 sm:p-1 md:m-1"
|
||||
export let enabled : Store<boolean> = new ImmutableStore(true)
|
||||
export let arialabel: Translation = undefined
|
||||
</script>
|
||||
|
||||
|
@ -16,7 +18,7 @@
|
|||
on:click={(e) => dispatch("click", e)}
|
||||
on:keydown
|
||||
use:ariaLabel={arialabel}
|
||||
class={twJoin("pointer-events-auto relative h-fit w-fit rounded-full", cls)}
|
||||
class={twJoin("pointer-events-auto relative h-fit w-fit rounded-full", cls, $enabled ? "" : "disabled")}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
|
|
@ -1,31 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Img from "./Img"
|
||||
import { twJoin, twMerge } from "tailwind-merge"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export let imageUrl: string | BaseUIElement = undefined
|
||||
export const message: string | BaseUIElement = undefined
|
||||
export let options: {
|
||||
imgSize?: string
|
||||
extraClasses?: string
|
||||
} = {}
|
||||
|
||||
let imgClasses = twJoin("block justify-center shrink-0 mr-4", options?.imgSize ?? "h-11 w-11")
|
||||
const dispatch = createEventDispatcher<{ click }>()
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={twMerge(options.extraClasses, "secondary no-image-background")}
|
||||
on:click={(e) => dispatch("click", e)}
|
||||
on:click
|
||||
>
|
||||
<slot name="image">
|
||||
{#if imageUrl !== undefined}
|
||||
{#if typeof imageUrl === "string"}
|
||||
<Img src={imageUrl} class={imgClasses} />
|
||||
{/if}
|
||||
{/if}
|
||||
</slot>
|
||||
|
||||
<slot name="image" />
|
||||
<slot name="message" />
|
||||
</button>
|
||||
|
|
|
@ -17,7 +17,8 @@ export default class Title extends BaseUIElement {
|
|||
constructor(embedded: string | BaseUIElement, level: number = 3) {
|
||||
super()
|
||||
if (embedded === undefined) {
|
||||
throw "A title should have some content. Undefined is not allowed"
|
||||
console.warn("A title should have some content. Undefined is not allowed")
|
||||
embedded = ""
|
||||
}
|
||||
if (typeof embedded === "string") {
|
||||
this.title = new FixedUiElement(embedded)
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
/>
|
||||
</If>
|
||||
|
||||
{filteredLayer.layerDef.name}
|
||||
<Tr t={filteredLayer.layerDef.name}/>
|
||||
|
||||
{#if $zoomlevel < layer.minzoom}
|
||||
<span class="alert">
|
||||
|
@ -82,7 +82,7 @@
|
|||
<!-- 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}
|
||||
<Checkbox selected={getBooleanStateFor(filter)}>
|
||||
{filter.options[0].question}
|
||||
<Tr t={filter.options[0].question}/>
|
||||
</Checkbox>
|
||||
{/if}
|
||||
|
||||
|
@ -94,7 +94,7 @@
|
|||
<Dropdown value={getStateFor(filter)}>
|
||||
{#each filter.options as option, i}
|
||||
<option value={i}>
|
||||
{option.question}
|
||||
<Tr t={option.question}/>
|
||||
</option>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { onDestroy } from "svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let filteredLayer: FilteredLayer
|
||||
export let option: FilterConfigOption
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
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"
|
||||
|
@ -15,7 +14,6 @@
|
|||
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Move_arrows from "../../assets/svg/Move_arrows.svelte"
|
||||
|
||||
/**
|
||||
|
@ -53,9 +51,6 @@
|
|||
lat: number
|
||||
}>(undefined)
|
||||
|
||||
const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>()
|
||||
|
||||
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
|
||||
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let initialMapProperties: Partial<MapProperties> & { location } = {
|
||||
zoom: new UIEventSource<number>(19),
|
||||
|
@ -73,6 +68,7 @@
|
|||
minzoom: new UIEventSource<number>(18),
|
||||
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
|
||||
}
|
||||
state?.showCurrentLocationOn(map)
|
||||
|
||||
if (targetLayer) {
|
||||
const featuresForLayer = state.perLayer.get(targetLayer.id)
|
||||
|
@ -120,7 +116,7 @@
|
|||
|
||||
<LocationInput
|
||||
{map}
|
||||
on:click={(data) => dispatch("click", data)}
|
||||
on:click
|
||||
mapProperties={initialMapProperties}
|
||||
value={preciseLocation}
|
||||
initialCoordinate={coordinate}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* A mapcontrol button which allows the user to select a different background.
|
||||
* Even though the component is very small, it gets it's own class as it is often reused
|
||||
* Even though the component is very small, it gets its own class as it is often reused
|
||||
*/
|
||||
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Translations from "../i18n/Translations"
|
||||
import MapControlButton from "../Base/MapControlButton.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import StyleLoadingIndicator from "../Map/StyleLoadingIndicator.svelte"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let state: ThemeViewState
|
||||
export let map: Store<MlMap> = undefined
|
||||
export let hideTooltip = false
|
||||
</script>
|
||||
|
||||
|
@ -17,7 +22,10 @@
|
|||
arialabel={Translations.t.general.labels.background}
|
||||
on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}
|
||||
>
|
||||
<Square3Stack3dIcon class="h-6 w-6" />
|
||||
|
||||
<StyleLoadingIndicator map={map ?? state.map} rasterLayer={state.mapProperties.rasterLayer} >
|
||||
<Square3Stack3dIcon class="h-6 w-6" />
|
||||
</StyleLoadingIndicator>
|
||||
{#if !hideTooltip}
|
||||
<Tr cls="mx-2" t={Translations.t.general.backgroundSwitch} />
|
||||
{/if}
|
||||
|
|
|
@ -13,20 +13,25 @@
|
|||
export let layer: LayerConfig
|
||||
export let selectedElement: Feature
|
||||
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(
|
||||
selectedElement.properties.id
|
||||
selectedElement.properties.id,
|
||||
)
|
||||
$: {
|
||||
tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
}
|
||||
|
||||
let isTesting = state.featureSwitchIsTesting
|
||||
let isDebugging = state.featureSwitches.featureSwitchIsDebugging
|
||||
|
||||
let metatags: Store<Record<string, string>> = state.userRelatedState.preferencesAsTags
|
||||
</script>
|
||||
|
||||
{#if $tags._deleted === "yes"}
|
||||
<Tr t={Translations.t.delete.isDeleted} />
|
||||
{:else}
|
||||
<div class="low-interaction flex border-b-2 border-black px-3 drop-shadow-md">
|
||||
<div class="h-fit w-full overflow-auto sm:p-2" style="max-height: 20vh;">
|
||||
<div class="low-interaction flex border-b-2 border-black px-3 drop-shadow-md">
|
||||
<div class="h-fit w-full overflow-auto sm:p-2" style="max-height: 20vh;">
|
||||
{#if $tags._deleted === "yes"}
|
||||
<h3 class="p-4">
|
||||
<Tr t={Translations.t.delete.deletedTitle} />
|
||||
</h3>
|
||||
{:else}
|
||||
<div class="flex h-full w-full flex-grow flex-col">
|
||||
<!-- Title element and title icons-->
|
||||
<h3 class="m-0">
|
||||
|
@ -34,12 +39,11 @@
|
|||
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} />
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 pt-0.5 sm:pt-1"
|
||||
>
|
||||
{#each layer.titleIcons as titleIconConfig}
|
||||
{#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...$metatags, ...$tags } ) ?? true) && titleIconConfig.IsKnown($tags)}
|
||||
{#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...$metatags, ...$tags }) ?? true) && titleIconConfig.IsKnown($tags)}
|
||||
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
|
||||
<TagRenderingAnswer
|
||||
config={titleIconConfig}
|
||||
|
@ -52,23 +56,27 @@
|
|||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if $isTesting || $isDebugging}
|
||||
<a class="subtle" href="https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Layers/{layer.id}.md"
|
||||
target="_blank" rel="noreferrer noopener ">{layer.id}</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={() => state.selectedElement.setData(undefined)}
|
||||
use:ariaLabel={Translations.t.general.backToMap}
|
||||
class="mt-2 h-fit shrink-0 rounded-full border-none p-0"
|
||||
style="border: 0 !important; padding: 0 !important;"
|
||||
>
|
||||
<XCircleIcon aria-hidden={true} class="h-8 w-8" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="mt-2 h-fit shrink-0 rounded-full border-none p-0"
|
||||
on:click={() => state.selectedElement.setData(undefined)}
|
||||
style="border: 0 !important; padding: 0 !important;"
|
||||
use:ariaLabel={Translations.t.general.backToMap}
|
||||
>
|
||||
<XCircleIcon aria-hidden={true} class="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.title-icons a) {
|
||||
display: block !important;
|
||||
}
|
||||
:global(.title-icons a) {
|
||||
display: block !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,17 +8,21 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import Delete_icon from "../../assets/svg/Delete_icon.svelte"
|
||||
import BackButton from "../Base/BackButton.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig
|
||||
export let selectedElement: Feature
|
||||
export let highlightedRendering: UIEventSource<string> = undefined
|
||||
|
||||
export let tags: UIEventSource<Record<string, string>> = state?.featureProperties?.getStore(
|
||||
selectedElement.properties.id
|
||||
)
|
||||
|
||||
|
||||
|
||||
let layer: LayerConfig = selectedElement.properties.id === "settings" ? UserRelatedState.usersettingsConfig : state.layout.getMatchingLayer(tags.data)
|
||||
|
||||
|
||||
let stillMatches = tags.map(tags => !layer?.source?.osmTags || layer.source.osmTags?.matchesProperties(tags))
|
||||
|
||||
let _metatags: Record<string, string>
|
||||
|
@ -27,7 +31,7 @@
|
|||
onDestroy(
|
||||
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
|
||||
_metatags = tags
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -36,22 +40,26 @@
|
|||
(config) =>
|
||||
(config.condition?.matchesProperties(tgs) ?? true) &&
|
||||
(config.metacondition?.matchesProperties({ ...tgs, ..._metatags }) ?? true) &&
|
||||
config.IsKnown(tgs)
|
||||
)
|
||||
config.IsKnown(tgs),
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if !$stillMatches}
|
||||
<div class="alert" aria-live="assertive">
|
||||
<Tr t={Translations.t.delete.isChanged}/>
|
||||
<div class="alert" aria-live="assertive">
|
||||
<Tr t={Translations.t.delete.isChanged} />
|
||||
</div>
|
||||
{:else if $tags._deleted === "yes"}
|
||||
<div aria-live="assertive">
|
||||
<Tr t={Translations.t.delete.isDeleted} />
|
||||
<div class="flex w-full flex-col p-2">
|
||||
<div aria-live="assertive" class="alert flex items-center justify-center self-stretch">
|
||||
<Delete_icon class="w-8 h-8 m-2" />
|
||||
<Tr t={Translations.t.delete.isDeleted} />
|
||||
</div>
|
||||
<BackButton clss="self-stretch mt-4" on:click={() => state.selectedElement.setData(undefined)}>
|
||||
<Tr t={Translations.t.general.returnToTheMap} />
|
||||
</BackButton>
|
||||
|
||||
</div>
|
||||
<button class="w-full" on:click={() => state.selectedElement.setData(undefined)}>
|
||||
<Tr t={Translations.t.general.returnToTheMap} />
|
||||
</button>
|
||||
{:else}
|
||||
<div
|
||||
class="selected-element-view flex h-full w-full flex-col gap-y-2 overflow-y-auto p-1 px-4"
|
||||
|
|
|
@ -11,10 +11,11 @@
|
|||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Utils } from "../../Utils"
|
||||
import Svg from "../../Svg"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import { DocumentDuplicateIcon } from "@rgossiaux/svelte-heroicons/outline"
|
||||
import Share from "../../assets/svg/Share.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Img from "../Base/Img"
|
||||
import Qr from "../../Utils/Qr"
|
||||
|
||||
export let state: ThemeViewState
|
||||
const tr = Translations.t.general.sharescreen
|
||||
|
@ -69,22 +70,32 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Tr t={tr.intro} />
|
||||
<div class="flex">
|
||||
{#if typeof navigator?.share === "function"}
|
||||
<button class="h-8 w-8 shrink-0 p-1" on:click={shareCurrentLink}>
|
||||
<Share />
|
||||
</button>
|
||||
{/if}
|
||||
{#if navigator.clipboard !== undefined}
|
||||
<button class="no-image-background h-8 w-8 shrink-0 p-1" on:click={copyCurrentLink}>
|
||||
<DocumentDuplicateIcon />
|
||||
</button>
|
||||
{/if}
|
||||
<div class="literal-code" on:click={(e) => Utils.selectTextIn(e.target)}>
|
||||
{linkToShare}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between items-start">
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
||||
<Tr t={tr.intro} />
|
||||
<div class="flex">
|
||||
{#if typeof navigator?.share === "function"}
|
||||
<button class="h-8 w-8 shrink-0 p-1" on:click={shareCurrentLink}>
|
||||
<Share />
|
||||
</button>
|
||||
{/if}
|
||||
{#if navigator.clipboard !== undefined}
|
||||
<button class="no-image-background h-8 w-8 shrink-0 p-1" on:click={copyCurrentLink}>
|
||||
<DocumentDuplicateIcon />
|
||||
</button>
|
||||
{/if}
|
||||
<div class="literal-code" on:click={(e) => Utils.selectTextIn(e.target)}>
|
||||
{linkToShare}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToSvelte construct={() => new Img(new Qr(linkToShare).toImageElement(125)).SetStyle(
|
||||
"width: 125px"
|
||||
)} />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
|
@ -95,29 +106,31 @@
|
|||
|
||||
<Tr t={tr.embedIntro} />
|
||||
|
||||
<div class="link-underline my-1 flex flex-col">
|
||||
<label>
|
||||
<input bind:checked={showWelcomeMessage} type="checkbox" />
|
||||
<Tr t={tr.fsWelcomeMessage} />
|
||||
</label>
|
||||
<div class="flex flex-col interactive p-1">
|
||||
|
||||
<label>
|
||||
<input bind:checked={enableLogin} type="checkbox" />
|
||||
<Tr t={tr.fsUserbadge} />
|
||||
</label>
|
||||
</div>
|
||||
<div class="literal-code m-1">
|
||||
<span class="literal-code iframe-code-block"> <br />
|
||||
<iframe src="{linkToShare}"
|
||||
<br />
|
||||
allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px"
|
||||
<br />
|
||||
title="{state.layout.title?.txt ?? "MapComplete"} with MapComplete">
|
||||
<br />
|
||||
</iframe>
|
||||
<br />
|
||||
</span>
|
||||
</div>
|
||||
<div class="link-underline my-1 flex flex-col">
|
||||
<label>
|
||||
<input bind:checked={showWelcomeMessage} type="checkbox" id="share_show_welcome" />
|
||||
<Tr t={tr.fsWelcomeMessage} />
|
||||
</label>
|
||||
|
||||
<div class="literal-code m-1">
|
||||
<span class="literal-code iframe-code-block"> <br />
|
||||
<iframe src="${url}"
|
||||
<br />
|
||||
allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px"
|
||||
<br />
|
||||
title="${state.layout.title?.txt ?? "MapComplete"} with MapComplete">
|
||||
<br />
|
||||
</iframe>
|
||||
<br />
|
||||
</span>
|
||||
<label>
|
||||
<input bind:checked={enableLogin} type="checkbox" id="share_enable_login"/>
|
||||
<Tr t={tr.fsUserbadge} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Tr t={tr.documentation} cls="link-underline" />
|
||||
<Tr cls="link-underline" t={tr.documentation} />
|
||||
</div>
|
||||
|
|
|
@ -17,14 +17,17 @@ export default class StatisticsForLayerPanel extends VariableUiElement {
|
|||
return new Loading("Loading data")
|
||||
}
|
||||
if (features.length === 0) {
|
||||
return "No elements in view"
|
||||
return new Combine([
|
||||
"No elements in view for layer ",
|
||||
layer.id
|
||||
]).SetClass("block")
|
||||
}
|
||||
const els: BaseUIElement[] = []
|
||||
const featuresForLayer = features
|
||||
if (featuresForLayer.length === 0) {
|
||||
return
|
||||
}
|
||||
els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8"))
|
||||
els.push(new Title(layer.name, 1).SetClass("mt-8"))
|
||||
|
||||
const layerStats = []
|
||||
for (const tagRendering of layer?.tagRenderings ?? []) {
|
||||
|
|
|
@ -8,7 +8,9 @@ import { OsmFeature } from "../../Models/OsmFeature"
|
|||
|
||||
export interface TagRenderingChartOptions {
|
||||
groupToOtherCutoff?: 3 | number
|
||||
sort?: boolean
|
||||
sort?: boolean,
|
||||
hideUnkown?: boolean,
|
||||
hideNotApplicable?: boolean
|
||||
}
|
||||
|
||||
export class StackedRenderingChart extends ChartJs {
|
||||
|
@ -19,12 +21,16 @@ export class StackedRenderingChart extends ChartJs {
|
|||
period: "day" | "month"
|
||||
groupToOtherCutoff?: 3 | number
|
||||
// If given, take the sum of these fields to get the feature weight
|
||||
sumFields?: string[]
|
||||
sumFields?: string[],
|
||||
hideUnknown?: boolean,
|
||||
hideNotApplicable?: boolean
|
||||
}
|
||||
) {
|
||||
const { labels, data } = TagRenderingChart.extractDataAndLabels(tr, features, {
|
||||
sort: true,
|
||||
groupToOtherCutoff: options?.groupToOtherCutoff,
|
||||
hideNotApplicable: options?.hideNotApplicable,
|
||||
hideUnkown: options?.hideUnknown
|
||||
})
|
||||
if (labels === undefined || data === undefined) {
|
||||
console.error(
|
||||
|
@ -36,7 +42,6 @@ export class StackedRenderingChart extends ChartJs {
|
|||
)
|
||||
throw "No labels or data given..."
|
||||
}
|
||||
// labels: ["cyclofix", "buurtnatuur", ...]; data : [ ["cyclofix-changeset", "cyclofix-changeset", ...], ["buurtnatuur-cs", "buurtnatuur-cs"], ... ]
|
||||
|
||||
for (let i = labels.length; i >= 0; i--) {
|
||||
if (data[i]?.length != 0) {
|
||||
|
@ -116,13 +121,13 @@ export class StackedRenderingChart extends ChartJs {
|
|||
datasets.push({
|
||||
data: countsPerDay,
|
||||
backgroundColor,
|
||||
label,
|
||||
label
|
||||
})
|
||||
}
|
||||
|
||||
const perDayData = {
|
||||
labels: trimmedDays,
|
||||
datasets,
|
||||
datasets
|
||||
}
|
||||
|
||||
const config = <ChartConfiguration>{
|
||||
|
@ -131,17 +136,17 @@ export class StackedRenderingChart extends ChartJs {
|
|||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false,
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
stacked: true
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
stacked: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super(config)
|
||||
}
|
||||
|
@ -194,7 +199,7 @@ export default class TagRenderingChart extends Combine {
|
|||
"rgba(255, 206, 86, 0.2)",
|
||||
"rgba(75, 192, 192, 0.2)",
|
||||
"rgba(153, 102, 255, 0.2)",
|
||||
"rgba(255, 159, 64, 0.2)",
|
||||
"rgba(255, 159, 64, 0.2)"
|
||||
]
|
||||
|
||||
public static readonly borderColors = [
|
||||
|
@ -203,7 +208,7 @@ export default class TagRenderingChart extends Combine {
|
|||
"rgba(255, 206, 86, 1)",
|
||||
"rgba(75, 192, 192, 1)",
|
||||
"rgba(153, 102, 255, 1)",
|
||||
"rgba(255, 159, 64, 1)",
|
||||
"rgba(255, 159, 64, 1)"
|
||||
]
|
||||
|
||||
/**
|
||||
|
@ -239,12 +244,12 @@ export default class TagRenderingChart extends Combine {
|
|||
const borderColor = [
|
||||
TagRenderingChart.unkownBorderColor,
|
||||
TagRenderingChart.otherBorderColor,
|
||||
TagRenderingChart.notApplicableBorderColor,
|
||||
TagRenderingChart.notApplicableBorderColor
|
||||
]
|
||||
const backgroundColor = [
|
||||
TagRenderingChart.unkownColor,
|
||||
TagRenderingChart.otherColor,
|
||||
TagRenderingChart.notApplicableColor,
|
||||
TagRenderingChart.notApplicableColor
|
||||
]
|
||||
|
||||
while (borderColor.length < data.length) {
|
||||
|
@ -276,17 +281,17 @@ export default class TagRenderingChart extends Combine {
|
|||
backgroundColor,
|
||||
borderColor,
|
||||
borderWidth: 1,
|
||||
label: undefined,
|
||||
},
|
||||
],
|
||||
label: undefined
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: !barchartMode,
|
||||
},
|
||||
},
|
||||
},
|
||||
display: !barchartMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chart = new ChartJs(config).SetClass(options?.chartclasses ?? "w-32 h-32")
|
||||
|
@ -297,7 +302,7 @@ export default class TagRenderingChart extends Combine {
|
|||
|
||||
super([
|
||||
options?.includeTitle ? tagRendering.question.Clone() ?? tagRendering.id : undefined,
|
||||
chart,
|
||||
chart
|
||||
])
|
||||
|
||||
this.SetClass("block")
|
||||
|
@ -386,20 +391,26 @@ export default class TagRenderingChart extends Combine {
|
|||
}
|
||||
}
|
||||
|
||||
const labels = [
|
||||
"Unknown",
|
||||
"Other",
|
||||
"Not applicable",
|
||||
const labels = []
|
||||
const data: T[][] = []
|
||||
|
||||
if (!options.hideUnkown) {
|
||||
data.push(unknownCount)
|
||||
labels.push("Unknown")
|
||||
}
|
||||
data.push(otherGrouped)
|
||||
labels.push("Other")
|
||||
if (!options.hideNotApplicable) {
|
||||
data.push(notApplicable)
|
||||
labels.push(
|
||||
"Not applicable"
|
||||
)
|
||||
}
|
||||
data.push(...categoryCounts,
|
||||
...otherData)
|
||||
labels.push(
|
||||
...(mappings?.map((m) => m.then.txt) ?? []),
|
||||
...otherLabels,
|
||||
]
|
||||
const data: T[][] = [
|
||||
unknownCount,
|
||||
otherGrouped,
|
||||
notApplicable,
|
||||
...categoryCounts,
|
||||
...otherData,
|
||||
]
|
||||
...otherLabels)
|
||||
|
||||
return { labels, data }
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import Constants from "../../Models/Constants"
|
||||
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import SubtleLink from "../Base/SubtleLink.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
|
||||
|
@ -86,8 +85,10 @@
|
|||
</script>
|
||||
|
||||
{#if theme.id !== personal.id || $unlockedPersonal}
|
||||
<SubtleLink href={$href} options={{ extraClasses: "w-full" }}>
|
||||
<img slot="image" src={theme.icon} class="m-1 mr-2 block h-11 w-11 sm:m-2 sm:mr-4" alt="" />
|
||||
<a
|
||||
class={"w-full button text-ellipsis"}
|
||||
href={$href}
|
||||
> <img src={theme.icon} class="m-1 mr-2 block h-11 w-11 sm:m-2 sm:mr-4" alt="" />
|
||||
<span class="flex flex-col overflow-hidden text-ellipsis">
|
||||
<Tr t={title} />
|
||||
|
||||
|
@ -96,6 +97,5 @@
|
|||
<Tr t={Translations.t.general.morescreen.enterToOpen} />
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</SubtleLink>
|
||||
</span></a>
|
||||
{/if}
|
||||
|
|
|
@ -23,18 +23,20 @@
|
|||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import type { Feature, LineString, Point } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import SmallZoomButtons from "../Map/SmallZoomButtons.svelte"
|
||||
|
||||
const splitpoint_style = new LayerConfig(
|
||||
<LayerConfigJson>split_point,
|
||||
"(BUILTIN) SplitRoadWizard.ts",
|
||||
true
|
||||
) as const
|
||||
true,
|
||||
)
|
||||
|
||||
const splitroad_style = new LayerConfig(
|
||||
<LayerConfigJson>split_road,
|
||||
"(BUILTIN) SplitRoadWizard.ts",
|
||||
true
|
||||
) as const
|
||||
true,
|
||||
)
|
||||
|
||||
/**
|
||||
* The way to focus on
|
||||
|
@ -45,6 +47,7 @@
|
|||
* A default is given
|
||||
*/
|
||||
export let layer: LayerConfig = splitroad_style
|
||||
export let state: SpecialVisualizationState | undefined = undefined
|
||||
/**
|
||||
* Optional: use these properties to set e.g. background layer
|
||||
*/
|
||||
|
@ -58,6 +61,7 @@
|
|||
adaptor.bounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
|
||||
state?.showCurrentLocationOn(map)
|
||||
new ShowDataLayer(map, {
|
||||
features: new StaticFeatureSource([wayGeojson]),
|
||||
drawMarkers: false,
|
||||
|
@ -101,6 +105,7 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="h-full w-full">
|
||||
<MaplibreMap {map} />
|
||||
<div class="h-full w-full relative">
|
||||
<MaplibreMap {map} mapProperties={adaptor} />
|
||||
<SmallZoomButtons {adaptor} />
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
import { SvgToPdf } from "../../Utils/svgToPdf"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import DownloadPdf from "./DownloadPdf.svelte"
|
||||
import { PngMapCreator } from "../../Utils/pngMapCreator"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import ValidatedInput from "../InputElement/ValidatedInput.svelte"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
|
||||
export let state: ThemeViewState
|
||||
let isLoading = state.dataIsLoading
|
||||
|
@ -34,9 +38,21 @@
|
|||
mapExtent: state.mapProperties.bounds.data,
|
||||
width: maindiv.offsetWidth,
|
||||
height: maindiv.offsetHeight,
|
||||
noSelfIntersectingLines,
|
||||
noSelfIntersectingLines
|
||||
})
|
||||
}
|
||||
|
||||
let customWidth = LocalStorageSource.Get("custom-png-width", "20")
|
||||
let customHeight = LocalStorageSource.Get("custom-png-height", "20")
|
||||
|
||||
async function offerCustomPng(): Promise<Blob> {
|
||||
console.log("Creating a custom size png with dimensions", customWidth.data + "mm *", customHeight.data + "mm")
|
||||
const creator = new PngMapCreator(state, {
|
||||
height: Number(customHeight.data), width: Number(customWidth.data)
|
||||
})
|
||||
return await creator.CreatePng("belowmap")
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if $isLoading}
|
||||
|
@ -107,5 +123,26 @@
|
|||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="low-interaction p-2 mt-4">
|
||||
<h3 class="m-0 mb-2">
|
||||
<Tr t={t.custom.title}/></h3>
|
||||
<div class="flex">
|
||||
<Tr t={t.custom.width} />
|
||||
<ValidatedInput {state} type="pnat" value={customWidth} />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<Tr t={t.custom.height} />
|
||||
<ValidatedInput {state} type="pnat" value={customHeight} />
|
||||
</div>
|
||||
<DownloadButton
|
||||
mainText={t.custom.download.Subs({width: $customWidth, height: $customHeight})}
|
||||
helperText={t.custom.downloadHelper}
|
||||
extension="png"
|
||||
construct={() => offerCustomPng()}
|
||||
{state}
|
||||
mimetype="image/png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tr cls="link-underline" t={t.licenseInfo} />
|
||||
{/if}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
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.maptilerDefaultLayer
|
||||
const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.defaultBackgroundLayer
|
||||
const creator = new SvgToPdf(title, templates, {
|
||||
state,
|
||||
freeComponentId: "belowmap",
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
*/
|
||||
export let image: ProvidedImage
|
||||
let license: Store<LicenseInfo> = UIEventSource.FromPromise(
|
||||
image.provider?.DownloadAttribution(image.url)
|
||||
image.provider?.DownloadAttribution(image)
|
||||
)
|
||||
let icon = image.provider?.SourceIcon(image.id)?.SetClass("block h-8 w-8 pr-2")
|
||||
</script>
|
||||
|
@ -38,26 +38,28 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-between">
|
||||
<div class="flex justify-between w-full gap-x-1">
|
||||
{#if $license.license !== undefined || $license.licenseShortName !== undefined}
|
||||
<div>
|
||||
{$license?.license ?? $license?.licenseShortName}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $license.date}
|
||||
<div>
|
||||
{$license.date.toLocaleDateString()}
|
||||
{#if $license.views}
|
||||
<div class="flex justify-around self-center">
|
||||
<EyeIcon class="h-4 w-4 pr-1" />
|
||||
{$license.views}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $license.views}
|
||||
<div class="flex justify-around self-center">
|
||||
<EyeIcon class="h-4 w-4 pr-1" />
|
||||
{$license.views}
|
||||
{#if $license.date}
|
||||
<div>
|
||||
{$license.date.toLocaleDateString()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
29
src/UI/Image/UploadFailedMessage.svelte
Normal file
29
src/UI/Image/UploadFailedMessage.svelte
Normal file
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
|
||||
export let failed: number
|
||||
const t = Translations.t.image
|
||||
|
||||
</script>
|
||||
<div class="alert flex">
|
||||
|
||||
<div class="flex flex-col items-start">
|
||||
{#if failed === 1}
|
||||
<Tr t={t.upload.one.failed} />
|
||||
{:else}
|
||||
<Tr t={t.upload.multiple.someFailed.Subs({ count: failed })} />
|
||||
{/if}
|
||||
<Tr cls="text-normal" t={t.upload.failReasons} />
|
||||
<Tr cls="text-xs" t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
<button
|
||||
class="mt-2 h-fit shrink-0 rounded-full border-none p-0 pointer-events-auto"
|
||||
on:click
|
||||
style="border: 0 !important; padding: 0 !important;"
|
||||
>
|
||||
<XCircleIcon aria-hidden={true} class="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
|
@ -11,6 +11,8 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import UploadFailedMessage from "./UploadFailedMessage.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: Store<OsmTags> = undefined
|
||||
|
@ -22,31 +24,40 @@
|
|||
const { uploadStarted, uploadFinished, retried, failed } =
|
||||
state.imageUploadManager.getCountsFor(featureId)
|
||||
const t = Translations.t.image
|
||||
const debugging = state.featureSwitches.featureSwitchIsDebugging
|
||||
let dismissed = 0
|
||||
</script>
|
||||
|
||||
{#if $uploadStarted === 1}
|
||||
{#if $debugging}
|
||||
<div class="low-interaction">Started {$uploadStarted} Done {$uploadFinished} Retry {$retried} Err {$failed}</div>
|
||||
{/if}
|
||||
{#if dismissed === $uploadStarted}
|
||||
<!-- We don't show anything as we ignore this number of failed items-->
|
||||
{:else if $uploadStarted === 1}
|
||||
{#if $uploadFinished === 1}
|
||||
{#if showThankYou}
|
||||
<Tr cls="thanks" t={t.upload.one.done} />
|
||||
{/if}
|
||||
{:else if $failed === 1}
|
||||
<div class="alert flex flex-col">
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
<UploadFailedMessage failed={$failed} on:click={() => dismissed = $failed}/>
|
||||
{:else if $retried === 1}
|
||||
<Loading cls="alert">
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
<Tr t={t.upload.one.retrying} />
|
||||
</Loading>
|
||||
</div>
|
||||
{:else}
|
||||
<Loading cls="alert">
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
<Tr t={t.upload.one.uploading} />
|
||||
</Loading>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if $uploadStarted > 1}
|
||||
{#if $uploadFinished + $failed === $uploadStarted && $uploadFinished > 0}
|
||||
{#if showThankYou}
|
||||
{#if $uploadFinished + $failed === $uploadStarted}
|
||||
{#if $uploadFinished === 0}
|
||||
<!-- pass -->
|
||||
{:else if showThankYou}
|
||||
<Tr cls="thanks" t={t.upload.multiple.done.Subs({ count: $uploadFinished })} />
|
||||
{/if}
|
||||
{:else if $uploadFinished === 0}
|
||||
|
@ -64,14 +75,7 @@
|
|||
</Loading>
|
||||
{/if}
|
||||
{#if $failed > 0}
|
||||
<div class="alert flex flex-col">
|
||||
{#if $failed === 1}
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
{:else}
|
||||
<Tr cls="self-center" t={t.upload.multiple.someFailed.Subs({ count: $failed })} />
|
||||
{/if}
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
|
||||
<UploadFailedMessage failed={$failed} on:click={() => dismissed = $failed}/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
on:touchstart={(e) => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}
|
||||
>
|
||||
<div class="absolute top-0 left-0 h-full w-full cursor-pointer">
|
||||
<MaplibreMap attribution={false} {map} />
|
||||
<MaplibreMap mapProperties={mla} {map} />
|
||||
</div>
|
||||
|
||||
<div bind:this={directionElem} class="absolute top-0 left-0 h-full w-full">
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import Move_arrows from "../../../assets/svg/Move_arrows.svelte"
|
||||
import SmallZoomButtons from "../../Map/SmallZoomButtons.svelte"
|
||||
|
||||
/**
|
||||
* A visualisation to pick a location on a map background
|
||||
|
@ -83,7 +84,7 @@
|
|||
|
||||
<div class="min-h-32 relative h-full cursor-pointer overflow-hidden">
|
||||
<div class="absolute top-0 left-0 h-full w-full cursor-pointer">
|
||||
<MaplibreMap center={{ lng: initialCoordinate.lon, lat: initialCoordinate.lat }} {map} />
|
||||
<MaplibreMap center={{ lng: initialCoordinate.lon, lat: initialCoordinate.lat }} {map} mapProperties={mla}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -95,4 +96,5 @@
|
|||
</div>
|
||||
|
||||
<DragInvitation hideSignal={mla.location} />
|
||||
<SmallZoomButtons adaptor={mla} />
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import LanguageUtils from "../../../Utils/LanguageUtils"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import ValidatedInput from "../ValidatedInput.svelte"
|
||||
import { del } from "idb-keyval"
|
||||
|
||||
export let value: UIEventSource<Record<string, string>> = new UIEventSource<
|
||||
Record<string, string>
|
||||
|
@ -18,14 +19,25 @@
|
|||
const allLanguages: string[] = LanguageUtils.usedLanguagesSorted
|
||||
let currentLang = new UIEventSource("en")
|
||||
const currentVal = new UIEventSource<string>("")
|
||||
/**
|
||||
* Mostly the same as currentVal, but might be the empty string as well
|
||||
*/
|
||||
const currentValRaw = new UIEventSource<string>("")
|
||||
|
||||
let dispatch = createEventDispatcher<{ submit }>()
|
||||
|
||||
function update() {
|
||||
const v = currentVal.data
|
||||
let v = currentValRaw.data
|
||||
const l = currentLang.data
|
||||
console.log("Updating translation input for value", v, " and language", l)
|
||||
if (<any>translations.data === "" || translations.data === undefined) {
|
||||
translations.data = {}
|
||||
}
|
||||
if (v === "") {
|
||||
delete translations.data[l]
|
||||
translations.ping()
|
||||
return
|
||||
}
|
||||
if (translations.data[l] === v) {
|
||||
return
|
||||
}
|
||||
|
@ -39,35 +51,52 @@
|
|||
translations.data = {}
|
||||
}
|
||||
translations.data[currentLang] = translations.data[currentLang] ?? ""
|
||||
currentVal.setData(translations.data[currentLang])
|
||||
if (translations.data[currentLang] === "") {
|
||||
delete translations.data[currentLang]
|
||||
}
|
||||
currentVal.setData(translations.data[currentLang] ?? "")
|
||||
currentValRaw.setData(translations.data[currentLang])
|
||||
})
|
||||
)
|
||||
|
||||
onDestroy(
|
||||
currentVal.addCallbackAndRunD(() => {
|
||||
currentValRaw.addCallbackAndRunD(() => {
|
||||
update()
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="interactive m-1 mt-2 flex space-x-1 font-bold">
|
||||
</script>
|
||||
<div class="flex flex-col gap-y-1">
|
||||
<div class="interactive m-1 mt-2 flex space-x-1 font-bold">
|
||||
<span>
|
||||
{prefix}
|
||||
</span>
|
||||
<select bind:value={$currentLang}>
|
||||
{#each allLanguages as language}
|
||||
<option value={language}>
|
||||
{language}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<ValidatedInput
|
||||
type="string"
|
||||
cls="w-full"
|
||||
value={currentVal}
|
||||
on:submit={() => dispatch("submit")}
|
||||
/>
|
||||
<span>
|
||||
<select bind:value={$currentLang}>
|
||||
{#each allLanguages as language}
|
||||
<option value={language}>
|
||||
{language}
|
||||
{#if $translations[language] !== undefined}
|
||||
*
|
||||
{/if}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<ValidatedInput
|
||||
type="string"
|
||||
cls="w-full"
|
||||
value={currentVal}
|
||||
unvalidatedText={currentValRaw}
|
||||
on:submit={() => dispatch("submit")}
|
||||
/>
|
||||
<span>
|
||||
{postfix}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
You have currently set translations for
|
||||
<ul>
|
||||
{#each Object.keys($translations) as l}
|
||||
<li><button class="small" on:click={() => currentLang.setData(l)}><b>{l}:</b> {$translations[l]}</button></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -60,6 +60,11 @@ export default class InputHelpers {
|
|||
if (!mapProperties.zoom) {
|
||||
mapProperties = { ...mapProperties, zoom: new UIEventSource<number>(zoom) }
|
||||
}
|
||||
if (!mapProperties.rasterLayer) {
|
||||
/* mapProperties = {
|
||||
...mapProperties, rasterLayer: properties?.mapProperties?.rasterLayer
|
||||
}*/
|
||||
}
|
||||
return mapProperties
|
||||
}
|
||||
|
||||
|
@ -69,11 +74,10 @@ export default class InputHelpers {
|
|||
) {
|
||||
const inputHelperOptions = props
|
||||
const args = inputHelperOptions.args ?? []
|
||||
const searchKey = <string>args[0] ?? "name"
|
||||
const searchKey: string = <string>args[0] ?? "name"
|
||||
|
||||
const searchFor = <string>(
|
||||
(inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "")
|
||||
)
|
||||
const searchFor: string = searchKey.split(";").map(k => inputHelperOptions.feature?.properties[k]?.toLowerCase())
|
||||
.find(foundValue => !!foundValue) ?? ""
|
||||
|
||||
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
|
||||
const options: any = args[1]
|
||||
|
@ -121,7 +125,7 @@ export default class InputHelpers {
|
|||
value,
|
||||
searchText: searchForValue,
|
||||
instanceOf,
|
||||
notInstanceOf,
|
||||
notInstanceOf
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,11 +40,14 @@
|
|||
{#if availableLanguages?.length > 1}
|
||||
<form class={twMerge("flex max-w-full items-center pr-4", clss)}>
|
||||
<label
|
||||
for="pick-language"
|
||||
class="neutral-label flex max-w-full"
|
||||
use:ariaLabel={Translations.t.general.pickLanguage}
|
||||
>
|
||||
<LanguageIcon class="mr-1 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
<Dropdown cls="max-w-full" value={assignTo}>
|
||||
</label>
|
||||
|
||||
<Dropdown cls="max-w-full" value={assignTo} id="pick-language">
|
||||
{#if preferredFiltered}
|
||||
{#each preferredFiltered as language}
|
||||
<option value={language} class="font-bold">
|
||||
|
@ -70,6 +73,5 @@
|
|||
</option>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
</label>
|
||||
</form>
|
||||
{/if}
|
||||
|
|
|
@ -57,8 +57,8 @@ export abstract class Validator {
|
|||
*
|
||||
* Returns 'undefined' if the element is valid
|
||||
*/
|
||||
public getFeedback(s: string, _?: () => string): Translation | undefined {
|
||||
if (this.isValid(s)) {
|
||||
public getFeedback(s: string, getCountry?: () => string): Translation | undefined {
|
||||
if (this.isValid(s, getCountry)) {
|
||||
return undefined
|
||||
}
|
||||
const tr = Translations.t.validation[this.name]
|
||||
|
@ -71,7 +71,7 @@ export abstract class Validator {
|
|||
return Translations.t.validation[this.name].description
|
||||
}
|
||||
|
||||
public isValid(_: string): boolean {
|
||||
public isValid(_: string, getCountry?: () => string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,4 @@ export default class OpeningHoursValidator extends Validator {
|
|||
)
|
||||
}
|
||||
|
||||
reformat(s: string, _?: () => string): string {
|
||||
return super.reformat(s, _)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MLMap } from "maplibre-gl"
|
||||
import { Map as MLMap } from "maplibre-gl"
|
||||
import { Map as MlMap, SourceSpecification } from "maplibre-gl"
|
||||
import maplibregl from "maplibre-gl"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
|
@ -11,6 +12,8 @@ import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
|||
import * as htmltoimage from "html-to-image"
|
||||
import RasterLayerHandler from "./RasterLayerHandler"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { Protocol } from "pmtiles"
|
||||
import { bool } from "sharp"
|
||||
|
||||
/**
|
||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||
|
@ -23,13 +26,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
"dragRotate",
|
||||
"dragPan",
|
||||
"keyboard",
|
||||
"touchZoomRotate",
|
||||
"touchZoomRotate"
|
||||
]
|
||||
private static maplibre_zoom_handlers = [
|
||||
"scrollZoom",
|
||||
"boxZoom",
|
||||
"doubleClickZoom",
|
||||
"touchZoomRotate",
|
||||
"touchZoomRotate"
|
||||
]
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
readonly zoom: UIEventSource<number>
|
||||
|
@ -46,6 +49,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
readonly pitch: UIEventSource<number>
|
||||
readonly useTerrain: Store<boolean>
|
||||
|
||||
private static pmtilesInited = false
|
||||
/**
|
||||
* Functions that are called when one of those actions has happened
|
||||
* @private
|
||||
|
@ -55,6 +59,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
private readonly _maplibreMap: Store<MLMap>
|
||||
|
||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>) {
|
||||
if (!MapLibreAdaptor.pmtilesInited) {
|
||||
maplibregl.addProtocol("pmtiles", new Protocol().tile)
|
||||
MapLibreAdaptor.pmtilesInited = true
|
||||
console.log("PM-tiles protocol added" +
|
||||
"")
|
||||
}
|
||||
this._maplibreMap = maplibreMap
|
||||
|
||||
this.location = state?.location ?? new UIEventSource(undefined)
|
||||
|
@ -103,6 +113,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
}
|
||||
|
||||
maplibreMap.addCallbackAndRunD((map) => {
|
||||
|
||||
map.on("load", () => {
|
||||
self.MoveMapToCurrentLoc(self.location.data)
|
||||
self.SetZoom(self.zoom.data)
|
||||
|
@ -205,14 +216,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
return {
|
||||
map: mlmap,
|
||||
ui: new SvelteUIElement(MaplibreMap, {
|
||||
map: mlmap,
|
||||
map: mlmap
|
||||
}),
|
||||
mapproperties: new MapLibreAdaptor(mlmap),
|
||||
mapproperties: new MapLibreAdaptor(mlmap)
|
||||
}
|
||||
}
|
||||
|
||||
public static prepareWmsSource(layer: RasterLayerProperties): SourceSpecification {
|
||||
return RasterLayerHandler.prepareWmsSource(layer)
|
||||
return RasterLayerHandler.prepareSource(layer)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -275,7 +286,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
) {
|
||||
const event = {
|
||||
date: new Date(),
|
||||
key: key,
|
||||
key: key
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._onKeyNavigation.length; i++) {
|
||||
|
@ -319,22 +330,51 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
rescaleIcons: number,
|
||||
pixelRatio: number
|
||||
) {
|
||||
|
||||
{
|
||||
const allimages = element.getElementsByTagName("img")
|
||||
for (const img of Array.from(allimages)) {
|
||||
let isLoaded: boolean = false
|
||||
while (!isLoaded) {
|
||||
console.log("Waiting for image", img.src, "to load", img.complete, img.naturalWidth, img)
|
||||
await Utils.waitFor(250)
|
||||
isLoaded = img.complete && img.width > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const style = element.style.transform
|
||||
let x = element.getBoundingClientRect().x
|
||||
let y = element.getBoundingClientRect().y
|
||||
element.style.transform = ""
|
||||
const offset = style.match(/translate\(([-0-9]+)%, ?([-0-9]+)%\)/)
|
||||
|
||||
let labels =<HTMLElement[]> Array.from(element.getElementsByClassName("marker-label"))
|
||||
const origLabelTransforms = labels.map(l => l.style.transform)
|
||||
// We save the original width (`w`) and height (`h`) in order to restore them later on
|
||||
const w = element.style.width
|
||||
const h = element.style.height
|
||||
const h = Number(element.style.height)
|
||||
const targetW = Math.max(element.getBoundingClientRect().width * 4,
|
||||
...labels.map(l => l.getBoundingClientRect().width))
|
||||
const targetH = element.getBoundingClientRect().height +
|
||||
Math.max(...labels.map(l => l.getBoundingClientRect().height * 2 /* A bit of buffer to catch eventual 'margin-top'*/))
|
||||
|
||||
// Force a wider view for icon badges
|
||||
element.style.width = element.getBoundingClientRect().width * 4 + "px"
|
||||
element.style.height = element.getBoundingClientRect().height + "px"
|
||||
element.style.width = targetW + "px"
|
||||
// Force more height to include labels
|
||||
element.style.height = targetH + "px"
|
||||
element.classList.add("w-full", "flex", "flex-col", "items-center")
|
||||
labels.forEach(l => {
|
||||
l.style.transform = ""
|
||||
})
|
||||
await Utils.awaitAnimationFrame()
|
||||
const svgSource = await htmltoimage.toSvg(element)
|
||||
const img = await MapLibreAdaptor.createImage(svgSource)
|
||||
element.style.width = w
|
||||
element.style.height = h
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
labels[i].style.transform = origLabelTransforms[i]
|
||||
}
|
||||
element.style.width = "" + w
|
||||
element.style.height = "" + h
|
||||
|
||||
if (offset && rescaleIcons !== 1) {
|
||||
const [_, __, relYStr] = offset
|
||||
|
@ -346,10 +386,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
y *= pixelRatio
|
||||
|
||||
try {
|
||||
drawOn.drawImage(img, x, y, img.width * rescaleIcons, img.height * rescaleIcons)
|
||||
const xdiff = img.width * rescaleIcons / 2
|
||||
drawOn.drawImage(img, x - xdiff, y, img.width * rescaleIcons, img.height * rescaleIcons)
|
||||
} catch (e) {
|
||||
console.log("Could not draw image because of", e)
|
||||
}
|
||||
element.classList.remove("w-full", "flex", "flex-col", "items-center")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -384,19 +427,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
const markers = Array.from(container.getElementsByClassName("marker"))
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
const marker = <HTMLElement>markers[i]
|
||||
const labels = Array.from(marker.getElementsByClassName("marker-label"))
|
||||
const style = marker.style.transform
|
||||
|
||||
if (isDisplayed(marker)) {
|
||||
await this.drawElement(drawOn, marker, rescaleIcons, pixelRatio)
|
||||
}
|
||||
|
||||
for (const label of labels) {
|
||||
if (isDisplayed(label)) {
|
||||
await this.drawElement(drawOn, <HTMLElement>label, rescaleIcons, pixelRatio)
|
||||
}
|
||||
}
|
||||
|
||||
if (progress) {
|
||||
progress.setData({ current: i, total: markers.length })
|
||||
}
|
||||
|
@ -425,7 +461,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
const bounds = map.getBounds()
|
||||
const bbox = new BBox([
|
||||
[bounds.getEast(), bounds.getNorth()],
|
||||
[bounds.getWest(), bounds.getSouth()],
|
||||
[bounds.getWest(), bounds.getSouth()]
|
||||
])
|
||||
if (this.bounds.data === undefined || !isSetup) {
|
||||
this.bounds.setData(bbox)
|
||||
|
@ -603,14 +639,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
type: "raster-dem",
|
||||
url:
|
||||
"https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
|
||||
Constants.maptilerApiKey,
|
||||
Constants.maptilerApiKey
|
||||
})
|
||||
try {
|
||||
while (!map?.isStyleLoaded()) {
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
map.setTerrain({
|
||||
source: id,
|
||||
source: id
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
import type { Map } from "maplibre-gl"
|
||||
import type { Map, MapOptions } from "maplibre-gl"
|
||||
import * as maplibre from "maplibre-gl"
|
||||
import type { Readable, Writable } from "svelte/store"
|
||||
import { get, writable } from "svelte/store"
|
||||
import type { Writable } from "svelte/store"
|
||||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import type { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
|
||||
/**
|
||||
* The 'MaplibreMap' maps various event sources onto MapLibre.
|
||||
|
@ -17,40 +18,43 @@
|
|||
* 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<Map>
|
||||
export let map: Writable<Map> = undefined
|
||||
export let mapProperties: MapProperties = undefined
|
||||
|
||||
export let interactive: boolean = true
|
||||
|
||||
let container: HTMLElement
|
||||
|
||||
export let center: { lng: number; lat: number } | Readable<{ lng: number; lat: number }> =
|
||||
writable({ lng: 0, lat: 0 })
|
||||
export let zoom: Readable<number> = writable(1)
|
||||
|
||||
const styleUrl = AvailableRasterLayers.maptilerDefaultLayer.properties.url
|
||||
|
||||
let _map: Map
|
||||
onMount(() => {
|
||||
let _center: { lng: number; lat: number }
|
||||
if (typeof center["lng"] === "number" && typeof center["lat"] === "number") {
|
||||
_center = <any>center
|
||||
const { lon, lat } = mapProperties?.location?.data ?? { lon: 0, lat: 0 }
|
||||
|
||||
const rasterLayer: RasterLayerProperties = mapProperties?.rasterLayer?.data?.properties
|
||||
let styleUrl: string
|
||||
if (rasterLayer?.type === "vector") {
|
||||
styleUrl = rasterLayer?.style ?? rasterLayer?.url ?? AvailableRasterLayers.defaultBackgroundLayer.properties.url
|
||||
} else {
|
||||
_center = get(<any>center)
|
||||
const defaultLayer = AvailableRasterLayers.defaultBackgroundLayer.properties
|
||||
styleUrl = defaultLayer.style ?? defaultLayer.url
|
||||
}
|
||||
|
||||
_map = new maplibre.Map({
|
||||
console.log("Initing mapLIbremap with style", styleUrl)
|
||||
|
||||
const options: MapOptions = {
|
||||
container,
|
||||
style: styleUrl,
|
||||
zoom: get(zoom),
|
||||
center: _center,
|
||||
zoom: mapProperties?.zoom?.data ?? 1,
|
||||
center: { lng: lon, lat },
|
||||
maxZoom: 24,
|
||||
interactive: true,
|
||||
attributionControl: false,
|
||||
})
|
||||
attributionControl: false
|
||||
}
|
||||
_map = new maplibre.Map(options)
|
||||
window.requestAnimationFrame(() => {
|
||||
_map.resize()
|
||||
})
|
||||
_map.on("load", function () {
|
||||
_map.on("load", function() {
|
||||
_map.resize()
|
||||
const canvas = _map.getCanvas()
|
||||
if (interactive) {
|
||||
|
|
|
@ -74,4 +74,4 @@
|
|||
style="z-index: 100">
|
||||
<StyleLoadingIndicator map={altmap} />
|
||||
</div>
|
||||
<MaplibreMap {interactive} map={altmap} />
|
||||
<MaplibreMap {interactive} map={altmap} mapProperties={altproperties} />
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Map as MLMap, SourceSpecification } from "maplibre-gl"
|
||||
import { Map as MLMap, RasterSourceSpecification, VectorTileSource } from "maplibre-gl"
|
||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
import { Utils } from "../../Utils"
|
||||
import { VectorSourceSpecification } from "@maplibre/maplibre-gl-style-spec"
|
||||
|
||||
class SingleBackgroundHandler {
|
||||
// Value between 0 and 1.0
|
||||
|
@ -17,6 +18,7 @@ class SingleBackgroundHandler {
|
|||
*/
|
||||
public static readonly DEACTIVATE_AFTER = 60
|
||||
private fadeStep = 0.1
|
||||
|
||||
constructor(
|
||||
map: Store<MLMap>,
|
||||
targetLayer: RasterLayerPolygon,
|
||||
|
@ -75,6 +77,7 @@ class SingleBackgroundHandler {
|
|||
this.fadeIn()
|
||||
}
|
||||
}
|
||||
|
||||
private async awaitStyleIsLoaded(): Promise<void> {
|
||||
const map = this._map.data
|
||||
if (!map) {
|
||||
|
@ -85,11 +88,11 @@ class SingleBackgroundHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private async enable(){
|
||||
private async enable() {
|
||||
let ttl = 15
|
||||
await this.awaitStyleIsLoaded()
|
||||
while(!this.tryEnable() && ttl > 0){
|
||||
ttl --;
|
||||
while (!this.tryEnable() && ttl > 0) {
|
||||
ttl--
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
}
|
||||
|
@ -105,14 +108,19 @@ class SingleBackgroundHandler {
|
|||
}
|
||||
const background = this._targetLayer.properties
|
||||
console.debug("Enabling", background.id)
|
||||
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 = "transit_pier" // this is the first non-landuse item in the stylesheet, we add the raster layer before the roads but above the landuse
|
||||
if(!map.getLayer(addLayerBeforeId)){
|
||||
console.warn("Layer", addLayerBeforeId,"not foundhttp://127.0.0.1:1234/theme.html?layout=cyclofix&z=14.8&lat=51.05282501324558&lon=3.720591622281745&layer-range=true")
|
||||
addLayerBeforeId = undefined
|
||||
}
|
||||
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
|
||||
addLayerBeforeId = undefined
|
||||
}
|
||||
|
||||
if (!map.getSource(background.id)) {
|
||||
try {
|
||||
map.addSource(background.id, RasterLayerHandler.prepareWmsSource(background))
|
||||
map.addSource(background.id, RasterLayerHandler.prepareSource(background))
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
|
@ -122,21 +130,30 @@ class SingleBackgroundHandler {
|
|||
.getStyle()
|
||||
.layers.find((l) => l.id.startsWith("mapcomplete_"))?.id
|
||||
|
||||
map.addLayer(
|
||||
{
|
||||
id: background.id,
|
||||
type: "raster",
|
||||
source: background.id,
|
||||
paint: {
|
||||
"raster-opacity": 0,
|
||||
if (background.type === "vector") {
|
||||
const styleToSet = background.style ?? background.url
|
||||
map.setStyle(styleToSet)
|
||||
} else {
|
||||
map.addLayer(
|
||||
{
|
||||
id: background.id,
|
||||
type: "raster",
|
||||
source: background.id,
|
||||
paint: {
|
||||
"raster-opacity": 0
|
||||
}
|
||||
},
|
||||
},
|
||||
addLayerBeforeId
|
||||
)
|
||||
|
||||
this.opacity.addCallbackAndRun((o) => {
|
||||
map.setPaintProperty(background.id, "raster-opacity", o)
|
||||
})
|
||||
addLayerBeforeId
|
||||
)
|
||||
this.opacity.addCallbackAndRun((o) => {
|
||||
try{
|
||||
map.setPaintProperty(background.id, "raster-opacity", o)
|
||||
}catch (e) {
|
||||
console.debug("Could not set raster-opacity of", background.id)
|
||||
return true // This layer probably doesn't exist anymore, so we unregister
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -168,7 +185,14 @@ export default class RasterLayerHandler {
|
|||
})
|
||||
}
|
||||
|
||||
public static prepareWmsSource(layer: RasterLayerProperties): SourceSpecification {
|
||||
public static prepareSource(layer: RasterLayerProperties): RasterSourceSpecification | VectorSourceSpecification {
|
||||
if (layer.type === "vector") {
|
||||
const vs: VectorSourceSpecification = {
|
||||
type: "vector",
|
||||
url: layer.url
|
||||
}
|
||||
return vs
|
||||
}
|
||||
return {
|
||||
type: "raster",
|
||||
// use the tiles option to specify a 256WMS tile source URL
|
||||
|
@ -178,7 +202,7 @@ export default class RasterLayerHandler {
|
|||
minzoom: layer["min_zoom"] ?? 1,
|
||||
maxzoom: layer["max_zoom"] ?? 25,
|
||||
// Bit of a hack, but seems to work
|
||||
scheme: layer.url.includes("{-y}") ? "tms" : "xyz",
|
||||
scheme: layer.url.includes("{-y}") ? "tms" : "xyz"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,7 +216,7 @@ export default class RasterLayerHandler {
|
|||
"{width}": "" + size,
|
||||
"{height}": "" + size,
|
||||
"{zoom}": "{z}",
|
||||
"{-y}": "{y}",
|
||||
"{-y}": "{y}"
|
||||
}
|
||||
|
||||
for (const key in toReplace) {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import StyleLoadingIndicator from "./StyleLoadingIndicator.svelte"
|
||||
|
||||
/***
|
||||
* Chooses a background-layer out of available options
|
||||
|
|
|
@ -154,7 +154,7 @@ class PointRenderingLayer {
|
|||
|
||||
if (this._onClick) {
|
||||
const self = this
|
||||
el.addEventListener("click", function (ev) {
|
||||
el.addEventListener("click", function(ev) {
|
||||
ev.preventDefault()
|
||||
self._onClick(feature)
|
||||
// Workaround to signal the MapLibreAdaptor to ignore this click
|
||||
|
@ -200,7 +200,7 @@ class LineRenderingLayer {
|
|||
"lineCap",
|
||||
"offset",
|
||||
"fill",
|
||||
"fillColor",
|
||||
"fillColor"
|
||||
] as const
|
||||
|
||||
private static readonly lineConfigKeysColor = ["color", "fillColor"] as const
|
||||
|
@ -249,16 +249,8 @@ class LineRenderingLayer {
|
|||
imageAlongWay.map(async (img, i) => {
|
||||
const imgId = img.then.replaceAll(/[/.-]/g, "_")
|
||||
if (map.getImage(imgId) === undefined) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
map.loadImage(img.then, (err, image) => {
|
||||
if (err) {
|
||||
console.error("Could not add symbol layer to line due to", err)
|
||||
return
|
||||
}
|
||||
map.addImage(imgId, image)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
const loadedImage = await map.loadImage(img.then)
|
||||
map.addImage(imgId, loadedImage.data)
|
||||
}
|
||||
|
||||
const spec: AddLayerObject = {
|
||||
|
@ -272,11 +264,10 @@ class LineRenderingLayer {
|
|||
"icon-rotation-alignment": "map",
|
||||
"icon-pitch-alignment": "map",
|
||||
"icon-image": imgId,
|
||||
"icon-size": 0.055,
|
||||
},
|
||||
"icon-size": 0.055
|
||||
}
|
||||
}
|
||||
const filter = img.if?.asMapboxExpression()
|
||||
console.log(">>>", this._layername, imgId, img.if, "-->", filter)
|
||||
if (filter) {
|
||||
spec.filter = filter
|
||||
}
|
||||
|
@ -347,12 +338,12 @@ class LineRenderingLayer {
|
|||
type: "geojson",
|
||||
data: {
|
||||
type: "FeatureCollection",
|
||||
features,
|
||||
features
|
||||
},
|
||||
promoteId: "id",
|
||||
promoteId: "id"
|
||||
})
|
||||
const linelayer = this._layername + "_line"
|
||||
map.addLayer({
|
||||
const layer: AddLayerObject = {
|
||||
source: this._layername,
|
||||
id: linelayer,
|
||||
type: "line",
|
||||
|
@ -360,12 +351,17 @@ class LineRenderingLayer {
|
|||
"line-color": ["feature-state", "color"],
|
||||
"line-opacity": ["feature-state", "color-opacity"],
|
||||
"line-width": ["feature-state", "width"],
|
||||
"line-offset": ["feature-state", "offset"],
|
||||
"line-offset": ["feature-state", "offset"]
|
||||
},
|
||||
layout: {
|
||||
"line-cap": "round",
|
||||
},
|
||||
})
|
||||
"line-cap": "round"
|
||||
}
|
||||
}
|
||||
if (this._config.dashArray) {
|
||||
|
||||
layer.paint["line-dasharray"] = this._config.dashArray?.split(" ")?.map(s => Number(s)) ?? null
|
||||
}
|
||||
map.addLayer(layer)
|
||||
|
||||
if (this._config.imageAlongWay) {
|
||||
this.addSymbolLayer(this._layername, this._config.imageAlongWay)
|
||||
|
@ -397,8 +393,8 @@ class LineRenderingLayer {
|
|||
layout: {},
|
||||
paint: {
|
||||
"fill-color": ["feature-state", "fillColor"],
|
||||
"fill-opacity": ["feature-state", "fillColor-opacity"],
|
||||
},
|
||||
"fill-opacity": ["feature-state", "fillColor-opacity"]
|
||||
}
|
||||
})
|
||||
if (this._onClick) {
|
||||
map.on("click", polylayer, (e) => {
|
||||
|
@ -429,7 +425,7 @@ class LineRenderingLayer {
|
|||
this.currentSourceData = features
|
||||
src.setData({
|
||||
type: "FeatureCollection",
|
||||
features: this.currentSourceData,
|
||||
features: this.currentSourceData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -513,14 +509,14 @@ export default class ShowDataLayer {
|
|||
layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
|
||||
features,
|
||||
{
|
||||
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
|
||||
constructStore: (features, layer) => new SimpleFeatureSource(layer, features)
|
||||
}
|
||||
)
|
||||
perLayer.forEach((fs) => {
|
||||
new ShowDataLayer(mlmap, {
|
||||
layer: fs.layer.layerDef,
|
||||
features: fs,
|
||||
...(options ?? {}),
|
||||
...(options ?? {})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -533,11 +529,12 @@ export default class ShowDataLayer {
|
|||
return new ShowDataLayer(map, {
|
||||
layer: ShowDataLayer.rangeLayer,
|
||||
features,
|
||||
doShowLayer,
|
||||
doShowLayer
|
||||
})
|
||||
}
|
||||
|
||||
public destruct() {}
|
||||
public destruct() {
|
||||
}
|
||||
|
||||
private zoomToCurrentFeatures(map: MlMap) {
|
||||
if (this._options.zoomToFeatures) {
|
||||
|
@ -546,21 +543,21 @@ export default class ShowDataLayer {
|
|||
map.resize()
|
||||
map.fitBounds(bbox.toLngLat(), {
|
||||
padding: { top: 10, bottom: 10, left: 10, right: 10 },
|
||||
animate: false,
|
||||
animate: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private initDrawFeatures(map: MlMap) {
|
||||
let { features, doShowLayer, fetchStore, selectedElement, selectedLayer } = this._options
|
||||
const onClick =
|
||||
this._options.onClick ??
|
||||
(this._options.layer.title === undefined
|
||||
let { features, doShowLayer, fetchStore, selectedElement } = this._options
|
||||
let onClick = this._options.onClick
|
||||
if (!onClick && selectedElement) {
|
||||
onClick = (this._options.layer.title === undefined
|
||||
? undefined
|
||||
: (feature: Feature) => {
|
||||
selectedElement?.setData(feature)
|
||||
selectedLayer?.setData(this._options.layer)
|
||||
})
|
||||
selectedElement?.setData(feature)
|
||||
})
|
||||
}
|
||||
if (this._options.drawLines !== false) {
|
||||
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
|
||||
const lineRenderingConfig = this._options.layer.lineRendering[i]
|
||||
|
|
29
src/UI/Map/SmallZoomButtons.svelte
Normal file
29
src/UI/Map/SmallZoomButtons.svelte
Normal file
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import Translations from "../i18n/Translations.js";
|
||||
import Min from "../../assets/svg/Min.svelte";
|
||||
import MapControlButton from "../Base/MapControlButton.svelte";
|
||||
import Plus from "../../assets/svg/Plus.svelte";
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
|
||||
export let adaptor: MapProperties
|
||||
let canZoomIn = adaptor.maxzoom.map(mz => adaptor.zoom.data < mz, [adaptor.zoom] )
|
||||
let canZoomOut = adaptor.minzoom.map(mz => adaptor.zoom.data > mz, [adaptor.zoom] )
|
||||
</script>
|
||||
<div class="absolute bottom-0 right-0 pointer-events-none flex flex-col">
|
||||
<MapControlButton
|
||||
enabled={canZoomIn}
|
||||
cls="m-0.5 p-1"
|
||||
arialabel={Translations.t.general.labels.zoomIn}
|
||||
on:click={() => adaptor.zoom.update((z) => z + 1)}
|
||||
>
|
||||
<Plus class="h-5 w-5" />
|
||||
</MapControlButton>
|
||||
<MapControlButton
|
||||
enabled={canZoomOut}
|
||||
cls={"m-0.5 p-1"}
|
||||
arialabel={Translations.t.general.labels.zoomOut}
|
||||
on:click={() => adaptor.zoom.update((z) => z - 1)}
|
||||
>
|
||||
<Min class="h-5 w-5" />
|
||||
</MapControlButton>
|
||||
</div>
|
|
@ -6,14 +6,30 @@
|
|||
|
||||
let isLoading = false
|
||||
export let map: UIEventSource<MlMap>
|
||||
/**
|
||||
* Optional. Only used for the 'global' change indicator so that it won't spin on pan/zoom but only when a change _actually_ occured
|
||||
*/
|
||||
export let rasterLayer: UIEventSource<any> = undefined
|
||||
|
||||
let didChange = undefined
|
||||
onDestroy(rasterLayer?.addCallback(() => {
|
||||
didChange = true
|
||||
}) ??( () => {}))
|
||||
|
||||
onDestroy(Stores.Chronic(250).addCallback(
|
||||
() => {
|
||||
isLoading = !map.data?.isStyleLoaded()
|
||||
const mapIsLoading = !map.data?.isStyleLoaded()
|
||||
isLoading = mapIsLoading && (didChange || rasterLayer === undefined)
|
||||
if(didChange && !mapIsLoading){
|
||||
didChange = false
|
||||
}
|
||||
},
|
||||
))
|
||||
</script>
|
||||
|
||||
|
||||
{#if isLoading}
|
||||
<Loading />
|
||||
<Loading cls="h-6 w-6" />
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
|
|
@ -364,7 +364,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Loading>Creating point...</Loading>
|
||||
<Loading><Tr t={Translations.t.general.add.creating}/> </Loading>
|
||||
{/if}
|
||||
</div>
|
||||
</LoginToggle>
|
||||
|
|
|
@ -110,7 +110,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, { mapProperties: mla, map: mlmap }).SetClass("h-48")
|
||||
|
||||
const features = this.target_feature_ids.map((id) =>
|
||||
this.state.indexedFeatures.featuresById.data.get(id)
|
||||
|
|
|
@ -48,10 +48,10 @@
|
|||
<ImportFlow {importFlow} on:confirm={() => importFlow.onConfirm()}>
|
||||
<div slot="map" class="relative">
|
||||
<div class="h-32">
|
||||
<MaplibreMap {map} />
|
||||
<MaplibreMap {map} mapProperties={mla} />
|
||||
</div>
|
||||
<div class="absolute bottom-0">
|
||||
<OpenBackgroundSelectorButton />
|
||||
<OpenBackgroundSelectorButton {state} {map} />
|
||||
</div>
|
||||
</div>
|
||||
</ImportFlow>
|
||||
|
|
|
@ -109,7 +109,7 @@ export class MinimapViz implements SpecialVisualization {
|
|||
state.layout.layers
|
||||
)
|
||||
|
||||
return new SvelteUIElement(MaplibreMap, { interactive: false, map: mlmap })
|
||||
return new SvelteUIElement(MaplibreMap, { interactive: false, map: mlmap, mapProperties: mla })
|
||||
.SetClass("h-40 rounded")
|
||||
.SetStyle("overflow: hidden; pointer-events: none;")
|
||||
}
|
||||
|
|
|
@ -130,6 +130,16 @@ export class MoveWizardState {
|
|||
this.moveDisallowedReason.setData(t.partOfRelation)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// This is a new point. Check if it was snapped to an existing way due to the '_referencing_ways'-tag
|
||||
const store = this._state.featureProperties.getStore(id)
|
||||
store?.addCallbackAndRunD((tags) => {
|
||||
if (tags._referencing_ways !== undefined && tags._referencing_ways !== "[]") {
|
||||
console.log("Got referencing ways according to the tags")
|
||||
this.moveDisallowedReason.setData(t.partOfAWay)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
112
src/UI/Popup/SplitRoadWizard.svelte
Normal file
112
src/UI/Popup/SplitRoadWizard.svelte
Normal file
|
@ -0,0 +1,112 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature, Point } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Scissors from "../../assets/svg/Scissors.svelte"
|
||||
import WaySplitMap from "../BigComponents/WaySplitMap.svelte"
|
||||
import BackButton from "../Base/BackButton.svelte"
|
||||
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
|
||||
import Translations from "../i18n/Translations"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { OsmWay } from "../../Logic/Osm/OsmObject"
|
||||
import type { WayId } from "../../Models/OsmFeature"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let id: WayId
|
||||
const t = Translations.t.split
|
||||
let step: "initial" | "loading_way" | "splitting" | "applying_split" | "has_been_split" | "deleted" = "initial"
|
||||
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
|
||||
let splitPoints = new UIEventSource<Feature<
|
||||
Point,
|
||||
{
|
||||
id: number
|
||||
index: number
|
||||
dist: number
|
||||
location: number
|
||||
}
|
||||
>[]>([])
|
||||
let splitpointsNotEmpty = splitPoints.map(sp => sp.length > 0)
|
||||
|
||||
let osmWay: OsmWay
|
||||
|
||||
async function downloadWay() {
|
||||
step = "loading_way"
|
||||
const dloaded = await state.osmObjectDownloader.DownloadObjectAsync(id)
|
||||
if (dloaded === "deleted") {
|
||||
step = "deleted"
|
||||
return
|
||||
}
|
||||
osmWay = dloaded
|
||||
|
||||
step = "splitting"
|
||||
}
|
||||
|
||||
async function doSplit() {
|
||||
step = "applying_split"
|
||||
const splitAction = new SplitAction(
|
||||
id,
|
||||
splitPoints.data.map((ff) => <[number, number]>(<Point>ff.geometry).coordinates),
|
||||
{
|
||||
theme: state?.layout?.id,
|
||||
},
|
||||
5,
|
||||
)
|
||||
await state.changes?.applyAction(splitAction)
|
||||
// We throw away the old map and splitpoints, and create a new map from scratch
|
||||
splitPoints.setData([])
|
||||
|
||||
// Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219
|
||||
state.selectedElement?.setData(undefined)
|
||||
step = "has_been_split"
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<Tr slot="not-logged-in" t={t.loginToSplit} />
|
||||
|
||||
{#if step === "deleted"}
|
||||
<!-- Empty -->
|
||||
{:else if step === "initial"}
|
||||
<button on:click={() => downloadWay()}>
|
||||
<Scissors class="w-6 h-6 shrink-0" />
|
||||
<Tr t={t.inviteToSplit} />
|
||||
</button>
|
||||
{:else if step === "loading_way"}
|
||||
<Loading />
|
||||
|
||||
{:else if step === "splitting"}
|
||||
<div class="flex flex-col interactive border-interactive p-2">
|
||||
<div class="w-full h-80">
|
||||
<WaySplitMap {state} {splitPoints} {osmWay} />
|
||||
</div>
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap w-full">
|
||||
<BackButton clss="w-full" on:click={() => {
|
||||
splitPoints.set([])
|
||||
step = "initial"
|
||||
}}>
|
||||
<Tr t={Translations.t.general.cancel} />
|
||||
</BackButton>
|
||||
<NextButton clss={ ($splitpointsNotEmpty ? "": "disabled ") + "w-full primary"} on:click={() => doSplit()}>
|
||||
<Tr t={t.split} />
|
||||
</NextButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{:else if step === "has_been_split"}
|
||||
<Tr cls="thanks" t={ t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full")} />
|
||||
<button on:click={() => downloadWay()}>
|
||||
<Scissors class="w-6 h-6" />
|
||||
<Tr t={t.splitAgain} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
</LoginToggle>
|
||||
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
import Toggle from "../Input/Toggle"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Combine from "../Base/Combine"
|
||||
import { Button } from "../Base/Button"
|
||||
import Translations from "../i18n/Translations"
|
||||
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
|
||||
import Title from "../Base/Title"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { LoginToggle } from "./LoginButton"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import WaySplitMap from "../BigComponents/WaySplitMap.svelte"
|
||||
import { Feature, Point } from "geojson"
|
||||
import { WayId } from "../../Models/OsmFeature"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
|
||||
import Scissors from "../../assets/svg/Scissors.svelte"
|
||||
|
||||
export default class SplitRoadWizard extends Combine {
|
||||
public dialogIsOpened: UIEventSource<boolean>
|
||||
|
||||
/**
|
||||
* A UI Element used for splitting roads
|
||||
*
|
||||
* @param id The id of the road to remove
|
||||
* @param state the state of the application
|
||||
*/
|
||||
constructor(
|
||||
id: WayId,
|
||||
state: {
|
||||
layout?: LayoutConfig
|
||||
osmConnection?: OsmConnection
|
||||
osmObjectDownloader?: OsmObjectDownloader
|
||||
changes?: Changes
|
||||
indexedFeatures?: IndexedFeatureSource
|
||||
selectedElement?: UIEventSource<Feature>
|
||||
}
|
||||
) {
|
||||
const t = Translations.t.split
|
||||
|
||||
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
|
||||
const splitPoints = new UIEventSource<Feature<Point>[]>([])
|
||||
|
||||
const hasBeenSplit = new UIEventSource(false)
|
||||
|
||||
// Toggle variable between show split button and map
|
||||
const splitClicked = new UIEventSource<boolean>(false)
|
||||
|
||||
const leafletMap = new UIEventSource<BaseUIElement>(undefined)
|
||||
|
||||
function initMap() {
|
||||
;(async function (
|
||||
id: WayId,
|
||||
splitPoints: UIEventSource<Feature[]>
|
||||
): Promise<BaseUIElement> {
|
||||
return new SvelteUIElement(WaySplitMap, {
|
||||
osmWay: await state.osmObjectDownloader.DownloadObjectAsync(id),
|
||||
splitPoints,
|
||||
})
|
||||
})(id, splitPoints).then((mapComponent) =>
|
||||
leafletMap.setData(mapComponent.SetClass("w-full h-80"))
|
||||
)
|
||||
}
|
||||
|
||||
// Toggle between splitmap
|
||||
const splitButton = new SubtleButton(
|
||||
new SvelteUIElement(Scissors).SetClass("h-6 w-6"),
|
||||
new Toggle(
|
||||
t.splitAgain.Clone().SetClass("text-lg font-bold"),
|
||||
t.inviteToSplit.Clone().SetClass("text-lg font-bold"),
|
||||
hasBeenSplit
|
||||
)
|
||||
)
|
||||
|
||||
const splitToggle = new LoginToggle(splitButton, t.loginToSplit.Clone(), state)
|
||||
|
||||
// Save button
|
||||
const saveButton = new Button(t.split.Clone(), async () => {
|
||||
hasBeenSplit.setData(true)
|
||||
splitClicked.setData(false)
|
||||
const splitAction = new SplitAction(
|
||||
id,
|
||||
splitPoints.data.map((ff) => <[number, number]>(<Point>ff.geometry).coordinates),
|
||||
{
|
||||
theme: state?.layout?.id,
|
||||
},
|
||||
5
|
||||
)
|
||||
await state.changes?.applyAction(splitAction)
|
||||
// We throw away the old map and splitpoints, and create a new map from scratch
|
||||
splitPoints.setData([])
|
||||
|
||||
// Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219
|
||||
state.selectedElement?.setData(undefined)
|
||||
})
|
||||
|
||||
saveButton.SetClass("btn btn-primary mr-3")
|
||||
const disabledSaveButton = new Button(t.split.Clone(), undefined)
|
||||
disabledSaveButton.SetClass("btn btn-disabled mr-3")
|
||||
// Only show the save button if there are split points defined
|
||||
const saveToggle = new Toggle(
|
||||
disabledSaveButton,
|
||||
saveButton,
|
||||
splitPoints.map((data) => data.length === 0)
|
||||
)
|
||||
|
||||
const cancelButton = Translations.t.general.cancel
|
||||
.Clone() // Not using Button() element to prevent full width button
|
||||
.SetClass("btn btn-secondary mr-3")
|
||||
.onClick(() => {
|
||||
splitPoints.setData([])
|
||||
splitClicked.setData(false)
|
||||
})
|
||||
|
||||
cancelButton.SetClass("btn btn-secondary block")
|
||||
|
||||
const splitTitle = new Title(t.splitTitle)
|
||||
|
||||
const mapView = new Combine([
|
||||
splitTitle,
|
||||
new VariableUiElement(leafletMap),
|
||||
new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"),
|
||||
])
|
||||
mapView.SetClass("question")
|
||||
super([
|
||||
Toggle.If(hasBeenSplit, () =>
|
||||
t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full")
|
||||
),
|
||||
new Toggle(mapView, splitToggle, splitClicked),
|
||||
])
|
||||
splitClicked.addCallback((view) => {
|
||||
if (view) {
|
||||
initMap()
|
||||
}
|
||||
})
|
||||
this.dialogIsOpened = splitClicked
|
||||
const self = this
|
||||
splitButton.onClick(() => {
|
||||
splitClicked.setData(true)
|
||||
self.ScrollIntoView()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
</script>
|
||||
|
||||
{#if !userDetails || $userDetails.loggedIn}
|
||||
<div>
|
||||
<div class="break-words" style="word-break: break-word">
|
||||
{#if tags === undefined}
|
||||
<slot name="no-tags"><Tr cls="subtle" t={Translations.t.general.noTagsSelected} /></slot>
|
||||
{:else if embedIn === undefined}
|
||||
|
|
|
@ -76,6 +76,5 @@
|
|||
{value}
|
||||
{state}
|
||||
on:submit
|
||||
{unvalidatedText}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* The questions can either be shown all at once or one at a time (in which case they can be skipped)
|
||||
*/
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
|
@ -12,6 +12,7 @@
|
|||
import Tr from "../../Base/Tr.svelte"
|
||||
import Translations from "../../i18n/Translations.js"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let layer: LayerConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
@ -67,8 +68,25 @@
|
|||
},
|
||||
[skippedQuestions]
|
||||
)
|
||||
let firstQuestion = questionsToAsk.map((qta) => qta[0])
|
||||
let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>(undefined)
|
||||
let allQuestionsToAsk : UIEventSource<TagRenderingConfig[]> = new UIEventSource<TagRenderingConfig[]>([])
|
||||
|
||||
function calculateQuestions(){
|
||||
console.log("Applying questions to ask")
|
||||
const qta = questionsToAsk.data
|
||||
firstQuestion.setData(undefined)
|
||||
firstQuestion.setData(qta[0])
|
||||
|
||||
allQuestionsToAsk.setData([])
|
||||
allQuestionsToAsk.setData(qta)
|
||||
}
|
||||
|
||||
|
||||
onDestroy(questionsToAsk.addCallback(() =>calculateQuestions()))
|
||||
onDestroy(showAllQuestionsAtOnce.addCallback(() => calculateQuestions()))
|
||||
calculateQuestions()
|
||||
|
||||
|
||||
let answered: number = 0
|
||||
let skipped: number = 0
|
||||
|
||||
|
@ -92,7 +110,7 @@
|
|||
class="marker-questionbox-root"
|
||||
class:hidden={$questionsToAsk.length === 0 && skipped === 0 && answered === 0}
|
||||
>
|
||||
{#if $questionsToAsk.length === 0}
|
||||
{#if $allQuestionsToAsk.length === 0}
|
||||
{#if skipped + answered > 0}
|
||||
<div class="thanks">
|
||||
<Tr t={Translations.t.general.questionBox.done} />
|
||||
|
@ -140,11 +158,11 @@
|
|||
<div>
|
||||
{#if $showAllQuestionsAtOnce}
|
||||
<div class="flex flex-col gap-y-1">
|
||||
{#each $questionsToAsk as question (question.id)}
|
||||
{#each $allQuestionsToAsk as question (question.id)}
|
||||
<TagRenderingQuestion config={question} {tags} {selectedElement} {state} {layer} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{:else if $firstQuestion !== undefined}
|
||||
<TagRenderingQuestion
|
||||
config={$firstQuestion}
|
||||
{layer}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
import { placeholder } from "../../../Utils/placeholder"
|
||||
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import { get, writable } from "svelte/store"
|
||||
|
||||
export let config: TagRenderingConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
@ -46,7 +47,7 @@
|
|||
|
||||
// Will be bound if a freeform is available
|
||||
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
|
||||
let freeformInputUnvalidated = new UIEventSource<string>(freeformInput.data)
|
||||
let freeformInputUnvalidated = new UIEventSource<string>(get(freeformInput))
|
||||
|
||||
let selectedMapping: number = undefined
|
||||
/**
|
||||
|
@ -112,7 +113,7 @@
|
|||
unseenFreeformValues.splice(index, 1)
|
||||
}
|
||||
// TODO this has _to much_ values
|
||||
freeformInput.setData(unseenFreeformValues.join(";"))
|
||||
freeformInput.set(unseenFreeformValues.join(";"))
|
||||
if (checkedMappings.length + 1 < mappings.length) {
|
||||
checkedMappings.push(unseenFreeformValues.length > 0)
|
||||
}
|
||||
|
@ -121,10 +122,10 @@
|
|||
if (confg.freeform?.key) {
|
||||
if (!confg.multiAnswer) {
|
||||
// Somehow, setting multi-answer freeform values is broken if this is not set
|
||||
freeformInput.setData(tgs[confg.freeform.key])
|
||||
freeformInput.set(tgs[confg.freeform.key])
|
||||
}
|
||||
} else {
|
||||
freeformInput.setData(undefined)
|
||||
freeformInput.set(undefined)
|
||||
}
|
||||
feedback.setData(undefined)
|
||||
}
|
||||
|
@ -134,8 +135,8 @@
|
|||
// We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
|
||||
initialize($tags, config)
|
||||
}
|
||||
|
||||
freeformInput.addCallbackAndRun((freeformValue) => {
|
||||
onDestroy(
|
||||
freeformInput.subscribe((freeformValue) => {
|
||||
if (!mappings || mappings?.length == 0 || config.freeform?.key === undefined) {
|
||||
return
|
||||
}
|
||||
|
@ -151,7 +152,8 @@
|
|||
if (freeformValue?.length > 0) {
|
||||
selectedMapping = mappings.length
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
$: {
|
||||
if (
|
||||
allowDeleteOfFreeform &&
|
||||
|
@ -202,7 +204,7 @@
|
|||
theme: tags.data["_orig_theme"] ?? state.layout.id,
|
||||
changeType: "answer",
|
||||
})
|
||||
freeformInput.setData(undefined)
|
||||
freeformInput.set(undefined)
|
||||
selectedMapping = undefined
|
||||
selectedTags = undefined
|
||||
|
||||
|
@ -241,7 +243,7 @@
|
|||
<form
|
||||
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
|
||||
style="max-height: 75vh"
|
||||
on:submit|preventDefault={() => onSave()}
|
||||
on:submit|preventDefault={() =>{ /*onSave(); This submit is not needed and triggers to early, causing bugs: see #1808*/}}
|
||||
>
|
||||
<fieldset>
|
||||
<legend>
|
||||
|
@ -285,7 +287,7 @@
|
|||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
on:submit={onSave}
|
||||
on:submit={() => onSave()}
|
||||
/>
|
||||
{:else if mappings !== undefined && !config.multiAnswer}
|
||||
<!-- Simple radiobuttons as mapping -->
|
||||
|
@ -329,7 +331,7 @@
|
|||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
on:selected={() => (selectedMapping = config.mappings?.length)}
|
||||
on:submit={onSave}
|
||||
on:submit={() => onSave()}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
|
@ -372,7 +374,7 @@
|
|||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
on:submit={onSave}
|
||||
on:submit={() => onSave()}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
|
@ -397,13 +399,13 @@
|
|||
<slot name="cancel" />
|
||||
<slot name="save-button" {selectedTags}>
|
||||
{#if allowDeleteOfFreeform && (mappings?.length ?? 0) === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""}
|
||||
<button class="primary flex" on:click|stopPropagation|preventDefault={onSave}>
|
||||
<button class="primary flex" on:click|stopPropagation|preventDefault={() => onSave()}>
|
||||
<TrashIcon class="h-6 w-6 text-red-500" />
|
||||
<Tr t={Translations.t.general.eraseValue} />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
on:click={onSave}
|
||||
on:click={() => onSave()}
|
||||
class={twJoin(
|
||||
selectedTags === undefined ? "disabled" : "button-shadow",
|
||||
"primary"
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
let lang = (
|
||||
(navigator.languages && navigator.languages[0]) ||
|
||||
navigator.language ||
|
||||
navigator["userLanguage"] ||
|
||||
"en"
|
||||
).substr(0, 2)
|
||||
|
||||
function filterLangs(maindiv) {
|
||||
let foundLangs = 0
|
||||
for (const child of Array.from(maindiv.children)) {
|
||||
if (child.attributes.getNamedItem("lang")?.value === lang) {
|
||||
foundLangs++
|
||||
}
|
||||
}
|
||||
if (foundLangs === 0) {
|
||||
lang = "en"
|
||||
}
|
||||
for (const child of Array.from(maindiv.children)) {
|
||||
const childLang = child.attributes.getNamedItem("lang")
|
||||
if (childLang === undefined) {
|
||||
continue
|
||||
}
|
||||
if (childLang.value === lang) {
|
||||
continue
|
||||
}
|
||||
child.parentElement.removeChild(child)
|
||||
}
|
||||
}
|
||||
|
||||
filterLangs(document.getElementById("descriptions-while-loading"))
|
||||
filterLangs(document.getElementById("default-title"))
|
32
src/UI/RemoveOtherLanguages.ts
Normal file
32
src/UI/RemoveOtherLanguages.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
export {}
|
||||
let lang = (
|
||||
(navigator.languages && navigator.languages[0]) ||
|
||||
navigator.language ||
|
||||
navigator["userLanguage"] ||
|
||||
"en"
|
||||
).substr(0, 2)
|
||||
|
||||
function filterLangs(maindiv) {
|
||||
let foundLangs = 0
|
||||
for (const child of Array.from(maindiv.children)) {
|
||||
if (child.attributes.getNamedItem("lang")?.value === lang) {
|
||||
foundLangs++
|
||||
}
|
||||
}
|
||||
if (foundLangs === 0) {
|
||||
lang = "en"
|
||||
}
|
||||
for (const child of Array.from(maindiv.children)) {
|
||||
const childLang = child.attributes.getNamedItem("lang")
|
||||
if (childLang === undefined) {
|
||||
continue
|
||||
}
|
||||
if (childLang.value === lang) {
|
||||
continue
|
||||
}
|
||||
child.parentElement.removeChild(child)
|
||||
}
|
||||
}
|
||||
|
||||
filterLangs(document.getElementById("descriptions-while-loading"))
|
||||
filterLangs(document.getElementById("default-title"))
|
|
@ -61,17 +61,12 @@
|
|||
opinion: opinion.data,
|
||||
metadata: { nickname, is_affiliated: isAffiliated.data },
|
||||
}
|
||||
if (state.featureSwitchIsTesting?.data ?? true) {
|
||||
console.log("Testing - not actually saving review", review)
|
||||
await Utils.waitFor(1000)
|
||||
} else {
|
||||
try {
|
||||
await reviews.createReview(review)
|
||||
} catch (e) {
|
||||
console.error("Could not create review due to", e)
|
||||
uploadFailed = "" + e
|
||||
}
|
||||
}
|
||||
_state = "done"
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -6,12 +6,15 @@
|
|||
import LoginButton from "../Base/LoginButton.svelte"
|
||||
import SingleReview from "./SingleReview.svelte"
|
||||
import Mangrove_logo from "../../assets/svg/Mangrove_logo.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
/**
|
||||
* A panel showing all the reviews by the logged-in user
|
||||
*/
|
||||
export let state: SpecialVisualizationState
|
||||
let reviews = state.userRelatedState.mangroveIdentity.getAllReviews()
|
||||
let allReviews = state.userRelatedState.mangroveIdentity.getAllReviews()
|
||||
let reviews = state.userRelatedState.mangroveIdentity.getGeoReviews()
|
||||
let kid = state.userRelatedState.mangroveIdentity.getKeyId()
|
||||
const t = Translations.t.reviews
|
||||
</script>
|
||||
|
||||
|
@ -22,23 +25,42 @@
|
|||
</LoginButton>
|
||||
</div>
|
||||
|
||||
{#if $reviews?.length > 0}
|
||||
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
{#each $reviews as review (review.sub)}
|
||||
<SingleReview {review} showSub={true} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
{#if $reviews === undefined}
|
||||
<Loading />
|
||||
{:else}
|
||||
<Tr t={t.your_reviews_empty} />
|
||||
{#if $reviews?.length > 0}
|
||||
<div class="flex flex-col gap-y-1" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
{#each $reviews as review (review.sub)}
|
||||
<SingleReview {review} showSub={true} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Tr t={t.your_reviews_empty} />
|
||||
{/if}
|
||||
|
||||
{#if $allReviews?.length > $reviews?.length}
|
||||
{#if $allReviews?.length - $reviews?.length === 1}
|
||||
<Tr t={t.non_place_review} />
|
||||
{:else}
|
||||
<Tr t={t.non_place_reviews.Subs({n:$allReviews?.length - $reviews?.length })} />
|
||||
{/if}
|
||||
<a target="_blank"
|
||||
class="link-underline"
|
||||
rel="noopener nofollow"
|
||||
href={`https://mangrove.reviews/list?kid=${encodeURIComponent($kid)}`}
|
||||
>
|
||||
<Tr t={t.see_all} />
|
||||
</a>
|
||||
{/if}
|
||||
<a
|
||||
class="link-underline"
|
||||
href="https://github.com/pietervdvn/MapComplete/issues/1782"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Tr t={t.reviews_bug} />
|
||||
</a>
|
||||
{/if}
|
||||
<a
|
||||
class="link-underline"
|
||||
href="https://github.com/pietervdvn/MapComplete/issues/1782"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Tr t={t.reviews_bug} />
|
||||
</a>
|
||||
<div class="flex justify-end">
|
||||
<Mangrove_logo class="h-12 w-12 shrink-0 p-1" />
|
||||
<Tr cls="text-sm subtle" t={t.attribution} />
|
||||
|
|
|
@ -22,6 +22,8 @@ import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
|||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import { SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
|
||||
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import ShowDataLayer from "./Map/ShowDataLayer"
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
|
@ -86,6 +88,8 @@ export interface SpecialVisualizationState {
|
|||
|
||||
readonly previewedImage: UIEventSource<ProvidedImage>
|
||||
readonly geolocation: GeoLocationHandler
|
||||
|
||||
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer
|
||||
}
|
||||
|
||||
export interface SpecialVisualization {
|
||||
|
|
|
@ -42,8 +42,6 @@ import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
|
|||
import UserProfile from "./BigComponents/UserProfile.svelte"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { WayId } from "../Models/OsmFeature"
|
||||
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
||||
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
|
||||
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
|
||||
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"
|
||||
|
@ -90,6 +88,7 @@ import LoginButton from "./Base/LoginButton.svelte"
|
|||
import Toggle from "./Input/Toggle"
|
||||
import ImportReviewIdentity from "./Reviews/ImportReviewIdentity.svelte"
|
||||
import LinkedDataLoader from "../Logic/Web/LinkedDataLoader"
|
||||
import SplitRoadWizard from "./Popup/SplitRoadWizard.svelte"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -429,7 +428,7 @@ export default class SpecialVisualizations {
|
|||
.map((tags) => tags.id)
|
||||
.map((id) => {
|
||||
if (id.startsWith("way/")) {
|
||||
return new SplitRoadWizard(<WayId>id, state)
|
||||
return new SvelteUIElement(SplitRoadWizard, { id, state })
|
||||
}
|
||||
return undefined
|
||||
}),
|
||||
|
@ -666,6 +665,7 @@ export default class SpecialVisualizations {
|
|||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
},
|
||||
state.featureSwitchIsTesting
|
||||
)
|
||||
return new SvelteUIElement(StarsBarIcon, {
|
||||
score: reviews.average,
|
||||
|
@ -699,6 +699,7 @@ export default class SpecialVisualizations {
|
|||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
},
|
||||
state.featureSwitchIsTesting
|
||||
)
|
||||
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
|
||||
},
|
||||
|
@ -731,6 +732,7 @@ export default class SpecialVisualizations {
|
|||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
},
|
||||
state.featureSwitchIsTesting
|
||||
)
|
||||
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
|
||||
},
|
||||
|
@ -750,7 +752,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
const [text] = argument
|
||||
return new SvelteUIElement(ImportReviewIdentity, { state, text })
|
||||
|
@ -1151,10 +1153,11 @@ export default class SpecialVisualizations {
|
|||
constr: (state) => {
|
||||
return new Combine(
|
||||
state.layout.layers
|
||||
.filter((l) => l.name !== null)
|
||||
.filter((l) => l.name !== null && l.title && state.perLayer.get(l.id) !== undefined )
|
||||
.map(
|
||||
(l) => {
|
||||
const fs = state.perLayer.get(l.id)
|
||||
console.log(">>>", l.id, fs)
|
||||
const bbox = state.mapProperties.bounds
|
||||
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
|
||||
return new StatisticsPanel(fsBboxed)
|
||||
|
@ -1596,6 +1599,9 @@ export default class SpecialVisualizations {
|
|||
feature: Feature,
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const smallSize = 100
|
||||
const bigSize = 200
|
||||
const size = new UIEventSource(smallSize)
|
||||
return new VariableUiElement(
|
||||
tagSource
|
||||
.map((tags) => tags.id)
|
||||
|
@ -1615,11 +1621,17 @@ export default class SpecialVisualizations {
|
|||
const url =
|
||||
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
|
||||
`#${id}`
|
||||
return new Img(new Qr(url).toImageElement(75)).SetStyle(
|
||||
"width: 75px",
|
||||
return new Img(new Qr(url).toImageElement(size.data)).SetStyle(
|
||||
`width: ${size.data}px`
|
||||
)
|
||||
}),
|
||||
)
|
||||
}, [size])
|
||||
).onClick(()=> {
|
||||
if(size.data !== bigSize){
|
||||
size.setData(bigSize)
|
||||
}else{
|
||||
size.setData(smallSize)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
)
|
||||
let osmConnection = new OsmConnection({
|
||||
oauth_token,
|
||||
checkOnlineRegularly: true
|
||||
})
|
||||
const expertMode = UIEventSource.asBoolean(
|
||||
osmConnection.GetPreference("studio-expert-mode", "false", {
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
</script>
|
||||
|
||||
No tests
|
||||
|
||||
|
|
|
@ -118,7 +118,8 @@
|
|||
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
|
||||
let mapproperties: MapProperties = state.mapProperties
|
||||
state.mapProperties.installCustomKeyboardHandler(viewport)
|
||||
|
||||
let canZoomIn = mapproperties.maxzoom.map(mz => mapproperties.zoom.data < mz, [mapproperties.zoom] )
|
||||
let canZoomOut = mapproperties.minzoom.map(mz => mapproperties.zoom.data > mz, [mapproperties.zoom] )
|
||||
function updateViewport() {
|
||||
const rect = viewport.data?.getBoundingClientRect()
|
||||
if (!rect) {
|
||||
|
@ -148,7 +149,7 @@
|
|||
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
|
||||
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
|
||||
let rasterLayerName =
|
||||
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
|
||||
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.defaultBackgroundLayer.properties.name
|
||||
onDestroy(
|
||||
rasterLayer.addCallbackAndRunD((l) => {
|
||||
rasterLayerName = l.properties.name
|
||||
|
@ -179,7 +180,7 @@
|
|||
</script>
|
||||
|
||||
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
|
||||
<MaplibreMap map={maplibremap} />
|
||||
<MaplibreMap map={maplibremap} mapProperties={mapproperties} />
|
||||
</div>
|
||||
|
||||
{#if $visualFeedback}
|
||||
|
@ -256,6 +257,9 @@
|
|||
<If condition={state.featureSwitchIsTesting}>
|
||||
<div class="alert w-fit">Testmode</div>
|
||||
</If>
|
||||
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
||||
<div class="alert w-fit">Faking a user (Testmode)</div>
|
||||
</If>
|
||||
</div>
|
||||
<div class="flex w-full flex-col items-center justify-center">
|
||||
<!-- Flex and w-full are needed for the positioning -->
|
||||
|
@ -329,12 +333,14 @@
|
|||
</If>
|
||||
<MapControlButton
|
||||
arialabel={Translations.t.general.labels.zoomIn}
|
||||
enabled={canZoomIn}
|
||||
on:click={() => mapproperties.zoom.update((z) => z + 1)}
|
||||
on:keydown={forwardEventToMap}
|
||||
>
|
||||
<Plus class="h-8 w-8" />
|
||||
</MapControlButton>
|
||||
<MapControlButton
|
||||
enabled={canZoomOut}
|
||||
arialabel={Translations.t.general.labels.zoomOut}
|
||||
on:click={() => mapproperties.zoom.update((z) => z - 1)}
|
||||
on:keydown={forwardEventToMap}
|
||||
|
@ -402,7 +408,7 @@
|
|||
<div slot="close-button" />
|
||||
<div class="normal-background absolute flex h-full w-full flex-col">
|
||||
<SelectedElementTitle {state} layer={$selectedLayer} selectedElement={$selectedElement} />
|
||||
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
|
||||
<SelectedElementView {state} selectedElement={$selectedElement} />
|
||||
</div>
|
||||
</ModalRight>
|
||||
{/if}
|
||||
|
@ -580,10 +586,9 @@
|
|||
</div>
|
||||
<SelectedElementView
|
||||
highlightedRendering={state.guistate.highlightedUserSetting}
|
||||
layer={UserRelatedState.usersettingsConfig}
|
||||
selectedElement={{
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
properties: {id:"settings"},
|
||||
geometry: { type: "Point", coordinates: [0, 0] },
|
||||
}}
|
||||
{state}
|
||||
|
|
|
@ -20,7 +20,7 @@ export default class WikidataSearchBox extends InputElement<string> {
|
|||
new Table(
|
||||
["name", "doc"],
|
||||
[
|
||||
["key", "the value of this tag will initialize search (default: name)"],
|
||||
["key", "the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search"],
|
||||
[
|
||||
"options",
|
||||
new Combine([
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue