forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
cac87a5467
727 changed files with 27522 additions and 15764 deletions
|
@ -61,7 +61,7 @@
|
|||
|
||||
let userLanguages = osmConnection.userDetails.map((ud) => ud?.languages ?? [])
|
||||
let search: UIEventSource<string | undefined> = new UIEventSource<string>("")
|
||||
let searchStable = search.stabilized(100)
|
||||
let searchStable: Store<string | undefined> = search.stabilized(100)
|
||||
|
||||
const officialThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
|
||||
(th) => th.hideFromOverview === false
|
||||
|
@ -83,13 +83,12 @@
|
|||
).mapD((stableIds) => Lists.noNullInplace(stableIds.map((id) => state.getUnofficialTheme(id))))
|
||||
|
||||
function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> {
|
||||
const searchIndex = Locale.language.map(
|
||||
(language) => {
|
||||
return new ThemeSearchIndex(language, themes.data)
|
||||
},
|
||||
[themes]
|
||||
const searchIndex = themes.mapD(
|
||||
(themes) => new ThemeSearchIndex(Locale.language.data, themes),
|
||||
[Locale.language]
|
||||
)
|
||||
|
||||
|
||||
return searchStable.map(
|
||||
(searchTerm) => {
|
||||
if (!themes.data) {
|
||||
|
@ -99,11 +98,9 @@
|
|||
return themes.data
|
||||
}
|
||||
|
||||
const index = searchIndex.data
|
||||
|
||||
return index.search(searchTerm)
|
||||
return searchIndex.data?.search(searchTerm)
|
||||
},
|
||||
[searchIndex]
|
||||
[searchIndex, themes]
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -134,9 +131,6 @@
|
|||
{ returnToIndex: new ImmutableStore(false) }
|
||||
)
|
||||
|
||||
const topSPace = AndroidPolyfill.getInsetSizes().top
|
||||
const bottom = AndroidPolyfill.getInsetSizes().bottom
|
||||
|
||||
/**
|
||||
* Opens the first search candidate
|
||||
*/
|
||||
|
@ -156,11 +150,14 @@
|
|||
</script>
|
||||
|
||||
<main>
|
||||
<div class="low-interaction w-full">
|
||||
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top} />
|
||||
</div>
|
||||
|
||||
<DrawerLeft shown={guistate.pageStates.menu}>
|
||||
<MenuDrawerIndex onlyLink={true} state={menuDrawerState} />
|
||||
</DrawerLeft>
|
||||
<div class="flex w-screen flex-col p-2 sm:p-4">
|
||||
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top}/>
|
||||
<!-- Top menu bar -->
|
||||
<div class="flex w-full justify-between overflow-hidden pb-4">
|
||||
<button on:click={() => guistate.pageStates.menu.set(true)} class="m-0 rounded-full p-2">
|
||||
|
@ -183,7 +180,7 @@
|
|||
<div class="link-underline flex w-full flex-col">
|
||||
<div class="flex items-center">
|
||||
<div class="m-1 flex-none md:hidden">
|
||||
<Logo alt="MapComplete Logo" class="mr-1 sm:mr-2 md:mr-4 h-10 w-10 sm:h-20 sm:w-20" />
|
||||
<Logo alt="MapComplete Logo" class="mr-1 h-10 w-10 sm:mr-2 sm:h-20 sm:w-20 md:mr-4" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<h1 class="m-0 break-words font-extrabold tracking-tight md:text-6xl">
|
||||
|
@ -272,8 +269,7 @@
|
|||
v{Constants.vNumber}
|
||||
</div>
|
||||
|
||||
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom}/>
|
||||
|
||||
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom} />
|
||||
|
||||
<div class="absolute top-0 h-0 w-0" style="margin-left: -10em">
|
||||
<MenuDrawer onlyLink={false} state={menuDrawerState} />
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<SubtleButton
|
||||
on:click={() => dispatch("click")}
|
||||
options={{ extraClasses: twMerge("flex items-center", clss) }}
|
||||
options={{ extraClasses: twMerge("flex items-center justify-start", clss) }}
|
||||
>
|
||||
<ChevronLeftIcon class={imageClass ?? "h-12 w-12"} slot="image" />
|
||||
<slot slot="message" />
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"} />
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
use:ariaLabelStore={arialabelString}
|
||||
disabled={!$enabled}
|
||||
class={twJoin(
|
||||
"pointer-events-auto relative h-fit w-fit rounded-full",
|
||||
"pointer-events-auto relative h-fit w-fit rounded-full border-gray-500",
|
||||
cls,
|
||||
$enabled ? "" : "disabled"
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
<form class="w-full" on:submit|preventDefault={() => dispatch("search")}>
|
||||
<label
|
||||
class="neutral-label normal-background box-shadow flex w-full items-center rounded-full border border-black"
|
||||
class="neutral-label normal-background box-shadow flex w-full items-center rounded-md border border-black"
|
||||
>
|
||||
<SearchIcon aria-hidden="true" class="ml-2 h-6 w-6 shrink-0" />
|
||||
|
||||
|
@ -76,7 +76,7 @@
|
|||
value.set("")
|
||||
e.preventDefault()
|
||||
}}
|
||||
color="var(--button-background)"
|
||||
color="var(--foreground-color)"
|
||||
class="mr-3 h-6 w-6 cursor-pointer"
|
||||
/>
|
||||
{:else}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -187,7 +187,7 @@
|
|||
}
|
||||
|
||||
:global(.tab-selected svg) {
|
||||
fill: var(--catch-detail-color-contrast);
|
||||
fill: var(--interactive-contrast);
|
||||
}
|
||||
|
||||
:global(.tab-unselected) {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
<script >
|
||||
export let contentStyle= ""
|
||||
</script>
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="low-interaction flex items-center justify-between p-4 drop-shadow-md">
|
||||
<div class="flex items-center gap-x-2">
|
||||
|
@ -9,7 +12,7 @@
|
|||
<slot name="title-end" />
|
||||
</div>
|
||||
|
||||
<div class="flex h-full flex-col overflow-auto border-b-2 p-4">
|
||||
<div class={"flex h-full flex-col overflow-auto border-b-2 p-4 "+contentStyle}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -79,7 +79,6 @@ export default abstract class BaseUIElement {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return el
|
||||
} catch (e) {
|
||||
const domExc = e as DOMException
|
||||
|
|
|
@ -1,56 +1,67 @@
|
|||
<script lang="ts">/**
|
||||
* Show a compass. The compass outline rotates with the map, the compass needle points to the actual north
|
||||
*/
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Orientation } from "../../Sensors/Orientation"
|
||||
import Compass_back from "../../assets/svg/Compass_back.svelte"
|
||||
import Compass_needle from "../../assets/svg/Compass_needle.svelte"
|
||||
import North_arrow from "../../assets/svg/North_arrow.svelte"
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Show a compass. The compass outline rotates with the map, the compass needle points to the actual north
|
||||
*/
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Orientation } from "../../Sensors/Orientation"
|
||||
import Compass_back from "../../assets/svg/Compass_back.svelte"
|
||||
import Compass_needle from "../../assets/svg/Compass_needle.svelte"
|
||||
import North_arrow from "../../assets/svg/North_arrow.svelte"
|
||||
|
||||
export let mapProperties: { rotation: UIEventSource<number>, allowRotating: Store<boolean> }
|
||||
let orientation = Orientation.singleton.alpha
|
||||
let gotNonZero = new UIEventSource(false)
|
||||
orientation.addCallbackAndRunD(o => {
|
||||
if (o !== undefined && o !== 0) {
|
||||
gotNonZero.set(true)
|
||||
}
|
||||
})
|
||||
let mapRotation = mapProperties.rotation
|
||||
export let size = "h-10 w-10"
|
||||
let wrapperClass = "absolute top-0 left-0 " + size
|
||||
let compassLoaded = Orientation.singleton.gotMeasurement
|
||||
let allowRotation = mapProperties.allowRotating
|
||||
|
||||
function clicked(e: Event) {
|
||||
if (mapProperties.rotation.data === 0) {
|
||||
if (mapProperties.allowRotating.data && compassLoaded.data) {
|
||||
mapProperties.rotation.set(orientation.data)
|
||||
export let mapProperties: { rotation: UIEventSource<number>; allowRotating: Store<boolean> }
|
||||
let orientation = Orientation.singleton.alpha
|
||||
let gotNonZero = new UIEventSource(false)
|
||||
orientation.addCallbackAndRunD((o) => {
|
||||
if (o !== undefined && o !== 0) {
|
||||
gotNonZero.set(true)
|
||||
}
|
||||
} else {
|
||||
mapProperties.rotation.set(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
let mapRotation = mapProperties.rotation
|
||||
export let size = "h-10 w-10"
|
||||
let wrapperClass = "absolute top-0 left-0 " + size
|
||||
let compassLoaded = Orientation.singleton.gotMeasurement
|
||||
let allowRotation = mapProperties.allowRotating
|
||||
|
||||
function clicked() {
|
||||
if (mapProperties.rotation.data === 0) {
|
||||
if (mapProperties.allowRotating.data && compassLoaded.data) {
|
||||
mapProperties.rotation.set(orientation.data)
|
||||
}
|
||||
} else {
|
||||
mapProperties.rotation.set(0)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{#if $allowRotation || !$gotNonZero}
|
||||
<button style="z-index: -1" class={"relative as-link pointer-events-auto "+size} on:click={(e) => clicked(e)}>
|
||||
{#if $allowRotation && !$compassLoaded && !$gotNonZero}
|
||||
<div class={"border-2 rounded-full border-gray-500 border-dotted "+wrapperClass}
|
||||
style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}>
|
||||
<North_arrow class="w-full" />
|
||||
</div>
|
||||
{:else}
|
||||
{#if $allowRotation}
|
||||
<div class={wrapperClass} style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}>
|
||||
<Compass_back class="w-full" />
|
||||
|
||||
{#if $allowRotation || $gotNonZero}
|
||||
<button
|
||||
class={"as-link pointer-events-auto relative " + size}
|
||||
on:click={() => clicked()}
|
||||
>
|
||||
{#if $allowRotation && !$compassLoaded && !$gotNonZero}
|
||||
<div
|
||||
class={"rounded-full border-2 border-dotted border-gray-500 " + wrapperClass}
|
||||
style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}
|
||||
>
|
||||
<North_arrow class="w-full" />
|
||||
</div>
|
||||
{:else}
|
||||
{#if $allowRotation}
|
||||
<div
|
||||
class={wrapperClass}
|
||||
style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}
|
||||
>
|
||||
<Compass_back class="w-full" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $compassLoaded && $gotNonZero}
|
||||
<div
|
||||
class={wrapperClass + (!$allowRotation ? " rounded-full bg-white bg-opacity-50" : "")}
|
||||
style={`transform: rotate(${-$orientation}deg)`}
|
||||
>
|
||||
<Compass_needle class="w-full" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if $compassLoaded && $gotNonZero}
|
||||
<div class={wrapperClass+ (!$allowRotation ? " rounded-full bg-white bg-opacity-50" : "")}
|
||||
style={`transform: rotate(${-$orientation}deg)`}>
|
||||
<Compass_needle class="w-full" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
@ -151,12 +151,12 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-col items-center gap-x-2 border-t border-dashed border-gray-300 pt-4">
|
||||
<div class="mr-4 flex w-96">
|
||||
<div class="mr-4 flex w-96 flex-wrap items-center justify-center md:flex-nowrap">
|
||||
<a href="https://nlnet.nl/entrust" class="p-2">
|
||||
<NGI0Entrust_tag />
|
||||
<NGI0Entrust_tag class="w-48 grow-0" />
|
||||
</a>
|
||||
<a href="https://nlnet.nl" class="p-2">
|
||||
<Nlnet />
|
||||
<Nlnet class="w-48 grow-0" />
|
||||
</a>
|
||||
</div>
|
||||
<span>
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
function showFor(timeoutSeconds: number = 3) {
|
||||
open = true
|
||||
console.trace("Showing gpshelperelement")
|
||||
window.setTimeout(() => {
|
||||
open = false
|
||||
}, timeoutSeconds * 1000)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -51,7 +51,8 @@
|
|||
}
|
||||
})
|
||||
let hotkeys = Hotkeys._docs
|
||||
|
||||
let showBackground = state.featureSwitches.featureSwitchBackgroundSelection
|
||||
let showFilters = state.featureSwitches.featureSwitchFilter
|
||||
</script>
|
||||
|
||||
<div class:h-0={!onlyLink} class:h-full={onlyLink} class="overflow-hidden">
|
||||
|
@ -76,10 +77,12 @@
|
|||
<ThemeIntroPanel {state} />
|
||||
</Page>
|
||||
|
||||
<FilterPage {onlyLink} {state} />
|
||||
|
||||
<RasterLayerOverview {onlyLink} {state} />
|
||||
|
||||
{#if $showFilters}
|
||||
<FilterPage {onlyLink} {state} />
|
||||
{/if}
|
||||
{#if $showBackground}
|
||||
<RasterLayerOverview {onlyLink} {state} />
|
||||
{/if}
|
||||
<Page {onlyLink} shown={pg.share}>
|
||||
<svelte:fragment slot="header">
|
||||
<Share />
|
||||
|
|
|
@ -30,18 +30,15 @@
|
|||
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import usersettings from "../../../public/assets/generated/layers/usersettings.json"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
|
||||
import Favourites from "../Favourites/Favourites.svelte"
|
||||
import ReviewsOverview from "../Reviews/ReviewsOverview.svelte"
|
||||
import Share from "@babeard/svelte-heroicons/solid/Share"
|
||||
import LogoutButton from "../Base/LogoutButton.svelte"
|
||||
import { BoltIcon, ShareIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import Copyright from "../../assets/svg/Copyright.svelte"
|
||||
import Pencil from "../../assets/svg/Pencil.svelte"
|
||||
import SidebarUnit from "../Base/SidebarUnit.svelte"
|
||||
import Squares2x2 from "@babeard/svelte-heroicons/mini/Squares2x2"
|
||||
import EnvelopeOpen from "@babeard/svelte-heroicons/mini/EnvelopeOpen"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import MagnifyingGlassCircle from "@babeard/svelte-heroicons/mini/MagnifyingGlassCircle"
|
||||
import { AndroidPolyfill } from "../../Logic/Web/AndroidPolyfill"
|
||||
import Forgejo from "../../assets/svg/Forgejo.svelte"
|
||||
|
@ -52,7 +49,6 @@
|
|||
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import FavouritesFeatureSource from "../../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp"
|
||||
import ArrowTopRightOnSquare from "@babeard/svelte-heroicons/mini/ArrowTopRightOnSquare"
|
||||
import { PhotoIcon } from "@babeard/svelte-heroicons/outline"
|
||||
|
@ -84,7 +80,7 @@
|
|||
let usersettingslayer = new LayerConfig(<LayerConfigJson>usersettings, "usersettings", true)
|
||||
|
||||
let featureSwitches = state.featureSwitches
|
||||
let showHome = featureSwitches?.featureSwitchBackToThemeOverview
|
||||
let showHome = featureSwitches?.featureSwitchBackToThemeOverview ?? new ImmutableStore(true)
|
||||
let pg = state.guistate.pageStates
|
||||
let pendingChanges = state?.changes?.pendingChanges
|
||||
export let onlyLink: boolean
|
||||
|
@ -105,16 +101,15 @@
|
|||
let isAndroid = AndroidPolyfill.inAndroid
|
||||
let nrOfFailedImages = ImageUploadQueue.singleton.imagesInQueue
|
||||
let failedImagesOpen = pg.failedImages
|
||||
|
||||
|
||||
let loggedIn = state.osmConnection.isLoggedIn
|
||||
</script>
|
||||
|
||||
<div class="low-interaction flex h-full flex-col overflow-hidden" class:hidden={!$shown}>
|
||||
{#if onlyLink}
|
||||
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top} />
|
||||
{/if}
|
||||
<div class="flex justify-between border-b border-black p-2">
|
||||
<h2>
|
||||
<div class="flex justify-between items-center border-b border-black p-2">
|
||||
<h2 class="m-0">
|
||||
<Tr t={t.title} />
|
||||
</h2>
|
||||
<CloseButton
|
||||
|
@ -126,7 +121,7 @@
|
|||
|
||||
<div class="flex flex-col gap-y-2 overflow-y-auto px-2 sm:gap-y-3 sm:px-3">
|
||||
{#if $showHome}
|
||||
<a class="button primary flex" href={Utils.HomepageLink()}>
|
||||
<a class="button flex" class:primary={$loggedIn} href={Utils.HomepageLink()}>
|
||||
<Squares2x2 class="h-10 w-10" />
|
||||
{#if Utils.isIframe}
|
||||
<Tr t={Translations.t.general.seeIndex} />
|
||||
|
@ -139,13 +134,14 @@
|
|||
<!-- User related: avatar, settings, favourits, logout -->
|
||||
<SidebarUnit>
|
||||
<LoginToggle {state} offline>
|
||||
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in" />
|
||||
<div class="flex items-center gap-x-4 w-full m-2">
|
||||
<LoginButton clss="primary" osmConnection={state.osmConnection} slot="not-logged-in" />
|
||||
<div class="m-2 flex w-full items-center gap-x-4">
|
||||
<Avatar userdetails={state.osmConnection.userDetails} />
|
||||
<div class="flex flex-col w-full gap-y-2">
|
||||
|
||||
<b>{$userdetails?.name ?? '<Username>'}</b>
|
||||
<LogoutButton clss="as-link small subtle text-sm" osmConnection={state.osmConnection} />
|
||||
<div class="flex w-full flex-col gap-y-2">
|
||||
<b>{$userdetails.name}</b>
|
||||
<LogoutButton clss="as-link small subtle text-sm" osmConnection={state.osmConnection} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoginToggle>
|
||||
|
@ -214,7 +210,8 @@
|
|||
|
||||
<LanguagePicker
|
||||
preferredLanguages={state.userRelatedState.osmConnection.userDetails.mapD(
|
||||
(ud) => ud.languages, onDestroy
|
||||
(ud) => ud.languages,
|
||||
onDestroy
|
||||
)}
|
||||
/>
|
||||
</SidebarUnit>
|
||||
|
@ -228,16 +225,12 @@
|
|||
<Tr t={Translations.t.general.menu.aboutMapComplete} />
|
||||
</h3>
|
||||
|
||||
<a
|
||||
class="flex"
|
||||
href={($isAndroid
|
||||
? "https://mapcomplete.org"
|
||||
: ".")+"/studio.html"}
|
||||
>
|
||||
<Pencil class="mr-2 h-6 w-6" />
|
||||
<Tr t={Translations.t.general.morescreen.createYourOwnTheme} />
|
||||
</a>
|
||||
|
||||
{#if $showHome}
|
||||
<a class="flex" href={($isAndroid ? "https://mapcomplete.org" : ".") + "/studio.html"}>
|
||||
<Pencil class="mr-2 h-6 w-6" />
|
||||
<Tr t={Translations.t.general.morescreen.createYourOwnTheme} />
|
||||
</a>
|
||||
{/if}
|
||||
<a class="flex" href="mailto:info@mapcomplete.org">
|
||||
<EnvelopeOpen class="h-6 w-6" />
|
||||
<Tr t={Translations.t.general.attribution.emailCreators} />
|
||||
|
@ -343,5 +336,4 @@
|
|||
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -76,7 +76,10 @@
|
|||
allowMoving: new UIEventSource<boolean>(true),
|
||||
allowZooming: new UIEventSource<boolean>(true),
|
||||
minzoom: new UIEventSource<number>(18),
|
||||
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
|
||||
rasterLayer: state.mapProperties.rasterLayer.followingClone(),
|
||||
allowRotating: state.mapProperties.allowRotating,
|
||||
rotation: state.mapProperties.rotation.followingClone()
|
||||
|
||||
}
|
||||
state?.showCurrentLocationOn(map)
|
||||
|
||||
|
@ -114,7 +117,7 @@
|
|||
})
|
||||
}
|
||||
const snappedLocation = new SnappingFeatureSource(
|
||||
new FeatureSourceMerger(...(Lists.noNull(snapSources))),
|
||||
new FeatureSourceMerger(...Lists.noNull(snapSources)),
|
||||
// We snap to the (constantly updating) map location
|
||||
mapProperties.location,
|
||||
{
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { Map as MlMap } from "maplibre-gl"
|
||||
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
@ -21,7 +20,10 @@
|
|||
import type { AreaDescription } from "../../Logic/OfflineBasemapManager"
|
||||
import { OfflineBasemapManager } from "../../Logic/OfflineBasemapManager"
|
||||
import Checkbox from "../Base/Checkbox.svelte"
|
||||
|
||||
import Translations from "../i18n/Translations"
|
||||
import { default as Trans } from "../Base/Tr.svelte"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
import { Lists } from "../../Utils/Lists"
|
||||
|
||||
export let state: ThemeViewState & SpecialVisualizationState = undefined
|
||||
export let autoDownload = state.autoDownloadOfflineBasemap
|
||||
|
@ -35,20 +37,30 @@
|
|||
mapProperties.location.set(state.mapProperties.location.data)
|
||||
mapProperties.allowRotating.set(false)
|
||||
|
||||
|
||||
const offlineMapManager = OfflineBasemapManager.singleton
|
||||
let installing: Store<ReadonlyMap<string, object>> = offlineMapManager.installing
|
||||
let installed = offlineMapManager.installedAreas
|
||||
let focusTile: Store<{
|
||||
x: number;
|
||||
y: number;
|
||||
z: number
|
||||
} | undefined> = mapProperties.location.mapD(location => Tiles.embedded_tile(location.lat, location.lon, focusZ))
|
||||
let focusTileIsInstalled = focusTile.mapD(tile => offlineMapManager.isInstalled(tile), [installed])
|
||||
let focusTileIsInstalling = focusTile.mapD(tile => {
|
||||
const { x, y, z } = tile
|
||||
return installing.data?.has(`${z}-${x}-${y}.pmtiles`)
|
||||
}, [installing])
|
||||
let focusTile: Store<
|
||||
| {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
| undefined
|
||||
> = mapProperties.location.mapD((location) =>
|
||||
Tiles.embedded_tile(location.lat, location.lon, focusZ)
|
||||
)
|
||||
let focusTileIsInstalled = focusTile.mapD(
|
||||
(tile) => offlineMapManager.isInstalled(tile),
|
||||
[installed]
|
||||
)
|
||||
let focusTileIsInstalling = focusTile.mapD(
|
||||
(tile) => {
|
||||
const { x, y, z } = tile
|
||||
return installing.data?.has(`${z}-${x}-${y}.pmtiles`)
|
||||
},
|
||||
[installing]
|
||||
)
|
||||
|
||||
async function del(areaDescr: AreaDescription) {
|
||||
await offlineMapManager.deleteArea(areaDescr)
|
||||
|
@ -63,112 +75,125 @@
|
|||
const f = Tiles.asGeojson(z, x, y)
|
||||
f.properties = {
|
||||
id: "center_point_" + z + "_" + x + "_" + y,
|
||||
txt: "Tile " + x + " " + y
|
||||
txt: "Tile " + x + " " + y,
|
||||
}
|
||||
return [f]
|
||||
})
|
||||
let installedFeature: Store<Feature<Polygon>[]> = installed.map(meta =>
|
||||
(meta ?? [])
|
||||
.map(area => {
|
||||
const f = Tiles.asGeojson(area.minzoom, area.x, area.y)
|
||||
f.properties = {
|
||||
id: area.minzoom + "-" + area.x + "-" + area.y,
|
||||
downloaded: "yes",
|
||||
text: area.name + " " + new Date(area.dataVersion).toLocaleDateString() + " " + Utils.toHumanByteSize(Number(area.size))
|
||||
}
|
||||
return f
|
||||
}
|
||||
)
|
||||
let installedFeature: Store<Feature<Polygon>[]> = installed.map((meta) =>
|
||||
(meta ?? []).map((area) => {
|
||||
const f = Tiles.asGeojson(area.minzoom, area.x, area.y)
|
||||
f.properties = {
|
||||
id: area.minzoom + "-" + area.x + "-" + area.y,
|
||||
downloaded: "yes",
|
||||
text:
|
||||
area.name +
|
||||
" " +
|
||||
new Date(area.dataVersion).toLocaleDateString() +
|
||||
" " +
|
||||
Utils.toHumanByteSize(Number(area.size)),
|
||||
}
|
||||
return f
|
||||
})
|
||||
)
|
||||
new ShowDataLayer(map, {
|
||||
features: new StaticFeatureSource(installedFeature),
|
||||
layer: new LayerConfig({
|
||||
id: "downloaded",
|
||||
source: "special",
|
||||
lineRendering: [{
|
||||
color: "blue",
|
||||
width: {
|
||||
mappings: [
|
||||
{
|
||||
if: `id!~${focusZ}-.*`,
|
||||
then: "1"
|
||||
}
|
||||
]
|
||||
lineRendering: [
|
||||
{
|
||||
color: "blue",
|
||||
width: {
|
||||
mappings: [
|
||||
{
|
||||
if: `id!~${focusZ}-.*`,
|
||||
then: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
fillColor: {
|
||||
mappings: [
|
||||
{
|
||||
if: `id!~${focusZ}-.*`,
|
||||
then: "#00000000",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
fillColor: {
|
||||
mappings: [
|
||||
{
|
||||
if: `id!~${focusZ}-.*`,
|
||||
then: "#00000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}],
|
||||
],
|
||||
pointRendering: [
|
||||
{
|
||||
location: ["point", "centroid"],
|
||||
label: "{text}",
|
||||
labelCss: "width: w-min",
|
||||
labelCssClasses: "bg-white rounded px-2 items-center flex flex-col"
|
||||
}
|
||||
]
|
||||
})
|
||||
labelCssClasses: "bg-white rounded px-2 items-center flex flex-col",
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
new ShowDataLayer(map, {
|
||||
features: new StaticFeatureSource(focusTileFeature),
|
||||
layer: new LayerConfig({
|
||||
id: "focustile",
|
||||
source: "special",
|
||||
lineRendering: [{
|
||||
color: "black"
|
||||
}],
|
||||
lineRendering: [
|
||||
{
|
||||
color: "black",
|
||||
},
|
||||
],
|
||||
pointRendering: [
|
||||
{
|
||||
location: ["point", "centroid"],
|
||||
label: "{txt}",
|
||||
labelCss: "width: max-content",
|
||||
labelCssClasses: "bg-white rounded px-2 flex"
|
||||
}
|
||||
]
|
||||
})
|
||||
labelCssClasses: "bg-white rounded px-2 flex",
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
const t = Translations.t.offline
|
||||
</script>
|
||||
<div class="flex flex-col h-full max-h-leave-room">
|
||||
<Checkbox selected={autoDownload}>Automatically download the basemap when browsing around</Checkbox>
|
||||
<div>
|
||||
If checked, MapComplete will automatically download the basemap to the cache for the area.
|
||||
This results in bigger initial data loads, but requires less internet over the long run.
|
||||
|
||||
If you plan to visit a region with less connectivity, you can also select the area you want to download below.
|
||||
</div>
|
||||
<div class="max-h-leave-room flex h-full flex-col overflow-auto">
|
||||
<Checkbox selected={autoDownload}>
|
||||
<Trans t={t.autoCheckmark} />
|
||||
</Checkbox>
|
||||
<AccordionSingle noBorder>
|
||||
<Trans slot="header" cls="text-sm" t={t.autoExplanationIntro} />
|
||||
<div class="low-interaction">
|
||||
<Trans t={t.autoExplanation} />
|
||||
</div>
|
||||
</AccordionSingle>
|
||||
<div />
|
||||
{#if $installed === undefined}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="h-full overflow-auto pb-16">
|
||||
|
||||
<div class="pb-16">
|
||||
<Accordion class="" inactiveClass="text-black">
|
||||
<AccordionItem paddingDefault="p-2">
|
||||
<div slot="header">Map</div>
|
||||
<div class="relative leave-room">
|
||||
<div class="rounded-lg absolute top-0 left-0 h-full w-full">
|
||||
<Trans slot="header" t={t.localOnMap} />
|
||||
<div class="leave-room relative">
|
||||
<div class="absolute left-0 top-0 h-full w-full rounded-lg">
|
||||
<MaplibreMap {map} {mapProperties} />
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 left-0 h-full w-full flex flex-col justify-center items-center pointer-events-none">
|
||||
<div class="w-16 h-32 mb-16"></div>
|
||||
class="pointer-events-none absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center"
|
||||
>
|
||||
<div class="mb-16 h-32 w-16" />
|
||||
{#if $focusTileIsInstalling}
|
||||
<div class="normal-background rounded-lg">
|
||||
<Loading>
|
||||
Data is being downloaded
|
||||
<Trans t={t.installing} />
|
||||
</Loading>
|
||||
</div>
|
||||
{:else}
|
||||
<button class="primary pointer-events-auto" on:click={() => download()}
|
||||
class:disabled={$focusTileIsInstalled}>
|
||||
<DownloadIcon class="w-8 h-8" />
|
||||
Download
|
||||
<button
|
||||
class="primary pointer-events-auto"
|
||||
on:click={() => download()}
|
||||
class:disabled={$focusTileIsInstalled}
|
||||
>
|
||||
<DownloadIcon class="h-8 w-8" />
|
||||
<Trans t={t.download} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -176,34 +201,42 @@
|
|||
</AccordionItem>
|
||||
|
||||
<AccordionItem paddingDefault="p-2">
|
||||
<div slot="header">
|
||||
Offline tile management
|
||||
</div>
|
||||
|
||||
<Trans t={t.overview} slot="header" />
|
||||
<div class="leave-room">
|
||||
|
||||
|
||||
{Utils.toHumanByteSize(Utils.sum($installed.map(area => area.size)))}
|
||||
<button on:click={() => {
|
||||
installed?.data?.forEach(area => del(area))
|
||||
}}>
|
||||
{Utils.toHumanByteSize(Lists.sum($installed.map((area) => area.size)))}
|
||||
<button
|
||||
on:click={() => {
|
||||
installed?.data?.forEach((area) => del(area))
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="w-6" />
|
||||
Delete all
|
||||
<Trans t={t.deleteAll} />
|
||||
</button>
|
||||
<table class="w-full ">
|
||||
<table class="w-full">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Map generation date</th>
|
||||
<th>Size</th>
|
||||
<th>Zoom ranges</th>
|
||||
<th>Actions</th>
|
||||
<th>
|
||||
<Trans t={t.name} />
|
||||
</th>
|
||||
<th>
|
||||
<Trans t={t.date} />
|
||||
</th>
|
||||
<th>
|
||||
<Trans t={t.size} />
|
||||
</th>
|
||||
<th>
|
||||
<Trans t={t.range} />
|
||||
</th>
|
||||
<th>
|
||||
<Trans t={t.actions} />
|
||||
</th>
|
||||
</tr>
|
||||
{#each ($installed ?? []) as area }
|
||||
{#each $installed ?? [] as area}
|
||||
<tr>
|
||||
<td>{area.name}</td>
|
||||
<td>{area.dataVersion}</td>
|
||||
<td>{Utils.toHumanByteSize(area.size ?? -1)}</td>
|
||||
<td>{area.minzoom}
|
||||
<td>
|
||||
{area.minzoom}
|
||||
{#if area.maxzoom !== undefined}
|
||||
- {area.maxzoom}
|
||||
{:else}
|
||||
|
@ -213,15 +246,13 @@
|
|||
<td>
|
||||
<button on:click={() => del(area)}>
|
||||
<TrashIcon class="w-6" />
|
||||
Delete this map
|
||||
<Trans t={t.delete} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
@ -229,10 +260,10 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.leave-room {
|
||||
height: calc(100vh - 18rem);
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
color: var(--foreground-color);
|
||||
}
|
||||
.leave-room {
|
||||
height: calc(100vh - 18rem);
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
color: var(--foreground-color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -25,21 +25,22 @@
|
|||
</script>
|
||||
|
||||
<div class="low-interaction flex border-b-2 border-black px-3 drop-shadow-md">
|
||||
<div class="h-fit w-full overflow-auto sm:p-2" style="max-height: 20vh;">
|
||||
<div class="h-full w-full overflow-auto sm:p-2 flex items-center" style="max-height: 20vh;">
|
||||
{#if $tags._deleted === "yes"}
|
||||
<h3 class="p-4">
|
||||
<Tr t={Translations.t.delete.deletedTitle} />
|
||||
</h3>
|
||||
{:else}
|
||||
<div class="flex h-full w-full flex-grow flex-col">
|
||||
<div class="flex h-full w-full flex-grow flex-col justify-center">
|
||||
<!-- Title element and title icons-->
|
||||
<h3 class="m-0">
|
||||
<a href={`#${$tags.id}`}>
|
||||
<a href={`#${$tags?.id}`}>
|
||||
{#if layer.title}
|
||||
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} />
|
||||
{/if}
|
||||
</a>
|
||||
</h3>
|
||||
{#if layer.titleIcons.length > 0}
|
||||
<div
|
||||
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 pt-0.5 sm:pt-1"
|
||||
>
|
||||
|
@ -69,11 +70,12 @@
|
|||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<slot name="close-button">
|
||||
<div class="mt-4">
|
||||
<div class="m-2">
|
||||
<CloseButton on:click={() => state.selectedElement.setData(undefined)} />
|
||||
</div>
|
||||
</slot>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Lists } from "../../Utils/Lists"
|
||||
|
||||
export let search: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
export let themes: MinimalThemeInformation[]
|
||||
|
@ -23,7 +24,7 @@
|
|||
? "flex flex-wrap items-center justify-center gap-x-2"
|
||||
: "theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"}
|
||||
>
|
||||
{#each Utils.DedupOnId(Utils.noNull(themes)) as theme (theme.id)}
|
||||
{#each Lists.dedupOnId(Utils.noNull(themes)) as theme (theme.id)}
|
||||
<ThemeButton {theme} {state} iconOnly={onlyIcons}>
|
||||
{#if $search && hasSelection && themes?.[0] === theme}
|
||||
<span class="thanks hidden-on-mobile" aria-hidden="true">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* The visual feedback panel gives visual (and auditive) feedback on the main map view
|
||||
* This is an accessibility feature for people with screen readers
|
||||
*/
|
||||
|
||||
import Translations from "../i18n/Translations"
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
import type { Feature, LineString, Point } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import SmallZoomButtons from "../Map/SmallZoomButtons.svelte"
|
||||
import CompassWidget from "./CompassWidget.svelte"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
|
||||
const splitpoint_style = new LayerConfig(
|
||||
<LayerConfigJson>split_point,
|
||||
|
@ -51,7 +53,7 @@
|
|||
/**
|
||||
* Optional: use these properties to set e.g. background layer
|
||||
*/
|
||||
export let mapProperties: undefined | Partial<MapProperties> = undefined
|
||||
export let mapProperties: undefined | Partial<Omit<MapProperties, "lastClickLocation">> = undefined
|
||||
|
||||
/**
|
||||
* Reuse a point if the clicked location is within this amount of meter
|
||||
|
@ -61,14 +63,14 @@
|
|||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let adaptor = new MapLibreAdaptor(map, mapProperties)
|
||||
|
||||
let wayGeojson: Feature<LineString> = <Feature<LineString>>osmWay.asGeoJson()
|
||||
let wayGeojson: Feature<LineString, OsmTags> = <Feature<LineString, OsmTags>>osmWay.asGeoJson()
|
||||
adaptor.location.setData(GeoOperations.centerpointCoordinatesObj(wayGeojson))
|
||||
adaptor.bounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
|
||||
state?.showCurrentLocationOn(map)
|
||||
new ShowDataLayer(map, {
|
||||
features: new StaticFeatureSource([wayGeojson]),
|
||||
features: new StaticFeatureSource<Feature<LineString, { id: string }>>([wayGeojson]),
|
||||
drawMarkers: true,
|
||||
layer: layer,
|
||||
})
|
||||
|
@ -77,10 +79,10 @@
|
|||
Feature<
|
||||
Point,
|
||||
{
|
||||
id: number
|
||||
id: string
|
||||
index: number
|
||||
dist: number
|
||||
location: number
|
||||
dist?: number
|
||||
location?: number
|
||||
}
|
||||
>[]
|
||||
> = new UIEventSource([])
|
||||
|
@ -96,15 +98,15 @@
|
|||
return
|
||||
}
|
||||
splitPoints.data.splice(i, 1)
|
||||
splitPoints.ping()
|
||||
splitPoints.update(ls => [...ls])
|
||||
},
|
||||
})
|
||||
let id = 0
|
||||
adaptor.lastClickLocation.addCallbackD(({ lon, lat }) => {
|
||||
let projected: Feature<Point, { index: number; id?: number; reuse?: string }> =
|
||||
let projected: Feature<Point, { index: number; id: string; reuse?: string }> = <any>(
|
||||
GeoOperations.nearestPoint(wayGeojson, [lon, lat])
|
||||
|
||||
console.log("Added splitpoint", projected, id)
|
||||
)
|
||||
projected.properties.id = "" + id
|
||||
|
||||
// We check the next and the previous point. If those are closer then the tolerance, we reuse those instead
|
||||
|
||||
|
@ -116,7 +118,6 @@
|
|||
const previousPoint = <[number, number]>way[i]
|
||||
const previousDistance = GeoOperations.distanceBetween(previousPoint, p)
|
||||
|
||||
console.log("ND", nextDistance, "PD", previousDistance)
|
||||
if (nextDistance <= snapTolerance && previousDistance >= nextDistance) {
|
||||
projected = {
|
||||
type: "Feature",
|
||||
|
@ -126,6 +127,7 @@
|
|||
},
|
||||
properties: {
|
||||
index: i + 1,
|
||||
id: "" + id,
|
||||
reuse: "yes",
|
||||
},
|
||||
}
|
||||
|
@ -139,19 +141,22 @@
|
|||
},
|
||||
properties: {
|
||||
index: i,
|
||||
id: "" + id,
|
||||
reuse: "yes",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
projected.properties["id"] = id
|
||||
id++
|
||||
splitPoints.data.push(<any>projected)
|
||||
splitPoints.ping()
|
||||
splitPoints.data.push(projected)
|
||||
splitPoints.update(ls => [...ls])
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="relative h-full w-full">
|
||||
<MaplibreMap {map} mapProperties={adaptor} />
|
||||
<SmallZoomButtons {adaptor} />
|
||||
<div class="absolute top-0 left-0">
|
||||
<CompassWidget mapProperties={adaptor} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,10 +3,17 @@
|
|||
import { fade } from "svelte/transition"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { onDestroy } from "svelte"
|
||||
import If from "../Base/If.svelte"
|
||||
import type { Store } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Avatar from "../Base/Avatar.svelte"
|
||||
|
||||
let open = false
|
||||
export let state: { osmConnection: OsmConnection }
|
||||
export let state: {
|
||||
osmConnection: OsmConnection
|
||||
featureSwitches: { featureSwitchEnableLogin: Store<boolean> }
|
||||
}
|
||||
let userdetails = state.osmConnection.userDetails
|
||||
let username = userdetails.mapD((ud) => ud.name, onDestroy)
|
||||
username.addCallbackAndRunD((ud) => {
|
||||
|
@ -19,23 +26,25 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<Popover
|
||||
class="mt-4 hidden sm:block"
|
||||
defaultClass="py-2 px-3 w-fit "
|
||||
trigger="null"
|
||||
placement="bottom"
|
||||
transition={(e) => fade(e, { duration: 150 })}
|
||||
bind:open
|
||||
>
|
||||
{#if $username !== undefined}
|
||||
<div style="width: max-content" class="flex items-center">
|
||||
<If condition={state.featureSwitches.featureSwitchEnableLogin}>
|
||||
<Popover
|
||||
class="mt-4 hidden sm:block"
|
||||
defaultClass="py-2 px-3 w-fit "
|
||||
trigger="null"
|
||||
placement="bottom"
|
||||
transition={(e) => fade(e, { duration: 150 })}
|
||||
bind:open
|
||||
>
|
||||
{#if $username !== undefined}
|
||||
<div style="width: max-content" class="flex items-center">
|
||||
<Avatar {userdetails} />
|
||||
<div>
|
||||
<div>Welcome back</div>
|
||||
<div class="normal-background" style="width: max-content">
|
||||
<b>{$username}</b>
|
||||
<div>
|
||||
<Tr t={Translations.t.general.welcomeBack} />
|
||||
<div class="normal-background" style="width: max-content">
|
||||
<b>{$username}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Popover>
|
||||
{/if}
|
||||
</Popover>
|
||||
</If>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -40,11 +40,9 @@
|
|||
</script>
|
||||
|
||||
<LoginToggle {state}>
|
||||
<div slot="not-logged-in">
|
||||
<LoginButton osmConnection={state.osmConnection}>
|
||||
<Tr t={Translations.t.favouritePoi.loginToSeeList} />
|
||||
</LoginButton>
|
||||
</div>
|
||||
<LoginButton slot="not-logged-in" osmConnection={state.osmConnection}>
|
||||
<Tr t={Translations.t.favouritePoi.loginToSeeList} />
|
||||
</LoginButton>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if $favourites.length === 0}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -3,14 +3,17 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import ArrowPath from "@babeard/svelte-heroicons/mini/ArrowPath"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let failed: number
|
||||
export let state: SpecialVisualizationState
|
||||
const t = Translations.t.image
|
||||
let dispatch = createEventDispatcher<{ retry }>()
|
||||
</script>
|
||||
|
||||
<div class="alert flex">
|
||||
<div class="flex flex-col items-start w-full items-center">
|
||||
<div class="flex w-full flex-col items-start items-center">
|
||||
{#if failed === 1}
|
||||
<Tr t={t.upload.one.failed} />
|
||||
{:else}
|
||||
|
@ -19,7 +22,10 @@
|
|||
<Tr cls="text-normal" t={t.upload.failReasons} />
|
||||
<Tr cls="text-xs" t={t.upload.failReasonsAdvanced} />
|
||||
{#if state}
|
||||
<button class="primary pointer-events-auto" on:click={() => state.imageUploadManager.uploadQueue()}></button>
|
||||
<button class="primary pointer-events-auto" on:click={() => dispatch("retry")}>
|
||||
<ArrowPath class="w-6" />
|
||||
<Tr t={Translations.t.general.retry} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
Number of images uploaded succesfully
|
||||
*/
|
||||
function getCount(input: Store<string[]>): Store<number> {
|
||||
if(!input){
|
||||
if (!input) {
|
||||
return new ImmutableStore(0)
|
||||
}
|
||||
if (featureId == "*") {
|
||||
|
@ -62,6 +62,10 @@
|
|||
})
|
||||
let online = IsOnline.isOnline
|
||||
let progress = state.imageUploadManager.progressCurrentImage
|
||||
|
||||
function retry() {
|
||||
state.imageUploadManager.uploadQueue()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $debugging}
|
||||
|
@ -97,10 +101,16 @@
|
|||
<div class="alert flex flex-col">
|
||||
<Tr cls="block" t={t.upload.offline} />
|
||||
<Tr cls="block" t={t.upload.offlinePending.Subs({count: $pending})} />
|
||||
|
||||
</div>
|
||||
{:else if $failed > dismissed}
|
||||
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} {state} />
|
||||
<UploadFailedMessage
|
||||
failed={$failed}
|
||||
on:click={() => (dismissed = $failed)}
|
||||
on:retry={() => {
|
||||
retry()
|
||||
}}
|
||||
{state}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if showThankYou}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
|
||||
import TimeInput from "../TimeInput.svelte"
|
||||
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Checkbox from "../../../Base/Checkbox.svelte"
|
||||
|
@ -9,6 +8,7 @@
|
|||
import { OH } from "../../../OpeningHours/OpeningHours"
|
||||
import { Lists } from "../../../../Utils/Lists"
|
||||
import { Translation } from "../../../i18n/Translation"
|
||||
import PlusCircle from "@babeard/svelte-heroicons/mini/PlusCircle"
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
|
||||
|
@ -16,16 +16,28 @@
|
|||
/*
|
||||
Single rule for collection times, e.g. "Mo-Fr 10:00, 17:00"
|
||||
*/
|
||||
let weekdays: Translation[] = [wt.monday, wt.tuesday, wt.wednesday, wt.thursday, wt.friday, wt.saturday, wt.sunday, Translations.t.general.opening_hours.ph]
|
||||
let weekdays: Translation[] = [
|
||||
wt.monday,
|
||||
wt.tuesday,
|
||||
wt.wednesday,
|
||||
wt.thursday,
|
||||
wt.friday,
|
||||
wt.saturday,
|
||||
wt.sunday,
|
||||
Translations.t.general.opening_hours.ph,
|
||||
]
|
||||
|
||||
let initialTimes= Lists.noEmpty(value.data?.split(" ")?.[1]?.split(",")?.map(s => s.trim()) ?? [])
|
||||
let values = new UIEventSource(initialTimes.map(t => new UIEventSource(t)))
|
||||
if(values.data.length === 0){
|
||||
let initialTimes = Lists.noEmpty(
|
||||
value.data
|
||||
?.split(" ")?.[1]
|
||||
?.split(",")
|
||||
?.map((s) => s.trim()) ?? []
|
||||
)
|
||||
let values = new UIEventSource(initialTimes.map((t) => new UIEventSource(t)))
|
||||
if (values.data.length === 0) {
|
||||
values.data.push(new UIEventSource(""))
|
||||
}
|
||||
|
||||
|
||||
|
||||
let daysOfTheWeek = [...OH.days, "PH"]
|
||||
let selectedDays = daysOfTheWeek.map(() => new UIEventSource(false))
|
||||
|
||||
|
@ -39,7 +51,6 @@
|
|||
selectedDays[i]?.set(true)
|
||||
}
|
||||
} else {
|
||||
|
||||
let index = daysOfTheWeek.indexOf(initialDay)
|
||||
if (index >= 0) {
|
||||
selectedDays[index]?.set(true)
|
||||
|
@ -47,19 +58,26 @@
|
|||
}
|
||||
}
|
||||
|
||||
let selectedDaysBound = Stores.concat(selectedDays)
|
||||
.mapD(days => Lists.noNull(days.map((selected, i) => selected ? daysOfTheWeek[i] : undefined)))
|
||||
let valuesConcat: Store<string[]> = values.bindD(values => Stores.concat(values))
|
||||
.mapD(values => Lists.noEmpty(values))
|
||||
valuesConcat.mapD(times => {
|
||||
console.log(times)
|
||||
times = Lists.noNull(times)
|
||||
if (!times || times?.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
times?.sort(/*concatted, new array*/)
|
||||
return (Lists.noEmpty(selectedDaysBound.data).join(",") + " " + times).trim()
|
||||
}, [selectedDaysBound]).addCallbackAndRunD(v => value.set(v))
|
||||
let selectedDaysBound = Stores.concat(selectedDays).mapD((days) =>
|
||||
Lists.noNull(days.map((selected, i) => (selected ? daysOfTheWeek[i] : undefined)))
|
||||
)
|
||||
let valuesConcat: Store<string[]> = values
|
||||
.bindD((values) => Stores.concat(values))
|
||||
.mapD((values) => Lists.noEmpty(values))
|
||||
valuesConcat
|
||||
.mapD(
|
||||
(times) => {
|
||||
console.log(times)
|
||||
times = Lists.noNull(times)
|
||||
if (!times || times?.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
times?.sort(/*concatted, new array*/)
|
||||
return (Lists.noEmpty(selectedDaysBound.data).join(",") + " " + times).trim()
|
||||
},
|
||||
[selectedDaysBound]
|
||||
)
|
||||
.addCallbackAndRunD((v) => value.set(v))
|
||||
|
||||
function selectWeekdays() {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
|
@ -75,28 +93,29 @@
|
|||
selectedDays[i].set(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl my-2 p-2 low-interaction flex w-full justify-between flex-wrap">
|
||||
|
||||
<div class="low-interaction my-2 flex w-full flex-wrap justify-between rounded-xl p-2">
|
||||
<div class="flex flex-col">
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
{#each $values as value, i}
|
||||
<div class="flex mx-4 gap-x-1 items-center">
|
||||
<div class="mx-4 flex items-center gap-x-1">
|
||||
<TimeInput {value} />
|
||||
{#if $values.length > 1}
|
||||
<button class="as-link">
|
||||
<TrashIcon class="w-6 h-6" />
|
||||
<TrashIcon class="h-6 w-6" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<button on:click={() => {values.data.push(new UIEventSource(undefined)); values.ping()}}>
|
||||
<PlusCircle class="w-6 h-6" />
|
||||
Add time
|
||||
<button
|
||||
on:click={() => {
|
||||
values.data.push(new UIEventSource(undefined))
|
||||
values.ping()
|
||||
}}
|
||||
>
|
||||
<PlusCircle class="h-6 w-6" />
|
||||
<Tr t={Translations.t.collectionTimes.addTime} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex w-fit flex-wrap">
|
||||
|
@ -108,14 +127,11 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-between w-full">
|
||||
|
||||
<div class="flex w-full flex-wrap justify-between">
|
||||
<div class="flex flex-wrap gap-x-4">
|
||||
<button class="as-link text-sm" on:click={() => selectWeekdays()}>Select weekdays</button>
|
||||
<button class="as-link text-sm" on:click={() => clearDays()}>Clear days</button>
|
||||
|
||||
</div>
|
||||
<slot name="right" />
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { UIEventSource, Store } from "../../../Logic/UIEventSource"
|
||||
import type { MapProperties } from "../../../Models/MapProperties"
|
||||
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||
import type { Feature } from "geojson"
|
||||
import type { Feature, LineString } from "geojson"
|
||||
import type { RasterLayerPolygon } from "../../../Models/RasterLayers"
|
||||
import { RasterLayerUtils } from "../../../Models/RasterLayers"
|
||||
import { eliCategory } from "../../../Models/RasterLayerProperties"
|
||||
|
@ -22,13 +22,18 @@
|
|||
import Tr from "../../Base/Tr.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let value: UIEventSource<number>
|
||||
export let value: UIEventSource<string>
|
||||
export let feature: Feature
|
||||
export let args: { background?: string; zoom?: number }
|
||||
export let state: ThemeViewState = undefined
|
||||
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
|
||||
let center = GeoOperations.centerpointCoordinates(feature)
|
||||
if (feature.geometry.type === "LineString") {
|
||||
center = <[number, number]>(
|
||||
GeoOperations.nearestPoint(<Feature<LineString>>feature, center).geometry.coordinates
|
||||
)
|
||||
}
|
||||
export let initialCoordinate: { lon: number; lat: number } = { lon: center[0], lat: center[1] }
|
||||
let mapLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(
|
||||
initialCoordinate
|
||||
|
@ -46,10 +51,12 @@
|
|||
state?.mapProperties.rasterLayer.addCallbackD((layer) => rasterLayer.set(layer))
|
||||
}
|
||||
}
|
||||
let mapProperties: Partial<MapProperties> = {
|
||||
let mapProperties: Partial<MapProperties> & { location } = {
|
||||
rasterLayer: rasterLayer,
|
||||
location: mapLocation,
|
||||
zoom: new UIEventSource(args?.zoom ?? 18),
|
||||
rotation: state.mapProperties.rotation.followingClone(),
|
||||
lastClickLocation: new UIEventSource(undefined)
|
||||
}
|
||||
|
||||
let start: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
|
||||
|
@ -66,7 +73,7 @@
|
|||
// A bit of a double task: calculate the actual value _and_ the map rendering
|
||||
const end = mapLocation.data
|
||||
const distance = GeoOperations.distanceBetween([start.lon, start.lat], [end.lon, end.lat])
|
||||
value.set(distance.toFixed(2))
|
||||
value.set(distance.toFixed(1))
|
||||
|
||||
return <Feature[]>[
|
||||
{
|
||||
|
@ -85,13 +92,22 @@
|
|||
},
|
||||
]
|
||||
},
|
||||
[mapLocation], onDestroy
|
||||
[mapLocation],
|
||||
onDestroy
|
||||
)
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: new LayerConfig(conflation),
|
||||
features: new StaticFeatureSource(lengthFeature),
|
||||
})
|
||||
|
||||
mapProperties.lastClickLocation.addCallbackAndRunD(lonlat => {
|
||||
if (start.data === undefined) {
|
||||
start.set(lonlat)
|
||||
}
|
||||
mapProperties.location.set(lonlat)
|
||||
}, onDestroy)
|
||||
const t = Translations.t.input_helpers.distance
|
||||
</script>
|
||||
|
||||
<div class="relative h-64 w-full">
|
||||
|
@ -101,6 +117,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<button class="primary" on:click={() => selectStart()}>
|
||||
<Tr t={Translations.t.input_helpers.distance.setFirst} />
|
||||
<button class:primary={$start === undefined} on:click={() => selectStart()}>
|
||||
<Tr t={$start === undefined ? t.setFirst : t.measureAgain} />
|
||||
</button>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import Move_arrows from "../../../assets/svg/Move_arrows.svelte"
|
||||
import SmallZoomButtons from "../../Map/SmallZoomButtons.svelte"
|
||||
import CompassWidget from "../../BigComponents/CompassWidget.svelte"
|
||||
|
||||
/**
|
||||
* A visualisation to pick a location on a map background
|
||||
|
@ -50,6 +51,7 @@
|
|||
mla.lastClickLocation.addCallbackAndRunD((lastClick) => {
|
||||
dispatch("click", lastClick)
|
||||
})
|
||||
mla.installQuicklocation()
|
||||
mapProperties.location.syncWith(value)
|
||||
if (onCreated) {
|
||||
onCreated(value, map, mla)
|
||||
|
@ -91,7 +93,6 @@
|
|||
<div class="relative h-full min-h-32 cursor-pointer overflow-hidden">
|
||||
<div class="absolute left-0 top-0 h-full w-full cursor-pointer">
|
||||
<MaplibreMap
|
||||
center={{ lng: initialCoordinate.lon, lat: initialCoordinate.lat }}
|
||||
{map}
|
||||
mapProperties={mla}
|
||||
/>
|
||||
|
@ -107,4 +108,8 @@
|
|||
|
||||
<DragInvitation hideSignal={mla.location} />
|
||||
<SmallZoomButtons adaptor={mla} />
|
||||
<div class="absolute top-0 left-0 ">
|
||||
<CompassWidget mapProperties={mla} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
import Tr from "../../Base/Tr.svelte"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Check from "@babeard/svelte-heroicons/mini/Check"
|
||||
import { CloseButton } from "flowbite-svelte"
|
||||
import Cross from "../../../assets/svg/Cross.svelte"
|
||||
import XMark from "@babeard/svelte-heroicons/mini/XMark"
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
export let args: string
|
||||
|
@ -31,16 +34,29 @@
|
|||
|
||||
const state = new OpeningHoursState(value, prefix, postfix)
|
||||
let expanded = new UIEventSource(false)
|
||||
|
||||
function abort() {
|
||||
expanded.set(false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popup bodyPadding="p-0" shown={expanded}>
|
||||
<OHTable value={state.normalOhs} />
|
||||
<div class="absolute flex w-full justify-center" style="bottom: -3rem">
|
||||
<button on:click={() => abort()}>
|
||||
<XMark class="m-0 h-6 w-6" />
|
||||
<Tr t={Translations.t.general.cancel} />
|
||||
</button>
|
||||
<button class=" primary" on:click={() => expanded.set(false)}>
|
||||
<Check class="m-0 h-6 w-6 shrink-0 p-0" color="white" />
|
||||
<Tr t={Translations.t.general.confirm} />
|
||||
</button>
|
||||
</div>
|
||||
<CloseButton
|
||||
class="absolute right-0 top-0 z-10"
|
||||
style="margin-top: -1.0rem"
|
||||
on:click={() => abort()}
|
||||
/>
|
||||
</Popup>
|
||||
|
||||
<button on:click={() => expanded.set(true)}>Pick opening hours</button>
|
||||
|
|
|
@ -78,7 +78,8 @@
|
|||
previewDegrees.setData(beta + "°")
|
||||
previewPercentage.setData(degreesToPercentage(beta))
|
||||
},
|
||||
[valuesign, beta], onDestroy
|
||||
[valuesign, beta],
|
||||
onDestroy
|
||||
)
|
||||
|
||||
function onSave() {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { ValidatorType } from "./Validators"
|
||||
import Validators from "./Validators"
|
||||
|
||||
import type { Feature } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import type { Validator } from "./Validator"
|
||||
import DistanceInput from "./Helpers/DistanceInput.svelte"
|
||||
|
||||
export let type: ValidatorType
|
||||
|
@ -15,11 +13,13 @@
|
|||
export let args: (string | number | boolean)[] | any = undefined
|
||||
export let state: SpecialVisualizationState = undefined
|
||||
let validator = Validators.get(type)
|
||||
let validatorHelper: Validator = validator.inputHelper
|
||||
let validatorHelper = validator.inputHelper
|
||||
</script>
|
||||
|
||||
{#if type === "distance"}
|
||||
<DistanceInput {value} {feature} {state} {args} />
|
||||
{:else if validatorHelper !== undefined}
|
||||
<svelte:component this={validatorHelper} {value} {feature} {state} {args} on:submit />
|
||||
{:else}
|
||||
<slot name="fallback" /> <!-- Used in the FilterWithView to inject the helper -->
|
||||
{/if}
|
||||
|
|
|
@ -87,10 +87,13 @@
|
|||
* Side effect: sets the feedback, returns true/false if valid
|
||||
* @param canonicalValue
|
||||
*/
|
||||
function validateRange(canonicalValue: number): boolean {
|
||||
function validateRange(canonicalValue: number | string): boolean {
|
||||
if (!range) {
|
||||
return true
|
||||
}
|
||||
if (typeof canonicalValue === "string") {
|
||||
canonicalValue = Number(canonicalValue)
|
||||
}
|
||||
if (canonicalValue < range.warnBelow) {
|
||||
feedback.set(t.suspiciouslyLow)
|
||||
}
|
||||
|
@ -100,7 +103,7 @@
|
|||
if (canonicalValue > range.max) {
|
||||
let max: number | string | BaseUIElement = range.max
|
||||
if (unit) {
|
||||
max = unit.asHumanLongValue(max)
|
||||
max = unit.asHumanLongValue(max, getCountry)
|
||||
}
|
||||
feedback.set(t.tooHigh.Subs({ max }))
|
||||
return false
|
||||
|
@ -108,7 +111,7 @@
|
|||
if (canonicalValue < range.min) {
|
||||
let min: number | string | BaseUIElement = range.min
|
||||
if (unit) {
|
||||
min = unit.asHumanLongValue(min)
|
||||
min = unit.asHumanLongValue(min, getCountry)
|
||||
}
|
||||
feedback.set(t.tooLow.Subs({ min }))
|
||||
return false
|
||||
|
@ -131,7 +134,7 @@
|
|||
}
|
||||
|
||||
if (selectedUnit.data) {
|
||||
const canonicalValue = unit.valueInCanonical(v + selectedUnit.data)
|
||||
const canonicalValue = unit.valueInCanonical(v + selectedUnit.data, getCountry)
|
||||
if (validateRange(canonicalValue)) {
|
||||
value.setData(unit.toOsm(v, selectedUnit.data))
|
||||
} else {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -81,9 +81,9 @@ export default class Validators {
|
|||
new ColorValidator(),
|
||||
|
||||
new DirectionValidator(),
|
||||
new DistanceValidator(),
|
||||
new SlopeValidator(),
|
||||
|
||||
|
||||
new UrlValidator(),
|
||||
new EmailValidator(),
|
||||
new PhoneValidator(),
|
||||
|
@ -107,8 +107,6 @@ export default class Validators {
|
|||
new TagValidator(),
|
||||
|
||||
new NameSuggestionIndexValidator(),
|
||||
|
||||
new DistanceValidator(),
|
||||
]
|
||||
|
||||
private static _byType = Validators._byTypeConstructor()
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -7,5 +7,4 @@ export default class ColorValidator extends Validator {
|
|||
constructor() {
|
||||
super("color", "Shows a color picker")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,5 +29,4 @@ export default class DateValidator extends Validator {
|
|||
|
||||
return [year, month, day].join("-")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,6 +39,4 @@ export default class ImageUrlValidator extends UrlValidator {
|
|||
}
|
||||
return ImageUrlValidator.hasValidExternsion(str)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,5 +21,4 @@ export default class TagValidator extends Validator {
|
|||
isValid(tag: string, _): boolean {
|
||||
return this.getFeedback(tag, _) === undefined
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -17,5 +17,4 @@ export default class TranslationValidator extends Validator {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -188,5 +188,4 @@ Another example is to search for species and trees:
|
|||
}
|
||||
return clipped
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -70,7 +70,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
constructor(
|
||||
maplibreMap: Store<MLMap>,
|
||||
state?: Partial<MapProperties>,
|
||||
state?: Partial<MapProperties & {
|
||||
lastClickLocation: UIEventSource<{
|
||||
lon: number
|
||||
lat: number
|
||||
mode: "left" | "right" | "middle"
|
||||
nearestFeature?: Feature
|
||||
}>
|
||||
}>,
|
||||
options?: {
|
||||
correctClick?: number
|
||||
}
|
||||
|
@ -78,7 +85,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
if (!MapLibreAdaptor.pmtilesInited) {
|
||||
const offlineManager = OfflineBasemapManager.singleton
|
||||
maplibregl.addProtocol("pmtiles", new Protocol().tile)
|
||||
maplibregl.addProtocol("pmtilesoffl", (request, abort) => offlineManager.tilev4(request, abort))
|
||||
maplibregl.addProtocol("pmtilesoffl", (request, abort) =>
|
||||
offlineManager.tilev4(request, abort)
|
||||
)
|
||||
MapLibreAdaptor.pmtilesInited = true
|
||||
}
|
||||
this._maplibreMap = maplibreMap
|
||||
|
@ -112,7 +121,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
|
||||
this.showScale = state?.showScale ?? new UIEventSource<boolean>(false)
|
||||
|
||||
const lastClickLocation = new UIEventSource<{
|
||||
const lastClickLocation = state?.lastClickLocation ?? new UIEventSource<{
|
||||
lat: number
|
||||
lon: number
|
||||
mode: "left" | "right" | "middle"
|
||||
|
@ -154,7 +163,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
const way = <Feature<LineString>>feature
|
||||
const lngLat: [number, number] = [e.lngLat.lng, e.lngLat.lat]
|
||||
const p = GeoOperations.nearestPoint(way, lngLat)
|
||||
console.log(">>>", p, way, lngLat)
|
||||
if (!p) {
|
||||
continue
|
||||
}
|
||||
|
@ -180,7 +188,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
this.setMinzoom(this.minzoom.data)
|
||||
this.setMaxzoom(this.maxzoom.data)
|
||||
this.setBounds(this.bounds.data)
|
||||
this.setRotation(this.rotation.data)
|
||||
this.setRotation(Math.floor(this.rotation.data))
|
||||
this.setScale(this.showScale.data)
|
||||
this.updateStores(true)
|
||||
}
|
||||
|
@ -560,6 +568,11 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
if (!map || bearing === undefined) {
|
||||
return
|
||||
}
|
||||
if (Math.abs(map.getBearing() - bearing) < 0.1) {
|
||||
// We don't bother to actually rotate
|
||||
// Aborting small changes helps to dampen changes
|
||||
return
|
||||
}
|
||||
map.rotateTo(bearing, { duration: 500 })
|
||||
}
|
||||
|
||||
|
@ -756,4 +769,26 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* In general, the 'location'-attribute is only updated when the map stops moving.
|
||||
* In some cases, we'd like to update the map faster, especially when the map is used for an input-element
|
||||
* such as distance, snap-to, ...
|
||||
*
|
||||
* In that case, calling this method will install an extra handler on 'drag', updating the location faster.
|
||||
* To avoid rendering artefacts or too frequenting pinging, this is ratelimited to one update every 'rateLimitMs' milliseconds
|
||||
*/
|
||||
public installQuicklocation(ratelimitMs = 50) {
|
||||
this._maplibreMap.addCallbackAndRunD((map) => {
|
||||
let lastUpdate = new Date().getTime()
|
||||
map.on("drag", () => {
|
||||
const now = new Date().getTime()
|
||||
if (now - lastUpdate < ratelimitMs) {
|
||||
return
|
||||
}
|
||||
lastUpdate = now
|
||||
const center = map.getCenter()
|
||||
this.location.set({ lon: center.lng, lat: center.lat })
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
maxZoom: 24,
|
||||
interactive: true,
|
||||
attributionControl: false,
|
||||
bearing: mapProperties?.rotation?.data ?? 0
|
||||
}
|
||||
_map = new maplibre.Map(options)
|
||||
window.requestAnimationFrame(() => {
|
||||
|
|
|
@ -24,9 +24,9 @@
|
|||
let altmap: UIEventSource<MlMap> = new UIEventSource(undefined)
|
||||
let altproperties = new MapLibreAdaptor(altmap, {
|
||||
rasterLayer,
|
||||
zoom: UIEventSource.feedFrom(placedOverMapProperties.zoom),
|
||||
rotation: UIEventSource.feedFrom(placedOverMapProperties.rotation),
|
||||
pitch: UIEventSource.feedFrom(placedOverMapProperties.pitch),
|
||||
zoom: placedOverMapProperties.zoom.followingClone(),
|
||||
rotation: placedOverMapProperties.rotation.followingClone(),
|
||||
pitch: placedOverMapProperties.pitch.followingClone()
|
||||
})
|
||||
altproperties.allowMoving.setData(false)
|
||||
altproperties.allowZooming.setData(false)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import PointRenderingConfig, {
|
||||
allowed_location_codes,
|
||||
PointRenderingLocation
|
||||
PointRenderingLocation,
|
||||
} from "../../Models/ThemeConfig/PointRenderingConfig"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import { type Alignment, Map as MlMap, Marker } from "maplibre-gl"
|
||||
|
@ -51,15 +51,17 @@ export class PointRenderingLayer {
|
|||
/**
|
||||
* Basically 'features', but only if 'visible' is true
|
||||
*/
|
||||
const featuresIfVisibleStore: Store<(Feature<Point, { id: string }> & {
|
||||
locationType: PointRenderingLocation
|
||||
})[]> =
|
||||
new IfVisibleFeatureSource(features, visibility).features.map(features =>
|
||||
PointRenderingLayer.extractLocations(features, config.location)
|
||||
)
|
||||
const featuresIfVisibleStore: Store<
|
||||
(Feature<Point, { id: string }> & {
|
||||
locationType: PointRenderingLocation
|
||||
})[]
|
||||
> = new IfVisibleFeatureSource(features, visibility).features.map((features) =>
|
||||
PointRenderingLayer.extractLocations(features, config.location)
|
||||
)
|
||||
|
||||
|
||||
let featuresToDraw: FeatureSource<Feature<Point, { id: string }> & { locationType: PointRenderingLocation }>
|
||||
let featuresToDraw: FeatureSource<
|
||||
Feature<Point, { id: string }> & { locationType: PointRenderingLocation }
|
||||
>
|
||||
if (preprocess) {
|
||||
featuresToDraw = preprocess(new StaticFeatureSource(featuresIfVisibleStore))
|
||||
} else {
|
||||
|
@ -78,7 +80,8 @@ export class PointRenderingLayer {
|
|||
return
|
||||
}
|
||||
allowed_location_codes.forEach((code) => {
|
||||
const marker = this._allMarkers.get(<OsmId>selected.properties.id)
|
||||
const marker = this._allMarkers
|
||||
.get(<OsmId>selected.properties.id)
|
||||
?.get(code)
|
||||
?.getElement()
|
||||
if (marker === undefined) {
|
||||
|
@ -88,43 +91,54 @@ export class PointRenderingLayer {
|
|||
this._markedAsSelected.push(marker)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* All locations that this layer should be rendered
|
||||
* @private
|
||||
*/
|
||||
private static extractLocations(features: Feature<Geometry, {
|
||||
id: string
|
||||
}>[], locations: Set<PointRenderingLocation>): (Feature<Point, { id: string }> & {
|
||||
private static extractLocations(
|
||||
features: Feature<
|
||||
Geometry,
|
||||
{
|
||||
id: string
|
||||
}
|
||||
>[],
|
||||
locations: Set<PointRenderingLocation>
|
||||
): (Feature<Point, { id: string }> & {
|
||||
locationType: PointRenderingLocation
|
||||
})[] {
|
||||
const resultingFeatures: (Feature<Point, { id: string }> & { locationType: PointRenderingLocation })[] = []
|
||||
if (!features) {
|
||||
return []
|
||||
}
|
||||
const resultingFeatures: (Feature<Point, { id: string }> & {
|
||||
locationType: PointRenderingLocation
|
||||
})[] = []
|
||||
|
||||
function registerFeature(feature: Feature<any, {
|
||||
id: string
|
||||
}>, location: [number, number], locationType: PointRenderingLocation) {
|
||||
function registerFeature(
|
||||
feature: Feature<
|
||||
any,
|
||||
{
|
||||
id: string
|
||||
}
|
||||
>,
|
||||
location: [number, number],
|
||||
locationType: PointRenderingLocation
|
||||
) {
|
||||
resultingFeatures.push({
|
||||
...feature,
|
||||
locationType,
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: location
|
||||
}
|
||||
coordinates: location,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for (const feature of features) {
|
||||
for (const location of locations) {
|
||||
if (feature?.geometry === undefined) {
|
||||
console.warn(
|
||||
"Got an invalid feature:",
|
||||
feature,
|
||||
" while rendering",
|
||||
location
|
||||
)
|
||||
console.warn("Got an invalid feature:", feature, " while rendering", location)
|
||||
}
|
||||
if (location === "waypoints") {
|
||||
if (feature.geometry.type === "LineString") {
|
||||
|
@ -154,7 +168,6 @@ export class PointRenderingLayer {
|
|||
}
|
||||
|
||||
return resultingFeatures
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -162,12 +175,11 @@ export class PointRenderingLayer {
|
|||
* @private
|
||||
*/
|
||||
private hideUnneededElements(featuresToDraw: Feature<Geometry, { id: string }>[]) {
|
||||
const idsToShow = new Set(featuresToDraw.map(f => f.properties.id))
|
||||
const idsToShow = new Set(featuresToDraw.map((f) => f.properties.id))
|
||||
|
||||
for (const key of this._allMarkers.keys()) {
|
||||
const shouldBeShown = idsToShow.has(key)
|
||||
for (const marker of this._allMarkers.get(key).values()) {
|
||||
|
||||
if (!shouldBeShown) {
|
||||
marker.addClassName("hidden")
|
||||
} else {
|
||||
|
@ -177,13 +189,14 @@ export class PointRenderingLayer {
|
|||
}
|
||||
}
|
||||
|
||||
private updateFeatures(allPointLocations: (Feature<Point> & { locationType: PointRenderingLocation })[]) {
|
||||
private updateFeatures(
|
||||
allPointLocations: (Feature<Point> & { locationType: PointRenderingLocation })[]
|
||||
) {
|
||||
const cache = this._allMarkers
|
||||
for (const feature of allPointLocations) {
|
||||
const id = <OsmId>feature.properties.id
|
||||
const locationType: PointRenderingLocation = feature.locationType
|
||||
|
||||
|
||||
let marker = cache.get(id)?.get(locationType)
|
||||
if (marker) {
|
||||
const oldLoc = marker.getLngLat()
|
||||
|
@ -215,11 +228,11 @@ export class PointRenderingLayer {
|
|||
.setLngLat(feature.geometry.coordinates)
|
||||
.addTo(this._map)*/
|
||||
|
||||
|
||||
let store: Store<Record<string, string>>
|
||||
if (this._fetchStore) {
|
||||
store = this._fetchStore(feature.properties.id)
|
||||
} else {
|
||||
}
|
||||
if(!store){
|
||||
store = new ImmutableStore(<OsmTags>feature.properties)
|
||||
}
|
||||
const { html, iconAnchor } = this._config.RenderIcon(store, { metatags: this._metatags })
|
||||
|
|
|
@ -59,14 +59,14 @@ class SingleBackgroundHandler {
|
|||
return
|
||||
}
|
||||
|
||||
console.debug(
|
||||
"Removing raster layer",
|
||||
this._targetLayer.properties.id,
|
||||
"map moved and not been used for",
|
||||
SingleBackgroundHandler.DEACTIVATE_AFTER
|
||||
)
|
||||
try {
|
||||
if (map.getLayer(<string>this._targetLayer.properties.id)) {
|
||||
console.debug(
|
||||
"Removing raster layer",
|
||||
this._targetLayer.properties.id,
|
||||
"map moved and not been used for",
|
||||
SingleBackgroundHandler.DEACTIVATE_AFTER
|
||||
)
|
||||
map.removeLayer(<string>this._targetLayer.properties.id)
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
)
|
||||
}
|
||||
|
||||
let rasterLayerOnMap = UIEventSource.feedFrom(rasterLayer)
|
||||
let rasterLayerOnMap = rasterLayer.followingClone()
|
||||
|
||||
if (shown) {
|
||||
onDestroy(
|
||||
|
|
|
@ -357,18 +357,17 @@ export default class ShowDataLayer {
|
|||
|
||||
public static showMultipleLayers(
|
||||
mlmap: UIEventSource<MlMap>,
|
||||
features: FeatureSource,
|
||||
features: FeatureSource<Feature<Geometry, Record<string, any> & {id: string}>>,
|
||||
layers: LayerConfig[],
|
||||
options?: Partial<Omit<ShowDataLayerOptions, "features" | "layer">>
|
||||
) {
|
||||
const perLayer =
|
||||
new PerLayerFeatureSourceSplitter(
|
||||
layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
|
||||
features,
|
||||
{
|
||||
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
|
||||
}
|
||||
)
|
||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||
layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
|
||||
features,
|
||||
{
|
||||
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
|
||||
}
|
||||
)
|
||||
if (options?.zoomToFeatures) {
|
||||
options.zoomToFeatures = false
|
||||
features.features.addCallbackD((features) => {
|
||||
|
@ -397,17 +396,20 @@ export default class ShowDataLayer {
|
|||
* @param state
|
||||
* @param options
|
||||
*/
|
||||
public static showLayerClustered(mlmap: Store<MlMap>,
|
||||
state: { mapProperties: { zoom: UIEventSource<number> } },
|
||||
options: ShowDataLayerOptions & { layer: LayerConfig }
|
||||
public static showLayerClustered(
|
||||
mlmap: Store<MlMap>,
|
||||
state: { mapProperties: { zoom: UIEventSource<number> } },
|
||||
options: ShowDataLayerOptions & { layer: LayerConfig }
|
||||
) {
|
||||
options.preprocessPoints = feats =>
|
||||
new ClusteringFeatureSource(feats, state.mapProperties.zoom.map(z => z + 2),
|
||||
options.preprocessPoints = (feats) =>
|
||||
new ClusteringFeatureSource(
|
||||
feats,
|
||||
state.mapProperties.zoom.map((z) => z + 2),
|
||||
options.layer.id,
|
||||
{
|
||||
cutoff: 7,
|
||||
showSummaryAt: "tilecenter"
|
||||
})
|
||||
cutoff: 7
|
||||
}
|
||||
)
|
||||
new ShowDataLayer(mlmap, options)
|
||||
}
|
||||
|
||||
|
@ -453,14 +455,9 @@ export default class ShowDataLayer {
|
|||
layer,
|
||||
drawLines,
|
||||
drawMarkers,
|
||||
preprocessPoints
|
||||
preprocessPoints,
|
||||
} = this._options
|
||||
let onClick = this._options.onClick
|
||||
if (!onClick && selectedElement && layer.title !== undefined) {
|
||||
onClick = (feature: Feature) => {
|
||||
selectedElement?.setData(feature)
|
||||
}
|
||||
}
|
||||
if (drawLines !== false) {
|
||||
for (let i = 0; i < layer.lineRendering.length; i++) {
|
||||
const lineRenderingConfig = layer.lineRendering[i]
|
||||
|
|
|
@ -7,7 +7,7 @@ export interface ShowDataLayerOptions {
|
|||
/**
|
||||
* Features to show
|
||||
*/
|
||||
features: FeatureSource<Feature<Geometry, OsmTags>>
|
||||
features: FeatureSource<Feature<Geometry, Record<string, any> & { id: string }>>
|
||||
/**
|
||||
* Indication of the current selected element; overrides some filters.
|
||||
* When a feature is tapped, the feature will be put in there
|
||||
|
@ -32,6 +32,6 @@ export interface ShowDataLayerOptions {
|
|||
onClick?: (feature: Feature) => void
|
||||
metaTags?: Store<Record<string, string>>
|
||||
|
||||
prefix?: string,
|
||||
prefix?: string
|
||||
preprocessPoints?: <T extends Feature<Point>>(fs: FeatureSource<T>) => FeatureSource<T>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -9,10 +9,9 @@
|
|||
import OpeningHoursRangeElement from "./OpeningHoursRangeElement.svelte"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { OH } from "../OpeningHours"
|
||||
import type { OpeningRange } from "../OpeningHours"
|
||||
|
||||
import { Utils } from "../../../Utils"
|
||||
import { OH } from "../OpeningHours"
|
||||
import { Lists } from "../../../Utils/Lists"
|
||||
|
||||
export let oh: opening_hours
|
||||
export let ranges: OpeningRange[][] // Per weekday
|
||||
|
@ -47,7 +46,7 @@
|
|||
changeTexts: string[]
|
||||
}[] = OH.partitionOHForDistance(changeHoursWeekend, changeHourTextWeekend)
|
||||
|
||||
let allChangeMoments: number[] = Utils.DedupT([...changeHours, ...changeHoursWeekend])
|
||||
let allChangeMoments: number[] = Lists.dedupT([...changeHours, ...changeHoursWeekend])
|
||||
let todayChangeMoments: Set<number> = new Set(OH.allChangeMoments(todayRanges)[0])
|
||||
// By default, we always show the range between 8 - 19h, in order to give a stable impression
|
||||
// Ofc, a bigger range is used if needed
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||
import TagExplanation from "../TagExplanation.svelte"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
|
||||
import DocumentDuplicate from "@babeard/svelte-heroicons/solid/DocumentDuplicate"
|
||||
|
@ -26,6 +24,7 @@
|
|||
import { EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Layers from "../../../assets/svg/Layers.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import TagHint from "../TagHint.svelte"
|
||||
|
||||
export let state: ThemeViewState
|
||||
export let layer: LayerConfig
|
||||
|
@ -42,10 +41,12 @@
|
|||
let preciseInputIsTapped = false
|
||||
|
||||
const forbiddenKeys = new Set(["id", "timestamp", "user", "changeset", "version", "uid"])
|
||||
let asTags = tags.map((tgs) =>
|
||||
Object.keys(tgs)
|
||||
.filter((k) => !k.startsWith("_") && !forbiddenKeys.has(k))
|
||||
.map((k) => new Tag(k, tgs[k])), onDestroy
|
||||
let asTags = tags.map(
|
||||
(tgs) =>
|
||||
Object.keys(tgs)
|
||||
.filter((k) => !k.startsWith("_") && !forbiddenKeys.has(k))
|
||||
.map((k) => new Tag(k, tgs[k])),
|
||||
onDestroy
|
||||
)
|
||||
let showPopup: UIEventSource<boolean> = new UIEventSource(false)
|
||||
|
||||
|
@ -178,7 +179,7 @@
|
|||
</div>
|
||||
{#if showTags}
|
||||
<div class="subtle">
|
||||
<TagExplanation tagsFilter={new And($asTags)} linkToWiki />
|
||||
<TagHint tags={$asTags} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -100,9 +100,8 @@
|
|||
}>()
|
||||
</script>
|
||||
|
||||
<TitledPanel>
|
||||
<TitledPanel contentStyle="interactive">
|
||||
<Tr slot="title" t={Translations.t.general.add.intro} />
|
||||
|
||||
{#each presets as preset}
|
||||
<NextButton on:click={() => dispatch("select", preset)}>
|
||||
<ToSvelte slot="image" construct={() => preset.icon} />
|
||||
|
|
|
@ -30,17 +30,20 @@
|
|||
}
|
||||
|
||||
const url = tag2linkData.find((item) => item.key === `Key:${key}`)?.url
|
||||
|
||||
const values: string[] =
|
||||
typeof $tags[key] === "string" ? $tags[key].split(";").map((v: string) => v.trim()) : []
|
||||
</script>
|
||||
|
||||
{#if url}
|
||||
<a
|
||||
href={url.replace("$1", $tags[key])}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="tag-link"
|
||||
>
|
||||
{$tags[key]}
|
||||
</a>
|
||||
{#if url && values.length > 0}
|
||||
{#each values as value, index}
|
||||
<span class="tag-link">
|
||||
{#if index > 0}; {/if}
|
||||
<a href={url.replace("$1", value)} target="_blank" rel="noopener noreferrer" class="tag-link">
|
||||
{value}
|
||||
</a>
|
||||
</span>
|
||||
{/each}
|
||||
{:else}
|
||||
{$tags[key]}
|
||||
{/if}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
|
@ -13,6 +12,8 @@
|
|||
import type { AutoAction } from "./AutoApplyButtonVis"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Lists } from "../../Utils/Lists"
|
||||
import type { OsmFeature } from "../../Models/OsmFeature"
|
||||
|
||||
/**
|
||||
* The ids to handle. Might be data from an external dataset, we cannot assume an OSM-id
|
||||
|
@ -42,12 +43,12 @@
|
|||
mla.allowZooming.setData(false)
|
||||
mla.allowMoving.setData(false)
|
||||
|
||||
const features = ids.mapD((ids) =>
|
||||
ids.map((id) => state.indexedFeatures.featuresById.data.get(id))
|
||||
const features: Store<OsmFeature[]> = ids.mapD((ids) =>
|
||||
ids.map<OsmFeature>((id) => state.indexedFeatures.featuresById.data.get(id))
|
||||
)
|
||||
|
||||
new ShowDataLayer(mlmap, {
|
||||
features: StaticFeatureSource.fromGeojson(features),
|
||||
features: new StaticFeatureSource(features),
|
||||
zoomToFeatures: true,
|
||||
layer: layer.layerDef,
|
||||
})
|
||||
|
@ -66,7 +67,9 @@
|
|||
const feature = state.indexedFeatures.featuresById.data.get(targetFeatureId)
|
||||
const featureTags = state.featureProperties.getStore(targetFeatureId)
|
||||
const rendering = tagRenderingConfig.GetRenderValue(featureTags.data).txt
|
||||
const specialRenderings = Lists.noNull(SpecialVisualizations.constructSpecification(rendering)).filter((v) => typeof v !== "string" && v.func["supportsAutoAction"] === true)
|
||||
const specialRenderings = Lists.noNull(
|
||||
SpecialVisualizations.constructSpecification(rendering)
|
||||
).filter((v) => typeof v !== "string" && v.func["supportsAutoAction"] === true)
|
||||
|
||||
if (specialRenderings.length == 0) {
|
||||
console.warn(
|
||||
|
@ -119,10 +122,10 @@
|
|||
<div class="alert">Target tagrendering {options.targetTagRendering} not found"</div>
|
||||
{:else if $ids.length === 0}
|
||||
<div>No elements found to perform action</div>
|
||||
{:else if $buttonState.error !== undefined}
|
||||
{:else if $buttonState["error"] !== undefined}
|
||||
<div class="flex flex-col">
|
||||
<div class="alert">Something went wrong</div>
|
||||
<div>{$buttonState.error}</div>
|
||||
<div>{$buttonState["error"]}</div>
|
||||
</div>
|
||||
{:else if $buttonState === "done"}
|
||||
<div class="thanks">All done!</div>
|
||||
|
|
|
@ -2,14 +2,16 @@ import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
|||
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import {
|
||||
SpecialVisualisationArg,
|
||||
SpecialVisualisationParams,
|
||||
SpecialVisualization,
|
||||
SpecialVisualizationState,
|
||||
SpecialVisualizationSvelte,
|
||||
} from "../SpecialVisualization"
|
||||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import { Feature } from "geojson"
|
||||
import AutoApplyButton from "./AutoApplyButton.svelte"
|
||||
import { OsmFeature } from "../../Models/OsmFeature"
|
||||
|
||||
export abstract class AutoAction extends SpecialVisualization {
|
||||
supportsAutoAction: boolean
|
||||
|
@ -19,7 +21,7 @@ export abstract class AutoAction extends SpecialVisualization {
|
|||
state: {
|
||||
theme: ThemeConfig
|
||||
changes: Changes
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
indexedFeatures: IndexedFeatureSource<OsmFeature>
|
||||
},
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[]
|
||||
|
@ -31,12 +33,7 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
|
|||
public readonly funcName: string = "auto_apply"
|
||||
public readonly needsUrls = []
|
||||
public readonly group = "data_import"
|
||||
public readonly args: {
|
||||
name: string
|
||||
defaultValue?: string
|
||||
doc: string
|
||||
required?: boolean
|
||||
}[] = [
|
||||
public readonly args: SpecialVisualisationArg[] = [
|
||||
{
|
||||
name: "target_layer",
|
||||
doc: "The layer that the target features will reside in",
|
||||
|
@ -54,6 +51,7 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
|
|||
},
|
||||
{
|
||||
name: "text",
|
||||
type: "translation",
|
||||
doc: "The text to show on the button",
|
||||
required: true,
|
||||
},
|
||||
|
@ -86,15 +84,11 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
|
|||
4. At last, add this component`
|
||||
}
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[]
|
||||
): SvelteUIElement {
|
||||
const target_layer_id = argument[0]
|
||||
const targetTagRendering = argument[2]
|
||||
const text = argument[3]
|
||||
const icon = argument[4]
|
||||
constr({ state, tags, args }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const target_layer_id = args[0]
|
||||
const targetTagRendering = args[2]
|
||||
const text = args[3]
|
||||
const icon = args[4]
|
||||
const options = {
|
||||
target_layer_id,
|
||||
targetTagRendering,
|
||||
|
@ -105,7 +99,7 @@ export default class AutoApplyButtonVis extends SpecialVisualizationSvelte {
|
|||
const to_parse: UIEventSource<string[]> = new UIEventSource<string[]>(undefined)
|
||||
Stores.chronic(500, () => to_parse.data === undefined)
|
||||
.map(() => {
|
||||
const applicable = <string | string[]>tagSource.data[argument[1]]
|
||||
const applicable = <string | string[]>tags.data[args[1]]
|
||||
if (typeof applicable === "string") {
|
||||
return <string[]>JSON.parse(applicable)
|
||||
} else {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import {
|
||||
SpecialVisualisationParams,
|
||||
SpecialVisualization,
|
||||
SpecialVisualizationState,
|
||||
SpecialVisualizationSvelte,
|
||||
} from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Feature, LineString } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Translations from "../i18n/Translations"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import ExportFeatureButton from "./ExportFeatureButton.svelte"
|
||||
|
@ -17,13 +15,7 @@ class ExportAsGpxVis extends SpecialVisualizationSvelte {
|
|||
args = []
|
||||
needsUrls = []
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
) {
|
||||
constr({ tags, feature, layer }: SpecialVisualisationParams) {
|
||||
if (feature.geometry.type !== "LineString") {
|
||||
return undefined
|
||||
}
|
||||
|
@ -48,7 +40,7 @@ class ExportAsGeojsonVis extends SpecialVisualizationSvelte {
|
|||
docs = "Exports the selected feature as GeoJson-file"
|
||||
args = []
|
||||
|
||||
constr(state, tags, args, feature, layer) {
|
||||
constr({ tags, feature, layer }: SpecialVisualisationParams) {
|
||||
const t = Translations.t.general.download
|
||||
return new SvelteUIElement(ExportFeatureButton, {
|
||||
tags,
|
||||
|
@ -64,7 +56,7 @@ class ExportAsGeojsonVis extends SpecialVisualizationSvelte {
|
|||
}
|
||||
|
||||
export class DataExportVisualisations {
|
||||
public static initList(): SpecialVisualization[] {
|
||||
public static initList(): SpecialVisualizationSvelte[] {
|
||||
return [new ExportAsGpxVis(), new ExportAsGeojsonVis()]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { SpecialVisualization, SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
|
||||
import {
|
||||
SpecialVisualisationArg,
|
||||
SpecialVisualisationParams,
|
||||
SpecialVisualization,
|
||||
SpecialVisualizationSvelte,
|
||||
} from "../SpecialVisualization"
|
||||
import { HistogramViz } from "./HistogramViz"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Feature } from "geojson"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
|
||||
|
@ -16,15 +20,18 @@ import NextChangeViz from "../OpeningHours/NextChangeViz.svelte"
|
|||
import { Unit } from "../../Models/Unit"
|
||||
import AllFeaturesStatistics from "../Statistics/AllFeaturesStatistics.svelte"
|
||||
import { LanguageElement } from "./LanguageElement/LanguageElement"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import TagRenderingEditable from "./TagRendering/TagRenderingEditable.svelte"
|
||||
import AllTagsPanel from "./AllTagsPanel/AllTagsPanel.svelte"
|
||||
import CollectionTimes from "../CollectionTimes/CollectionTimes.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Combine from "../Base/Combine"
|
||||
import Marker from "../Map/Marker.svelte"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
|
||||
class DirectionIndicatorVis extends SpecialVisualization {
|
||||
class DirectionIndicatorVis extends SpecialVisualizationSvelte {
|
||||
funcName = "direction_indicator"
|
||||
args = []
|
||||
|
||||
|
@ -32,13 +39,8 @@ class DirectionIndicatorVis extends SpecialVisualization {
|
|||
"Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object"
|
||||
group = "data"
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
): BaseUIElement {
|
||||
return new SvelteUIElement(DirectionIndicator, { state, feature })
|
||||
constr(params: SpecialVisualisationParams): SvelteUIElement {
|
||||
return new SvelteUIElement(DirectionIndicator, params)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,27 +63,23 @@ class DirectionAbsolute extends SpecialVisualization {
|
|||
]
|
||||
group = "data"
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
): BaseUIElement {
|
||||
constr({ tags, args }: SpecialVisualisationParams): BaseUIElement {
|
||||
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
|
||||
const offset = args[1] === "" ? 0 : Number(args[1])
|
||||
|
||||
return new VariableUiElement(
|
||||
tagSource
|
||||
tags
|
||||
.map((tags) => {
|
||||
console.log("Direction value", tags[key], key)
|
||||
return tags[key]
|
||||
})
|
||||
.mapD((value) => {
|
||||
const dir = GeoOperations.bearingToHuman(
|
||||
GeoOperations.parseBearing(value) + offset,
|
||||
GeoOperations.parseBearing(value) + offset
|
||||
)
|
||||
console.log("Human dir", dir)
|
||||
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -113,12 +111,12 @@ class OpeningHoursTableVis extends SpecialVisualizationSvelte {
|
|||
example =
|
||||
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`"
|
||||
|
||||
constr(state, tagSource: UIEventSource<any>, args) {
|
||||
constr({ tags, args }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const [key, prefix, postfix] = args
|
||||
const openingHoursStore: Store<opening_hours | "error" | undefined> =
|
||||
OH.CreateOhObjectStore(tagSource, key, prefix, postfix)
|
||||
OH.CreateOhObjectStore(tags, key, prefix, postfix)
|
||||
return new SvelteUIElement(OpeningHoursWithError, {
|
||||
tags: tagSource,
|
||||
tags,
|
||||
key,
|
||||
opening_hours_obj: openingHoursStore,
|
||||
})
|
||||
|
@ -148,11 +146,7 @@ class OpeningHoursState extends SpecialVisualizationSvelte {
|
|||
},
|
||||
]
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
): SvelteUIElement {
|
||||
constr({ state, tags, args }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const keyToUse = args[0]
|
||||
const prefix = args[1]
|
||||
const postfix = args[2]
|
||||
|
@ -183,25 +177,25 @@ class Canonical extends SpecialVisualization {
|
|||
},
|
||||
]
|
||||
|
||||
constr(state, tagSource, args) {
|
||||
constr({ state, tags, args }: SpecialVisualisationParams) {
|
||||
const key = args[0]
|
||||
return new VariableUiElement(
|
||||
tagSource
|
||||
tags
|
||||
.map((tags) => tags[key])
|
||||
.map((value) => {
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const allUnits: Unit[] = [].concat(
|
||||
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? []),
|
||||
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? [])
|
||||
)
|
||||
const unit = allUnits.filter((unit) => unit.isApplicableToKey(key))[0]
|
||||
if (unit === undefined) {
|
||||
return value
|
||||
}
|
||||
const getCountry = () => tagSource.data._country
|
||||
const getCountry = () => tags.data._country
|
||||
return unit.asHumanLongValue(value, getCountry)
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -213,41 +207,37 @@ class StatisticsVis extends SpecialVisualizationSvelte {
|
|||
"Show general statistics about all the elements currently in view. Intended to use on the `current_view`-layer. They will be split per layer"
|
||||
args = []
|
||||
|
||||
constr(state) {
|
||||
return new SvelteUIElement(AllFeaturesStatistics, { state })
|
||||
constr(params: SpecialVisualisationParams) {
|
||||
return new SvelteUIElement(AllFeaturesStatistics, params)
|
||||
}
|
||||
}
|
||||
|
||||
class PresetDescription extends SpecialVisualization {
|
||||
class PresetDescription extends SpecialVisualizationSvelte {
|
||||
funcName = "preset_description"
|
||||
docs =
|
||||
"Shows the extra description from the presets of the layer, if one matches. It will pick the most specific one (e.g. if preset `A` implies `B`, but `B` does not imply `A`, it'll pick B) or the first one if no ordering can be made. Might be empty"
|
||||
args = []
|
||||
group = "UI"
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
): BaseUIElement {
|
||||
const translation = tagSource.map((tags) => {
|
||||
constr({ state, tags }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const translation = tags.map((tags) => {
|
||||
const layer = state.theme.getMatchingLayer(tags)
|
||||
return layer?.getMostMatchingPreset(tags)?.description
|
||||
})
|
||||
return new VariableUiElement(translation)
|
||||
return new SvelteUIElement(Tr, { t: translation })
|
||||
}
|
||||
}
|
||||
|
||||
class PresetTypeSelect extends SpecialVisualizationSvelte {
|
||||
funcName = "preset_type_select"
|
||||
docs = "An editable tag rendering which allows to change the type"
|
||||
docs = "An editable tag rendering which allows to change the type. The options are the presets of the layer, effectively allowing to change act as if the object was made with a different preset. " +
|
||||
"For example\n\n" +
|
||||
"How this element looks like (in question mode) for [`tourism_accomodation`](./Layers/tourism_accomodation.md): " +
|
||||
"The presets "
|
||||
args = []
|
||||
group = "ui"
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
selectedElement: Feature,
|
||||
layer: LayerConfig,
|
||||
): SvelteUIElement {
|
||||
constr({ state, tags, feature, layer }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const t = Translations.t.preset_type
|
||||
if (layer._basedOn !== layer.id) {
|
||||
console.warn("Trying to use the _original_ layer")
|
||||
|
@ -273,7 +263,7 @@ class PresetTypeSelect extends SpecialVisualizationSvelte {
|
|||
return new SvelteUIElement(TagRenderingEditable, {
|
||||
config,
|
||||
tags,
|
||||
selectedElement,
|
||||
selectedElement: feature,
|
||||
state,
|
||||
layer,
|
||||
})
|
||||
|
@ -286,8 +276,8 @@ class AllTagsVis extends SpecialVisualizationSvelte {
|
|||
args = []
|
||||
group = "data"
|
||||
|
||||
constr(state, tags: UIEventSource<Record<string, string>>, _, __, layer: LayerConfig) {
|
||||
return new SvelteUIElement(AllTagsPanel, { tags, layer })
|
||||
constr(params: SpecialVisualisationParams) {
|
||||
return new SvelteUIElement(AllTagsPanel, params)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -298,19 +288,82 @@ class PointsInTimeVis extends SpecialVisualization {
|
|||
args = [
|
||||
{
|
||||
name: "key",
|
||||
type: "key",
|
||||
required: true,
|
||||
doc: "The key out of which the points_in_time will be parsed",
|
||||
},
|
||||
]
|
||||
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
|
||||
constr({ tags, args }: SpecialVisualisationParams): BaseUIElement {
|
||||
const key = args[0]
|
||||
const points_in_time = tagSource.map(tags => tags[key])
|
||||
const times = points_in_time.map(times =>
|
||||
OH.createOhObject(<any>tagSource.data, times, tagSource.data["_country"], 1), [tagSource])
|
||||
return new VariableUiElement(times.map(times =>
|
||||
new SvelteUIElement(CollectionTimes, { times }),
|
||||
))
|
||||
const points_in_time = tags.map((tags) => tags[key])
|
||||
const times = points_in_time.map(
|
||||
(times) => OH.createOhObject(<any>tags.data, times, tags.data["_country"], 1),
|
||||
[tags]
|
||||
)
|
||||
return new VariableUiElement(
|
||||
times.map((times) => new SvelteUIElement(CollectionTimes, { times }))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class KnownIcons extends SpecialVisualization {
|
||||
docs =
|
||||
"Displays all icons from the specified tagRenderings (if they are known and have an icon) together, e.g. to give a summary of the dietary options"
|
||||
needsUrls = []
|
||||
group = "UI"
|
||||
funcName = "show_icons"
|
||||
args: SpecialVisualisationArg[] = [
|
||||
{
|
||||
name: "labels",
|
||||
doc: "A ';'-separated list of labels and/or ids of tagRenderings",
|
||||
type: "key",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "class",
|
||||
doc: "CSS-classes of the container, space-separated",
|
||||
type: "css",
|
||||
required: false,
|
||||
defaultValue: "inline-flex mx-4",
|
||||
},
|
||||
]
|
||||
|
||||
private static readonly emojiHeights = {
|
||||
small: "2rem",
|
||||
medium: "3rem",
|
||||
large: "5rem",
|
||||
}
|
||||
|
||||
constr(options: SpecialVisualisationParams): BaseUIElement {
|
||||
const labels = new Set(options.args[0].split(";").map((s) => s.trim()))
|
||||
const matchingTrs = options.layer.tagRenderings.filter(
|
||||
(tr) => labels.has(tr.id) || tr.labels.some((l) => labels.has(l))
|
||||
)
|
||||
return new VariableUiElement(
|
||||
options.tags.map((tags) =>
|
||||
new Combine(
|
||||
matchingTrs.map((tr) => {
|
||||
const mapping = tr.GetRenderValueWithImage(tags)
|
||||
if (!mapping?.icon) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return new SvelteUIElement(Marker, {
|
||||
emojiHeight: KnownIcons.emojiHeights[mapping.iconClass] ?? "2rem",
|
||||
clss: `mapping-icon-${mapping.iconClass ?? "small"}`,
|
||||
icons: mapping.icon,
|
||||
size: twJoin(
|
||||
"shrink-0",
|
||||
`mapping-icon-${mapping.iconClass ?? "small"}-height mapping-icon-${
|
||||
mapping.iconClass ?? "small"
|
||||
}-width`
|
||||
),
|
||||
})
|
||||
})
|
||||
).SetClass(options.args[1] ?? "inline-flex mx-4")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,6 +382,7 @@ export class DataVisualisations {
|
|||
new PresetDescription(),
|
||||
new PresetTypeSelect(),
|
||||
new AllTagsVis(),
|
||||
new KnownIcons(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import {
|
||||
SpecialVisualisationParams,
|
||||
SpecialVisualization,
|
||||
SpecialVisualizationState,
|
||||
} from "../SpecialVisualization"
|
||||
import { Feature } from "geojson"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import Histogram from "../BigComponents/Histogram.svelte"
|
||||
|
@ -14,10 +18,10 @@ export class HistogramViz extends SpecialVisualization {
|
|||
args = [
|
||||
{
|
||||
name: "key",
|
||||
type: "key",
|
||||
doc: "The key to be read and to generate a histogram from",
|
||||
required: true,
|
||||
}
|
||||
|
||||
},
|
||||
]
|
||||
|
||||
structuredExamples(): { feature: Feature; args: string[] }[] {
|
||||
|
@ -36,23 +40,24 @@ export class HistogramViz extends SpecialVisualization {
|
|||
]
|
||||
}
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
) {
|
||||
const values: Store<string[]> = tagSource.map((tags) => {
|
||||
constr({ tags, args }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const values: Store<string[]> = tags.map((tags) => {
|
||||
const value = tags[args[0]]
|
||||
try {
|
||||
if (value === "" || value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if(Array.isArray(value)){
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
console.error("Could not load histogram: parsing of the list failed: ", e,"\nthe data to parse is",value)
|
||||
console.error(
|
||||
"Could not load histogram: parsing of the list failed: ",
|
||||
e,
|
||||
"\nthe data to parse is",
|
||||
value
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import {
|
||||
SpecialVisualisationArg,
|
||||
SpecialVisualisationParams,
|
||||
SpecialVisualizationSvelte,
|
||||
SpecialVisualizationUtils,
|
||||
} from "../../SpecialVisualization"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { Feature, Geometry, LineString, Polygon } from "geojson"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import { ImportFlowArguments, ImportFlowUtils } from "./ImportFlow"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { Utils } from "../../../Utils"
|
||||
|
@ -14,6 +18,7 @@ import { Changes } from "../../../Logic/Osm/Changes"
|
|||
import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig"
|
||||
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
|
||||
import { OsmTags } from "../../../Models/OsmFeature"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
|
||||
export interface ConflateFlowArguments extends ImportFlowArguments {
|
||||
way_to_conflate: string
|
||||
|
@ -22,21 +27,20 @@ export interface ConflateFlowArguments extends ImportFlowArguments {
|
|||
snap_onto_layers?: string
|
||||
}
|
||||
|
||||
export default class ConflateImportButtonViz extends SpecialVisualization implements AutoAction {
|
||||
export default class ConflateImportButtonViz
|
||||
extends SpecialVisualizationSvelte
|
||||
implements AutoAction
|
||||
{
|
||||
supportsAutoAction: boolean = true
|
||||
needsUrls = []
|
||||
group = "data_import"
|
||||
|
||||
public readonly funcName: string = "conflate_button"
|
||||
public readonly args: {
|
||||
name: string
|
||||
defaultValue?: string
|
||||
doc: string
|
||||
required?: boolean
|
||||
}[] = [
|
||||
public readonly args: SpecialVisualisationArg[] = [
|
||||
...ImportFlowUtils.generalArguments,
|
||||
{
|
||||
name: "way_to_conflate",
|
||||
type: "key",
|
||||
doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag",
|
||||
},
|
||||
]
|
||||
|
@ -84,32 +88,30 @@ export default class ConflateImportButtonViz extends SpecialVisualization implem
|
|||
await state.changes.applyAction(action)
|
||||
}
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<OsmTags>,
|
||||
argument: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
constr({ state, tags, args, feature }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const canBeImported =
|
||||
feature.geometry.type === "LineString" ||
|
||||
(feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
||||
if (!canBeImported) {
|
||||
return Translations.t.general.add.import.wrongTypeToConflate.SetClass("alert")
|
||||
return new SvelteUIElement(Tr, {
|
||||
t: Translations.t.general.add.import.wrongTypeToConflate,
|
||||
cls: "alert",
|
||||
})
|
||||
}
|
||||
const args: ConflateFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
|
||||
const idOfWayToReplaceGeometry = tagSource.data[args.way_to_conflate]
|
||||
const argsParsed: ConflateFlowArguments = <any>(
|
||||
SpecialVisualizationUtils.parseArgs(this.args, args)
|
||||
)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(<UIEventSource<OsmTags>>tags, argsParsed)
|
||||
const idOfWayToReplaceGeometry = tags.data[argsParsed.way_to_conflate]
|
||||
const importFlow = new ConflateImportFlowState(
|
||||
state,
|
||||
<Feature<LineString | Polygon>>feature,
|
||||
args,
|
||||
argsParsed,
|
||||
tagsToApply,
|
||||
tagSource,
|
||||
tags,
|
||||
idOfWayToReplaceGeometry
|
||||
)
|
||||
return new SvelteUIElement(WayImportFlow, {
|
||||
importFlow,
|
||||
})
|
||||
return new SvelteUIElement(WayImportFlow, { importFlow })
|
||||
}
|
||||
|
||||
getLayerDependencies = (args: string[]) =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { Feature, Point } from "geojson"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../../SpecialVisualization"
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement"
|
||||
import PointImportFlow from "./PointImportFlow.svelte"
|
||||
import { PointImportFlowArguments, PointImportFlowState } from "./PointImportFlowState"
|
||||
|
@ -9,12 +7,14 @@ import { Utils } from "../../../Utils"
|
|||
import { ImportFlowUtils } from "./ImportFlow"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { OsmTags } from "../../../Models/OsmFeature"
|
||||
|
||||
/**
|
||||
* The wrapper to make the special visualisation for the PointImportFlow
|
||||
*/
|
||||
export class PointImportButtonViz extends SpecialVisualization {
|
||||
export class PointImportButtonViz extends SpecialVisualizationSvelte {
|
||||
public readonly funcName = "import_button"
|
||||
public readonly docs: string =
|
||||
"This button will copy the point from an external dataset into OpenStreetMap" +
|
||||
|
@ -47,29 +47,26 @@ export class PointImportButtonViz extends SpecialVisualization {
|
|||
public needsUrls = []
|
||||
group = "data_import"
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<OsmTags>,
|
||||
argument: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
constr({ state, tags, args, feature }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const to_point_index = this.args.findIndex((arg) => arg.name === "to_point")
|
||||
const summarizePointArg = argument[to_point_index].toLowerCase()
|
||||
const summarizePointArg = args[to_point_index].toLowerCase()
|
||||
if (feature.geometry.type !== "Point") {
|
||||
if (summarizePointArg !== "no" && summarizePointArg !== "false") {
|
||||
feature = GeoOperations.centerpoint(feature)
|
||||
} else {
|
||||
return Translations.t.general.add.import.wrongType.SetClass("alert")
|
||||
return new SvelteUIElement(Tr, {
|
||||
t: Translations.t.general.add.import.wrongType.SetClass("alert"),
|
||||
})
|
||||
}
|
||||
}
|
||||
const baseArgs: PointImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, baseArgs)
|
||||
const baseArgs: PointImportFlowArguments = <any>Utils.ParseVisArgs(this.args, args)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(<UIEventSource<OsmTags>>tags, baseArgs)
|
||||
const importFlow = new PointImportFlowState(
|
||||
state,
|
||||
<Feature<Point>>feature,
|
||||
baseArgs,
|
||||
tagsToApply,
|
||||
tagSource
|
||||
tags
|
||||
)
|
||||
|
||||
return new SvelteUIElement(PointImportFlow, {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import {
|
||||
SpecialVisualisationParams,
|
||||
SpecialVisualizationSvelte,
|
||||
SpecialVisualizationUtils,
|
||||
} from "../../SpecialVisualization"
|
||||
import { AutoAction } from "../AutoApplyButtonVis"
|
||||
import { Feature, LineString, Polygon } from "geojson"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import { ImportFlowUtils } from "./ImportFlow"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement"
|
||||
import WayImportFlow from "./WayImportFlow.svelte"
|
||||
import WayImportFlowState, { WayImportFlowArguments } from "./WayImportFlowState"
|
||||
|
@ -13,12 +15,11 @@ import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig"
|
|||
import { Changes } from "../../../Logic/Osm/Changes"
|
||||
import { IndexedFeatureSource } from "../../../Logic/FeatureSource/FeatureSource"
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { OsmTags } from "../../../Models/OsmFeature"
|
||||
|
||||
/**
|
||||
* Wrapper around 'WayImportFlow' to make it a special visualisation
|
||||
*/
|
||||
export default class WayImportButtonViz extends SpecialVisualization implements AutoAction {
|
||||
export default class WayImportButtonViz extends SpecialVisualizationSvelte implements AutoAction {
|
||||
public readonly funcName: string = "import_way_button"
|
||||
needsUrls = []
|
||||
group = "data_import"
|
||||
|
@ -60,25 +61,22 @@ export default class WayImportButtonViz extends SpecialVisualization implements
|
|||
public readonly supportsAutoAction = true
|
||||
public readonly needsNodeDatabase = true
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<OsmTags>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
_: LayerConfig
|
||||
): BaseUIElement {
|
||||
constr({ state, tags, args, feature }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const geometry = feature.geometry
|
||||
if (!(geometry.type == "LineString" || geometry.type === "Polygon")) {
|
||||
throw "Invalid type to import " + geometry.type
|
||||
throw "Invalid type to import, expected linestring of polygon but got " + geometry.type
|
||||
}
|
||||
const args: WayImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
|
||||
const parsedArgs: WayImportFlowArguments = <any>(
|
||||
SpecialVisualizationUtils.parseArgs(this.args, args)
|
||||
)
|
||||
console.log("Parsed args are", parsedArgs)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tags, parsedArgs)
|
||||
const importFlow = new WayImportFlowState(
|
||||
state,
|
||||
<Feature<LineString | Polygon>>feature,
|
||||
args,
|
||||
parsedArgs,
|
||||
tagsToApply,
|
||||
tagSource
|
||||
tags
|
||||
)
|
||||
return new SvelteUIElement(WayImportFlow, {
|
||||
importFlow,
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
const map = new UIEventSource<MlMap>(undefined)
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(importFlow.originalFeature)
|
||||
const mla = new MapLibreAdaptor(map, {
|
||||
allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting),
|
||||
allowMoving: state.featureSwitchIsTesting.followingClone(),
|
||||
allowZooming: state.featureSwitchIsTesting.followingClone(),
|
||||
rasterLayer: state.mapProperties.rasterLayer,
|
||||
location: new UIEventSource<{ lon: number; lat: number }>({ lon, lat }),
|
||||
zoom: new UIEventSource<number>(18),
|
||||
|
|
|
@ -14,6 +14,7 @@ import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/C
|
|||
import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig"
|
||||
import { Changes } from "../../../Logic/Osm/Changes"
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { OsmFeature } from "../../../Models/OsmFeature"
|
||||
|
||||
export interface WayImportFlowArguments extends ImportFlowArguments {
|
||||
max_snap_distance: string
|
||||
|
@ -54,7 +55,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
|
|||
state: {
|
||||
theme: ThemeConfig
|
||||
changes: Changes
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
indexedFeatures: IndexedFeatureSource<OsmFeature>
|
||||
fullNodeDatabase?: FullNodeDatabaseSource
|
||||
},
|
||||
tagsToApply: Store<Tag[]>,
|
||||
|
@ -82,7 +83,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
|
|||
const coors = feature.geometry.coordinates
|
||||
return new CreateWayWithPointReuseAction(tagsToApply.data, coors, state, mergeConfigs)
|
||||
} else {
|
||||
throw "Unsupported type"
|
||||
throw "Unsupported type: cannot import something of type "+feature.geometry["type"]+", only Polygon and LineString are supported"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import * as all_languages from "../../../assets/language_translations.json"
|
||||
import Locale from "../../i18n/Locale"
|
||||
|
||||
/**
|
||||
* Visualizes a list of the known languages
|
||||
|
@ -22,6 +23,7 @@
|
|||
export let layer: LayerConfig | undefined
|
||||
|
||||
let [beforeListing, afterListing] = (render_all ?? "{list()}").split("{list()}")
|
||||
let currentLanguage: Store<string> = Locale.language
|
||||
</script>
|
||||
|
||||
{#if $languages.length === 1}
|
||||
|
@ -30,9 +32,8 @@
|
|||
{tags}
|
||||
{feature}
|
||||
{layer}
|
||||
t={new TypedTranslation({ "*": single_render }).PartialSubsTr(
|
||||
"language()",
|
||||
new Translation(all_languages[$languages[0]], undefined)
|
||||
t={new TypedTranslation({ "*": single_render }).PartialSubs(
|
||||
{"language()": new Translation(all_languages[$languages[0]]).textFor($currentLanguage)}
|
||||
)}
|
||||
/>
|
||||
{:else}
|
||||
|
@ -45,9 +46,8 @@
|
|||
{tags}
|
||||
{feature}
|
||||
{layer}
|
||||
t={new TypedTranslation({ "*": item_render }).PartialSubsTr(
|
||||
"language()",
|
||||
new Translation(all_languages[language], undefined)
|
||||
t={new TypedTranslation({ "*": item_render }).PartialSubs(
|
||||
{"language()": new Translation(all_languages[language]).textFor($currentLanguage)}
|
||||
)}
|
||||
/>
|
||||
</li>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
{#if $foundLanguages.length === 0 && on_no_known_languages && !$forceInputMode}
|
||||
<div class="low-interaction flex items-center justify-between rounded p-1">
|
||||
<div>
|
||||
<div class="ml-1">
|
||||
{on_no_known_languages}
|
||||
</div>
|
||||
<EditButton on:click={(_) => forceInputMode.setData(true)} />
|
||||
|
|
|
@ -1,46 +1,55 @@
|
|||
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../../SpecialVisualization"
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement"
|
||||
import { Feature } from "geojson"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { default as LanguageElementSvelte } from "./LanguageElement.svelte"
|
||||
|
||||
export class LanguageElement extends SpecialVisualization {
|
||||
export class LanguageElement extends SpecialVisualizationSvelte {
|
||||
funcName: string = "language_chooser"
|
||||
needsUrls = []
|
||||
|
||||
docs: string | BaseUIElement =
|
||||
"The language element allows to show and pick all known (modern) languages. The key can be set"
|
||||
docs: string =
|
||||
"The language element allows to show and pick all known (modern) languages (includes sign languages). The key can be set"
|
||||
|
||||
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
|
||||
args: {
|
||||
name: string
|
||||
defaultValue?: string
|
||||
doc: string
|
||||
required?: boolean
|
||||
type?: string
|
||||
}[] = [
|
||||
{
|
||||
name: "key",
|
||||
required: true,
|
||||
type: "key",
|
||||
doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `<key>:nl=yes` if _nl_ is picked ",
|
||||
},
|
||||
{
|
||||
name: "question",
|
||||
required: true,
|
||||
type: "translation",
|
||||
doc: "What to ask if no questions are known",
|
||||
},
|
||||
{
|
||||
name: "render_list_item",
|
||||
type: "translation",
|
||||
|
||||
doc: "How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain).",
|
||||
defaultValue: "{language()}",
|
||||
},
|
||||
{
|
||||
name: "render_single_language",
|
||||
type: "translation",
|
||||
doc: "What will be shown if the feature only supports a single language",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: "translation",
|
||||
name: "render_all",
|
||||
doc: "The full rendering. Use `{list}` to show where the list of languages must come. Optional if mode=single",
|
||||
doc: "The full rendering. U0se `{list}` to show where the list of languages must come. Optional if mode=single",
|
||||
defaultValue: "{list()}",
|
||||
},
|
||||
{
|
||||
name: "no_known_languages",
|
||||
type: "translation",
|
||||
doc: "The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead",
|
||||
},
|
||||
]
|
||||
|
@ -58,15 +67,8 @@ export class LanguageElement extends SpecialVisualization {
|
|||
\`\`\`
|
||||
`
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
let [key, question, item_render, single_render, all_render, on_no_known_languages] =
|
||||
argument
|
||||
constr({ state, tags, args, feature, layer }: SpecialVisualisationParams): SvelteUIElement {
|
||||
let [key, question, item_render, single_render, all_render, on_no_known_languages] = args
|
||||
if (item_render === undefined || item_render.trim() === "") {
|
||||
item_render = "{language()}"
|
||||
}
|
||||
|
@ -94,7 +96,7 @@ export class LanguageElement extends SpecialVisualization {
|
|||
|
||||
return new SvelteUIElement(LanguageElementSvelte, {
|
||||
key,
|
||||
tags: tagSource,
|
||||
tags,
|
||||
state,
|
||||
feature,
|
||||
layer,
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
<Tr t={Translations.t.general.useSearch} />
|
||||
</label>
|
||||
|
||||
<div class="overflow-auto" style="max-height: 25vh">
|
||||
<div style="max-height: 25vh">
|
||||
{#each knownLanguagecodes as lng}
|
||||
{#if isChecked[lng] && $newlyChecked.indexOf(lng) < 0 && probableLanguages.indexOf(lng) < 0}
|
||||
<label class="no-image-background flex items-center gap-1">
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
import TagHint from "../TagHint.svelte"
|
||||
|
||||
export let question: string
|
||||
export let prefix: string
|
||||
|
@ -67,6 +68,10 @@
|
|||
}
|
||||
dispatch("save")
|
||||
}
|
||||
|
||||
const showTags = state.userRelatedState.showTagsB
|
||||
// For the preview
|
||||
let asTags = selectedLanguages.mapD(lngs => lngs.map((ln) => new Tag(prefix + ln, "yes")))
|
||||
</script>
|
||||
|
||||
<div class="disable-links interactive border-interactive flex flex-col p-2">
|
||||
|
@ -74,7 +79,6 @@
|
|||
<SpecialTranslation {feature} {layer} {state} t={new Translation({ "*": question })} {tags} />
|
||||
</div>
|
||||
<LanguageOptions {selectedLanguages} countries={$countries} />
|
||||
|
||||
<div class="flex w-full flex-wrap-reverse justify-end">
|
||||
<slot name="cancel-button" />
|
||||
<button
|
||||
|
@ -85,4 +89,9 @@
|
|||
<Tr t={Translations.t.general.save} />
|
||||
</button>
|
||||
</div>
|
||||
{#if showTags}
|
||||
<div class="subtle w-fit">
|
||||
<TagHint tags={$asTags} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
|
||||
import { Feature } from "geojson"
|
||||
import { ImmutableStore } from "../../Logic/UIEventSource"
|
||||
import { SpecialVisualisationParams, SpecialVisualizationSvelte } from "../SpecialVisualization"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MapillaryLink from "../BigComponents/MapillaryLink.svelte"
|
||||
|
||||
|
@ -20,12 +19,7 @@ export class MapillaryLinkVis extends SpecialVisualizationSvelte {
|
|||
},
|
||||
]
|
||||
|
||||
public constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagsSource: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature
|
||||
): SvelteUIElement {
|
||||
public constr({ args, feature }: SpecialVisualisationParams): SvelteUIElement {
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
let zoom = Number(args[0])
|
||||
if (isNaN(zoom)) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue