Merge develop

This commit is contained in:
Pieter Vander Vennet 2025-08-26 23:52:32 +02:00
commit cac87a5467
727 changed files with 27522 additions and 15764 deletions

View file

@ -61,7 +61,7 @@
let userLanguages = osmConnection.userDetails.map((ud) => ud?.languages ?? [])
let search: UIEventSource<string | undefined> = new UIEventSource<string>("")
let searchStable = search.stabilized(100)
let searchStable: Store<string | undefined> = search.stabilized(100)
const officialThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
(th) => th.hideFromOverview === false
@ -83,13 +83,12 @@
).mapD((stableIds) => Lists.noNullInplace(stableIds.map((id) => state.getUnofficialTheme(id))))
function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> {
const searchIndex = Locale.language.map(
(language) => {
return new ThemeSearchIndex(language, themes.data)
},
[themes]
const searchIndex = themes.mapD(
(themes) => new ThemeSearchIndex(Locale.language.data, themes),
[Locale.language]
)
return searchStable.map(
(searchTerm) => {
if (!themes.data) {
@ -99,11 +98,9 @@
return themes.data
}
const index = searchIndex.data
return index.search(searchTerm)
return searchIndex.data?.search(searchTerm)
},
[searchIndex]
[searchIndex, themes]
)
}
@ -134,9 +131,6 @@
{ returnToIndex: new ImmutableStore(false) }
)
const topSPace = AndroidPolyfill.getInsetSizes().top
const bottom = AndroidPolyfill.getInsetSizes().bottom
/**
* Opens the first search candidate
*/
@ -156,11 +150,14 @@
</script>
<main>
<div class="low-interaction w-full">
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top} />
</div>
<DrawerLeft shown={guistate.pageStates.menu}>
<MenuDrawerIndex onlyLink={true} state={menuDrawerState} />
</DrawerLeft>
<div class="flex w-screen flex-col p-2 sm:p-4">
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top}/>
<!-- Top menu bar -->
<div class="flex w-full justify-between overflow-hidden pb-4">
<button on:click={() => guistate.pageStates.menu.set(true)} class="m-0 rounded-full p-2">
@ -183,7 +180,7 @@
<div class="link-underline flex w-full flex-col">
<div class="flex items-center">
<div class="m-1 flex-none md:hidden">
<Logo alt="MapComplete Logo" class="mr-1 sm:mr-2 md:mr-4 h-10 w-10 sm:h-20 sm:w-20" />
<Logo alt="MapComplete Logo" class="mr-1 h-10 w-10 sm:mr-2 sm:h-20 sm:w-20 md:mr-4" />
</div>
<div class="flex flex-col">
<h1 class="m-0 break-words font-extrabold tracking-tight md:text-6xl">
@ -272,8 +269,7 @@
v{Constants.vNumber}
</div>
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom}/>
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom} />
<div class="absolute top-0 h-0 w-0" style="margin-left: -10em">
<MenuDrawer onlyLink={false} state={menuDrawerState} />

View file

@ -15,7 +15,7 @@
<SubtleButton
on:click={() => dispatch("click")}
options={{ extraClasses: twMerge("flex items-center", clss) }}
options={{ extraClasses: twMerge("flex items-center justify-start", clss) }}
>
<ChevronLeftIcon class={imageClass ?? "h-12 w-12"} slot="image" />
<slot slot="message" />

View file

@ -36,11 +36,10 @@ export interface TagRenderingChartOptions {
groupToOtherCutoff?: 3 | number
sort?: boolean
hideUnkown?: boolean
hideNotApplicable?: boolean,
hideNotApplicable?: boolean
chartType?: "pie" | "bar" | "doughnut"
}
export class ChartJsUtils {
/**
* Gets the 'date' out of all features.properties,
* returns a range with all dates from 'earliest' to 'latest' as to get one continuous range
@ -178,7 +177,7 @@ export class ChartJsUtils {
}
data.push(...categoryCounts, ...otherData)
labels.push(...(mappings?.map((m) => m.then.txt) ?? []), ...otherLabels)
if(data.length === 0){
if (data.length === 0) {
return undefined
}
return { labels, data }
@ -196,139 +195,139 @@ export class ChartJsUtils {
* @param options
*/
static createPerDayConfigForTagRendering(
tr: TagRenderingConfig,
features: (OsmFeature & { properties: { date: string } })[],
options?: {
period: "day" | "month"
groupToOtherCutoff?: 3 | number
// If given, take the sum of these fields to get the feature weight
sumFields?: ReadonlyArray<string>
hideUnknown?: boolean
hideNotApplicable?: boolean
}
tr: TagRenderingConfig,
features: (OsmFeature & { properties: { date: string } })[],
options?: {
period: "day" | "month"
groupToOtherCutoff?: 3 | number
// If given, take the sum of these fields to get the feature weight
sumFields?: ReadonlyArray<string>
hideUnknown?: boolean
hideNotApplicable?: boolean
}
): ChartConfiguration {
const { labels, data } = ChartJsUtils.extractDataAndLabels(tr, features, {
sort: true,
groupToOtherCutoff: options?.groupToOtherCutoff,
hideNotApplicable: options?.hideNotApplicable,
hideUnkown: options?.hideUnknown,
})
if (labels === undefined || data === undefined) {
console.error(
"Could not extract data and labels for ",
tr,
" with features",
features,
": no labels or no data"
)
throw "No labels or data given..."
const { labels, data } = ChartJsUtils.extractDataAndLabels(tr, features, {
sort: true,
groupToOtherCutoff: options?.groupToOtherCutoff,
hideNotApplicable: options?.hideNotApplicable,
hideUnkown: options?.hideUnknown,
})
if (labels === undefined || data === undefined) {
console.error(
"Could not extract data and labels for ",
tr,
" with features",
features,
": no labels or no data"
)
throw "No labels or data given..."
}
for (let i = labels.length; i >= 0; i--) {
if (data[i]?.length != 0) {
continue
}
data.splice(i, 1)
labels.splice(i, 1)
}
const datasets: {
label: string /*themename*/
data: number[] /*counts per day*/
backgroundColor: string
}[] = []
const allDays = ChartJsUtils.getAllDays(features)
let trimmedDays = allDays.map((d) => d.substring(0, 10))
if (options?.period === "month") {
trimmedDays = trimmedDays.map((d) => d.substring(0, 7))
}
trimmedDays = Lists.dedup(trimmedDays)
for (let i = 0; i < labels.length; i++) {
const label = labels[i]
const changesetsForTheme = data[i]
const perDay: Record<string, OsmFeature[]> = {}
for (const changeset of changesetsForTheme) {
const csDate = new Date(changeset.properties.date)
Utils.SetMidnight(csDate)
let str = csDate.toISOString()
str = str.substr(0, 10)
if (options?.period === "month") {
str = str.substr(0, 7)
}
if (perDay[str] === undefined) {
perDay[str] = [changeset]
} else {
perDay[str].push(changeset)
}
}
for (let i = labels.length; i >= 0; i--) {
if (data[i]?.length != 0) {
const countsPerDay: number[] = []
for (let i = 0; i < trimmedDays.length; i++) {
const day = trimmedDays[i]
const featuresForDay = perDay[day]
if (!featuresForDay) {
continue
}
data.splice(i, 1)
labels.splice(i, 1)
}
const datasets: {
label: string /*themename*/
data: number[] /*counts per day*/
backgroundColor: string
}[] = []
const allDays = ChartJsUtils.getAllDays(features)
let trimmedDays = allDays.map((d) => d.substring(0, 10))
if (options?.period === "month") {
trimmedDays = trimmedDays.map((d) => d.substring(0, 7))
}
trimmedDays = Lists.dedup(trimmedDays)
for (let i = 0; i < labels.length; i++) {
const label = labels[i]
const changesetsForTheme = data[i]
const perDay: Record<string, OsmFeature[]> = {}
for (const changeset of changesetsForTheme) {
const csDate = new Date(changeset.properties.date)
Utils.SetMidnight(csDate)
let str = csDate.toISOString()
str = str.substr(0, 10)
if (options?.period === "month") {
str = str.substr(0, 7)
}
if (perDay[str] === undefined) {
perDay[str] = [changeset]
} else {
perDay[str].push(changeset)
}
}
const countsPerDay: number[] = []
for (let i = 0; i < trimmedDays.length; i++) {
const day = trimmedDays[i]
const featuresForDay = perDay[day]
if (!featuresForDay) {
continue
}
if (options.sumFields !== undefined) {
let sum = 0
for (const featuresForDayElement of featuresForDay) {
const props = featuresForDayElement.properties
for (const key of options.sumFields) {
if (!props[key]) {
continue
}
const v = Number(props[key])
if (isNaN(v)) {
continue
}
sum += v
if (options.sumFields !== undefined) {
let sum = 0
for (const featuresForDayElement of featuresForDay) {
const props = featuresForDayElement.properties
for (const key of options.sumFields) {
if (!props[key]) {
continue
}
const v = Number(props[key])
if (isNaN(v)) {
continue
}
sum += v
}
countsPerDay[i] = sum
} else {
countsPerDay[i] = featuresForDay?.length ?? 0
}
countsPerDay[i] = sum
} else {
countsPerDay[i] = featuresForDay?.length ?? 0
}
let backgroundColor =
ChartJsColours.borderColors[i % ChartJsColours.borderColors.length]
if (label === "Unknown") {
backgroundColor = ChartJsColours.unknownBorderColor
}
if (label === "Other") {
backgroundColor = ChartJsColours.otherBorderColor
}
datasets.push({
data: countsPerDay,
backgroundColor,
label,
})
}
const perDayData = {
labels: trimmedDays,
datasets,
let backgroundColor =
ChartJsColours.borderColors[i % ChartJsColours.borderColors.length]
if (label === "Unknown") {
backgroundColor = ChartJsColours.unknownBorderColor
}
if (label === "Other") {
backgroundColor = ChartJsColours.otherBorderColor
}
datasets.push({
data: countsPerDay,
backgroundColor,
label,
})
}
return <ChartConfiguration>{
type: "bar",
data: perDayData,
options: {
responsive: true,
legend: {
display: false,
const perDayData = {
labels: trimmedDays,
datasets,
}
return <ChartConfiguration>{
type: "bar",
data: perDayData,
options: {
responsive: true,
legend: {
display: false,
},
scales: {
x: {
stacked: true,
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
},
y: {
stacked: true,
},
},
}
},
}
}
/**
@ -336,17 +335,16 @@ export class ChartJsUtils {
*
* @returns undefined if not enough parameters
*/
static createConfigForTagRendering<T extends { properties: Record<string, string> }>(tagRendering: TagRenderingConfig, features: T[],
options?: TagRenderingChartOptions): ChartConfiguration{
static createConfigForTagRendering<T extends { properties: Record<string, string> }>(
tagRendering: TagRenderingConfig,
features: T[],
options?: TagRenderingChartOptions
): ChartConfiguration {
if (tagRendering.mappings?.length === 0 && tagRendering.freeform?.key === undefined) {
return undefined
}
const { labels, data } = ChartJsUtils.extractDataAndLabels(
tagRendering,
features,
options
)
const { labels, data } = ChartJsUtils.extractDataAndLabels(tagRendering, features, options)
if (labels === undefined || data === undefined) {
return undefined
}
@ -403,22 +401,18 @@ export class ChartJsUtils {
},
},
}
}
static createHistogramConfig(keys: string[], counts: Map<string, number>){
const borderColor = [
]
const backgroundColor = [
]
static createHistogramConfig(keys: string[], counts: Map<string, number>) {
const borderColor = []
const backgroundColor = []
while (borderColor.length < keys.length) {
borderColor.push(...ChartJsColours.borderColors)
backgroundColor.push(...ChartJsColours.backgroundColors)
}
return <ChartConfiguration>{
return <ChartConfiguration>{
type: "bar",
data: {
labels: keys,
@ -432,11 +426,12 @@ export class ChartJsUtils {
},
],
},
options: { scales: {
options: {
scales: {
y: {
ticks: {
stepSize: 1,
callback: (value) =>Number(value).toFixed(0),
callback: (value) => Number(value).toFixed(0),
},
},
},

View file

@ -12,7 +12,7 @@
export let state: ThemeViewState = undefined
async function shareCurrentLink() {
const title = state?.theme?.title?.txt ?? "MapComplete";
const title = state?.theme?.title?.txt ?? "MapComplete"
const textToShow = state?.theme?.description?.txt ?? ""
await navigator.share({
title,

View file

@ -4,12 +4,12 @@
export let size = "w-12 h-12"
</script>
<div class={size+" relative"}>
<div class="absolute top-0 left-0">
<div class={size + " relative"}>
<div class="absolute left-0 top-0">
<slot />
</div>
<div class="absolute top-0 left-0">
<div class="absolute left-0 top-0">
<Cross_bottom_right class={size} color="red" />
</div>
</div>

View file

@ -32,7 +32,10 @@
return { bearing, dist }
}
)
let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD((coordinate) => GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter),onDestroy)
let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD(
(coordinate) => GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter),
onDestroy
)
let compass = Orientation.singleton.alpha
let relativeDirections = Translations.t.general.visualFeedback.directionsRelative
@ -102,7 +105,8 @@
})
return mainTr.textFor(lang)
},
[compass, Locale.language], onDestroy
[compass, Locale.language],
onDestroy
)
let label = labelFromCenter.map(
@ -115,7 +119,8 @@
}
return labelFromCenter
},
[labelFromGps], onDestroy
[labelFromGps],
onDestroy
)
function focusMap() {

View file

@ -21,14 +21,13 @@
// Does not need a 'top-inset-spacer' as the code below will apply the padding automatically
let height = 0
function setHeight(){
function setHeight() {
let topbar = document.getElementById("top-bar")
height = (topbar?.clientHeight ?? 0) + AndroidPolyfill.getInsetSizes().top.data
}
onMount(() => setHeight())
AndroidPolyfill.getInsetSizes().top.addCallback(() => setHeight())
</script>
<Drawer
@ -43,15 +42,14 @@
rightOffset="inset-y-0 right-0"
bind:hidden
>
<div class="flex flex-col h-full">
<div class="low-interaction h-screen">
<div style={`padding-top: ${height}px`}>
<div class="flex h-full flex-col overflow-y-auto">
<slot />
<div class="flex h-full flex-col">
<div class="low-interaction h-screen">
<div style={`padding-top: ${height}px`}>
<div class="flex h-full flex-col overflow-y-auto">
<slot />
</div>
</div>
</div>
</div>
<InsetSpacer clss="low-interaction" height={AndroidPolyfill.getInsetSizes().bottom}/>
<InsetSpacer clss="low-interaction" height={AndroidPolyfill.getInsetSizes().bottom} />
</div>
</Drawer>

View file

@ -24,7 +24,9 @@
<div
class="pointer-events-none absolute bottom-0 right-0 h-full w-screen p-4 md:p-6"
style="z-index: 21"
on:click={() => { dispatch("close") }}
on:click={() => {
dispatch("close")
}}
>
<div
class="content normal-background pointer-events-auto relative h-full"
@ -44,9 +46,9 @@
</div>
<style>
.content {
border-radius: 0.5rem;
overflow-x: hidden;
box-shadow: 0 0 1rem #00000088;
}
.content {
border-radius: 0.5rem;
overflow-x: hidden;
box-shadow: 0 0 1rem #00000088;
}
</style>

View file

@ -8,4 +8,4 @@
export let clss: string = ""
</script>
<div class={clss+" shrink-0"} style={"height: "+$height+"px"} />
<div class={clss + " shrink-0"} style={"height: " + $height + "px"} />

View file

@ -22,7 +22,7 @@
use:ariaLabelStore={arialabelString}
disabled={!$enabled}
class={twJoin(
"pointer-events-auto relative h-fit w-fit rounded-full",
"pointer-events-auto relative h-fit w-fit rounded-full border-gray-500",
cls,
$enabled ? "" : "disabled"
)}

View file

@ -34,16 +34,16 @@
</script>
{#if $showButton}
<button class="as-link sidebar-button" on:click={openJosm}>
<Josm_logo class="h-6 w-6" />
<Tr t={t.editJosm} />
</button>
<button class="as-link sidebar-button" on:click={openJosm}>
<Josm_logo class="h-6 w-6" />
<Tr t={t.editJosm} />
</button>
{#if $josmState === undefined}
<!-- empty -->
{:else if $josmState === "OK"}
<Tr cls="thanks shrink-0 w-fit" t={t.josmOpened} />
{:else}
<Tr cls="alert shrink-0 w-fit" t={t.josmNotOpened} />
{/if}
{#if $josmState === undefined}
<!-- empty -->
{:else if $josmState === "OK"}
<Tr cls="thanks shrink-0 w-fit" t={t.josmOpened} />
{:else}
<Tr cls="alert shrink-0 w-fit" t={t.josmNotOpened} />
{/if}
{/if}

View file

@ -18,7 +18,8 @@
const shared =
"in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md"
let defaultClass = "relative flex flex-col mx-auto w-full divide-y border-4 border-red-500 " + shared
let defaultClass =
"relative flex flex-col mx-auto w-full divide-y border-4 border-red-500 " + shared
if (fullscreen) {
defaultClass = shared
}
@ -43,7 +44,6 @@
})
let marginTop = AndroidPolyfill.getInsetSizes().top
let marginBottom = AndroidPolyfill.getInsetSizes().bottom
</script>
<Modal

View file

@ -44,7 +44,7 @@
<form class="w-full" on:submit|preventDefault={() => dispatch("search")}>
<label
class="neutral-label normal-background box-shadow flex w-full items-center rounded-full border border-black"
class="neutral-label normal-background box-shadow flex w-full items-center rounded-md border border-black"
>
<SearchIcon aria-hidden="true" class="ml-2 h-6 w-6 shrink-0" />
@ -76,7 +76,7 @@
value.set("")
e.preventDefault()
}}
color="var(--button-background)"
color="var(--foreground-color)"
class="mr-3 h-6 w-6 cursor-pointer"
/>
{:else}

View file

@ -16,7 +16,8 @@
let layer = state.getMatchingLayer(selected.properties)
let stillMatches = tags.map(
(tags) => !layer?.source?.osmTags || layer?.source?.osmTags?.matchesProperties(tags), onDestroy
(tags) => !layer?.source?.osmTags || layer?.source?.osmTags?.matchesProperties(tags),
onDestroy
)
onDestroy(
stillMatches.addCallbackAndRunD((matches) => {

View file

@ -2,7 +2,6 @@ import BaseUIElement from "../BaseUIElement"
import { SvelteComponentTyped } from "svelte"
/**
* The SvelteUIComponent serves as a translating class which which wraps a SvelteElement into the BaseUIElement framework.
* Also see ToSvelte.svelte for the opposite conversion

View file

@ -187,7 +187,7 @@
}
:global(.tab-selected svg) {
fill: var(--catch-detail-color-contrast);
fill: var(--interactive-contrast);
}
:global(.tab-unselected) {

View file

@ -1,3 +1,6 @@
<script >
export let contentStyle= ""
</script>
<div class="flex h-full flex-col">
<div class="low-interaction flex items-center justify-between p-4 drop-shadow-md">
<div class="flex items-center gap-x-2">
@ -9,7 +12,7 @@
<slot name="title-end" />
</div>
<div class="flex h-full flex-col overflow-auto border-b-2 p-4">
<div class={"flex h-full flex-col overflow-auto border-b-2 p-4 "+contentStyle}>
<slot />
</div>

View file

@ -79,7 +79,6 @@ export default abstract class BaseUIElement {
}
}
return el
} catch (e) {
const domExc = e as DOMException

View file

@ -1,56 +1,67 @@
<script lang="ts">/**
* Show a compass. The compass outline rotates with the map, the compass needle points to the actual north
*/
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Orientation } from "../../Sensors/Orientation"
import Compass_back from "../../assets/svg/Compass_back.svelte"
import Compass_needle from "../../assets/svg/Compass_needle.svelte"
import North_arrow from "../../assets/svg/North_arrow.svelte"
<script lang="ts">
/**
* Show a compass. The compass outline rotates with the map, the compass needle points to the actual north
*/
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Orientation } from "../../Sensors/Orientation"
import Compass_back from "../../assets/svg/Compass_back.svelte"
import Compass_needle from "../../assets/svg/Compass_needle.svelte"
import North_arrow from "../../assets/svg/North_arrow.svelte"
export let mapProperties: { rotation: UIEventSource<number>, allowRotating: Store<boolean> }
let orientation = Orientation.singleton.alpha
let gotNonZero = new UIEventSource(false)
orientation.addCallbackAndRunD(o => {
if (o !== undefined && o !== 0) {
gotNonZero.set(true)
}
})
let mapRotation = mapProperties.rotation
export let size = "h-10 w-10"
let wrapperClass = "absolute top-0 left-0 " + size
let compassLoaded = Orientation.singleton.gotMeasurement
let allowRotation = mapProperties.allowRotating
function clicked(e: Event) {
if (mapProperties.rotation.data === 0) {
if (mapProperties.allowRotating.data && compassLoaded.data) {
mapProperties.rotation.set(orientation.data)
export let mapProperties: { rotation: UIEventSource<number>; allowRotating: Store<boolean> }
let orientation = Orientation.singleton.alpha
let gotNonZero = new UIEventSource(false)
orientation.addCallbackAndRunD((o) => {
if (o !== undefined && o !== 0) {
gotNonZero.set(true)
}
} else {
mapProperties.rotation.set(0)
}
}
})
let mapRotation = mapProperties.rotation
export let size = "h-10 w-10"
let wrapperClass = "absolute top-0 left-0 " + size
let compassLoaded = Orientation.singleton.gotMeasurement
let allowRotation = mapProperties.allowRotating
function clicked() {
if (mapProperties.rotation.data === 0) {
if (mapProperties.allowRotating.data && compassLoaded.data) {
mapProperties.rotation.set(orientation.data)
}
} else {
mapProperties.rotation.set(0)
}
}
</script>
{#if $allowRotation || !$gotNonZero}
<button style="z-index: -1" class={"relative as-link pointer-events-auto "+size} on:click={(e) => clicked(e)}>
{#if $allowRotation && !$compassLoaded && !$gotNonZero}
<div class={"border-2 rounded-full border-gray-500 border-dotted "+wrapperClass}
style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}>
<North_arrow class="w-full" />
</div>
{:else}
{#if $allowRotation}
<div class={wrapperClass} style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}>
<Compass_back class="w-full" />
{#if $allowRotation || $gotNonZero}
<button
class={"as-link pointer-events-auto relative " + size}
on:click={() => clicked()}
>
{#if $allowRotation && !$compassLoaded && !$gotNonZero}
<div
class={"rounded-full border-2 border-dotted border-gray-500 " + wrapperClass}
style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}
>
<North_arrow class="w-full" />
</div>
{:else}
{#if $allowRotation}
<div
class={wrapperClass}
style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}
>
<Compass_back class="w-full" />
</div>
{/if}
{#if $compassLoaded && $gotNonZero}
<div
class={wrapperClass + (!$allowRotation ? " rounded-full bg-white bg-opacity-50" : "")}
style={`transform: rotate(${-$orientation}deg)`}
>
<Compass_needle class="w-full" />
</div>
{/if}
{/if}
{#if $compassLoaded && $gotNonZero}
<div class={wrapperClass+ (!$allowRotation ? " rounded-full bg-white bg-opacity-50" : "")}
style={`transform: rotate(${-$orientation}deg)`}>
<Compass_needle class="w-full" />
</div>
{/if}
{/if}
</button>
</button>
{/if}

View file

@ -151,12 +151,12 @@
</div>
<div class="mt-8 flex flex-col items-center gap-x-2 border-t border-dashed border-gray-300 pt-4">
<div class="mr-4 flex w-96">
<div class="mr-4 flex w-96 flex-wrap items-center justify-center md:flex-nowrap">
<a href="https://nlnet.nl/entrust" class="p-2">
<NGI0Entrust_tag />
<NGI0Entrust_tag class="w-48 grow-0" />
</a>
<a href="https://nlnet.nl" class="p-2">
<Nlnet />
<Nlnet class="w-48 grow-0" />
</a>
</div>
<span>

View file

@ -16,7 +16,6 @@
function showFor(timeoutSeconds: number = 3) {
open = true
console.trace("Showing gpshelperelement")
window.setTimeout(() => {
open = false
}, timeoutSeconds * 1000)

View file

@ -6,29 +6,31 @@
import { Lists } from "../../Utils/Lists"
export let values: Store<string[]>
let counts: Store<Map<string, number>> = values.map(
(values) => {
if (values === undefined) {
return undefined
}
let counts: Store<Map<string, number>> = values.map((values) => {
if (values === undefined) {
return undefined
}
values = Utils.noNull(values)
const counts = new Map<string, number>()
for (const value of values) {
const c = counts.get(value) ?? 0
counts.set(value, c + 1)
}
values = Utils.noNull(values)
const counts = new Map<string, number>()
for (const value of values) {
const c = counts.get(value) ?? 0
counts.set(value, c + 1)
}
return counts
})
return counts
})
let max: Store<number> = counts.mapD(counts => Math.max(...Array.from(counts.values())))
let keys: Store<string> = counts.mapD(counts => {
let max: Store<number> = counts.mapD((counts) => Math.max(...Array.from(counts.values())))
let keys: Store<string> = counts.mapD((counts) => {
const keys = Lists.dedup(counts.keys())
keys.sort(/*inplace sort*/)
return keys
})
let config: Store<ChartConfiguration> = keys.mapD(keys => ChartJsUtils.createHistogramConfig(keys, counts.data), [counts])
let config: Store<ChartConfiguration> = keys.mapD(
(keys) => ChartJsUtils.createHistogramConfig(keys, counts.data),
[counts]
)
</script>
{#if $config}

View file

@ -51,7 +51,8 @@
}
})
let hotkeys = Hotkeys._docs
let showBackground = state.featureSwitches.featureSwitchBackgroundSelection
let showFilters = state.featureSwitches.featureSwitchFilter
</script>
<div class:h-0={!onlyLink} class:h-full={onlyLink} class="overflow-hidden">
@ -76,10 +77,12 @@
<ThemeIntroPanel {state} />
</Page>
<FilterPage {onlyLink} {state} />
<RasterLayerOverview {onlyLink} {state} />
{#if $showFilters}
<FilterPage {onlyLink} {state} />
{/if}
{#if $showBackground}
<RasterLayerOverview {onlyLink} {state} />
{/if}
<Page {onlyLink} shown={pg.share}>
<svelte:fragment slot="header">
<Share />

View file

@ -30,18 +30,15 @@
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import usersettings from "../../../public/assets/generated/layers/usersettings.json"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
import Favourites from "../Favourites/Favourites.svelte"
import ReviewsOverview from "../Reviews/ReviewsOverview.svelte"
import Share from "@babeard/svelte-heroicons/solid/Share"
import LogoutButton from "../Base/LogoutButton.svelte"
import { BoltIcon, ShareIcon } from "@babeard/svelte-heroicons/mini"
import Copyright from "../../assets/svg/Copyright.svelte"
import Pencil from "../../assets/svg/Pencil.svelte"
import SidebarUnit from "../Base/SidebarUnit.svelte"
import Squares2x2 from "@babeard/svelte-heroicons/mini/Squares2x2"
import EnvelopeOpen from "@babeard/svelte-heroicons/mini/EnvelopeOpen"
import { UIEventSource } from "../../Logic/UIEventSource"
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
import MagnifyingGlassCircle from "@babeard/svelte-heroicons/mini/MagnifyingGlassCircle"
import { AndroidPolyfill } from "../../Logic/Web/AndroidPolyfill"
import Forgejo from "../../assets/svg/Forgejo.svelte"
@ -52,7 +49,6 @@
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import type { MapProperties } from "../../Models/MapProperties"
import FavouritesFeatureSource from "../../Logic/FeatureSource/Sources/FavouritesFeatureSource"
import Hotkeys from "../Base/Hotkeys"
import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp"
import ArrowTopRightOnSquare from "@babeard/svelte-heroicons/mini/ArrowTopRightOnSquare"
import { PhotoIcon } from "@babeard/svelte-heroicons/outline"
@ -84,7 +80,7 @@
let usersettingslayer = new LayerConfig(<LayerConfigJson>usersettings, "usersettings", true)
let featureSwitches = state.featureSwitches
let showHome = featureSwitches?.featureSwitchBackToThemeOverview
let showHome = featureSwitches?.featureSwitchBackToThemeOverview ?? new ImmutableStore(true)
let pg = state.guistate.pageStates
let pendingChanges = state?.changes?.pendingChanges
export let onlyLink: boolean
@ -105,16 +101,15 @@
let isAndroid = AndroidPolyfill.inAndroid
let nrOfFailedImages = ImageUploadQueue.singleton.imagesInQueue
let failedImagesOpen = pg.failedImages
let loggedIn = state.osmConnection.isLoggedIn
</script>
<div class="low-interaction flex h-full flex-col overflow-hidden" class:hidden={!$shown}>
{#if onlyLink}
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top} />
{/if}
<div class="flex justify-between border-b border-black p-2">
<h2>
<div class="flex justify-between items-center border-b border-black p-2">
<h2 class="m-0">
<Tr t={t.title} />
</h2>
<CloseButton
@ -126,7 +121,7 @@
<div class="flex flex-col gap-y-2 overflow-y-auto px-2 sm:gap-y-3 sm:px-3">
{#if $showHome}
<a class="button primary flex" href={Utils.HomepageLink()}>
<a class="button flex" class:primary={$loggedIn} href={Utils.HomepageLink()}>
<Squares2x2 class="h-10 w-10" />
{#if Utils.isIframe}
<Tr t={Translations.t.general.seeIndex} />
@ -139,13 +134,14 @@
<!-- User related: avatar, settings, favourits, logout -->
<SidebarUnit>
<LoginToggle {state} offline>
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in" />
<div class="flex items-center gap-x-4 w-full m-2">
<LoginButton clss="primary" osmConnection={state.osmConnection} slot="not-logged-in" />
<div class="m-2 flex w-full items-center gap-x-4">
<Avatar userdetails={state.osmConnection.userDetails} />
<div class="flex flex-col w-full gap-y-2">
<b>{$userdetails?.name ?? '<Username>'}</b>
<LogoutButton clss="as-link small subtle text-sm" osmConnection={state.osmConnection} />
<div class="flex w-full flex-col gap-y-2">
<b>{$userdetails.name}</b>
<LogoutButton clss="as-link small subtle text-sm" osmConnection={state.osmConnection} />
</div>
</div>
</div>
</LoginToggle>
@ -214,7 +210,8 @@
<LanguagePicker
preferredLanguages={state.userRelatedState.osmConnection.userDetails.mapD(
(ud) => ud.languages, onDestroy
(ud) => ud.languages,
onDestroy
)}
/>
</SidebarUnit>
@ -228,16 +225,12 @@
<Tr t={Translations.t.general.menu.aboutMapComplete} />
</h3>
<a
class="flex"
href={($isAndroid
? "https://mapcomplete.org"
: ".")+"/studio.html"}
>
<Pencil class="mr-2 h-6 w-6" />
<Tr t={Translations.t.general.morescreen.createYourOwnTheme} />
</a>
{#if $showHome}
<a class="flex" href={($isAndroid ? "https://mapcomplete.org" : ".") + "/studio.html"}>
<Pencil class="mr-2 h-6 w-6" />
<Tr t={Translations.t.general.morescreen.createYourOwnTheme} />
</a>
{/if}
<a class="flex" href="mailto:info@mapcomplete.org">
<EnvelopeOpen class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.emailCreators} />
@ -343,5 +336,4 @@
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom} />
{/if}
</div>
</div>

View file

@ -76,7 +76,10 @@
allowMoving: new UIEventSource<boolean>(true),
allowZooming: new UIEventSource<boolean>(true),
minzoom: new UIEventSource<number>(18),
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
rasterLayer: state.mapProperties.rasterLayer.followingClone(),
allowRotating: state.mapProperties.allowRotating,
rotation: state.mapProperties.rotation.followingClone()
}
state?.showCurrentLocationOn(map)
@ -114,7 +117,7 @@
})
}
const snappedLocation = new SnappingFeatureSource(
new FeatureSourceMerger(...(Lists.noNull(snapSources))),
new FeatureSourceMerger(...Lists.noNull(snapSources)),
// We snap to the (constantly updating) map location
mapProperties.location,
{

View file

@ -1,5 +1,4 @@
<script lang="ts">
import type { Map as MlMap } from "maplibre-gl"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
@ -21,7 +20,10 @@
import type { AreaDescription } from "../../Logic/OfflineBasemapManager"
import { OfflineBasemapManager } from "../../Logic/OfflineBasemapManager"
import Checkbox from "../Base/Checkbox.svelte"
import Translations from "../i18n/Translations"
import { default as Trans } from "../Base/Tr.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import { Lists } from "../../Utils/Lists"
export let state: ThemeViewState & SpecialVisualizationState = undefined
export let autoDownload = state.autoDownloadOfflineBasemap
@ -35,20 +37,30 @@
mapProperties.location.set(state.mapProperties.location.data)
mapProperties.allowRotating.set(false)
const offlineMapManager = OfflineBasemapManager.singleton
let installing: Store<ReadonlyMap<string, object>> = offlineMapManager.installing
let installed = offlineMapManager.installedAreas
let focusTile: Store<{
x: number;
y: number;
z: number
} | undefined> = mapProperties.location.mapD(location => Tiles.embedded_tile(location.lat, location.lon, focusZ))
let focusTileIsInstalled = focusTile.mapD(tile => offlineMapManager.isInstalled(tile), [installed])
let focusTileIsInstalling = focusTile.mapD(tile => {
const { x, y, z } = tile
return installing.data?.has(`${z}-${x}-${y}.pmtiles`)
}, [installing])
let focusTile: Store<
| {
x: number
y: number
z: number
}
| undefined
> = mapProperties.location.mapD((location) =>
Tiles.embedded_tile(location.lat, location.lon, focusZ)
)
let focusTileIsInstalled = focusTile.mapD(
(tile) => offlineMapManager.isInstalled(tile),
[installed]
)
let focusTileIsInstalling = focusTile.mapD(
(tile) => {
const { x, y, z } = tile
return installing.data?.has(`${z}-${x}-${y}.pmtiles`)
},
[installing]
)
async function del(areaDescr: AreaDescription) {
await offlineMapManager.deleteArea(areaDescr)
@ -63,112 +75,125 @@
const f = Tiles.asGeojson(z, x, y)
f.properties = {
id: "center_point_" + z + "_" + x + "_" + y,
txt: "Tile " + x + " " + y
txt: "Tile " + x + " " + y,
}
return [f]
})
let installedFeature: Store<Feature<Polygon>[]> = installed.map(meta =>
(meta ?? [])
.map(area => {
const f = Tiles.asGeojson(area.minzoom, area.x, area.y)
f.properties = {
id: area.minzoom + "-" + area.x + "-" + area.y,
downloaded: "yes",
text: area.name + " " + new Date(area.dataVersion).toLocaleDateString() + " " + Utils.toHumanByteSize(Number(area.size))
}
return f
}
)
let installedFeature: Store<Feature<Polygon>[]> = installed.map((meta) =>
(meta ?? []).map((area) => {
const f = Tiles.asGeojson(area.minzoom, area.x, area.y)
f.properties = {
id: area.minzoom + "-" + area.x + "-" + area.y,
downloaded: "yes",
text:
area.name +
" " +
new Date(area.dataVersion).toLocaleDateString() +
" " +
Utils.toHumanByteSize(Number(area.size)),
}
return f
})
)
new ShowDataLayer(map, {
features: new StaticFeatureSource(installedFeature),
layer: new LayerConfig({
id: "downloaded",
source: "special",
lineRendering: [{
color: "blue",
width: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "1"
}
]
lineRendering: [
{
color: "blue",
width: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "1",
},
],
},
fillColor: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "#00000000",
},
],
},
},
fillColor: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "#00000000"
}
]
}
}],
],
pointRendering: [
{
location: ["point", "centroid"],
label: "{text}",
labelCss: "width: w-min",
labelCssClasses: "bg-white rounded px-2 items-center flex flex-col"
}
]
})
labelCssClasses: "bg-white rounded px-2 items-center flex flex-col",
},
],
}),
})
new ShowDataLayer(map, {
features: new StaticFeatureSource(focusTileFeature),
layer: new LayerConfig({
id: "focustile",
source: "special",
lineRendering: [{
color: "black"
}],
lineRendering: [
{
color: "black",
},
],
pointRendering: [
{
location: ["point", "centroid"],
label: "{txt}",
labelCss: "width: max-content",
labelCssClasses: "bg-white rounded px-2 flex"
}
]
})
labelCssClasses: "bg-white rounded px-2 flex",
},
],
}),
})
const t = Translations.t.offline
</script>
<div class="flex flex-col h-full max-h-leave-room">
<Checkbox selected={autoDownload}>Automatically download the basemap when browsing around</Checkbox>
<div>
If checked, MapComplete will automatically download the basemap to the cache for the area.
This results in bigger initial data loads, but requires less internet over the long run.
If you plan to visit a region with less connectivity, you can also select the area you want to download below.
</div>
<div class="max-h-leave-room flex h-full flex-col overflow-auto">
<Checkbox selected={autoDownload}>
<Trans t={t.autoCheckmark} />
</Checkbox>
<AccordionSingle noBorder>
<Trans slot="header" cls="text-sm" t={t.autoExplanationIntro} />
<div class="low-interaction">
<Trans t={t.autoExplanation} />
</div>
</AccordionSingle>
<div />
{#if $installed === undefined}
<Loading />
{:else}
<div class="h-full overflow-auto pb-16">
<div class="pb-16">
<Accordion class="" inactiveClass="text-black">
<AccordionItem paddingDefault="p-2">
<div slot="header">Map</div>
<div class="relative leave-room">
<div class="rounded-lg absolute top-0 left-0 h-full w-full">
<Trans slot="header" t={t.localOnMap} />
<div class="leave-room relative">
<div class="absolute left-0 top-0 h-full w-full rounded-lg">
<MaplibreMap {map} {mapProperties} />
</div>
<div
class="absolute top-0 left-0 h-full w-full flex flex-col justify-center items-center pointer-events-none">
<div class="w-16 h-32 mb-16"></div>
class="pointer-events-none absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center"
>
<div class="mb-16 h-32 w-16" />
{#if $focusTileIsInstalling}
<div class="normal-background rounded-lg">
<Loading>
Data is being downloaded
<Trans t={t.installing} />
</Loading>
</div>
{:else}
<button class="primary pointer-events-auto" on:click={() => download()}
class:disabled={$focusTileIsInstalled}>
<DownloadIcon class="w-8 h-8" />
Download
<button
class="primary pointer-events-auto"
on:click={() => download()}
class:disabled={$focusTileIsInstalled}
>
<DownloadIcon class="h-8 w-8" />
<Trans t={t.download} />
</button>
{/if}
</div>
@ -176,34 +201,42 @@
</AccordionItem>
<AccordionItem paddingDefault="p-2">
<div slot="header">
Offline tile management
</div>
<Trans t={t.overview} slot="header" />
<div class="leave-room">
{Utils.toHumanByteSize(Utils.sum($installed.map(area => area.size)))}
<button on:click={() => {
installed?.data?.forEach(area => del(area))
}}>
{Utils.toHumanByteSize(Lists.sum($installed.map((area) => area.size)))}
<button
on:click={() => {
installed?.data?.forEach((area) => del(area))
}}
>
<TrashIcon class="w-6" />
Delete all
<Trans t={t.deleteAll} />
</button>
<table class="w-full ">
<table class="w-full">
<tr>
<th>Name</th>
<th>Map generation date</th>
<th>Size</th>
<th>Zoom ranges</th>
<th>Actions</th>
<th>
<Trans t={t.name} />
</th>
<th>
<Trans t={t.date} />
</th>
<th>
<Trans t={t.size} />
</th>
<th>
<Trans t={t.range} />
</th>
<th>
<Trans t={t.actions} />
</th>
</tr>
{#each ($installed ?? []) as area }
{#each $installed ?? [] as area}
<tr>
<td>{area.name}</td>
<td>{area.dataVersion}</td>
<td>{Utils.toHumanByteSize(area.size ?? -1)}</td>
<td>{area.minzoom}
<td>
{area.minzoom}
{#if area.maxzoom !== undefined}
- {area.maxzoom}
{:else}
@ -213,15 +246,13 @@
<td>
<button on:click={() => del(area)}>
<TrashIcon class="w-6" />
Delete this map
<Trans t={t.delete} />
</button>
</td>
</tr>
{/each}
</table>
</div>
</AccordionItem>
</Accordion>
</div>
@ -229,10 +260,10 @@
</div>
<style>
.leave-room {
height: calc(100vh - 18rem);
overflow-x: auto;
width: 100%;
color: var(--foreground-color);
}
.leave-room {
height: calc(100vh - 18rem);
overflow-x: auto;
width: 100%;
color: var(--foreground-color);
}
</style>

View file

@ -25,21 +25,22 @@
</script>
<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="h-full w-full overflow-auto sm:p-2 flex items-center" 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">
<div class="flex h-full w-full flex-grow flex-col justify-center">
<!-- Title element and title icons-->
<h3 class="m-0">
<a href={`#${$tags.id}`}>
<a href={`#${$tags?.id}`}>
{#if layer.title}
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} />
{/if}
</a>
</h3>
{#if layer.titleIcons.length > 0}
<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"
>
@ -69,11 +70,12 @@
</a>
{/if}
</div>
{/if}
</div>
{/if}
</div>
<slot name="close-button">
<div class="mt-4">
<div class="m-2">
<CloseButton on:click={() => state.selectedElement.setData(undefined)} />
</div>
</slot>

View file

@ -23,7 +23,8 @@
)
let isAddNew = tags.mapD(
(t) => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false, onDestroy
(t) => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false,
onDestroy
)
export let layer: LayerConfig

View file

@ -8,6 +8,7 @@
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists"
export let search: UIEventSource<string> = new UIEventSource<string>(undefined)
export let themes: MinimalThemeInformation[]
@ -23,7 +24,7 @@
? "flex flex-wrap items-center justify-center gap-x-2"
: "theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"}
>
{#each Utils.DedupOnId(Utils.noNull(themes)) as theme (theme.id)}
{#each Lists.dedupOnId(Utils.noNull(themes)) as theme (theme.id)}
<ThemeButton {theme} {state} iconOnly={onlyIcons}>
{#if $search && hasSelection && themes?.[0] === theme}
<span class="thanks hidden-on-mobile" aria-hidden="true">

View file

@ -1,6 +1,7 @@
<script lang="ts">
/**
* The visual feedback panel gives visual (and auditive) feedback on the main map view
* This is an accessibility feature for people with screen readers
*/
import Translations from "../i18n/Translations"

View file

@ -25,6 +25,8 @@
import type { Feature, LineString, Point } from "geojson"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import SmallZoomButtons from "../Map/SmallZoomButtons.svelte"
import CompassWidget from "./CompassWidget.svelte"
import type { OsmTags } from "../../Models/OsmFeature"
const splitpoint_style = new LayerConfig(
<LayerConfigJson>split_point,
@ -51,7 +53,7 @@
/**
* Optional: use these properties to set e.g. background layer
*/
export let mapProperties: undefined | Partial<MapProperties> = undefined
export let mapProperties: undefined | Partial<Omit<MapProperties, "lastClickLocation">> = undefined
/**
* Reuse a point if the clicked location is within this amount of meter
@ -61,14 +63,14 @@
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let adaptor = new MapLibreAdaptor(map, mapProperties)
let wayGeojson: Feature<LineString> = <Feature<LineString>>osmWay.asGeoJson()
let wayGeojson: Feature<LineString, OsmTags> = <Feature<LineString, OsmTags>>osmWay.asGeoJson()
adaptor.location.setData(GeoOperations.centerpointCoordinatesObj(wayGeojson))
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]),
features: new StaticFeatureSource<Feature<LineString, { id: string }>>([wayGeojson]),
drawMarkers: true,
layer: layer,
})
@ -77,10 +79,10 @@
Feature<
Point,
{
id: number
id: string
index: number
dist: number
location: number
dist?: number
location?: number
}
>[]
> = new UIEventSource([])
@ -96,15 +98,15 @@
return
}
splitPoints.data.splice(i, 1)
splitPoints.ping()
splitPoints.update(ls => [...ls])
},
})
let id = 0
adaptor.lastClickLocation.addCallbackD(({ lon, lat }) => {
let projected: Feature<Point, { index: number; id?: number; reuse?: string }> =
let projected: Feature<Point, { index: number; id: string; reuse?: string }> = <any>(
GeoOperations.nearestPoint(wayGeojson, [lon, lat])
console.log("Added splitpoint", projected, id)
)
projected.properties.id = "" + id
// We check the next and the previous point. If those are closer then the tolerance, we reuse those instead
@ -116,7 +118,6 @@
const previousPoint = <[number, number]>way[i]
const previousDistance = GeoOperations.distanceBetween(previousPoint, p)
console.log("ND", nextDistance, "PD", previousDistance)
if (nextDistance <= snapTolerance && previousDistance >= nextDistance) {
projected = {
type: "Feature",
@ -126,6 +127,7 @@
},
properties: {
index: i + 1,
id: "" + id,
reuse: "yes",
},
}
@ -139,19 +141,22 @@
},
properties: {
index: i,
id: "" + id,
reuse: "yes",
},
}
}
projected.properties["id"] = id
id++
splitPoints.data.push(<any>projected)
splitPoints.ping()
splitPoints.data.push(projected)
splitPoints.update(ls => [...ls])
})
</script>
<div class="relative h-full w-full">
<MaplibreMap {map} mapProperties={adaptor} />
<SmallZoomButtons {adaptor} />
<div class="absolute top-0 left-0">
<CompassWidget mapProperties={adaptor} />
</div>
</div>

View file

@ -3,10 +3,17 @@
import { fade } from "svelte/transition"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { onDestroy } from "svelte"
import If from "../Base/If.svelte"
import type { Store } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import Avatar from "../Base/Avatar.svelte"
let open = false
export let state: { osmConnection: OsmConnection }
export let state: {
osmConnection: OsmConnection
featureSwitches: { featureSwitchEnableLogin: Store<boolean> }
}
let userdetails = state.osmConnection.userDetails
let username = userdetails.mapD((ud) => ud.name, onDestroy)
username.addCallbackAndRunD((ud) => {
@ -19,23 +26,25 @@
})
</script>
<Popover
class="mt-4 hidden sm:block"
defaultClass="py-2 px-3 w-fit "
trigger="null"
placement="bottom"
transition={(e) => fade(e, { duration: 150 })}
bind:open
>
{#if $username !== undefined}
<div style="width: max-content" class="flex items-center">
<If condition={state.featureSwitches.featureSwitchEnableLogin}>
<Popover
class="mt-4 hidden sm:block"
defaultClass="py-2 px-3 w-fit "
trigger="null"
placement="bottom"
transition={(e) => fade(e, { duration: 150 })}
bind:open
>
{#if $username !== undefined}
<div style="width: max-content" class="flex items-center">
<Avatar {userdetails} />
<div>
<div>Welcome back</div>
<div class="normal-background" style="width: max-content">
<b>{$username}</b>
<div>
<Tr t={Translations.t.general.welcomeBack} />
<div class="normal-background" style="width: max-content">
<b>{$username}</b>
</div>
</div>
</div>
</div>
{/if}
</Popover>
{/if}
</Popover>
</If>

View file

@ -10,7 +10,15 @@
export let range: OpeningRange[][]
const wt = Translations.t.general.weekdays
const weekdays: Translation[] = [wt.sunday, wt.monday, wt.tuesday, wt.wednesday, wt.thursday, wt.friday, wt.saturday]
const weekdays: Translation[] = [
wt.sunday,
wt.monday,
wt.tuesday,
wt.wednesday,
wt.thursday,
wt.friday,
wt.saturday,
]
let allTheSame = OH.weekdaysIdentical(range, 0, range.length - 1)
let today = new Date().getDay()
@ -24,8 +32,8 @@
const day = moment.startDate.getDay()
dayZero = day - i
}
function isToday(i:number ){
i = (i+dayZero) % 7
function isToday(i: number) {
i = (i + dayZero) % 7
return i === today
}
</script>
@ -37,16 +45,21 @@
<div class="ml-8">{moment.startDate.toLocaleTimeString()}</div>
{/each}
</div>
{:else if dayZero >= 0 } <!-- /*If dayZero == -1, then we got no valid values at all*/ -->
{:else if dayZero >= 0}
<!-- /*If dayZero == -1, then we got no valid values at all*/ -->
{#each range as moments, i (moments)}
<div class="flex gap-x-4 justify-between w-full px-2" class:interactive={isToday(i)} class:text-bold={isToday(i)} >
<div
class="flex w-full justify-between gap-x-4 px-2"
class:interactive={isToday(i)}
class:text-bold={isToday(i)}
>
<Tr t={weekdays[(i + dayZero) % 7]} />
{#if range[i].length > 0}
{#each moments as moment (moment)}
<div class="ml-8">{moment.startDate.toLocaleTimeString()}</div>
{/each}
{:else}
<Tr cls="italic subtle" t={Translations.t.general.points_in_time.closed}/>
<Tr cls="italic subtle" t={Translations.t.general.points_in_time.closed} />
{/if}
</div>
{/each}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import CollectionTimeRange from "./CollectionTimeRange.svelte"
import opening_hours from "opening_hours"
import { OH } from "../OpeningHours/OpeningHours"
@ -14,26 +13,23 @@
let weekdays = ranges.slice(0, 5)
let weekend = ranges.slice(5, 7)
let everyDaySame = OH.weekdaysIdentical(ranges, 0, ranges.length - 1)
let weekdaysAndWeekendsSame = OH.weekdaysIdentical(weekdays, 0, 4) && OH.weekdaysIdentical(weekend, 0, 1)
let weekdaysAndWeekendsSame =
OH.weekdaysIdentical(weekdays, 0, 4) && OH.weekdaysIdentical(weekend, 0, 1)
const t = Translations.t.general.points_in_time
</script>
<div class="m-4 border">
{#if everyDaySame || !weekdaysAndWeekendsSame}
<CollectionTimeRange range={ranges}>
<Tr t={t.daily} />
</CollectionTimeRange>
{:else if times.isWeekStable()}
<div class="flex flex-col w-fit">
<div class="flex w-fit flex-col">
<CollectionTimeRange range={weekdays}>
<Tr t={t.weekdays} />
</CollectionTimeRange>
<CollectionTimeRange range={weekend}>
<Tr t={t.weekends} />
</CollectionTimeRange>
</div>
{:else}

View file

@ -40,11 +40,9 @@
</script>
<LoginToggle {state}>
<div slot="not-logged-in">
<LoginButton osmConnection={state.osmConnection}>
<Tr t={Translations.t.favouritePoi.loginToSeeList} />
</LoginButton>
</div>
<LoginButton slot="not-logged-in" osmConnection={state.osmConnection}>
<Tr t={Translations.t.favouritePoi.loginToSeeList} />
</LoginButton>
<div class="flex flex-col">
{#if $favourites.length === 0}

View file

@ -138,13 +138,13 @@
</ul>
{#if diff.tr}
<div class="h-48 w-48">
<ChartJs config={ChartJsUtils.createConfigForTagRendering(
diff.tr, diff.features,{
<ChartJs
config={ChartJsUtils.createConfigForTagRendering(diff.tr, diff.features, {
groupToOtherCutoff: 0,
chartType: "pie",
sort: true,
}
)} />
})}
/>
</div>
{:else}
Could not create a graph - this item type has no associated question

View file

@ -136,29 +136,29 @@
</Popup>
{#if error}
{#if $online}
<div class="interactive flex h-80 w-60 flex-col items-center justify-center p-4 text-center">
{#if notFound}
<div class="alert flex items-center">
<TriangleOutline class="h-8 w-8 shrink-0" />
Not found
</div>
This image is probably incorrect or deleted.
{image.url}
<slot name="not-found-extra" />
{:else}
<div class="alert flex items-center">
<TriangleOutline class="h-8 w-8 shrink-0" />
<Tr t={Translations.t.image.loadingFailed} />
</div>
{#if image.provider.name.toLowerCase() === "mapillary" && $isInStrictMode}
<Tr t={Translations.t.image.mapillaryTrackingProtection} />
{:else if $isInStrictMode}
<Tr t={Translations.t.image.strictProtectionDetected} />
{image.provider.name}
<div class="subtle mt-8 text-sm">{image.url}</div>
<div class="interactive flex h-80 w-60 flex-col items-center justify-center p-4 text-center">
{#if notFound}
<div class="alert flex items-center">
<TriangleOutline class="h-8 w-8 shrink-0" />
Not found
</div>
This image is probably incorrect or deleted.
{image.url}
<slot name="not-found-extra" />
{:else}
<div class="alert flex items-center">
<TriangleOutline class="h-8 w-8 shrink-0" />
<Tr t={Translations.t.image.loadingFailed} />
</div>
{#if image.provider.name.toLowerCase() === "mapillary" && $isInStrictMode}
<Tr t={Translations.t.image.mapillaryTrackingProtection} />
{:else if $isInStrictMode}
<Tr t={Translations.t.image.strictProtectionDetected} />
{image.provider.name}
<div class="subtle mt-8 text-sm">{image.url}</div>
{/if}
{/if}
{/if}
</div>
</div>
{/if}
{:else if image.status !== undefined && image.status !== "ready" && image.status !== "hidden"}
<div class="flex h-80 w-60 flex-col justify-center p-4">

View file

@ -75,8 +75,8 @@
)
let selected = new UIEventSource<P4CPicture>(undefined)
let selectedAsFeature = selected.mapD((s) =>
[
let selectedAsFeature = selected.mapD(
(s) => [
<Feature<Point>>{
type: "Feature",
geometry: {
@ -89,13 +89,17 @@
rotation: s.direction,
},
},
], onDestroy)
let someLoading = imageState.state.mapD((stateRecord) =>
Object.values(stateRecord).some((v) => v === "loading"), onDestroy
],
onDestroy
)
let errors = imageState.state.mapD((stateRecord) =>
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error"), onDestroy
let someLoading = imageState.state.mapD(
(stateRecord) => Object.values(stateRecord).some((v) => v === "loading"),
onDestroy
)
let errors = imageState.state.mapD(
(stateRecord) => Object.keys(stateRecord).filter((k) => stateRecord[k] === "error"),
onDestroy
)
let highlighted = new UIEventSource<string>(undefined)

View file

@ -3,14 +3,17 @@
import Translations from "../i18n/Translations"
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import ArrowPath from "@babeard/svelte-heroicons/mini/ArrowPath"
import { createEventDispatcher } from "svelte"
export let failed: number
export let state: SpecialVisualizationState
const t = Translations.t.image
let dispatch = createEventDispatcher<{ retry }>()
</script>
<div class="alert flex">
<div class="flex flex-col items-start w-full items-center">
<div class="flex w-full flex-col items-start items-center">
{#if failed === 1}
<Tr t={t.upload.one.failed} />
{:else}
@ -19,7 +22,10 @@
<Tr cls="text-normal" t={t.upload.failReasons} />
<Tr cls="text-xs" t={t.upload.failReasonsAdvanced} />
{#if state}
<button class="primary pointer-events-auto" on:click={() => state.imageUploadManager.uploadQueue()}></button>
<button class="primary pointer-events-auto" on:click={() => dispatch("retry")}>
<ArrowPath class="w-6" />
<Tr t={Translations.t.general.retry} />
</button>
{/if}
</div>
<button

View file

@ -26,7 +26,7 @@
Number of images uploaded succesfully
*/
function getCount(input: Store<string[]>): Store<number> {
if(!input){
if (!input) {
return new ImmutableStore(0)
}
if (featureId == "*") {
@ -62,6 +62,10 @@
})
let online = IsOnline.isOnline
let progress = state.imageUploadManager.progressCurrentImage
function retry() {
state.imageUploadManager.uploadQueue()
}
</script>
{#if $debugging}
@ -97,10 +101,16 @@
<div class="alert flex flex-col">
<Tr cls="block" t={t.upload.offline} />
<Tr cls="block" t={t.upload.offlinePending.Subs({count: $pending})} />
</div>
{:else if $failed > dismissed}
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} {state} />
<UploadFailedMessage
failed={$failed}
on:click={() => (dismissed = $failed)}
on:retry={() => {
retry()
}}
{state}
/>
{/if}
{#if showThankYou}

View file

@ -1,48 +1,58 @@
<script lang="ts">/**
* Multiple 'SingleCollectionTime'-rules togehter
*/
import { Stores, UIEventSource } from "../../../../Logic/UIEventSource"
import SingleCollectionTime from "./SingleCollectionTime.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Lists } from "../../../../Utils/Lists"
<script lang="ts">
/**
* Multiple 'SingleCollectionTime'-rules togehter
*/
import { Stores, UIEventSource } from "../../../../Logic/UIEventSource"
import SingleCollectionTime from "./SingleCollectionTime.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Lists } from "../../../../Utils/Lists"
export let value: UIEventSource<string>
let initialRules: string[] = Lists.noEmpty(value.data?.split(";")?.map(v => v.trim()))
let singleRules: UIEventSource<UIEventSource<string>[]> = new UIEventSource(
initialRules?.map(v => new UIEventSource(v)) ?? []
)
if(singleRules.data.length === 0){
singleRules.data.push(new UIEventSource(undefined))
}
singleRules.bindD(stores => Stores.concat(stores)).addCallbackAndRunD(subrules => {
console.log("Setting subrules", subrules)
value.set(Lists.noEmpty(subrules).join("; "))
})
function rm(rule: UIEventSource){
const index = singleRules.data.indexOf(rule)
singleRules.data.splice(index, 1)
singleRules.ping()
}
export let value: UIEventSource<string>
let initialRules: string[] = Lists.noEmpty(value.data?.split(";")?.map((v) => v.trim()))
let singleRules: UIEventSource<UIEventSource<string>[]> = new UIEventSource(
initialRules?.map((v) => new UIEventSource(v)) ?? []
)
if (singleRules.data.length === 0) {
singleRules.data.push(new UIEventSource(undefined))
}
singleRules
.bindD((stores) => Stores.concat(stores))
.addCallbackAndRunD((subrules) => {
console.log("Setting subrules", subrules)
value.set(Lists.noEmpty(subrules).join("; "))
})
function rm(rule: UIEventSource) {
const index = singleRules.data.indexOf(rule)
singleRules.data.splice(index, 1)
singleRules.ping()
}
</script>
<div class="interactive">
{#each $singleRules as rule}
<SingleCollectionTime value={rule}>
<svelte:fragment slot="right">
{#if $singleRules.length > 1}
<button on:click={() => { rm(rule) }} class="as-link">
<TrashIcon class="w-6 h-6" />
</button>
{/if}
</svelte:fragment>
</SingleCollectionTime>
<SingleCollectionTime value={rule}>
<svelte:fragment slot="right">
{#if $singleRules.length > 1}
<button
on:click={() => {
rm(rule)
}}
class="as-link"
>
<TrashIcon class="h-6 w-6" />
</button>
{/if}
</svelte:fragment>
</SingleCollectionTime>
{/each}
<button on:click={() => {singleRules.data.push(new UIEventSource(undefined)); singleRules.ping()}}>Add schedule
<button
on:click={() => {
singleRules.data.push(new UIEventSource(undefined))
singleRules.ping()
}}
>
Add schedule
</button>
</div>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import TimeInput from "../TimeInput.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import Checkbox from "../../../Base/Checkbox.svelte"
@ -9,6 +8,7 @@
import { OH } from "../../../OpeningHours/OpeningHours"
import { Lists } from "../../../../Utils/Lists"
import { Translation } from "../../../i18n/Translation"
import PlusCircle from "@babeard/svelte-heroicons/mini/PlusCircle"
export let value: UIEventSource<string>
@ -16,16 +16,28 @@
/*
Single rule for collection times, e.g. "Mo-Fr 10:00, 17:00"
*/
let weekdays: Translation[] = [wt.monday, wt.tuesday, wt.wednesday, wt.thursday, wt.friday, wt.saturday, wt.sunday, Translations.t.general.opening_hours.ph]
let weekdays: Translation[] = [
wt.monday,
wt.tuesday,
wt.wednesday,
wt.thursday,
wt.friday,
wt.saturday,
wt.sunday,
Translations.t.general.opening_hours.ph,
]
let initialTimes= Lists.noEmpty(value.data?.split(" ")?.[1]?.split(",")?.map(s => s.trim()) ?? [])
let values = new UIEventSource(initialTimes.map(t => new UIEventSource(t)))
if(values.data.length === 0){
let initialTimes = Lists.noEmpty(
value.data
?.split(" ")?.[1]
?.split(",")
?.map((s) => s.trim()) ?? []
)
let values = new UIEventSource(initialTimes.map((t) => new UIEventSource(t)))
if (values.data.length === 0) {
values.data.push(new UIEventSource(""))
}
let daysOfTheWeek = [...OH.days, "PH"]
let selectedDays = daysOfTheWeek.map(() => new UIEventSource(false))
@ -39,7 +51,6 @@
selectedDays[i]?.set(true)
}
} else {
let index = daysOfTheWeek.indexOf(initialDay)
if (index >= 0) {
selectedDays[index]?.set(true)
@ -47,19 +58,26 @@
}
}
let selectedDaysBound = Stores.concat(selectedDays)
.mapD(days => Lists.noNull(days.map((selected, i) => selected ? daysOfTheWeek[i] : undefined)))
let valuesConcat: Store<string[]> = values.bindD(values => Stores.concat(values))
.mapD(values => Lists.noEmpty(values))
valuesConcat.mapD(times => {
console.log(times)
times = Lists.noNull(times)
if (!times || times?.length === 0) {
return undefined
}
times?.sort(/*concatted, new array*/)
return (Lists.noEmpty(selectedDaysBound.data).join(",") + " " + times).trim()
}, [selectedDaysBound]).addCallbackAndRunD(v => value.set(v))
let selectedDaysBound = Stores.concat(selectedDays).mapD((days) =>
Lists.noNull(days.map((selected, i) => (selected ? daysOfTheWeek[i] : undefined)))
)
let valuesConcat: Store<string[]> = values
.bindD((values) => Stores.concat(values))
.mapD((values) => Lists.noEmpty(values))
valuesConcat
.mapD(
(times) => {
console.log(times)
times = Lists.noNull(times)
if (!times || times?.length === 0) {
return undefined
}
times?.sort(/*concatted, new array*/)
return (Lists.noEmpty(selectedDaysBound.data).join(",") + " " + times).trim()
},
[selectedDaysBound]
)
.addCallbackAndRunD((v) => value.set(v))
function selectWeekdays() {
for (let i = 0; i < 5; i++) {
@ -75,28 +93,29 @@
selectedDays[i].set(false)
}
}
</script>
<div class="rounded-xl my-2 p-2 low-interaction flex w-full justify-between flex-wrap">
<div class="low-interaction my-2 flex w-full flex-wrap justify-between rounded-xl p-2">
<div class="flex flex-col">
<div class="flex flex-wrap">
{#each $values as value, i}
<div class="flex mx-4 gap-x-1 items-center">
<div class="mx-4 flex items-center gap-x-1">
<TimeInput {value} />
{#if $values.length > 1}
<button class="as-link">
<TrashIcon class="w-6 h-6" />
<TrashIcon class="h-6 w-6" />
</button>
{/if}
</div>
{/each}
<button on:click={() => {values.data.push(new UIEventSource(undefined)); values.ping()}}>
<PlusCircle class="w-6 h-6" />
Add time
<button
on:click={() => {
values.data.push(new UIEventSource(undefined))
values.ping()
}}
>
<PlusCircle class="h-6 w-6" />
<Tr t={Translations.t.collectionTimes.addTime} />
</button>
</div>
<div class="flex w-fit flex-wrap">
@ -108,14 +127,11 @@
</div>
{/each}
</div>
</div>
<div class="flex flex-wrap justify-between w-full">
<div class="flex w-full flex-wrap justify-between">
<div class="flex flex-wrap gap-x-4">
<button class="as-link text-sm" on:click={() => selectWeekdays()}>Select weekdays</button>
<button class="as-link text-sm" on:click={() => clearDays()}>Clear days</button>
</div>
<slot name="right" />
</div>

View file

@ -27,11 +27,14 @@
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
let rotation = new UIEventSource(value.data)
rotation.addCallbackD(rotation => {
const r = (rotation + mapProperties.rotation.data + 360) % 360
console.log("Setting value to", r)
value.setData(""+Math.floor(r))
}, [mapProperties.rotation])
rotation.addCallbackD(
(rotation) => {
const r = (rotation + mapProperties.rotation.data + 360) % 360
console.log("Setting value to", r)
value.setData("" + Math.floor(r))
},
[mapProperties.rotation]
)
let directionElem: HTMLElement | undefined
$: rotation.addCallbackAndRunD((degrees) => {
if (!directionElem?.style) {
@ -42,7 +45,6 @@
let mainElem: HTMLElement
function onPosChange(x: number, y: number) {
const rect = mainElem.getBoundingClientRect()
const dx = -(rect.left + rect.right) / 2 + x
@ -57,7 +59,7 @@
<div
bind:this={mainElem}
class="relative h-48 min-w-48 w-full cursor-pointer overflow-hidden rounded-xl"
class="relative h-48 w-full min-w-48 cursor-pointer overflow-hidden rounded-xl"
on:click={(e) => onPosChange(e.x, e.y)}
on:mousedown={(e) => {
isDown = true

View file

@ -7,7 +7,7 @@
import { UIEventSource, Store } from "../../../Logic/UIEventSource"
import type { MapProperties } from "../../../Models/MapProperties"
import ThemeViewState from "../../../Models/ThemeViewState"
import type { Feature } from "geojson"
import type { Feature, LineString } from "geojson"
import type { RasterLayerPolygon } from "../../../Models/RasterLayers"
import { RasterLayerUtils } from "../../../Models/RasterLayers"
import { eliCategory } from "../../../Models/RasterLayerProperties"
@ -22,13 +22,18 @@
import Tr from "../../Base/Tr.svelte"
import { onDestroy } from "svelte"
export let value: UIEventSource<number>
export let value: UIEventSource<string>
export let feature: Feature
export let args: { background?: string; zoom?: number }
export let state: ThemeViewState = undefined
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let center = GeoOperations.centerpointCoordinates(feature)
if (feature.geometry.type === "LineString") {
center = <[number, number]>(
GeoOperations.nearestPoint(<Feature<LineString>>feature, center).geometry.coordinates
)
}
export let initialCoordinate: { lon: number; lat: number } = { lon: center[0], lat: center[1] }
let mapLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(
initialCoordinate
@ -46,10 +51,12 @@
state?.mapProperties.rasterLayer.addCallbackD((layer) => rasterLayer.set(layer))
}
}
let mapProperties: Partial<MapProperties> = {
let mapProperties: Partial<MapProperties> & { location } = {
rasterLayer: rasterLayer,
location: mapLocation,
zoom: new UIEventSource(args?.zoom ?? 18),
rotation: state.mapProperties.rotation.followingClone(),
lastClickLocation: new UIEventSource(undefined)
}
let start: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
@ -66,7 +73,7 @@
// A bit of a double task: calculate the actual value _and_ the map rendering
const end = mapLocation.data
const distance = GeoOperations.distanceBetween([start.lon, start.lat], [end.lon, end.lat])
value.set(distance.toFixed(2))
value.set(distance.toFixed(1))
return <Feature[]>[
{
@ -85,13 +92,22 @@
},
]
},
[mapLocation], onDestroy
[mapLocation],
onDestroy
)
new ShowDataLayer(map, {
layer: new LayerConfig(conflation),
features: new StaticFeatureSource(lengthFeature),
})
mapProperties.lastClickLocation.addCallbackAndRunD(lonlat => {
if (start.data === undefined) {
start.set(lonlat)
}
mapProperties.location.set(lonlat)
}, onDestroy)
const t = Translations.t.input_helpers.distance
</script>
<div class="relative h-64 w-full">
@ -101,6 +117,6 @@
</div>
</div>
<button class="primary" on:click={() => selectStart()}>
<Tr t={Translations.t.input_helpers.distance.setFirst} />
<button class:primary={$start === undefined} on:click={() => selectStart()}>
<Tr t={$start === undefined ? t.setFirst : t.measureAgain} />
</button>

View file

@ -14,6 +14,7 @@
import { createEventDispatcher, onDestroy } from "svelte"
import Move_arrows from "../../../assets/svg/Move_arrows.svelte"
import SmallZoomButtons from "../../Map/SmallZoomButtons.svelte"
import CompassWidget from "../../BigComponents/CompassWidget.svelte"
/**
* A visualisation to pick a location on a map background
@ -50,6 +51,7 @@
mla.lastClickLocation.addCallbackAndRunD((lastClick) => {
dispatch("click", lastClick)
})
mla.installQuicklocation()
mapProperties.location.syncWith(value)
if (onCreated) {
onCreated(value, map, mla)
@ -91,7 +93,6 @@
<div class="relative h-full min-h-32 cursor-pointer overflow-hidden">
<div class="absolute left-0 top-0 h-full w-full cursor-pointer">
<MaplibreMap
center={{ lng: initialCoordinate.lon, lat: initialCoordinate.lat }}
{map}
mapProperties={mla}
/>
@ -107,4 +108,8 @@
<DragInvitation hideSignal={mla.location} />
<SmallZoomButtons adaptor={mla} />
<div class="absolute top-0 left-0 ">
<CompassWidget mapProperties={mla} />
</div>
</div>

View file

@ -10,6 +10,9 @@
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations"
import Check from "@babeard/svelte-heroicons/mini/Check"
import { CloseButton } from "flowbite-svelte"
import Cross from "../../../assets/svg/Cross.svelte"
import XMark from "@babeard/svelte-heroicons/mini/XMark"
export let value: UIEventSource<string>
export let args: string
@ -31,16 +34,29 @@
const state = new OpeningHoursState(value, prefix, postfix)
let expanded = new UIEventSource(false)
function abort() {
expanded.set(false)
}
</script>
<Popup bodyPadding="p-0" shown={expanded}>
<OHTable value={state.normalOhs} />
<div class="absolute flex w-full justify-center" style="bottom: -3rem">
<button on:click={() => abort()}>
<XMark class="m-0 h-6 w-6" />
<Tr t={Translations.t.general.cancel} />
</button>
<button class=" primary" on:click={() => expanded.set(false)}>
<Check class="m-0 h-6 w-6 shrink-0 p-0" color="white" />
<Tr t={Translations.t.general.confirm} />
</button>
</div>
<CloseButton
class="absolute right-0 top-0 z-10"
style="margin-top: -1.0rem"
on:click={() => abort()}
/>
</Popup>
<button on:click={() => expanded.set(true)}>Pick opening hours</button>

View file

@ -78,7 +78,8 @@
previewDegrees.setData(beta + "°")
previewPercentage.setData(degreesToPercentage(beta))
},
[valuesign, beta], onDestroy
[valuesign, beta],
onDestroy
)
function onSave() {

View file

@ -44,8 +44,12 @@
})
)
let instanceOf: number[] = Lists.noNull((options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)))
let notInstanceOf: number[] = Lists.noNull((options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)))
let instanceOf: number[] = Lists.noNull(
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let notInstanceOf: number[] = Lists.noNull(
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let allowMultipleArg = options?.["multiple"] ?? "yes"
let allowMultiple = allowMultipleArg === "yes" || "" + allowMultipleArg === "true"

View file

@ -1,12 +1,10 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import type { ValidatorType } from "./Validators"
import Validators from "./Validators"
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { Validator } from "./Validator"
import DistanceInput from "./Helpers/DistanceInput.svelte"
export let type: ValidatorType
@ -15,11 +13,13 @@
export let args: (string | number | boolean)[] | any = undefined
export let state: SpecialVisualizationState = undefined
let validator = Validators.get(type)
let validatorHelper: Validator = validator.inputHelper
let validatorHelper = validator.inputHelper
</script>
{#if type === "distance"}
<DistanceInput {value} {feature} {state} {args} />
{:else if validatorHelper !== undefined}
<svelte:component this={validatorHelper} {value} {feature} {state} {args} on:submit />
{:else}
<slot name="fallback" /> <!-- Used in the FilterWithView to inject the helper -->
{/if}

View file

@ -87,10 +87,13 @@
* Side effect: sets the feedback, returns true/false if valid
* @param canonicalValue
*/
function validateRange(canonicalValue: number): boolean {
function validateRange(canonicalValue: number | string): boolean {
if (!range) {
return true
}
if (typeof canonicalValue === "string") {
canonicalValue = Number(canonicalValue)
}
if (canonicalValue < range.warnBelow) {
feedback.set(t.suspiciouslyLow)
}
@ -100,7 +103,7 @@
if (canonicalValue > range.max) {
let max: number | string | BaseUIElement = range.max
if (unit) {
max = unit.asHumanLongValue(max)
max = unit.asHumanLongValue(max, getCountry)
}
feedback.set(t.tooHigh.Subs({ max }))
return false
@ -108,7 +111,7 @@
if (canonicalValue < range.min) {
let min: number | string | BaseUIElement = range.min
if (unit) {
min = unit.asHumanLongValue(min)
min = unit.asHumanLongValue(min, getCountry)
}
feedback.set(t.tooLow.Subs({ min }))
return false
@ -131,7 +134,7 @@
}
if (selectedUnit.data) {
const canonicalValue = unit.valueInCanonical(v + selectedUnit.data)
const canonicalValue = unit.valueInCanonical(v + selectedUnit.data, getCountry)
if (validateRange(canonicalValue)) {
value.setData(unit.toOsm(v, selectedUnit.data))
} else {

View file

@ -22,7 +22,7 @@ export abstract class Validator {
public readonly textArea: boolean
public readonly isMeta?: boolean
public readonly inputHelper : ComponentType = undefined
public readonly inputHelper: ComponentType = undefined
public readonly hideInputField: boolean = false
constructor(
@ -83,5 +83,4 @@ export abstract class Validator {
public validateArguments(args: string): undefined | string {
return undefined
}
}

View file

@ -81,9 +81,9 @@ export default class Validators {
new ColorValidator(),
new DirectionValidator(),
new DistanceValidator(),
new SlopeValidator(),
new UrlValidator(),
new EmailValidator(),
new PhoneValidator(),
@ -107,8 +107,6 @@ export default class Validators {
new TagValidator(),
new NameSuggestionIndexValidator(),
new DistanceValidator(),
]
private static _byType = Validators._byTypeConstructor()

View file

@ -2,11 +2,12 @@ import StringValidator from "./StringValidator"
import { ComponentType } from "svelte/types/runtime/internal/dev"
import CollectionTimes from "../Helpers/CollectionTimes/CollectionTimes.svelte"
export default class CollectionTimesValidator extends StringValidator{
export default class CollectionTimesValidator extends StringValidator {
public readonly inputHelper: ComponentType = CollectionTimes
constructor() {
super("points_in_time", "'Points in time' are points according to a fixed schedule, e.g. 'every monday at 10:00'. They are typically used for postbox collection times or times of mass at a place of worship")
super(
"points_in_time",
"'Points in time' are points according to a fixed schedule, e.g. 'every monday at 10:00'. They are typically used for postbox collection times or times of mass at a place of worship"
)
}
}

View file

@ -7,5 +7,4 @@ export default class ColorValidator extends Validator {
constructor() {
super("color", "Shows a color picker")
}
}

View file

@ -29,5 +29,4 @@ export default class DateValidator extends Validator {
return [year, month, day].join("-")
}
}

View file

@ -2,7 +2,6 @@ import IntValidator from "./IntValidator"
import DirectionInput from "../Helpers/DirectionInput.svelte"
export default class DirectionValidator extends IntValidator {
public readonly inputHelper = DirectionInput
constructor() {
super(

View file

@ -3,7 +3,6 @@ import { Utils } from "../../../Utils"
import { eliCategory } from "../../../Models/RasterLayerProperties"
export default class DistanceValidator extends Validator {
private readonly docs: string = [
"#### Helper-arguments",
"Options are:",
@ -59,5 +58,4 @@ export default class DistanceValidator extends Validator {
}
return undefined
}
}

View file

@ -39,6 +39,4 @@ export default class ImageUrlValidator extends UrlValidator {
}
return ImageUrlValidator.hasValidExternsion(str)
}
}

View file

@ -4,8 +4,7 @@ import { ComponentType } from "svelte/types/runtime/internal/dev"
import OpeningHoursInput from "../Helpers/OpeningHoursInput.svelte"
export default class OpeningHoursValidator extends Validator {
public readonly inputHelper= OpeningHoursInput
public readonly inputHelper = OpeningHoursInput
constructor() {
super(
@ -44,6 +43,4 @@ export default class OpeningHoursValidator extends Validator {
].join("\n")
)
}
}

View file

@ -10,7 +10,7 @@ import SimpleTagInput from "../Helpers/SimpleTagInput.svelte"
export default class SimpleTagValidator extends Validator {
private static readonly KeyValidator = new TagKeyValidator()
public readonly inputHelper = SimpleTagInput
public readonly hideInputField = true
public readonly hideInputField = true
public readonly isMeta = true
constructor() {
@ -53,6 +53,4 @@ export default class SimpleTagValidator extends Validator {
isValid(tag: string, _): boolean {
return this.getFeedback(tag, _) === undefined
}
}

View file

@ -2,7 +2,7 @@ import FloatValidator from "./FloatValidator"
import SlopeInput from "../Helpers/SlopeInput.svelte"
export default class SlopeValidator extends FloatValidator {
public readonly inputHelper =SlopeInput
public readonly inputHelper = SlopeInput
constructor() {
super(
@ -43,5 +43,4 @@ export default class SlopeValidator extends FloatValidator {
}
return super.reformat(str) + lastChar
}
}

View file

@ -21,5 +21,4 @@ export default class TagValidator extends Validator {
isValid(tag: string, _): boolean {
return this.getFeedback(tag, _) === undefined
}
}

View file

@ -2,7 +2,6 @@ import { Validator } from "../Validator"
import TimeInput from "../Helpers/TimeInput.svelte"
export class TimeValidator extends Validator {
public readonly inputmode = "time"
public readonly inputHelper = TimeInput
public readonly hideInputField = true
@ -10,6 +9,4 @@ export class TimeValidator extends Validator {
constructor() {
super("time", "A time picker")
}
}

View file

@ -17,5 +17,4 @@ export default class TranslationValidator extends Validator {
return false
}
}
}

View file

@ -188,5 +188,4 @@ Another example is to search for species and trees:
}
return clipped
}
}

View file

@ -21,7 +21,10 @@
? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt)
: new ImmutableStore("0deg")
if (rotation?.render?.txt === "{alpha}deg") {
_rotation = Orientation.singleton.alpha.map((alpha) => (alpha ? alpha + "deg" : "0deg "), onDestroy)
_rotation = Orientation.singleton.alpha.map(
(alpha) => (alpha ? alpha + "deg" : "0deg "),
onDestroy
)
}
</script>

View file

@ -70,7 +70,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
constructor(
maplibreMap: Store<MLMap>,
state?: Partial<MapProperties>,
state?: Partial<MapProperties & {
lastClickLocation: UIEventSource<{
lon: number
lat: number
mode: "left" | "right" | "middle"
nearestFeature?: Feature
}>
}>,
options?: {
correctClick?: number
}
@ -78,7 +85,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
if (!MapLibreAdaptor.pmtilesInited) {
const offlineManager = OfflineBasemapManager.singleton
maplibregl.addProtocol("pmtiles", new Protocol().tile)
maplibregl.addProtocol("pmtilesoffl", (request, abort) => offlineManager.tilev4(request, abort))
maplibregl.addProtocol("pmtilesoffl", (request, abort) =>
offlineManager.tilev4(request, abort)
)
MapLibreAdaptor.pmtilesInited = true
}
this._maplibreMap = maplibreMap
@ -112,7 +121,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
this.showScale = state?.showScale ?? new UIEventSource<boolean>(false)
const lastClickLocation = new UIEventSource<{
const lastClickLocation = state?.lastClickLocation ?? new UIEventSource<{
lat: number
lon: number
mode: "left" | "right" | "middle"
@ -154,7 +163,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
const way = <Feature<LineString>>feature
const lngLat: [number, number] = [e.lngLat.lng, e.lngLat.lat]
const p = GeoOperations.nearestPoint(way, lngLat)
console.log(">>>", p, way, lngLat)
if (!p) {
continue
}
@ -180,7 +188,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
this.setMinzoom(this.minzoom.data)
this.setMaxzoom(this.maxzoom.data)
this.setBounds(this.bounds.data)
this.setRotation(this.rotation.data)
this.setRotation(Math.floor(this.rotation.data))
this.setScale(this.showScale.data)
this.updateStores(true)
}
@ -560,6 +568,11 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
if (!map || bearing === undefined) {
return
}
if (Math.abs(map.getBearing() - bearing) < 0.1) {
// We don't bother to actually rotate
// Aborting small changes helps to dampen changes
return
}
map.rotateTo(bearing, { duration: 500 })
}
@ -756,4 +769,26 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
})
}
/**
* In general, the 'location'-attribute is only updated when the map stops moving.
* In some cases, we'd like to update the map faster, especially when the map is used for an input-element
* such as distance, snap-to, ...
*
* In that case, calling this method will install an extra handler on 'drag', updating the location faster.
* To avoid rendering artefacts or too frequenting pinging, this is ratelimited to one update every 'rateLimitMs' milliseconds
*/
public installQuicklocation(ratelimitMs = 50) {
this._maplibreMap.addCallbackAndRunD((map) => {
let lastUpdate = new Date().getTime()
map.on("drag", () => {
const now = new Date().getTime()
if (now - lastUpdate < ratelimitMs) {
return
}
lastUpdate = now
const center = map.getCenter()
this.location.set({ lon: center.lng, lat: center.lat })
})
})
}
}

View file

@ -56,6 +56,7 @@
maxZoom: 24,
interactive: true,
attributionControl: false,
bearing: mapProperties?.rotation?.data ?? 0
}
_map = new maplibre.Map(options)
window.requestAnimationFrame(() => {

View file

@ -24,9 +24,9 @@
let altmap: UIEventSource<MlMap> = new UIEventSource(undefined)
let altproperties = new MapLibreAdaptor(altmap, {
rasterLayer,
zoom: UIEventSource.feedFrom(placedOverMapProperties.zoom),
rotation: UIEventSource.feedFrom(placedOverMapProperties.rotation),
pitch: UIEventSource.feedFrom(placedOverMapProperties.pitch),
zoom: placedOverMapProperties.zoom.followingClone(),
rotation: placedOverMapProperties.rotation.followingClone(),
pitch: placedOverMapProperties.pitch.followingClone()
})
altproperties.allowMoving.setData(false)
altproperties.allowZooming.setData(false)

View file

@ -1,6 +1,6 @@
import PointRenderingConfig, {
allowed_location_codes,
PointRenderingLocation
PointRenderingLocation,
} from "../../Models/ThemeConfig/PointRenderingConfig"
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import { type Alignment, Map as MlMap, Marker } from "maplibre-gl"
@ -51,15 +51,17 @@ export class PointRenderingLayer {
/**
* Basically 'features', but only if 'visible' is true
*/
const featuresIfVisibleStore: Store<(Feature<Point, { id: string }> & {
locationType: PointRenderingLocation
})[]> =
new IfVisibleFeatureSource(features, visibility).features.map(features =>
PointRenderingLayer.extractLocations(features, config.location)
)
const featuresIfVisibleStore: Store<
(Feature<Point, { id: string }> & {
locationType: PointRenderingLocation
})[]
> = new IfVisibleFeatureSource(features, visibility).features.map((features) =>
PointRenderingLayer.extractLocations(features, config.location)
)
let featuresToDraw: FeatureSource<Feature<Point, { id: string }> & { locationType: PointRenderingLocation }>
let featuresToDraw: FeatureSource<
Feature<Point, { id: string }> & { locationType: PointRenderingLocation }
>
if (preprocess) {
featuresToDraw = preprocess(new StaticFeatureSource(featuresIfVisibleStore))
} else {
@ -78,7 +80,8 @@ export class PointRenderingLayer {
return
}
allowed_location_codes.forEach((code) => {
const marker = this._allMarkers.get(<OsmId>selected.properties.id)
const marker = this._allMarkers
.get(<OsmId>selected.properties.id)
?.get(code)
?.getElement()
if (marker === undefined) {
@ -88,43 +91,54 @@ export class PointRenderingLayer {
this._markedAsSelected.push(marker)
})
})
}
/**
* All locations that this layer should be rendered
* @private
*/
private static extractLocations(features: Feature<Geometry, {
id: string
}>[], locations: Set<PointRenderingLocation>): (Feature<Point, { id: string }> & {
private static extractLocations(
features: Feature<
Geometry,
{
id: string
}
>[],
locations: Set<PointRenderingLocation>
): (Feature<Point, { id: string }> & {
locationType: PointRenderingLocation
})[] {
const resultingFeatures: (Feature<Point, { id: string }> & { locationType: PointRenderingLocation })[] = []
if (!features) {
return []
}
const resultingFeatures: (Feature<Point, { id: string }> & {
locationType: PointRenderingLocation
})[] = []
function registerFeature(feature: Feature<any, {
id: string
}>, location: [number, number], locationType: PointRenderingLocation) {
function registerFeature(
feature: Feature<
any,
{
id: string
}
>,
location: [number, number],
locationType: PointRenderingLocation
) {
resultingFeatures.push({
...feature,
locationType,
geometry: {
type: "Point",
coordinates: location
}
coordinates: location,
},
})
}
for (const feature of features) {
for (const location of locations) {
if (feature?.geometry === undefined) {
console.warn(
"Got an invalid feature:",
feature,
" while rendering",
location
)
console.warn("Got an invalid feature:", feature, " while rendering", location)
}
if (location === "waypoints") {
if (feature.geometry.type === "LineString") {
@ -154,7 +168,6 @@ export class PointRenderingLayer {
}
return resultingFeatures
}
/**
@ -162,12 +175,11 @@ export class PointRenderingLayer {
* @private
*/
private hideUnneededElements(featuresToDraw: Feature<Geometry, { id: string }>[]) {
const idsToShow = new Set(featuresToDraw.map(f => f.properties.id))
const idsToShow = new Set(featuresToDraw.map((f) => f.properties.id))
for (const key of this._allMarkers.keys()) {
const shouldBeShown = idsToShow.has(key)
for (const marker of this._allMarkers.get(key).values()) {
if (!shouldBeShown) {
marker.addClassName("hidden")
} else {
@ -177,13 +189,14 @@ export class PointRenderingLayer {
}
}
private updateFeatures(allPointLocations: (Feature<Point> & { locationType: PointRenderingLocation })[]) {
private updateFeatures(
allPointLocations: (Feature<Point> & { locationType: PointRenderingLocation })[]
) {
const cache = this._allMarkers
for (const feature of allPointLocations) {
const id = <OsmId>feature.properties.id
const locationType: PointRenderingLocation = feature.locationType
let marker = cache.get(id)?.get(locationType)
if (marker) {
const oldLoc = marker.getLngLat()
@ -215,11 +228,11 @@ export class PointRenderingLayer {
.setLngLat(feature.geometry.coordinates)
.addTo(this._map)*/
let store: Store<Record<string, string>>
if (this._fetchStore) {
store = this._fetchStore(feature.properties.id)
} else {
}
if(!store){
store = new ImmutableStore(<OsmTags>feature.properties)
}
const { html, iconAnchor } = this._config.RenderIcon(store, { metatags: this._metatags })

View file

@ -59,14 +59,14 @@ class SingleBackgroundHandler {
return
}
console.debug(
"Removing raster layer",
this._targetLayer.properties.id,
"map moved and not been used for",
SingleBackgroundHandler.DEACTIVATE_AFTER
)
try {
if (map.getLayer(<string>this._targetLayer.properties.id)) {
console.debug(
"Removing raster layer",
this._targetLayer.properties.id,
"map moved and not been used for",
SingleBackgroundHandler.DEACTIVATE_AFTER
)
map.removeLayer(<string>this._targetLayer.properties.id)
}
} catch (e) {

View file

@ -64,7 +64,7 @@
</div>
{#if !$isOnline}
<div class="flex justify-center items-center">
<div class="flex items-center justify-center">
<div class="alert flex items-center gap-x-4">
<CrossedOut>
<WifiIcon />

View file

@ -45,7 +45,7 @@
)
}
let rasterLayerOnMap = UIEventSource.feedFrom(rasterLayer)
let rasterLayerOnMap = rasterLayer.followingClone()
if (shown) {
onDestroy(

View file

@ -357,18 +357,17 @@ export default class ShowDataLayer {
public static showMultipleLayers(
mlmap: UIEventSource<MlMap>,
features: FeatureSource,
features: FeatureSource<Feature<Geometry, Record<string, any> & {id: string}>>,
layers: LayerConfig[],
options?: Partial<Omit<ShowDataLayerOptions, "features" | "layer">>
) {
const perLayer =
new PerLayerFeatureSourceSplitter(
layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
features,
{
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
}
)
const perLayer = new PerLayerFeatureSourceSplitter(
layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
features,
{
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
}
)
if (options?.zoomToFeatures) {
options.zoomToFeatures = false
features.features.addCallbackD((features) => {
@ -397,17 +396,20 @@ export default class ShowDataLayer {
* @param state
* @param options
*/
public static showLayerClustered(mlmap: Store<MlMap>,
state: { mapProperties: { zoom: UIEventSource<number> } },
options: ShowDataLayerOptions & { layer: LayerConfig }
public static showLayerClustered(
mlmap: Store<MlMap>,
state: { mapProperties: { zoom: UIEventSource<number> } },
options: ShowDataLayerOptions & { layer: LayerConfig }
) {
options.preprocessPoints = feats =>
new ClusteringFeatureSource(feats, state.mapProperties.zoom.map(z => z + 2),
options.preprocessPoints = (feats) =>
new ClusteringFeatureSource(
feats,
state.mapProperties.zoom.map((z) => z + 2),
options.layer.id,
{
cutoff: 7,
showSummaryAt: "tilecenter"
})
cutoff: 7
}
)
new ShowDataLayer(mlmap, options)
}
@ -453,14 +455,9 @@ export default class ShowDataLayer {
layer,
drawLines,
drawMarkers,
preprocessPoints
preprocessPoints,
} = this._options
let onClick = this._options.onClick
if (!onClick && selectedElement && layer.title !== undefined) {
onClick = (feature: Feature) => {
selectedElement?.setData(feature)
}
}
if (drawLines !== false) {
for (let i = 0; i < layer.lineRendering.length; i++) {
const lineRenderingConfig = layer.lineRendering[i]

View file

@ -7,7 +7,7 @@ export interface ShowDataLayerOptions {
/**
* Features to show
*/
features: FeatureSource<Feature<Geometry, OsmTags>>
features: FeatureSource<Feature<Geometry, Record<string, any> & { id: string }>>
/**
* Indication of the current selected element; overrides some filters.
* When a feature is tapped, the feature will be put in there
@ -32,6 +32,6 @@ export interface ShowDataLayerOptions {
onClick?: (feature: Feature) => void
metaTags?: Store<Record<string, string>>
prefix?: string,
prefix?: string
preprocessPoints?: <T extends Feature<Point>>(fs: FeatureSource<T>) => FeatureSource<T>
}

View file

@ -571,7 +571,7 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
tags: Record<string, string | number> & { _lat: number; _lon: number; _country?: string },
textToParse: string,
country: string,
mode?: mode,
mode?: mode
) {
return new opening_hours(
textToParse,
@ -583,15 +583,13 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
state: undefined,
},
},
<optional_conf> {
<optional_conf>{
tag_key: "opening_hours",
mode,
},
}
)
}
/**
* Constructs the opening-ranges for either this week, or for next week if there are no more openings this week.
* Note: 'today' is mostly used for testing
@ -602,7 +600,7 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
*/
public static createRangesForApplicableWeek(
oh: opening_hours,
today?: Date,
today?: Date
): {
ranges: OpeningRange[][]
startingMonday: Date

View file

@ -15,16 +15,16 @@
let mode = new UIEventSource("")
onDestroy(
value
.map((ph) => OH.ParsePHRule(ph), onDestroy)
.addCallbackAndRunD((parsed) => {
if (parsed === null) {
return
}
mode.setData(parsed.mode)
startValue.setData(parsed.start)
endValue.setData(parsed.end)
})
value
.map((ph) => OH.ParsePHRule(ph), onDestroy)
.addCallbackAndRunD((parsed) => {
if (parsed === null) {
return
}
mode.setData(parsed.mode)
startValue.setData(parsed.start)
endValue.setData(parsed.end)
})
)
function updateValue() {
if (mode.data === undefined || mode.data === "") {
@ -72,11 +72,10 @@
</label>
{#if $mode === " "}
<div class="flex gap-x-1 items-center">
<div class="flex items-center gap-x-1">
<Tr t={t.opensAt} />
<TimeInput value={startValue} />
<Tr t={t.openTill} />
<TimeInput value={endValue} />
</div>
{/if}

View file

@ -1,5 +1,4 @@
<script lang="ts">
/**
* The element showing an "hour" in a bubble, above or below the opening hours table
* Dumbly shows one row of what is given.

View file

@ -9,10 +9,9 @@
import OpeningHoursRangeElement from "./OpeningHoursRangeElement.svelte"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { OH } from "../OpeningHours"
import type { OpeningRange } from "../OpeningHours"
import { Utils } from "../../../Utils"
import { OH } from "../OpeningHours"
import { Lists } from "../../../Utils/Lists"
export let oh: opening_hours
export let ranges: OpeningRange[][] // Per weekday
@ -47,7 +46,7 @@
changeTexts: string[]
}[] = OH.partitionOHForDistance(changeHoursWeekend, changeHourTextWeekend)
let allChangeMoments: number[] = Utils.DedupT([...changeHours, ...changeHoursWeekend])
let allChangeMoments: number[] = Lists.dedupT([...changeHours, ...changeHoursWeekend])
let todayChangeMoments: Set<number> = new Set(OH.allChangeMoments(todayRanges)[0])
// By default, we always show the range between 8 - 19h, in order to give a stable impression
// Ofc, a bigger range is used if needed

View file

@ -17,8 +17,6 @@
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"
import Tr from "../../Base/Tr.svelte"
import ThemeViewState from "../../../Models/ThemeViewState"
import TagExplanation from "../TagExplanation.svelte"
import { And } from "../../../Logic/Tags/And"
import Loading from "../../Base/Loading.svelte"
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
import DocumentDuplicate from "@babeard/svelte-heroicons/solid/DocumentDuplicate"
@ -26,6 +24,7 @@
import { EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import Layers from "../../../assets/svg/Layers.svelte"
import { onDestroy } from "svelte"
import TagHint from "../TagHint.svelte"
export let state: ThemeViewState
export let layer: LayerConfig
@ -42,10 +41,12 @@
let preciseInputIsTapped = false
const forbiddenKeys = new Set(["id", "timestamp", "user", "changeset", "version", "uid"])
let asTags = tags.map((tgs) =>
Object.keys(tgs)
.filter((k) => !k.startsWith("_") && !forbiddenKeys.has(k))
.map((k) => new Tag(k, tgs[k])), onDestroy
let asTags = tags.map(
(tgs) =>
Object.keys(tgs)
.filter((k) => !k.startsWith("_") && !forbiddenKeys.has(k))
.map((k) => new Tag(k, tgs[k])),
onDestroy
)
let showPopup: UIEventSource<boolean> = new UIEventSource(false)
@ -178,7 +179,7 @@
</div>
{#if showTags}
<div class="subtle">
<TagExplanation tagsFilter={new And($asTags)} linkToWiki />
<TagHint tags={$asTags} />
</div>
{/if}
</div>

View file

@ -100,9 +100,8 @@
}>()
</script>
<TitledPanel>
<TitledPanel contentStyle="interactive">
<Tr slot="title" t={Translations.t.general.add.intro} />
{#each presets as preset}
<NextButton on:click={() => dispatch("select", preset)}>
<ToSvelte slot="image" construct={() => preset.icon} />

View file

@ -30,17 +30,20 @@
}
const url = tag2linkData.find((item) => item.key === `Key:${key}`)?.url
const values: string[] =
typeof $tags[key] === "string" ? $tags[key].split(";").map((v: string) => v.trim()) : []
</script>
{#if url}
<a
href={url.replace("$1", $tags[key])}
target="_blank"
rel="noopener noreferrer"
class="tag-link"
>
{$tags[key]}
</a>
{#if url && values.length > 0}
{#each values as value, index}
<span class="tag-link">
{#if index > 0}; {/if}
<a href={url.replace("$1", value)} target="_blank" rel="noopener noreferrer" class="tag-link">
{value}
</a>
</span>
{/each}
{:else}
{$tags[key]}
{/if}

View file

@ -1,7 +1,6 @@
<script lang="ts">
import Loading from "../Base/Loading.svelte"
import MaplibreMap from "../Map/MaplibreMap.svelte"
import { Utils } from "../../Utils"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import FilteredLayer from "../../Models/FilteredLayer"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
@ -13,6 +12,8 @@
import type { AutoAction } from "./AutoApplyButtonVis"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
import { Lists } from "../../Utils/Lists"
import type { OsmFeature } from "../../Models/OsmFeature"
/**
* The ids to handle. Might be data from an external dataset, we cannot assume an OSM-id
@ -42,12 +43,12 @@
mla.allowZooming.setData(false)
mla.allowMoving.setData(false)
const features = ids.mapD((ids) =>
ids.map((id) => state.indexedFeatures.featuresById.data.get(id))
const features: Store<OsmFeature[]> = ids.mapD((ids) =>
ids.map<OsmFeature>((id) => state.indexedFeatures.featuresById.data.get(id))
)
new ShowDataLayer(mlmap, {
features: StaticFeatureSource.fromGeojson(features),
features: new StaticFeatureSource(features),
zoomToFeatures: true,
layer: layer.layerDef,
})
@ -66,7 +67,9 @@
const feature = state.indexedFeatures.featuresById.data.get(targetFeatureId)
const featureTags = state.featureProperties.getStore(targetFeatureId)
const rendering = tagRenderingConfig.GetRenderValue(featureTags.data).txt
const specialRenderings = Lists.noNull(SpecialVisualizations.constructSpecification(rendering)).filter((v) => typeof v !== "string" && v.func["supportsAutoAction"] === true)
const specialRenderings = Lists.noNull(
SpecialVisualizations.constructSpecification(rendering)
).filter((v) => typeof v !== "string" && v.func["supportsAutoAction"] === true)
if (specialRenderings.length == 0) {
console.warn(
@ -119,10 +122,10 @@
<div class="alert">Target tagrendering {options.targetTagRendering} not found"</div>
{:else if $ids.length === 0}
<div>No elements found to perform action</div>
{:else if $buttonState.error !== undefined}
{:else if $buttonState["error"] !== undefined}
<div class="flex flex-col">
<div class="alert">Something went wrong</div>
<div>{$buttonState.error}</div>
<div>{$buttonState["error"]}</div>
</div>
{:else if $buttonState === "done"}
<div class="thanks">All done!</div>

View file

@ -2,14 +2,16 @@ import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import { Changes } from "../../Logic/Osm/Changes"
import {
SpecialVisualisationArg,
SpecialVisualisationParams,
SpecialVisualization,
SpecialVisualizationState,
SpecialVisualizationSvelte,
} from "../SpecialVisualization"
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
import SvelteUIElement from "../Base/SvelteUIElement"
import { Feature } from "geojson"
import AutoApplyButton from "./AutoApplyButton.svelte"
import { OsmFeature } from "../../Models/OsmFeature"
export abstract class AutoAction extends SpecialVisualization {
supportsAutoAction: boolean
@ -19,7 +21,7 @@ export abstract class AutoAction extends SpecialVisualization {
state: {
theme: ThemeConfig
changes: Changes
indexedFeatures: IndexedFeatureSource
indexedFeatures: IndexedFeatureSource<OsmFeature>
},
tagSource: UIEventSource<Record<string, string>>,
argument: string[]
@ -31,12 +33,7 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
public readonly funcName: string = "auto_apply"
public readonly needsUrls = []
public readonly group = "data_import"
public readonly args: {
name: string
defaultValue?: string
doc: string
required?: boolean
}[] = [
public readonly args: SpecialVisualisationArg[] = [
{
name: "target_layer",
doc: "The layer that the target features will reside in",
@ -54,6 +51,7 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
},
{
name: "text",
type: "translation",
doc: "The text to show on the button",
required: true,
},
@ -86,15 +84,11 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
4. At last, add this component`
}
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[]
): SvelteUIElement {
const target_layer_id = argument[0]
const targetTagRendering = argument[2]
const text = argument[3]
const icon = argument[4]
constr({ state, tags, args }: SpecialVisualisationParams): SvelteUIElement {
const target_layer_id = args[0]
const targetTagRendering = args[2]
const text = args[3]
const icon = args[4]
const options = {
target_layer_id,
targetTagRendering,
@ -105,7 +99,7 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
const to_parse: UIEventSource<string[]> = new UIEventSource<string[]>(undefined)
Stores.chronic(500, () => to_parse.data === undefined)
.map(() => {
const applicable = <string | string[]>tagSource.data[argument[1]]
const applicable = <string | string[]>tags.data[args[1]]
if (typeof applicable === "string") {
return <string[]>JSON.parse(applicable)
} else {

View file

@ -1,11 +1,9 @@
import {
SpecialVisualisationParams,
SpecialVisualization,
SpecialVisualizationState,
SpecialVisualizationSvelte,
} from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Feature, LineString } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Translations from "../i18n/Translations"
import SvelteUIElement from "../Base/SvelteUIElement"
import ExportFeatureButton from "./ExportFeatureButton.svelte"
@ -17,13 +15,7 @@ class ExportAsGpxVis extends SpecialVisualizationSvelte {
args = []
needsUrls = []
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
) {
constr({ tags, feature, layer }: SpecialVisualisationParams) {
if (feature.geometry.type !== "LineString") {
return undefined
}
@ -48,7 +40,7 @@ class ExportAsGeojsonVis extends SpecialVisualizationSvelte {
docs = "Exports the selected feature as GeoJson-file"
args = []
constr(state, tags, args, feature, layer) {
constr({ tags, feature, layer }: SpecialVisualisationParams) {
const t = Translations.t.general.download
return new SvelteUIElement(ExportFeatureButton, {
tags,
@ -64,7 +56,7 @@ class ExportAsGeojsonVis extends SpecialVisualizationSvelte {
}
export class DataExportVisualisations {
public static initList(): SpecialVisualization[] {
public static initList(): SpecialVisualizationSvelte[] {
return [new ExportAsGpxVis(), new ExportAsGeojsonVis()]
}
}

View file

@ -1,7 +1,11 @@
import { SpecialVisualization, SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
import {
SpecialVisualisationArg,
SpecialVisualisationParams,
SpecialVisualization,
SpecialVisualizationSvelte,
} from "../SpecialVisualization"
import { HistogramViz } from "./HistogramViz"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Feature } from "geojson"
import { Store } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement"
import SvelteUIElement from "../Base/SvelteUIElement"
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
@ -16,15 +20,18 @@ import NextChangeViz from "../OpeningHours/NextChangeViz.svelte"
import { Unit } from "../../Models/Unit"
import AllFeaturesStatistics from "../Statistics/AllFeaturesStatistics.svelte"
import { LanguageElement } from "./LanguageElement/LanguageElement"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { And } from "../../Logic/Tags/And"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingEditable from "./TagRendering/TagRenderingEditable.svelte"
import AllTagsPanel from "./AllTagsPanel/AllTagsPanel.svelte"
import CollectionTimes from "../CollectionTimes/CollectionTimes.svelte"
import Tr from "../Base/Tr.svelte"
import Combine from "../Base/Combine"
import Marker from "../Map/Marker.svelte"
import { twJoin } from "tailwind-merge"
class DirectionIndicatorVis extends SpecialVisualization {
class DirectionIndicatorVis extends SpecialVisualizationSvelte {
funcName = "direction_indicator"
args = []
@ -32,13 +39,8 @@ class DirectionIndicatorVis extends SpecialVisualization {
"Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object"
group = "data"
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature })
constr(params: SpecialVisualisationParams): SvelteUIElement {
return new SvelteUIElement(DirectionIndicator, params)
}
}
@ -61,27 +63,23 @@ class DirectionAbsolute extends SpecialVisualization {
]
group = "data"
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
): BaseUIElement {
constr({ tags, args }: SpecialVisualisationParams): BaseUIElement {
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
const offset = args[1] === "" ? 0 : Number(args[1])
return new VariableUiElement(
tagSource
tags
.map((tags) => {
console.log("Direction value", tags[key], key)
return tags[key]
})
.mapD((value) => {
const dir = GeoOperations.bearingToHuman(
GeoOperations.parseBearing(value) + offset,
GeoOperations.parseBearing(value) + offset
)
console.log("Human dir", dir)
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
}),
})
)
}
}
@ -113,12 +111,12 @@ class OpeningHoursTableVis extends SpecialVisualizationSvelte {
example =
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`"
constr(state, tagSource: UIEventSource<any>, args) {
constr({ tags, args }: SpecialVisualisationParams): SvelteUIElement {
const [key, prefix, postfix] = args
const openingHoursStore: Store<opening_hours | "error" | undefined> =
OH.CreateOhObjectStore(tagSource, key, prefix, postfix)
OH.CreateOhObjectStore(tags, key, prefix, postfix)
return new SvelteUIElement(OpeningHoursWithError, {
tags: tagSource,
tags,
key,
opening_hours_obj: openingHoursStore,
})
@ -148,11 +146,7 @@ class OpeningHoursState extends SpecialVisualizationSvelte {
},
]
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[],
): SvelteUIElement {
constr({ state, tags, args }: SpecialVisualisationParams): SvelteUIElement {
const keyToUse = args[0]
const prefix = args[1]
const postfix = args[2]
@ -183,25 +177,25 @@ class Canonical extends SpecialVisualization {
},
]
constr(state, tagSource, args) {
constr({ state, tags, args }: SpecialVisualisationParams) {
const key = args[0]
return new VariableUiElement(
tagSource
tags
.map((tags) => tags[key])
.map((value) => {
if (value === undefined) {
return undefined
}
const allUnits: Unit[] = [].concat(
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? []),
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? [])
)
const unit = allUnits.filter((unit) => unit.isApplicableToKey(key))[0]
if (unit === undefined) {
return value
}
const getCountry = () => tagSource.data._country
const getCountry = () => tags.data._country
return unit.asHumanLongValue(value, getCountry)
}),
})
)
}
}
@ -213,41 +207,37 @@ class StatisticsVis extends SpecialVisualizationSvelte {
"Show general statistics about all the elements currently in view. Intended to use on the `current_view`-layer. They will be split per layer"
args = []
constr(state) {
return new SvelteUIElement(AllFeaturesStatistics, { state })
constr(params: SpecialVisualisationParams) {
return new SvelteUIElement(AllFeaturesStatistics, params)
}
}
class PresetDescription extends SpecialVisualization {
class PresetDescription extends SpecialVisualizationSvelte {
funcName = "preset_description"
docs =
"Shows the extra description from the presets of the layer, if one matches. It will pick the most specific one (e.g. if preset `A` implies `B`, but `B` does not imply `A`, it'll pick B) or the first one if no ordering can be made. Might be empty"
args = []
group = "UI"
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
): BaseUIElement {
const translation = tagSource.map((tags) => {
constr({ state, tags }: SpecialVisualisationParams): SvelteUIElement {
const translation = tags.map((tags) => {
const layer = state.theme.getMatchingLayer(tags)
return layer?.getMostMatchingPreset(tags)?.description
})
return new VariableUiElement(translation)
return new SvelteUIElement(Tr, { t: translation })
}
}
class PresetTypeSelect extends SpecialVisualizationSvelte {
funcName = "preset_type_select"
docs = "An editable tag rendering which allows to change the type"
docs = "An editable tag rendering which allows to change the type. The options are the presets of the layer, effectively allowing to change act as if the object was made with a different preset. " +
"For example\n\n" +
"How this element looks like (in question mode) for [`tourism_accomodation`](./Layers/tourism_accomodation.md): ![](./img/Special_preset_type_select_preview.png)" +
"The presets ![](./img/Special_preset_type_select_matching_presets.png)"
args = []
group = "ui"
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
selectedElement: Feature,
layer: LayerConfig,
): SvelteUIElement {
constr({ state, tags, feature, layer }: SpecialVisualisationParams): SvelteUIElement {
const t = Translations.t.preset_type
if (layer._basedOn !== layer.id) {
console.warn("Trying to use the _original_ layer")
@ -273,7 +263,7 @@ class PresetTypeSelect extends SpecialVisualizationSvelte {
return new SvelteUIElement(TagRenderingEditable, {
config,
tags,
selectedElement,
selectedElement: feature,
state,
layer,
})
@ -286,8 +276,8 @@ class AllTagsVis extends SpecialVisualizationSvelte {
args = []
group = "data"
constr(state, tags: UIEventSource<Record<string, string>>, _, __, layer: LayerConfig) {
return new SvelteUIElement(AllTagsPanel, { tags, layer })
constr(params: SpecialVisualisationParams) {
return new SvelteUIElement(AllTagsPanel, params)
}
}
@ -298,19 +288,82 @@ class PointsInTimeVis extends SpecialVisualization {
args = [
{
name: "key",
type: "key",
required: true,
doc: "The key out of which the points_in_time will be parsed",
},
]
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
constr({ tags, args }: SpecialVisualisationParams): BaseUIElement {
const key = args[0]
const points_in_time = tagSource.map(tags => tags[key])
const times = points_in_time.map(times =>
OH.createOhObject(<any>tagSource.data, times, tagSource.data["_country"], 1), [tagSource])
return new VariableUiElement(times.map(times =>
new SvelteUIElement(CollectionTimes, { times }),
))
const points_in_time = tags.map((tags) => tags[key])
const times = points_in_time.map(
(times) => OH.createOhObject(<any>tags.data, times, tags.data["_country"], 1),
[tags]
)
return new VariableUiElement(
times.map((times) => new SvelteUIElement(CollectionTimes, { times }))
)
}
}
class KnownIcons extends SpecialVisualization {
docs =
"Displays all icons from the specified tagRenderings (if they are known and have an icon) together, e.g. to give a summary of the dietary options"
needsUrls = []
group = "UI"
funcName = "show_icons"
args: SpecialVisualisationArg[] = [
{
name: "labels",
doc: "A ';'-separated list of labels and/or ids of tagRenderings",
type: "key",
required: true,
},
{
name: "class",
doc: "CSS-classes of the container, space-separated",
type: "css",
required: false,
defaultValue: "inline-flex mx-4",
},
]
private static readonly emojiHeights = {
small: "2rem",
medium: "3rem",
large: "5rem",
}
constr(options: SpecialVisualisationParams): BaseUIElement {
const labels = new Set(options.args[0].split(";").map((s) => s.trim()))
const matchingTrs = options.layer.tagRenderings.filter(
(tr) => labels.has(tr.id) || tr.labels.some((l) => labels.has(l))
)
return new VariableUiElement(
options.tags.map((tags) =>
new Combine(
matchingTrs.map((tr) => {
const mapping = tr.GetRenderValueWithImage(tags)
if (!mapping?.icon) {
return undefined
}
return new SvelteUIElement(Marker, {
emojiHeight: KnownIcons.emojiHeights[mapping.iconClass] ?? "2rem",
clss: `mapping-icon-${mapping.iconClass ?? "small"}`,
icons: mapping.icon,
size: twJoin(
"shrink-0",
`mapping-icon-${mapping.iconClass ?? "small"}-height mapping-icon-${
mapping.iconClass ?? "small"
}-width`
),
})
})
).SetClass(options.args[1] ?? "inline-flex mx-4")
)
)
}
}
@ -329,6 +382,7 @@ export class DataVisualisations {
new PresetDescription(),
new PresetTypeSelect(),
new AllTagsVis(),
new KnownIcons(),
]
}
}

View file

@ -12,10 +12,16 @@
const validator = new FediverseValidator()
let userinfo = tags
.mapD((t) => t[key], onDestroy)
.mapD((fediAccount) => FediverseValidator.extractServer(validator.reformat(fediAccount)), onDestroy)
.mapD(
(fediAccount) => FediverseValidator.extractServer(validator.reformat(fediAccount)),
onDestroy
)
let homeLocation: Store<string> = state.userRelatedState?.preferencesAsTags
.mapD((prefs) => prefs["_mastodon_link"], onDestroy)
.mapD((userhandle) => FediverseValidator.extractServer(validator.reformat(userhandle))?.server, onDestroy)
.mapD(
(userhandle) => FediverseValidator.extractServer(validator.reformat(userhandle))?.server,
onDestroy
)
</script>
<div class="flex w-full flex-col">

View file

@ -1,5 +1,9 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import {
SpecialVisualisationParams,
SpecialVisualization,
SpecialVisualizationState,
} from "../SpecialVisualization"
import { Feature } from "geojson"
import SvelteUIElement from "../Base/SvelteUIElement"
import Histogram from "../BigComponents/Histogram.svelte"
@ -14,10 +18,10 @@ export class HistogramViz extends SpecialVisualization {
args = [
{
name: "key",
type: "key",
doc: "The key to be read and to generate a histogram from",
required: true,
}
},
]
structuredExamples(): { feature: Feature; args: string[] }[] {
@ -36,23 +40,24 @@ export class HistogramViz extends SpecialVisualization {
]
}
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[]
) {
const values: Store<string[]> = tagSource.map((tags) => {
constr({ tags, args }: SpecialVisualisationParams): SvelteUIElement {
const values: Store<string[]> = tags.map((tags) => {
const value = tags[args[0]]
try {
if (value === "" || value === undefined) {
return undefined
}
if(Array.isArray(value)){
if (Array.isArray(value)) {
return value
}
return JSON.parse(value)
} catch (e) {
console.error("Could not load histogram: parsing of the list failed: ", e,"\nthe data to parse is",value)
console.error(
"Could not load histogram: parsing of the list failed: ",
e,
"\nthe data to parse is",
value
)
return undefined
}
})

View file

@ -1,7 +1,11 @@
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
import {
SpecialVisualisationArg,
SpecialVisualisationParams,
SpecialVisualizationSvelte,
SpecialVisualizationUtils,
} from "../../SpecialVisualization"
import { UIEventSource } from "../../../Logic/UIEventSource"
import { Feature, Geometry, LineString, Polygon } from "geojson"
import BaseUIElement from "../../BaseUIElement"
import { ImportFlowArguments, ImportFlowUtils } from "./ImportFlow"
import Translations from "../../i18n/Translations"
import { Utils } from "../../../Utils"
@ -14,6 +18,7 @@ import { Changes } from "../../../Logic/Osm/Changes"
import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig"
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
import { OsmTags } from "../../../Models/OsmFeature"
import Tr from "../../Base/Tr.svelte"
export interface ConflateFlowArguments extends ImportFlowArguments {
way_to_conflate: string
@ -22,21 +27,20 @@ export interface ConflateFlowArguments extends ImportFlowArguments {
snap_onto_layers?: string
}
export default class ConflateImportButtonViz extends SpecialVisualization implements AutoAction {
export default class ConflateImportButtonViz
extends SpecialVisualizationSvelte
implements AutoAction
{
supportsAutoAction: boolean = true
needsUrls = []
group = "data_import"
public readonly funcName: string = "conflate_button"
public readonly args: {
name: string
defaultValue?: string
doc: string
required?: boolean
}[] = [
public readonly args: SpecialVisualisationArg[] = [
...ImportFlowUtils.generalArguments,
{
name: "way_to_conflate",
type: "key",
doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag",
},
]
@ -84,32 +88,30 @@ export default class ConflateImportButtonViz extends SpecialVisualization implem
await state.changes.applyAction(action)
}
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<OsmTags>,
argument: string[],
feature: Feature
): BaseUIElement {
constr({ state, tags, args, feature }: SpecialVisualisationParams): SvelteUIElement {
const canBeImported =
feature.geometry.type === "LineString" ||
(feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
if (!canBeImported) {
return Translations.t.general.add.import.wrongTypeToConflate.SetClass("alert")
return new SvelteUIElement(Tr, {
t: Translations.t.general.add.import.wrongTypeToConflate,
cls: "alert",
})
}
const args: ConflateFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
const idOfWayToReplaceGeometry = tagSource.data[args.way_to_conflate]
const argsParsed: ConflateFlowArguments = <any>(
SpecialVisualizationUtils.parseArgs(this.args, args)
)
const tagsToApply = ImportFlowUtils.getTagsToApply(<UIEventSource<OsmTags>>tags, argsParsed)
const idOfWayToReplaceGeometry = tags.data[argsParsed.way_to_conflate]
const importFlow = new ConflateImportFlowState(
state,
<Feature<LineString | Polygon>>feature,
args,
argsParsed,
tagsToApply,
tagSource,
tags,
idOfWayToReplaceGeometry
)
return new SvelteUIElement(WayImportFlow, {
importFlow,
})
return new SvelteUIElement(WayImportFlow, { importFlow })
}
getLayerDependencies = (args: string[]) =>

View file

@ -66,13 +66,14 @@ ${Utils.special_visualizations_importRequirementDocs}
* Given the tagsstore of the point which represents the challenge, creates a new store with tags that should be applied onto the newly created point,
*/
public static getTagsToApply(
originalFeatureTags: UIEventSource<OsmTags>,
originalFeatureTags: Store<OsmTags>,
args: { tags: string }
): Store<Tag[]> {
if (originalFeatureTags === undefined) {
return undefined
}
let newTags: Store<Tag[]>
// Listing of the keys that should be transferred
const tags = args.tags
if (
tags.indexOf(" ") < 0 &&
@ -81,12 +82,6 @@ ${Utils.special_visualizations_importRequirementDocs}
) {
// This is a property to expand...
const items: string = originalFeatureTags.data[tags]
console.debug(
"The import button is using tags from properties[" +
tags +
"] of this object, namely ",
items
)
if (items.startsWith("{")) {
// This is probably a JSON

View file

@ -1,7 +1,5 @@
import { Feature, Point } from "geojson"
import { UIEventSource } from "../../../Logic/UIEventSource"
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
import BaseUIElement from "../../BaseUIElement"
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../../SpecialVisualization"
import SvelteUIElement from "../../Base/SvelteUIElement"
import PointImportFlow from "./PointImportFlow.svelte"
import { PointImportFlowArguments, PointImportFlowState } from "./PointImportFlowState"
@ -9,12 +7,14 @@ import { Utils } from "../../../Utils"
import { ImportFlowUtils } from "./ImportFlow"
import Translations from "../../i18n/Translations"
import { GeoOperations } from "../../../Logic/GeoOperations"
import Tr from "../../Base/Tr.svelte"
import { UIEventSource } from "../../../Logic/UIEventSource"
import { OsmTags } from "../../../Models/OsmFeature"
/**
* The wrapper to make the special visualisation for the PointImportFlow
*/
export class PointImportButtonViz extends SpecialVisualization {
export class PointImportButtonViz extends SpecialVisualizationSvelte {
public readonly funcName = "import_button"
public readonly docs: string =
"This button will copy the point from an external dataset into OpenStreetMap" +
@ -47,29 +47,26 @@ export class PointImportButtonViz extends SpecialVisualization {
public needsUrls = []
group = "data_import"
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<OsmTags>,
argument: string[],
feature: Feature
): BaseUIElement {
constr({ state, tags, args, feature }: SpecialVisualisationParams): SvelteUIElement {
const to_point_index = this.args.findIndex((arg) => arg.name === "to_point")
const summarizePointArg = argument[to_point_index].toLowerCase()
const summarizePointArg = args[to_point_index].toLowerCase()
if (feature.geometry.type !== "Point") {
if (summarizePointArg !== "no" && summarizePointArg !== "false") {
feature = GeoOperations.centerpoint(feature)
} else {
return Translations.t.general.add.import.wrongType.SetClass("alert")
return new SvelteUIElement(Tr, {
t: Translations.t.general.add.import.wrongType.SetClass("alert"),
})
}
}
const baseArgs: PointImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, baseArgs)
const baseArgs: PointImportFlowArguments = <any>Utils.ParseVisArgs(this.args, args)
const tagsToApply = ImportFlowUtils.getTagsToApply(<UIEventSource<OsmTags>>tags, baseArgs)
const importFlow = new PointImportFlowState(
state,
<Feature<Point>>feature,
baseArgs,
tagsToApply,
tagSource
tags
)
return new SvelteUIElement(PointImportFlow, {

View file

@ -1,10 +1,12 @@
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
import {
SpecialVisualisationParams,
SpecialVisualizationSvelte,
SpecialVisualizationUtils,
} from "../../SpecialVisualization"
import { AutoAction } from "../AutoApplyButtonVis"
import { Feature, LineString, Polygon } from "geojson"
import { UIEventSource } from "../../../Logic/UIEventSource"
import BaseUIElement from "../../BaseUIElement"
import { ImportFlowUtils } from "./ImportFlow"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import SvelteUIElement from "../../Base/SvelteUIElement"
import WayImportFlow from "./WayImportFlow.svelte"
import WayImportFlowState, { WayImportFlowArguments } from "./WayImportFlowState"
@ -13,12 +15,11 @@ import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig"
import { Changes } from "../../../Logic/Osm/Changes"
import { IndexedFeatureSource } from "../../../Logic/FeatureSource/FeatureSource"
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { OsmTags } from "../../../Models/OsmFeature"
/**
* Wrapper around 'WayImportFlow' to make it a special visualisation
*/
export default class WayImportButtonViz extends SpecialVisualization implements AutoAction {
export default class WayImportButtonViz extends SpecialVisualizationSvelte implements AutoAction {
public readonly funcName: string = "import_way_button"
needsUrls = []
group = "data_import"
@ -60,25 +61,22 @@ export default class WayImportButtonViz extends SpecialVisualization implements
public readonly supportsAutoAction = true
public readonly needsNodeDatabase = true
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<OsmTags>,
argument: string[],
feature: Feature,
_: LayerConfig
): BaseUIElement {
constr({ state, tags, args, feature }: SpecialVisualisationParams): SvelteUIElement {
const geometry = feature.geometry
if (!(geometry.type == "LineString" || geometry.type === "Polygon")) {
throw "Invalid type to import " + geometry.type
throw "Invalid type to import, expected linestring of polygon but got " + geometry.type
}
const args: WayImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
const parsedArgs: WayImportFlowArguments = <any>(
SpecialVisualizationUtils.parseArgs(this.args, args)
)
console.log("Parsed args are", parsedArgs)
const tagsToApply = ImportFlowUtils.getTagsToApply(tags, parsedArgs)
const importFlow = new WayImportFlowState(
state,
<Feature<LineString | Polygon>>feature,
args,
parsedArgs,
tagsToApply,
tagSource
tags
)
return new SvelteUIElement(WayImportFlow, {
importFlow,

View file

@ -21,8 +21,8 @@
const map = new UIEventSource<MlMap>(undefined)
const [lon, lat] = GeoOperations.centerpointCoordinates(importFlow.originalFeature)
const mla = new MapLibreAdaptor(map, {
allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting),
allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting),
allowMoving: state.featureSwitchIsTesting.followingClone(),
allowZooming: state.featureSwitchIsTesting.followingClone(),
rasterLayer: state.mapProperties.rasterLayer,
location: new UIEventSource<{ lon: number; lat: number }>({ lon, lat }),
zoom: new UIEventSource<number>(18),

View file

@ -14,6 +14,7 @@ import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/C
import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig"
import { Changes } from "../../../Logic/Osm/Changes"
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { OsmFeature } from "../../../Models/OsmFeature"
export interface WayImportFlowArguments extends ImportFlowArguments {
max_snap_distance: string
@ -54,7 +55,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
state: {
theme: ThemeConfig
changes: Changes
indexedFeatures: IndexedFeatureSource
indexedFeatures: IndexedFeatureSource<OsmFeature>
fullNodeDatabase?: FullNodeDatabaseSource
},
tagsToApply: Store<Tag[]>,
@ -82,7 +83,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
const coors = feature.geometry.coordinates
return new CreateWayWithPointReuseAction(tagsToApply.data, coors, state, mergeConfigs)
} else {
throw "Unsupported type"
throw "Unsupported type: cannot import something of type "+feature.geometry["type"]+", only Polygon and LineString are supported"
}
}

View file

@ -6,6 +6,7 @@
import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import * as all_languages from "../../../assets/language_translations.json"
import Locale from "../../i18n/Locale"
/**
* Visualizes a list of the known languages
@ -22,6 +23,7 @@
export let layer: LayerConfig | undefined
let [beforeListing, afterListing] = (render_all ?? "{list()}").split("{list()}")
let currentLanguage: Store<string> = Locale.language
</script>
{#if $languages.length === 1}
@ -30,9 +32,8 @@
{tags}
{feature}
{layer}
t={new TypedTranslation({ "*": single_render }).PartialSubsTr(
"language()",
new Translation(all_languages[$languages[0]], undefined)
t={new TypedTranslation({ "*": single_render }).PartialSubs(
{"language()": new Translation(all_languages[$languages[0]]).textFor($currentLanguage)}
)}
/>
{:else}
@ -45,9 +46,8 @@
{tags}
{feature}
{layer}
t={new TypedTranslation({ "*": item_render }).PartialSubsTr(
"language()",
new Translation(all_languages[language], undefined)
t={new TypedTranslation({ "*": item_render }).PartialSubs(
{"language()": new Translation(all_languages[language]).textFor($currentLanguage)}
)}
/>
</li>

View file

@ -42,7 +42,7 @@
{#if $foundLanguages.length === 0 && on_no_known_languages && !$forceInputMode}
<div class="low-interaction flex items-center justify-between rounded p-1">
<div>
<div class="ml-1">
{on_no_known_languages}
</div>
<EditButton on:click={(_) => forceInputMode.setData(true)} />

View file

@ -1,46 +1,55 @@
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
import BaseUIElement from "../../BaseUIElement"
import { UIEventSource } from "../../../Logic/UIEventSource"
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../../SpecialVisualization"
import SvelteUIElement from "../../Base/SvelteUIElement"
import { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { default as LanguageElementSvelte } from "./LanguageElement.svelte"
export class LanguageElement extends SpecialVisualization {
export class LanguageElement extends SpecialVisualizationSvelte {
funcName: string = "language_chooser"
needsUrls = []
docs: string | BaseUIElement =
"The language element allows to show and pick all known (modern) languages. The key can be set"
docs: string =
"The language element allows to show and pick all known (modern) languages (includes sign languages). The key can be set"
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
args: {
name: string
defaultValue?: string
doc: string
required?: boolean
type?: string
}[] = [
{
name: "key",
required: true,
type: "key",
doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `<key>:nl=yes` if _nl_ is picked ",
},
{
name: "question",
required: true,
type: "translation",
doc: "What to ask if no questions are known",
},
{
name: "render_list_item",
type: "translation",
doc: "How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain).",
defaultValue: "{language()}",
},
{
name: "render_single_language",
type: "translation",
doc: "What will be shown if the feature only supports a single language",
required: true,
},
{
type: "translation",
name: "render_all",
doc: "The full rendering. Use `{list}` to show where the list of languages must come. Optional if mode=single",
doc: "The full rendering. U0se `{list}` to show where the list of languages must come. Optional if mode=single",
defaultValue: "{list()}",
},
{
name: "no_known_languages",
type: "translation",
doc: "The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead",
},
]
@ -58,15 +67,8 @@ export class LanguageElement extends SpecialVisualization {
\`\`\`
`
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
let [key, question, item_render, single_render, all_render, on_no_known_languages] =
argument
constr({ state, tags, args, feature, layer }: SpecialVisualisationParams): SvelteUIElement {
let [key, question, item_render, single_render, all_render, on_no_known_languages] = args
if (item_render === undefined || item_render.trim() === "") {
item_render = "{language()}"
}
@ -94,7 +96,7 @@ export class LanguageElement extends SpecialVisualization {
return new SvelteUIElement(LanguageElementSvelte, {
key,
tags: tagSource,
tags,
state,
feature,
layer,

View file

@ -106,7 +106,7 @@
<Tr t={Translations.t.general.useSearch} />
</label>
<div class="overflow-auto" style="max-height: 25vh">
<div style="max-height: 25vh">
{#each knownLanguagecodes as lng}
{#if isChecked[lng] && $newlyChecked.indexOf(lng) < 0 && probableLanguages.indexOf(lng) < 0}
<label class="no-image-background flex items-center gap-1">

View file

@ -21,6 +21,7 @@
import { Tag } from "../../../Logic/Tags/Tag"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../../Logic/Tags/And"
import TagHint from "../TagHint.svelte"
export let question: string
export let prefix: string
@ -67,6 +68,10 @@
}
dispatch("save")
}
const showTags = state.userRelatedState.showTagsB
// For the preview
let asTags = selectedLanguages.mapD(lngs => lngs.map((ln) => new Tag(prefix + ln, "yes")))
</script>
<div class="disable-links interactive border-interactive flex flex-col p-2">
@ -74,7 +79,6 @@
<SpecialTranslation {feature} {layer} {state} t={new Translation({ "*": question })} {tags} />
</div>
<LanguageOptions {selectedLanguages} countries={$countries} />
<div class="flex w-full flex-wrap-reverse justify-end">
<slot name="cancel-button" />
<button
@ -85,4 +89,9 @@
<Tr t={Translations.t.general.save} />
</button>
</div>
{#if showTags}
<div class="subtle w-fit">
<TagHint tags={$asTags} />
</div>
{/if}
</div>

View file

@ -1,7 +1,6 @@
import { GeoOperations } from "../../Logic/GeoOperations"
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
import { Feature } from "geojson"
import { ImmutableStore } from "../../Logic/UIEventSource"
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../SpecialVisualization"
import SvelteUIElement from "../Base/SvelteUIElement"
import MapillaryLink from "../BigComponents/MapillaryLink.svelte"
@ -20,12 +19,7 @@ export class MapillaryLinkVis extends SpecialVisualizationSvelte {
},
]
public constr(
state: SpecialVisualizationState,
tagsSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature
): SvelteUIElement {
public constr({ args, feature }: SpecialVisualisationParams): SvelteUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
let zoom = Number(args[0])
if (isNaN(zoom)) {

Some files were not shown because too many files have changed in this diff Show more