Merge branch 'develop' into Robin-patch-1

This commit is contained in:
Robin van der Linde 2025-08-17 13:23:12 +02:00
commit 0a812b0236
597 changed files with 18999 additions and 15074 deletions

View file

@ -156,8 +156,8 @@
</script>
<main>
<div class="w-full low-interaction">
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top}/>
<div class="low-interaction w-full">
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top} />
</div>
<DrawerLeft shown={guistate.pageStates.menu}>
@ -186,7 +186,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">
@ -275,8 +275,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

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

@ -40,15 +40,13 @@
{#if $badge}
{#if !$online}
{#if !hiddenFail}
<div class="alert">
Your device is offline
</div>
<div class="alert">Your device is offline</div>
{/if}
{:else if !ignoreLoading && !hiddenFail && $loadingStatus === "loading"}
<slot name="loading">
<Loading />
</slot>
{:else if ($loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline")}
{:else if $loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline"}
{#if !hiddenFail}
<slot name="error">
<div class="alert flex flex-col items-center">

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

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

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

View file

@ -1,56 +1,68 @@
<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(e: Event) {
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" />
<button
style="z-index: -1"
class={"as-link pointer-events-auto relative " + size}
on:click={(e) => clicked(e)}
>
{#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 flex-wrap md:flex-nowrap items-center justify-center">
<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 class="grow-0 w-48"/>
<NGI0Entrust_tag class="w-48 grow-0" />
</a>
<a href="https://nlnet.nl" class="p-2">
<Nlnet class="grow-0 w-48"/>
<Nlnet class="w-48 grow-0" />
</a>
</div>
<span>

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,6 @@
}
})
let hotkeys = Hotkeys._docs
</script>
<div class:h-0={!onlyLink} class:h-full={onlyLink} class="overflow-hidden">

View file

@ -95,7 +95,6 @@
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}>
@ -129,14 +128,13 @@
<SidebarUnit>
<LoginToggle {state}>
<LoginButton clss="primary" osmConnection={state.osmConnection} slot="not-logged-in" />
<div class="flex items-center gap-x-4 w-full m-2">
<div class="m-2 flex w-full items-center gap-x-4">
{#if $userdetails.img}
<img alt="avatar" src={$userdetails.img} class="h-12 w-12 rounded-full" />
{:else}
<UserCircle class="h-14 w-14" color="gray"/>
{:else}
<UserCircle class="h-14 w-14" color="gray" />
{/if}
<div class="flex flex-col w-full gap-y-2">
<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>
@ -205,7 +203,8 @@
<LanguagePicker
preferredLanguages={state.userRelatedState.osmConnection.userDetails.mapD(
(ud) => ud.languages, onDestroy
(ud) => ud.languages,
onDestroy
)}
/>
</SidebarUnit>
@ -219,12 +218,7 @@
<Tr t={Translations.t.general.menu.aboutMapComplete} />
</h3>
<a
class="flex"
href={($isAndroid
? "https://mapcomplete.org"
: ".")+"/studio.html"}
>
<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>
@ -276,8 +270,11 @@
</a>
{#if !state.theme}
<a class="flex" href={($isAndroid ? "https://mapcomplete.org" : ".") +`/statistics.html`}
target="_blank">
<a
class="flex"
href={($isAndroid ? "https://mapcomplete.org" : ".") + `/statistics.html`}
target="_blank"
>
<ChartBar class="h-6 w-6" />
<Tr
t={Translations.t.general.attribution.openStatistics.Subs({ theme: "MapComplete" })}
@ -334,5 +331,4 @@
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom} />
{/if}
</div>
</div>

View file

@ -114,7 +114,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"
@ -22,7 +21,6 @@
import { OfflineBasemapManager } from "../../Logic/OfflineBasemapManager"
import Checkbox from "../Base/Checkbox.svelte"
export let state: ThemeViewState & SpecialVisualizationState = undefined
export let autoDownload = state.autoDownloadOfflineBasemap
@ -35,20 +33,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,111 +71,120 @@
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",
},
],
}),
})
</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 class="max-h-leave-room flex h-full flex-col">
<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>
{#if $installed === undefined}
<Loading />
{:else}
<div class="h-full overflow-auto 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">
<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
</Loading>
<Loading>Data is being downloaded</Loading>
</div>
{:else}
<button class="primary pointer-events-auto" on:click={() => download()}
class:disabled={$focusTileIsInstalled}>
<DownloadIcon class="w-8 h-8" />
<button
class="primary pointer-events-auto"
on:click={() => download()}
class:disabled={$focusTileIsInstalled}
>
<DownloadIcon class="h-8 w-8" />
Download
</button>
{/if}
@ -176,21 +193,19 @@
</AccordionItem>
<AccordionItem paddingDefault="p-2">
<div slot="header">
Offline tile management
</div>
<div slot="header">Offline tile management</div>
<div class="leave-room">
{Utils.toHumanByteSize(Utils.sum($installed.map(area => area.size)))}
<button on:click={() => {
installed?.data?.forEach(area => del(area))
}}>
{Utils.toHumanByteSize(Utils.sum($installed.map((area) => area.size)))}
<button
on:click={() => {
installed?.data?.forEach((area) => del(area))
}}
>
<TrashIcon class="w-6" />
Delete all
</button>
<table class="w-full ">
<table class="w-full">
<tr>
<th>Name</th>
<th>Map generation date</th>
@ -198,12 +213,13 @@
<th>Zoom ranges</th>
<th>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}
@ -218,10 +234,8 @@
</td>
</tr>
{/each}
</table>
</div>
</AccordionItem>
</Accordion>
</div>
@ -229,10 +243,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

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

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

@ -33,7 +33,7 @@
* A switch that signals that the information should be downloaded.
* The actual 'download' code is _not_ implemented here
*/
export let downloadInformation : UIEventSource<boolean>
export let downloadInformation: UIEventSource<boolean>
export let collapsed: boolean
const t = Translations.t.external
@ -53,7 +53,7 @@
<LoginToggle {state} silentFail>
{#if !$sourceUrl || !$enableLogin}
<!-- empty block -->
{:else if !$downloadInformation}
{:else if !$downloadInformation}
<button on:click={() => downloadInformation.set(true)}>
Attempt to download information from the website {$sourceUrl}
</button>

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

@ -9,11 +9,11 @@
export let failed: number
export let state: SpecialVisualizationState
const t = Translations.t.image
let dispatch = createEventDispatcher<{retry}>()
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}
@ -23,8 +23,8 @@
<Tr cls="text-xs" t={t.upload.failReasonsAdvanced} />
{#if state}
<button class="primary pointer-events-auto" on:click={() => dispatch("retry")}>
<ArrowPath class="w-6"/>
<Tr t={Translations.t.general.retry}/>
<ArrowPath class="w-6" />
<Tr t={Translations.t.general.retry} />
</button>
{/if}
</div>

View file

@ -25,7 +25,7 @@
Number of images uploaded succesfully
*/
function getCount(input: Store<string[]>): Store<number> {
if(!input){
if (!input) {
return new ImmutableStore(0)
}
if (featureId == "*") {
@ -62,10 +62,9 @@
let progress = state.imageUploadManager.progressCurrentImage
function retry(){
function retry() {
state.imageUploadManager.uploadQueue()
}
</script>
{#if $debugging}
@ -99,7 +98,14 @@
{/if}
{#if $failed > dismissed}
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} on:retry={() => {retry()}} {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"
@ -16,16 +15,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 +50,6 @@
selectedDays[i]?.set(true)
}
} else {
let index = daysOfTheWeek.indexOf(initialDay)
if (index >= 0) {
selectedDays[index]?.set(true)
@ -47,19 +57,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,27 +92,28 @@
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" />
<button
on:click={() => {
values.data.push(new UIEventSource(undefined))
values.ping()
}}
>
<PlusCircle class="h-6 w-6" />
Add time
</button>
</div>
@ -108,14 +126,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

@ -85,7 +85,8 @@
},
]
},
[mapLocation], onDestroy
[mapLocation],
onDestroy
)
new ShowDataLayer(map, {

View file

@ -35,7 +35,7 @@
const state = new OpeningHoursState(value, prefix, postfix)
let expanded = new UIEventSource(false)
function abort(){
function abort() {
expanded.set(false)
}
</script>
@ -44,17 +44,19 @@
<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"/>
<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 top-0 right-0 z-10" style="margin-top: -1.0rem" on:click={() => abort()} />
<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,5 +1,4 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import type { ValidatorType } from "./Validators"
import Validators from "./Validators"

View file

@ -91,7 +91,7 @@
if (!range) {
return true
}
if(typeof canonicalValue === "string"){
if (typeof canonicalValue === "string") {
canonicalValue = Number(canonicalValue)
}
if (canonicalValue < range.warnBelow) {

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

@ -83,7 +83,6 @@ export default class Validators {
new DirectionValidator(),
new SlopeValidator(),
new UrlValidator(),
new EmailValidator(),
new PhoneValidator(),

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

@ -78,7 +78,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
@ -755,5 +757,4 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
})
})
}
}

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,46 +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
})[] {
if(!features){
if (!features) {
return []
}
const resultingFeatures: (Feature<Point, { id: string }> & { locationType: PointRenderingLocation })[] = []
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") {
@ -157,7 +168,6 @@ export class PointRenderingLayer {
}
return resultingFeatures
}
/**
@ -165,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 {
@ -180,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()
@ -218,7 +228,6 @@ 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)

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

@ -361,14 +361,13 @@ export default class ShowDataLayer {
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,21 @@ 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"
})
showSummaryAt: "tilecenter",
}
)
new ShowDataLayer(mlmap, options)
}
@ -453,14 +456,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

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

@ -42,10 +42,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)

View file

@ -66,7 +66,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(

View file

@ -2,8 +2,9 @@ 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"
@ -31,12 +32,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 +50,7 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
},
{
name: "text",
type:"translation",
doc: "The text to show on the button",
required: true,
},
@ -86,15 +83,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 +98,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,5 @@
import {
SpecialVisualization,
SpecialVisualizationState,
SpecialVisualizationSvelte,
} from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import { SpecialVisualisationParams, SpecialVisualization, SpecialVisualizationSvelte } from "../SpecialVisualization"
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 +11,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 +36,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 +52,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,25 @@ 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
.map((tags) => {
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,14 +113,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,
key,
opening_hours_obj: openingHoursStore,
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,26 +207,24 @@ 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 })
}
}
@ -240,14 +232,9 @@ class PresetTypeSelect extends SpecialVisualizationSvelte {
funcName = "preset_type_select"
docs = "An editable tag rendering which allows to change the type"
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 +260,7 @@ class PresetTypeSelect extends SpecialVisualizationSvelte {
return new SvelteUIElement(TagRenderingEditable, {
config,
tags,
selectedElement,
selectedElement: feature,
state,
layer,
})
@ -286,8 +273,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,18 +285,75 @@ 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 +373,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,5 @@
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 +14,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 +36,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,17 @@ 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 +85,25 @@ 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,24 @@ 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,20 @@ 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

@ -1,46 +1,49 @@
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 =
docs: string =
"The language element allows to show and pick all known (modern) 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",
},
]
@ -59,14 +62,16 @@ export class LanguageElement extends SpecialVisualization {
`
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
{
state,
tags,
args,
feature,
layer,
}: SpecialVisualisationParams,
): SvelteUIElement {
let [key, question, item_render, single_render, all_render, on_no_known_languages] =
argument
args
if (item_render === undefined || item_render.trim() === "") {
item_render = "{language()}"
}
@ -94,7 +99,7 @@ export class LanguageElement extends SpecialVisualization {
return new SvelteUIElement(LanguageElementSvelte, {
key,
tags: tagSource,
tags,
state,
feature,
layer,

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

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import type { Feature, Geometry } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import type { SpecialVisualizationState } from "../SpecialVisualization"
@ -8,22 +8,23 @@
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import MaplibreMap from "../Map/MaplibreMap.svelte"
import DelayedComponent from "../Base/DelayedComponent.svelte"
import type { OsmTags } from "../../Models/OsmFeature"
import { onDestroy } from "svelte"
export let state: SpecialVisualizationState
export let tagSource: UIEventSource<Record<string, string>>
export let tags: UIEventSource<Record<string, string>>
export let defaultzoom: number
export let idkeys: string[]
export let feature: Feature
export let clss : string = "h-40 rounded"
const keys =idkeys
let featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map(
export let clss: string = "h-40 rounded"
const keys = idkeys
let featuresToShow: Store<Feature<Geometry, OsmTags>[]> = state.indexedFeatures.featuresById.map(
(featuresById) => {
if (featuresById === undefined) {
return []
}
const properties = tagSource.data
const features: Feature[] = []
const properties = tags.data
const features: Feature<Geometry, OsmTags>[] = []
for (const key of keys) {
const value = properties[key]
if (value === undefined || value === null) {
@ -44,12 +45,13 @@
console.warn("No feature found for id ", id)
continue
}
features.push(feature)
features.push(<Feature<Geometry, OsmTags>> feature)
}
}
return features
},
[tagSource], onDestroy
[tags],
onDestroy,
)
let mlmap = new UIEventSource(undefined)
@ -65,12 +67,11 @@
mla.location.setData({ lon, lat })
mla.zoom.setData(defaultzoom)
ShowDataLayer.showMultipleLayers(
mlmap,
new StaticFeatureSource(featuresToShow),
state.theme.layers,
{ zoomToFeatures: true }
{ zoomToFeatures: true },
)
</script>

View file

@ -62,16 +62,17 @@
reason.setData(moveWizardState.reasons[0])
}
let notAllowed = moveWizardState.moveDisallowedReason
let currentMapProperties: Store<Partial<MapProperties> & { location }> = reason.mapD((r) =>
initMapProperties(r), onDestroy
let currentMapProperties: Store<Partial<MapProperties> & { location }> = reason.mapD(
(r) => initMapProperties(r),
onDestroy
)
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let searchValue = new UIEventSource<string>("")
let isSearching = new UIEventSource<boolean>(false)
let zoomedInEnough = currentMapProperties.bindD(properties => properties.zoom, onDestroy).mapD(
(zoom) => zoom >= Constants.minZoomLevelToAddNewPoint, onDestroy
)
let zoomedInEnough = currentMapProperties
.bindD((properties) => properties.zoom, onDestroy)
.mapD((zoom) => zoom >= Constants.minZoomLevelToAddNewPoint, onDestroy)
const searcher = new NominatimGeocoding(1)
async function searchPressed() {
@ -163,9 +164,7 @@
</div>
<div class="flex flex-wrap">
<If
condition={zoomedInEnough}
>
<If condition={zoomedInEnough}>
<button
class="primary w-full"
on:click={() => {

View file

@ -1,6 +1,6 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Store } from "../../Logic/UIEventSource"
import { MultiApplyParams } from "./MultiApply"
import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../SpecialVisualization"
import SvelteUIElement from "../Base/SvelteUIElement"
import MultiApplyButton from "./MultiApplyButton.svelte"
@ -19,7 +19,9 @@ export class MultiApplyViz extends SpecialVisualizationSvelte {
doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.",
required: true,
},
{ name: "text", doc: "The text to show on the button" },
{ name: "text",
type: "translation",
doc: "The text to show on the button" },
{
name: "autoapply",
doc: "A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown",
@ -34,17 +36,13 @@ export class MultiApplyViz extends SpecialVisualizationSvelte {
example =
"{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}"
constr(
state: SpecialVisualizationState,
tagsSource: UIEventSource<Record<string, string>>,
args: string[]
): SvelteUIElement {
constr({ state, tags, args }: SpecialVisualisationParams): SvelteUIElement {
const featureIdsKey = args[0]
const keysToApply = args[1].split(";")
const text = args[2]
const autoapply = args[3]?.toLowerCase() === "true"
const overwrite = args[4]?.toLowerCase() === "true"
const featureIds: Store<string[]> = tagsSource.map((tags) => {
const featureIds: Store<string[]> = tags.map((tags) => {
const ids = tags[featureIdsKey]
try {
if (ids === undefined) {
@ -69,7 +67,7 @@ export class MultiApplyViz extends SpecialVisualizationSvelte {
text,
autoapply,
overwrite,
tagsSource,
tagsSource: tags,
state,
}
return new SvelteUIElement(MultiApplyButton, { params })

View file

@ -25,10 +25,12 @@
const t = Translations.t.notes
// Info about the user who made the comment
let userinfo = UIEventSource.fromPromise(Utils.downloadJsonCached<{ user: { img: { href: string } } }>(
"https://api.openstreetmap.org/api/0.6/user/" + comment.uid,
24 * 60 * 60 * 1000
))
let userinfo = UIEventSource.fromPromise(
Utils.downloadJsonCached<{ user: { img: { href: string } } }>(
"https://api.openstreetmap.org/api/0.6/user/" + comment.uid,
24 * 60 * 60 * 1000
)
)
const htmlElement = document.createElement("div")
htmlElement.innerHTML = Utils.purify(comment.html)

View file

@ -1,11 +1,11 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Store } from "../../Logic/UIEventSource"
import { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import Wikidata from "../../Logic/Web/Wikidata"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../Logic/Tags/And"
import { Tag } from "../../Logic/Tags/Tag"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../SpecialVisualization"
import SvelteUIElement from "../Base/SvelteUIElement"
import PlantNet from "../PlantNet/PlantNet.svelte"
import { default as PlantNetCode } from "../../Logic/Web/PlantNet"
@ -31,16 +31,13 @@ export class PlantNetDetectionViz extends SpecialVisualizationSvelte {
args = [
{
name: "image_key",
type:"key",
defaultValue: AllImageProviders.defaultKeys.join(","),
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ",
},
]
public constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[]
): SvelteUIElement {
public constr({ state, tags, args }: SpecialVisualisationParams): SvelteUIElement {
let imagePrefixes: string[] = undefined
if (args.length > 0) {
imagePrefixes = [].concat(...args.map((a) => a.split(",")))

View file

@ -48,17 +48,13 @@
return ""
}
})
.map(
(id) => {
let host = `${window.location.protocol}//${window.location.host}`
if(AndroidPolyfill.inAndroid.data){
host = "https://mapcomplete.org"
}
return `${host}${
window.location.pathname
}?${params.join("&")}${id}`
.map((id) => {
let host = `${window.location.protocol}//${window.location.host}`
if (AndroidPolyfill.inAndroid.data) {
host = "https://mapcomplete.org"
}
)
return `${host}${window.location.pathname}?${params.join("&")}${id}`
})
function toggleSize() {
if (size.data !== bigSize) {

View file

@ -1,6 +1,5 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../SpecialVisualization"
import SvelteUIElement from "../Base/SvelteUIElement"
import ShareButton from "../Base/ShareButton.svelte"
@ -17,25 +16,25 @@ export class ShareLinkViz extends SpecialVisualizationSvelte {
},
{
name: "text",
type:"translation",
doc: "The text to show on the button. If none is given, will act as a titleIcon",
},
]
needsUrls = []
public constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[]
public constr({
state,
tags,
args}:SpecialVisualisationParams
) {
const text = args[1]
const generateShareData = () => {
const title = state?.theme?.title?.txt ?? "MapComplete"
const matchingLayer: LayerConfig = state?.theme?.getMatchingLayer(tagSource?.data)
const matchingLayer: LayerConfig = state?.theme?.getMatchingLayer(tags?.data)
let name =
matchingLayer?.title?.GetRenderValue(tagSource.data)?.Subs(tagSource.data)?.txt ??
tagSource.data?.name ??
matchingLayer?.title?.GetRenderValue(tags.data)?.Subs(tags.data)?.txt ??
tags.data?.name ??
"POI"
if (name) {
name = `${name} (${title})`

View file

@ -34,7 +34,7 @@
onDestroy(
value.addCallbackD(() => {
dispatch("selected")
}),
})
)
function getCountry() {

View file

@ -51,7 +51,7 @@
{
try {
return specpart.func
.constr(state, tags, specpart.args, feature, layer)
.constr({state, tags, args : specpart.args, feature, layer})
?.SetClass(specpart.style)
} catch (e) {
console.error(

View file

@ -10,7 +10,6 @@
import { onDestroy } from "svelte"
import { Lists } from "../../../Utils/Lists"
export let tags: UIEventSource<Record<string, string> | undefined>
export let state: SpecialVisualizationState
@ -27,8 +26,9 @@
console.error("TagRenderingAnswer: Config is undefined")
throw "Config is undefined in tagRenderingAnswer"
}
let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) =>
Lists.noNull(config?.GetRenderValues(tags)),onDestroy
let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD(
(tags) => Lists.noNull(config?.GetRenderValues(tags)),
onDestroy
)
</script>

View file

@ -67,8 +67,9 @@
/**
* The tags to apply to mark this answer as "unknown"
*/
let onMarkUnknown: Store<UploadableTag[] | undefined> = tags.mapD((tags) =>
config.markUnknown(layer, tags), onDestroy
let onMarkUnknown: Store<UploadableTag[] | undefined> = tags.mapD(
(tags) => config.markUnknown(layer, tags),
onDestroy
)
let unknownModal = new UIEventSource(false)

View file

@ -1,4 +1,9 @@
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import {
SpecialVisualisationParams,
SpecialVisualization,
SpecialVisualizationState,
SpecialVisualizationSvelte,
} from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import { GeoOperations } from "../../Logic/GeoOperations"
import Constants from "../../Models/Constants"
@ -9,18 +14,14 @@ import { ServerSourceInfo } from "../../Models/SourceOverview"
/**
* Wrapper around 'UploadTraceToOsmUI'
*/
export class UploadToOsmViz extends SpecialVisualization {
export class UploadToOsmViz extends SpecialVisualizationSvelte {
funcName = "upload_to_osm"
docs =
"Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored."
args = []
needsUrls: ServerSourceInfo[] = [Constants.osmAuthConfig]
constr(
state: SpecialVisualizationState,
_: UIEventSource<Record<string, string>>,
__: string[]
) {
constr({ state }: SpecialVisualisationParams): SvelteUIElement {
const locations = state.historicalUserLocations.features.data
return new SvelteUIElement(UploadTraceToOsmUI, {
state,

View file

@ -20,12 +20,12 @@
export let state: ThemeViewState
export let tags: UIEventSource<Record<string, string>> = undefined
export let feature: Feature = undefined
export let layer: LayerConfig =undefined
export let layer: LayerConfig = undefined
let average = reviews.average
let allReviews: Store<
(Review & {
kid: string,
signature: string,
kid: string
signature: string
madeByLoggedInUser: Store<boolean>
})[]
> = reviews.reviews.map((r) => Lists.noNull(r))
@ -35,27 +35,27 @@
</script>
<ReviewPrivacyShield loadingAllowed={reviews.loadingAllowed} guistate={state.guistate}>
<div class="border-2 border-dashed border-gray-300 p-2 flex flex-col gap-y-2">
<div class="flex flex-col gap-y-2 border-2 border-dashed border-gray-300 p-2">
{#if $allReviews?.length > 1}
<StarsBar score={$average} />
{/if}
{#if !$allReviews}
<div class="flex justify-center">
<Loading/>
<Loading />
</div>
{:else if $allReviews.length > 0}
{#each $allReviews as review}
<SingleReview {review} {state} {tags} {feature} {layer} {reviews} showMenu={false}/>
<SingleReview {review} {state} {tags} {feature} {layer} {reviews} showMenu={false} />
{/each}
{:else}
<div class="subtle m-2 italic flex justify-center">
<div class="subtle m-2 flex justify-center italic">
<Tr t={Translations.t.reviews.no_reviews_yet} />
</div>
{/if}
<div class="flex justify-end">
<a
href={"https://mangrove.reviews" +
($allReviews?.length > 0 ?"/search?sub=" + encodeURIComponent($subject):"")}
($allReviews?.length > 0 ? "/search?sub=" + encodeURIComponent($subject) : "")}
target="_blank"
class="subtle text-sm"
>

View file

@ -29,7 +29,8 @@
*/
export let reviews: ReviewCollection
export let editReview: Review & { signature: string } = undefined
export let subject: Store<string> = editReview !== undefined ? new ImmutableStore(editReview.sub) : reviews.subjectUri
export let subject: Store<string> =
editReview !== undefined ? new ImmutableStore(editReview.sub) : reviews.subjectUri
let score = editReview?.rating ?? 0
let confirmedScore = editReview?.rating
let isAffiliated = new UIEventSource(editReview?.metadata?.is_affiliated ?? false)
@ -103,10 +104,13 @@
_state = "done"
}
</script>
<ReviewPrivacyShield hiddenIfNotAllowed loadingAllowed={reviews.loadingAllowed} guistate={state.guistate}>
<ReviewPrivacyShield
hiddenIfNotAllowed
loadingAllowed={reviews.loadingAllowed}
guistate={state.guistate}
>
{#if uploadFailed}
<div class="alert flex">
<ExclamationTriangle class="h-6 w-6" />
@ -186,7 +190,7 @@
<Tr slot="else" t={t.reviewing_as_anonymous} />
</If>
<button class="primary" class:disabled={$hasError !== undefined} on:click={save}>
<Tr t={ editReview === undefined ? t.save : t.edit} />
<Tr t={editReview === undefined ? t.save : t.edit} />
</button>
</div>

View file

@ -8,25 +8,29 @@
import Tr from "../Base/Tr.svelte"
export let guistate: MenuState
export let loadingAllowed : UIEventSource<boolean>
export let loadingAllowed: UIEventSource<boolean>
export let hiddenIfNotAllowed: boolean = false
let t = Translations.t.reviews
</script>
{#if $loadingAllowed}
<slot />
{:else if !hiddenIfNotAllowed && $loadingAllowed !== null}
<div class="low-interaction mx-1 flex flex-col rounded">
<Tr t={t.disabledForPrivacy}/>
<button on:click={() => {loadingAllowed.set(true)}} class="primary">
<Tr t={t.loadOnce}/>
<Tr t={t.disabledForPrivacy} />
<button
on:click={() => {
loadingAllowed.set(true)
}}
class="primary"
>
<Tr t={t.loadOnce} />
</button>
<button
class="as-link self-end"
on:click={() => guistate.openUsersettings("mangrove-reviews-allowed")}
>
<Tr t={t.editPrivacySettings}/>
<Tr t={t.editPrivacySettings} />
</button>
</div>
{/if}

View file

@ -13,7 +13,10 @@
/**
* A panel showing all the reviews by the logged-in user
*/
export let state: ThemeViewState & {osmConnection: OsmConnection, userRelatedState: { mangroveIdentity: MangroveIdentity }}
export let state: ThemeViewState & {
osmConnection: OsmConnection
userRelatedState: { mangroveIdentity: MangroveIdentity }
}
let allReviews = state.userRelatedState.mangroveIdentity.getAllReviews()
let reviews = state.userRelatedState.mangroveIdentity.getGeoReviews()
let kid = state.userRelatedState.mangroveIdentity.getKeyId()

View file

@ -25,9 +25,8 @@
import ValidatedInput from "../InputElement/ValidatedInput.svelte"
import type { MapProperties } from "../../Models/MapProperties"
export let state: ThemeViewState & {
mapProperties?: MapProperties,
mapProperties?: MapProperties
userRelatedState?: { mangroveIdentity: MangroveIdentity }
} = undefined
export let tags: UIEventSource<Record<string, string>> = undefined
@ -61,7 +60,6 @@
state?.guistate?.closeAll()
}
let isTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
async function report() {
@ -71,7 +69,6 @@
}
const identity: CryptoKeyPair = await state?.userRelatedState?.mangroveIdentity.getKeypair()
if (!isTesting.data) {
try {
await MangroveReviews.reportAbuseReview(identity, review, reason)
} catch (e) {
@ -91,7 +88,6 @@
const identity: CryptoKeyPair = await state?.userRelatedState?.mangroveIdentity.getKeypair()
console.log("Deleting review...", identity)
if (!isTesting.data) {
try {
await reviews.deleteReview(review)
} catch (e) {
@ -118,9 +114,9 @@
<Tr t={t.deleteTitle} />
</svelte:fragment>
<div class="flex gap-x-4 items-center m-8">
<div class="m-8 flex items-center gap-x-4">
<Delete_icon class="w-16" />
<div class="p-4 low-interaction border-interactive">
<div class="low-interaction border-interactive p-4">
<StarsBar starSize="w-4 h-4 md:w-6 md:h-6" readonly={true} score={review.rating} />
<div class="disable-links">
<Markdown src={review.opinion} />
@ -136,15 +132,14 @@
</NextButton>
</Popup>
<Popup shown={showReport}>
<svelte:fragment slot="header">
<Tr t={t.reportReviewTitle} />
</svelte:fragment>
<div class="flex gap-x-4 items-center m-8">
<div class="m-8 flex items-center gap-x-4">
<ShieldExclamation class="w-16" />
<div class="p-4 low-interaction border-interactive">
<div class="low-interaction border-interactive p-4">
<StarsBar starSize="w-4 h-4 md:w-6 md:h-6" readonly={true} score={review.rating} />
<div class="disable-links">
<Markdown src={review.opinion} />
@ -156,13 +151,17 @@
<Tr t={t.reportReason} />
</h3>
<div class="w-full">
<ValidatedInput type="text" value={reportReason} />
</div>
<Tr t={t.deleteText} />
<NextButton clss="primary float-right" on:click={() => {report()}}>
<NextButton
clss="primary float-right"
on:click={() => {
report()
}}
>
<TrashIcon class="w-12 text-red-600" />
<Tr t={t.reportReview} />
</NextButton>
@ -174,7 +173,6 @@
</svelte:fragment>
<ReviewForm {tags} {feature} {layer} {state} editReview={review} {reviews} />
</Popup>
<div
@ -248,21 +246,19 @@
</div>
{/if}
<div class="flex gap-x-2 overflow-x-auto items-center">
{#each review.images ?? [] as image}
<div class="w-32 min-w-32">
<AttributedImage image={{url: image.src, id: image.src}}></AttributedImage>
</div>
{/each}
<div class="flex items-center gap-x-2 overflow-x-auto">
{#each review.images ?? [] as image}
<div class="w-32 min-w-32">
<AttributedImage image={{ url: image.src, id: image.src }} />
</div>
{/each}
</div>
{#if review.metadata.is_affiliated}
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
{/if}
{#if $isDebugging}
<div class="subtle">
Maresi: {(review.signature)}
</div>
{/if}
{#if $isDebugging}
<div class="subtle">
Maresi: {review.signature}
</div>
{/if}
</div>

View file

@ -34,7 +34,8 @@
}
return layers
},
[activeLayers], onDestroy
[activeLayers],
onDestroy
)
let filterResultsClipped: Store<{
clipped: (FilterSearchResult[] | LayerConfig)[]
@ -48,7 +49,8 @@
}
return { clipped: ls.slice(0, 4), rest: ls.slice(4) }
},
[layerResults, activeLayers, Locale.language], onDestroy
[layerResults, activeLayers, Locale.language],
onDestroy
)
</script>

View file

@ -33,14 +33,20 @@
descriptionTr = layer?.tagRenderings?.find((tr) => tr.labels.indexOf("description") >= 0)
}
let distance = state.mapProperties.location.mapD((l) =>
GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat]), onDestroy
let distance = state.mapProperties.location.mapD(
(l) => GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat]),
onDestroy
)
let bearing = state.mapProperties.location.mapD(
(l) => GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat]),
onDestroy
)
let bearing = state.mapProperties.location.mapD((l) =>
GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat]), onDestroy)
let mapRotation = state.mapProperties.rotation
let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat]), onDestroy)
let inView = state.mapProperties.bounds.mapD(
(bounds) => bounds.contains([entry.lon, entry.lat]),
onDestroy
)
let dispatch = createEventDispatcher<{ select: GeocodeResult }>()
function select() {

View file

@ -19,10 +19,12 @@
suggestions: Store<GeocodeResult[]>
suggestionsSearchRunning: Store<boolean>
runningEngines: Store<string[]>
failedEngines: Store<{
source: GeocodingProvider;
error: any
}[]>
failedEngines: Store<
{
source: GeocodingProvider
error: any
}[]
>
}
featureSwitchIsTesting?: Store<boolean>
userRelatedState?: { showTagsB: Store<boolean> }
@ -35,7 +37,6 @@
let runningEngines = state.searchState.runningEngines ?? new ImmutableStore([])
let failedEngines = state.searchState.failedEngines
let isTesting = state.featureSwitchIsTesting ?? new ImmutableStore(false)
let showTags = state.userRelatedState?.showTagsB ?? new ImmutableStore(false)
const t = Translations.t.general.search
@ -59,7 +60,7 @@
<Tr t={t.searching} />
{#if $isTesting || $showTags}
<div class="subtle">
Querying {$runningEngines?.map(provider => provider.name)?.join(", ")}
Querying {$runningEngines?.map((provider) => provider.name)?.join(", ")}
</div>
{/if}
</Loading>
@ -73,7 +74,7 @@
{/if}
{#if $failedEngines?.length > 0}
{#each $failedEngines as failed}
<div class="warning flex flex-col items-center py-4 overflow-hidden">
<div class="warning flex flex-col items-center overflow-hidden py-4">
<ExclamationTriangle class="w-4" />
Search using {failed.source.name} failed
<div class="subtle text-sm">
@ -82,7 +83,6 @@
</div>
{/each}
{/if}
</SidebarUnit>
{:else}
<slot name="if-no-results" />

View file

@ -62,7 +62,7 @@
{#if !$isOnline}
<div class="alert flex items-center">
<CrossedOut size="w-8 h-8">
<Wifi class="w-8 h-8" />
<Wifi class="h-8 w-8" />
</CrossedOut>
Your device is currently offline. This impacts search results
</div>

View file

@ -16,9 +16,10 @@
export let state: WithSearchState
let searchTerm = state.searchState.searchTerm
let visitedThemes = state.userRelatedState.recentlyVisitedThemes.value
let visitedThemes = state.userRelatedState.recentlyVisitedThemes.value
let recentThemes: Store<string[]> = visitedThemes.map((themes) =>
Lists.dedup(themes.filter((th) => th !== state.theme.id)).slice(0, 6))
Lists.dedup(themes.filter((th) => th !== state.theme.id)).slice(0, 6)
)
let themeResults = state.searchState.themeSuggestions
const t = Translations.t.general.search

View file

@ -1,4 +1,9 @@
import { SpecialVisualization, SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
import {
SpecialVisualisationParams,
SpecialVisualization,
SpecialVisualizationState,
SpecialVisualizationSvelte,
} from "../SpecialVisualization"
import Maproulette from "../../Logic/Maproulette"
import SvelteUIElement from "../Base/SvelteUIElement"
import { PointImportButtonViz } from "../Popup/ImportButtons/PointImportButtonViz"
@ -43,6 +48,7 @@ class MaprouletteSetStatusVis extends SpecialVisualizationSvelte {
args = [
{
name: "message",
type: "translation",
doc: "A message to show to the user",
},
{
@ -52,6 +58,7 @@ class MaprouletteSetStatusVis extends SpecialVisualizationSvelte {
},
{
name: "message_confirm",
type: "translation",
doc: "What to show when the task is closed, either by the user or was already closed.",
},
{
@ -61,17 +68,19 @@ class MaprouletteSetStatusVis extends SpecialVisualizationSvelte {
},
{
name: "maproulette_id",
type:"key",
doc: "The property name containing the maproulette id",
defaultValue: "mr_taskId",
},
{
name: "ask_feedback",
type: "translation",
doc: "If not an empty string, this will be used as question to ask some additional feedback. A text field will be added",
defaultValue: "",
},
]
constr(state, tagsSource, args) {
constr({ state, tags, args }: SpecialVisualisationParams): SvelteUIElement {
let [message, image, message_closed, statusToSet, maproulette_id_key, askFeedback] = args
if (image === "") {
image = "confirm"
@ -82,7 +91,7 @@ class MaprouletteSetStatusVis extends SpecialVisualizationSvelte {
statusToSet = statusToSet ?? "1"
return new SvelteUIElement(MaprouletteSetStatus, {
state,
tags: tagsSource,
tags,
message,
image,
message_closed,
@ -102,6 +111,7 @@ class LinkedDataFromWebsite extends SpecialVisualization {
{
name: "key",
defaultValue: "website",
type:"key",
doc: "Attempt to load ld+json from the specified URL. This can be in an embedded <script type='ld+json'>",
},
{
@ -140,21 +150,15 @@ class LinkedDataFromWebsite extends SpecialVisualization {
},
]
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): SvelteUIElement {
if(state.theme.enableMorePrivacy){
constr({ state, tags, args, feature, layer }: SpecialVisualisationParams): SvelteUIElement {
if (state.theme.enableMorePrivacy) {
return undefined
}
const key = argument[0] ?? "website"
const useProxy = argument[1] !== "no"
const readonly = argument[3] === "readonly"
const isClosed = (argument[4] ?? "yes") === "yes"
const key = args[0] ?? "website"
const useProxy = args[1] !== "no"
const readonly = args[3] === "readonly"
const isClosed = (args[4] ?? "yes") === "yes"
const downloadInformation = new UIEventSource(false)
const countryStore: Store<string | undefined> = tags.mapD((tags) => tags._country)
const sourceUrl: Store<string | undefined> = tags.mapD((tags) => {
@ -165,51 +169,55 @@ class LinkedDataFromWebsite extends SpecialVisualization {
})
const externalData: Store<{ success: GeoJsonProperties } | { error }> = sourceUrl.bindD(
(url) => {
if(!downloadInformation.data){
if (!downloadInformation.data) {
return undefined
}
const country = countryStore.data
if (url.startsWith("https://data.velopark.be/")) {
return UIEventSource.fromPromiseWithErr((async () => {
try {
const loadAll = layer.id.toLowerCase().indexOf("maproulette") >= 0 // Dirty hack
const features = await LinkedDataLoader.fetchVeloparkEntry(
url,
loadAll
)
const feature =
features.find((f) => f.properties["ref:velopark"] === url) ??
features[0]
const properties = feature.properties
properties["ref:velopark"] = url
console.log("Got properties from velopark:", properties)
return properties
} catch (e) {
console.error(e)
throw e
}
})())
return UIEventSource.fromPromiseWithErr(
(async () => {
try {
const loadAll = layer.id.toLowerCase().indexOf("maproulette") >= 0 // Dirty hack
const features = await LinkedDataLoader.fetchVeloparkEntry(
url,
loadAll
)
const feature =
features.find((f) => f.properties["ref:velopark"] === url) ??
features[0]
const properties = feature.properties
properties["ref:velopark"] = url
console.log("Got properties from velopark:", properties)
return properties
} catch (e) {
console.error(e)
throw e
}
})()
)
}
if (country === undefined) {
return undefined
}
return UIEventSource.fromPromiseWithErr((async () => {
try {
return await LinkedDataLoader.fetchJsonLd(
url,
{ country },
useProxy ? "proxy" : "fetch-lod"
)
} catch (e) {
console.log(
"Could not get with proxy/download LOD, attempting to download directly. Error for ",
url,
"is",
e
)
return await LinkedDataLoader.fetchJsonLd(url, { country }, "fetch-raw")
}
})())
return UIEventSource.fromPromiseWithErr(
(async () => {
try {
return await LinkedDataLoader.fetchJsonLd(
url,
{ country },
useProxy ? "proxy" : "fetch-lod"
)
} catch (e) {
console.log(
"Could not get with proxy/download LOD, attempting to download directly. Error for ",
url,
"is",
e
)
return await LinkedDataLoader.fetchJsonLd(url, { country }, "fetch-raw")
}
})()
)
},
[countryStore, downloadInformation]
)
@ -218,23 +226,21 @@ class LinkedDataFromWebsite extends SpecialVisualization {
console.log("linked_data_from_website received the following data:", lod)
)
return new SvelteUIElement(ComparisonTool, {
feature,
state,
tags,
layer,
externalData,
sourceUrl,
readonly,
feature,
state,
tags,
layer,
externalData,
sourceUrl,
readonly,
downloadInformation,
collapsed: isClosed,
collapsed: isClosed,
})
}
}
class CompareData extends SpecialVisualization {
class CompareData extends SpecialVisualizationSvelte {
funcName = "compare_data"
group = "data_import"
needsUrls = (args) => args[1].split(";")
@ -242,6 +248,7 @@ class CompareData extends SpecialVisualization {
{
name: "url",
required: true,
type:"key",
doc: "The attribute containing the url where to fetch more data",
},
{
@ -258,20 +265,14 @@ class CompareData extends SpecialVisualization {
docs =
"Gives an interactive element which shows a tag comparison between the OSM-object and the upstream object. This allows to copy some or all tags into OSM"
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
constr({ state, tags, args, feature, layer }: SpecialVisualisationParams): SvelteUIElement {
const url = args[0]
const readonly = args[3] === "yes"
const externalData = UIEventSource.fromPromiseWithErr(Utils.downloadJson(url))
return new SvelteUIElement(ComparisonTool, {
url,
state,
tags: tagSource,
tags,
layer,
feature,
readonly,
@ -280,7 +281,7 @@ class CompareData extends SpecialVisualization {
}
}
export class DataImportSpecialVisualisations {
public static initList(): (SpecialVisualization & { group })[] {
public static initList(): SpecialVisualizationSvelte[] {
return [
new TagApplyViz(),
new PointImportButtonViz(),

View file

@ -1,7 +1,4 @@
import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../SpecialVisualization"
import SvelteUIElement from "../Base/SvelteUIElement"
import MarkAsFavourite from "../Popup/MarkAsFavourite.svelte"
import MarkAsFavouriteMini from "../Popup/MarkAsFavouriteMini.svelte"
@ -14,19 +11,8 @@ class FavouriteStatus extends SpecialVisualizationSvelte {
args = []
group = "favourites"
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): SvelteUIElement {
return new SvelteUIElement(MarkAsFavourite, {
tags: tagSource,
state,
layer,
feature,
})
constr(params: SpecialVisualisationParams): SvelteUIElement {
return new SvelteUIElement(MarkAsFavourite, params)
}
}
@ -37,19 +23,8 @@ class FavouriteIcon extends SpecialVisualizationSvelte {
"A small button that allows a (logged in) contributor to mark a location as a favourite location, sized to fit a title-icon"
args = []
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): SvelteUIElement {
return new SvelteUIElement(MarkAsFavouriteMini, {
tags: tagSource,
state,
layer,
feature,
})
constr(params: SpecialVisualisationParams): SvelteUIElement {
return new SvelteUIElement(MarkAsFavouriteMini, params)
}
}

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