Merge master

This commit is contained in:
Pieter Vander Vennet 2024-04-02 19:07:11 +02:00
commit 890816d2dd
424 changed files with 40595 additions and 3354 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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">
&lt;span class="literal-code iframe-code-block"&gt; <br />
&lt;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"&gt;
<br />
&lt;/iframe&gt;
<br />
&lt;/span&gt;
</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">
&lt;span class="literal-code iframe-code-block"&gt; <br />
&lt;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"&gt;
<br />
&lt;/iframe&gt;
<br />
&lt;/span&gt;
<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>

View file

@ -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 ?? []) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -52,7 +52,4 @@ export default class OpeningHoursValidator extends Validator {
)
}
reformat(s: string, _?: () => string): string {
return super.reformat(s, _)
}
}

View file

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

View file

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

View file

@ -74,4 +74,4 @@
style="z-index: 100">
<StyleLoadingIndicator map={altmap} />
</div>
<MaplibreMap {interactive} map={altmap} />
<MaplibreMap {interactive} map={altmap} mapProperties={altproperties} />

View file

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

View file

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

View file

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

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

View file

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

View file

@ -364,7 +364,7 @@
</div>
</div>
{:else}
<Loading>Creating point...</Loading>
<Loading><Tr t={Translations.t.general.add.creating}/> </Loading>
{/if}
</div>
</LoginToggle>

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -76,6 +76,5 @@
{value}
{state}
on:submit
{unvalidatedText}
/>
</div>

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,6 +42,7 @@
)
let osmConnection = new OsmConnection({
oauth_token,
checkOnlineRegularly: true
})
const expertMode = UIEventSource.asBoolean(
osmConnection.GetPreference("studio-expert-mode", "false", {

View file

@ -2,4 +2,4 @@
</script>
No tests

View file

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

View file

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