Merge master

This commit is contained in:
Pieter Vander Vennet 2024-06-16 16:30:36 +02:00
commit b315f3cebb
427 changed files with 31017 additions and 42642 deletions

View file

@ -42,16 +42,16 @@ export default class ChartJs<
this._config = config
}
public static ConstructDoughnut(data: Record<string, number>){
public static ConstructDoughnut(data: Record<string, number>) {
const borderColor = [
// ChartJsColours.unkownBorderColor,
// ChartJsColours.otherBorderColor,
// ChartJsColours.notApplicableBorderColor,
// ChartJsColours.unkownBorderColor,
// ChartJsColours.otherBorderColor,
// ChartJsColours.notApplicableBorderColor,
]
const backgroundColor = [
// ChartJsColours.unkownColor,
// ChartJsColours.otherColor,
// ChartJsColours.notApplicableColor,
// ChartJsColours.unkownColor,
// ChartJsColours.otherColor,
// ChartJsColours.notApplicableColor,
]
let i = 0
@ -59,10 +59,10 @@ export default class ChartJs<
const bg = ChartJsColours.backgroundColors
for (const key in data) {
if(key === ""){
if (key === "") {
borderColor.push(ChartJsColours.unknownBorderColor)
backgroundColor.push(ChartJsColours.unknownColor)
}else{
} else {
borderColor.push(borders[i % borders.length])
backgroundColor.push(bg[i % bg.length])
i++

View file

@ -7,7 +7,7 @@
export let isOpened: Store<boolean>
export let moveTo: Store<HTMLElement>
export let debug : string
export let debug: string
function copySizeOf(htmlElem: HTMLElement) {
const target = htmlElem.getBoundingClientRect()
elem.style.left = target.x + "px"
@ -31,18 +31,18 @@
}
}
onDestroy(isOpened.addCallback(opened => animate(opened)))
onMount(() => requestAnimationFrame(() => animate(isOpened.data)))
onDestroy(isOpened.addCallback((opened) => animate(opened)))
onMount(() => requestAnimationFrame(() => animate(isOpened.data)))
</script>
<div class={"absolute bottom-0 right-0 h-full w-screen p-4 md:p-6 pointer-events-none invisible"}>
<div class={"pointer-events-none invisible absolute bottom-0 right-0 h-full w-screen p-4 md:p-6"}>
<div class="content h-full" bind:this={targetOuter} style="background: red" />
</div>
<div bind:this={elem} class="pointer-events-none absolute bottom-0 right-0 low-interaction rounded-2xl"
style="transition: all 0.5s ease-out, background-color 1.4s ease-out; background: var(--background-color);">
<div
bind:this={elem}
class="low-interaction pointer-events-none absolute bottom-0 right-0 rounded-2xl"
style="transition: all 0.5s ease-out, background-color 1.4s ease-out; background: var(--background-color);"
>
<!-- Classes should be the same as the 'floatoaver' -->
</div>

View file

@ -8,7 +8,6 @@
* The slotted element will be shown on top, with a lower-opacity border
*/
const dispatch = createEventDispatcher<{ close }>()
</script>
<!-- Draw the background over the total screen -->

View file

@ -7,7 +7,7 @@
export let osmConnection: OsmConnection
export let clss: string | undefined = undefined
if(osmConnection === undefined){
if (osmConnection === undefined) {
console.error("No osmConnection passed into loginButton")
}
</script>

View file

@ -19,7 +19,7 @@
/**
* Only show the 'successful' state, don't show loading or error messages
*/
export let silentFail : boolean = false
export let silentFail: boolean = false
let loadingStatus = state?.osmConnection?.loadingStatus ?? new ImmutableStore("logged-in")
let badge = state?.featureSwitches?.featureSwitchEnableLogin ?? new ImmutableStore(true)
const t = Translations.t.general

View file

@ -13,7 +13,7 @@
export let enabled: Store<boolean> = new ImmutableStore(true)
export let arialabel: Translation = undefined
export let htmlElem: UIEventSource<HTMLElement> = undefined
let _htmlElem : HTMLElement
let _htmlElem: HTMLElement
$: {
htmlElem?.setData(_htmlElem)
}

View file

@ -4,15 +4,15 @@
export let src: string = undefined
export let srcWritable: Store<string> = undefined
srcWritable?.addCallbackAndRunD(t => {
srcWritable?.addCallbackAndRunD((t) => {
src = t
})
if(src !== undefined && typeof src !== "string") {
if (src !== undefined && typeof src !== "string") {
console.trace("Got a non-string object in Markdown", src)
throw "Markdown.svelte got a non-string object"
}
</script>
{#if src?.length > 0}
{@html marked.parse(src)}
{/if}

View file

@ -6,13 +6,13 @@ import { default as turndown } from "turndown"
import { Utils } from "../../Utils"
export default class TableOfContents {
private static asLinkableId(text: string): string {
return text
?.replace(/ /g, "-")
?.replace(/[?#.;:/]/, "")
?.toLowerCase() ?? ""
return (
text
?.replace(/ /g, "-")
?.replace(/[?#.;:/]/, "")
?.toLowerCase() ?? ""
)
}
private static mergeLevel(
@ -33,7 +33,7 @@ export default class TableOfContents {
if (running.length !== undefined) {
result.push({
content: new List(running),
level: maxLevel - 1
level: maxLevel - 1,
})
running = []
}
@ -42,7 +42,7 @@ export default class TableOfContents {
if (running.length !== undefined) {
result.push({
content: new List(running),
level: maxLevel - 1
level: maxLevel - 1,
})
}
@ -56,13 +56,16 @@ export default class TableOfContents {
const firstTitle = structure[1]
let minDepth = undefined
do {
minDepth = Math.min(...structure.map(s => s.depth))
const minDepthCount = structure.filter(s => s.depth === minDepth)
minDepth = Math.min(...structure.map((s) => s.depth))
const minDepthCount = structure.filter((s) => s.depth === minDepth)
if (minDepthCount.length > 1) {
break
}
// Erase a single top level heading
structure.splice(structure.findIndex(s => s.depth === minDepth), 1)
structure.splice(
structure.findIndex((s) => s.depth === minDepth),
1
)
} while (structure.length > 0)
if (structure.length <= 1) {
@ -71,7 +74,7 @@ export default class TableOfContents {
const separators = {
1: " -",
2: " +",
3: " *"
3: " *",
}
let toc = ""
@ -96,15 +99,16 @@ export default class TableOfContents {
const splitPoint = intro.lastIndexOf("\n")
return md.substring(0, splitPoint) + toc + md.substring(splitPoint)
}
public static generateStructure(html: Element): { depth: number, title: string, el: Element }[] {
public static generateStructure(
html: Element
): { depth: number; title: string; el: Element }[] {
if (html === undefined) {
return []
}
return [].concat(...Array.from(html.childNodes ?? []).map(
child => {
return [].concat(
...Array.from(html.childNodes ?? []).map((child) => {
const tag: string = child["tagName"]?.toLowerCase()
if (!tag) {
return []
@ -114,7 +118,7 @@ export default class TableOfContents {
return [{ depth, title: child.textContent, el: child }]
}
return TableOfContents.generateStructure(<Element>child)
}
))
})
)
}
}

View file

@ -17,7 +17,6 @@
txt = t?.current
lang = t?.currentLang
}
</script>
{#if $txt}

View file

@ -40,11 +40,9 @@ export default class CopyrightPanel extends Combine {
const t = Translations.t.general.attribution
const layoutToUse = state.layout
const iconAttributions: BaseUIElement[] = (layoutToUse.getUsedImages()).map(
CopyrightPanel.IconAttribution
)
const iconAttributions: BaseUIElement[] = layoutToUse
.getUsedImages()
.map(CopyrightPanel.IconAttribution)
let maintainer: BaseUIElement = undefined
if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") {

View file

@ -8,7 +8,6 @@
import Tr from "../Base/Tr.svelte"
import Icon from "../Map/Icon.svelte"
export let state: SpecialVisualizationState
let theme = state.layout?.id ?? ""
let config: ExtraLinkConfig = state.layout.extraLink
@ -23,7 +22,7 @@
...loc,
theme: theme,
basepath,
language: Locale.language.data
language: Locale.language.data,
}
return Utils.SubstituteKeys(config.href, subs)
},
@ -31,25 +30,21 @@
)
</script>
{#if config !== undefined && !(config.requirements.has("iframe") && !isIframe) && !(config.requirements.has("no-iframe") && isIframe) && !(config.requirements.has("welcome-message") && !$showWelcomeMessageSwitch) && !(config.requirements.has("no-welcome-message") && $showWelcomeMessageSwitch)}
<div class="links-as-button">
<a
href={$href}
target={config.newTab ? "_blank" : ""}
rel="noopener"
class="pointer-events-auto flex rounded-full border-black"
>
<Icon icon={config.icon} clss="w-6 h-6 m-2" />
{#if config !== undefined &&
!(config.requirements.has("iframe") && !isIframe) &&
!(config.requirements.has("no-iframe") && isIframe) &&
!(config.requirements.has("welcome-message") && !$showWelcomeMessageSwitch) &&
!(config.requirements.has("no-welcome-message") && $showWelcomeMessageSwitch)}
<div class="links-as-button">
<a href={$href} target={config.newTab ? "_blank" : ""} rel="noopener"
class="flex pointer-events-auto rounded-full border-black">
<Icon icon={config.icon} clss="w-6 h-6 m-2"/>
{#if config.text}
<Tr t={config.text} />
{:else}
<Tr t={t.screenToSmall.Subs({theme: state.layout.title})} />
{/if}
</a>
</div>
{#if config.text}
<Tr t={config.text} />
{:else}
<Tr t={t.screenToSmall.Subs({ theme: state.layout.title })} />
{/if}
</a>
</div>
{/if}

View file

@ -46,12 +46,12 @@
}
</script>
<div class="h-full flex flex-col">
<div class="flex h-full flex-col">
<h2 class="low-interaction m-0 flex items-center p-4 drop-shadow-md">
<Filter class="h-6 w-6 pr-2" />
<Tr t={Translations.t.general.menu.filter} />
</h2>
<div class="flex h-full flex-col overflow-auto p-4 border-b-2">
<div class="flex h-full flex-col overflow-auto border-b-2 p-4">
{#each layout.layers as layer}
<Filterview
zoomlevel={state.mapProperties.zoom}

View file

@ -15,7 +15,7 @@
export let state: ThemeViewState
export let map: Store<MlMap> = undefined
export let hideTooltip = false
export let htmlElem : UIEventSource<HTMLElement> = undefined
export let htmlElem: UIEventSource<HTMLElement> = undefined
</script>
<MapControlButton

View file

@ -8,7 +8,7 @@
const t = Translations.t.privacy
export let state: SpecialVisualizationState
const usersettings = UserRelatedState.usersettingsConfig
const editPrivacy = usersettings.tagRenderings.find(tr => tr.id === "more_privacy")
const editPrivacy = usersettings.tagRenderings.find((tr) => tr.id === "more_privacy")
const isLoggedIn = state.osmConnection.isLoggedIn
</script>
@ -48,16 +48,19 @@
<li>
{#if $isLoggedIn}
<TagRenderingEditable config={editPrivacy} selectedElement={{
type: "Feature",
properties: { id: "settings" },
geometry: { type: "Point", coordinates: [0, 0] },
}}
{state}
tags={state.userRelatedState.preferencesAsTags} />
{:else}
<Tr t={t.items.distanceIndicator} />
{/if}
<TagRenderingEditable
config={editPrivacy}
selectedElement={{
type: "Feature",
properties: { id: "settings" },
geometry: { type: "Point", coordinates: [0, 0] },
}}
{state}
tags={state.userRelatedState.preferencesAsTags}
/>
{:else}
<Tr t={t.items.distanceIndicator} />
{/if}
</li>
</ul>

View file

@ -185,8 +185,6 @@ export class StackedRenderingChart extends ChartJs {
}
export default class TagRenderingChart extends Combine {
/**
* Creates a chart about this tagRendering for the given data
*/
@ -223,9 +221,9 @@ export default class TagRenderingChart extends Combine {
ChartJsColours.notApplicableBorderColor,
]
const backgroundColor = [
ChartJsColours.unknownColor,
ChartJsColours.otherColor,
ChartJsColours.notApplicableColor,
ChartJsColours.unknownColor,
ChartJsColours.otherColor,
ChartJsColours.notApplicableColor,
]
while (borderColor.length < data.length) {

View file

@ -15,7 +15,6 @@
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
export let externalProperties: Record<string, string>
delete externalProperties["@context"]
console.log("External properties are", externalProperties)
@ -33,51 +32,60 @@
let externalKeys: string[] = Object.keys(externalProperties).sort()
const imageKeyRegex = /image|image:[0-9]+/
let knownImages: Store<Set<string>> = tags.map(osmProperties => new Set(
Object.keys(osmProperties)
let knownImages: Store<Set<string>> = tags.map(
(osmProperties) =>
new Set(
Object.keys(osmProperties)
.filter((k) => k.match(imageKeyRegex))
.map((k) => osmProperties[k])
)
)
let unknownImages: Store<string[]> = knownImages.map((images) =>
externalKeys
.filter((k) => k.match(imageKeyRegex))
.map((k) => osmProperties[k])
))
let unknownImages: Store<string[]> = knownImages.map(images => externalKeys
.filter((k) => k.match(imageKeyRegex))
.map((k) => externalProperties[k])
.filter((i) => !images.has(i)))
.map((k) => externalProperties[k])
.filter((i) => !images.has(i))
)
let propertyKeysExternal = externalKeys.filter((k) => k.match(imageKeyRegex) === null)
let missing: Store<string[]> = tags.map(osmProperties => propertyKeysExternal.filter((k) => {
if (k.startsWith("_")) {
return false
}
return osmProperties[k] === undefined && typeof externalProperties[k] === "string"
}))
// let same = propertyKeysExternal.filter((key) => osmProperties[key] === externalProperties[key])
let different: Store<string[]> = tags.map(osmProperties => propertyKeysExternal.filter((key) => {
if (key.startsWith("_")) {
return false
}
if (osmProperties[key] === undefined) {
return false
}
if (typeof externalProperties[key] !== "string") {
return false
}
if (osmProperties[key] === externalProperties[key]) {
return false
}
if (key === "website") {
const osmCanon = new URL(osmProperties[key]).toString()
const externalCanon = new URL(externalProperties[key]).toString()
if (osmCanon === externalCanon) {
let missing: Store<string[]> = tags.map((osmProperties) =>
propertyKeysExternal.filter((k) => {
if (k.startsWith("_")) {
return false
}
return osmProperties[k] === undefined && typeof externalProperties[k] === "string"
})
)
// let same = propertyKeysExternal.filter((key) => osmProperties[key] === externalProperties[key])
let different: Store<string[]> = tags.map((osmProperties) =>
propertyKeysExternal.filter((key) => {
if (key.startsWith("_")) {
return false
}
if (osmProperties[key] === undefined) {
return false
}
if (typeof externalProperties[key] !== "string") {
return false
}
if (osmProperties[key] === externalProperties[key]) {
return false
}
}
return true
}))
if (key === "website") {
const osmCanon = new URL(osmProperties[key]).toString()
const externalCanon = new URL(externalProperties[key]).toString()
if (osmCanon === externalCanon) {
return false
}
}
let hasDifferencesAtStart = (different.data.length + missing.data.length + unknownImages.data.length) > 0
return true
})
)
let hasDifferencesAtStart =
different.data.length + missing.data.length + unknownImages.data.length > 0
let currentStep: "init" | "applying_all" | "all_applied" = "init"
let applyAllHovered = false
@ -87,23 +95,23 @@
const tagsToApply = missing.data.map((k) => new Tag(k, externalProperties[k]))
const change = new ChangeTagAction(tags.data.id, new And(tagsToApply), tags.data, {
theme: state.layout.id,
changeType: "import"
changeType: "import",
})
await state.changes.applyChanges(await change.CreateChangeDescriptions())
currentStep = "all_applied"
}
</script>
{#if propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0}
<Tr cls="subtle" t={t.noDataLoaded} />
{:else if !hasDifferencesAtStart}
<span class="subtle text-sm">
<Tr t={t.allIncluded.Subs({source:sourceUrl})}/>
<Tr t={t.allIncluded.Subs({ source: sourceUrl })} />
</span>
{:else if $unknownImages.length === 0 && $missing.length === 0 && $different.length === 0}
<div class="thanks m-0 flex items-center gap-x-2 px-2">
<Party class="h-8 w-8 shrink-0" />
<Tr t={t.allIncluded.Subs({source: sourceUrl})} />
<Tr t={t.allIncluded.Subs({ source: sourceUrl })} />
</div>
{:else}
<div class="low-interaction border-interactive p-1">
@ -112,7 +120,6 @@
{/if}
<div class="flex flex-col" class:gap-y-8={!readonly}>
{#if $different.length > 0}
{#if !readonly}
<h3>
@ -137,9 +144,9 @@
{#if $missing.length > 0}
{#if !readonly}
<h3 class="m-0">
<Tr t={t.missing.title} />
</h3>
<h3 class="m-0">
<Tr t={t.missing.title} />
</h3>
<Tr t={t.missing.intro} />
{/if}

View file

@ -69,12 +69,12 @@
previewedImage={state.previewedImage}
/>
</div>
<LoginToggle {state} silentFail={true} >
{#if linkable}
<label>
<input bind:checked={$isLinked} type="checkbox" />
<SpecialTranslation t={t.link} {tags} {state} {layer} {feature} />
</label>
{/if}
<LoginToggle {state} silentFail={true}>
{#if linkable}
<label>
<input bind:checked={$isLinked} type="checkbox" />
<SpecialTranslation t={t.link} {tags} {state} {layer} {feature} />
</label>
{/if}
</LoginToggle>
</div>

View file

@ -30,7 +30,7 @@
lon,
lat,
allowSpherical: new UIEventSource<boolean>(false),
blacklist: AllImageProviders.LoadImagesFor(tags)
blacklist: AllImageProviders.LoadImagesFor(tags),
},
state.indexedFeatures
)
@ -53,9 +53,9 @@
{:else}
<div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $images as image (image.pictureUrl)}
<span class="w-fit shrink-0" style="scroll-snap-align: start">
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
</span>
<span class="w-fit shrink-0" style="scroll-snap-align: start">
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
</span>
{/each}
</div>
{/if}

View file

@ -25,31 +25,31 @@
let expanded = false
</script>
<div class="my-4">
{#if expanded}
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer}>
<button
slot="corner"
class="no-image-background h-6 w-6 cursor-pointer border-none p-0"
use:ariaLabel={t.close}
on:click={() => {
expanded = false
}}
>
<XCircleIcon />
</button>
</NearbyImages>
{:else}
<div class="my-4">
{#if expanded}
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer}>
<button
class="flex w-full items-center"
style="margin-left: 0; margin-right: 0"
slot="corner"
class="no-image-background h-6 w-6 cursor-pointer border-none p-0"
use:ariaLabel={t.close}
on:click={() => {
expanded = true
expanded = false
}}
aria-expanded={expanded}
>
<Camera_plus class="mr-2 block h-8 w-8 p-1" />
<Tr t={t.seeNearby} />
<XCircleIcon />
</button>
{/if}
</div>
</NearbyImages>
{:else}
<button
class="flex w-full items-center"
style="margin-left: 0; margin-right: 0"
on:click={() => {
expanded = true
}}
aria-expanded={expanded}
>
<Camera_plus class="mr-2 block h-8 w-8 p-1" />
<Tr t={t.seeNearby} />
</button>
{/if}
</div>

View file

@ -63,7 +63,7 @@ export default class Validators {
"slope",
"velopark",
"nsi",
"currency"
"currency",
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -94,7 +94,7 @@ export default class Validators {
new SlopeValidator(),
new VeloparkValidator(),
new NameSuggestionIndexValidator(),
new CurrencyValidator()
new CurrencyValidator(),
]
private static _byType = Validators._byTypeConstructor()

View file

@ -13,26 +13,26 @@ export default class CurrencyValidator extends Validator {
return
}
let locale = "en-US"
if(!Utils.runningFromConsole){
locale??= navigator.language
if (!Utils.runningFromConsole) {
locale ??= navigator.language
}
this.segmenter = new Intl.Segmenter(locale, {
granularity: "word"
granularity: "word",
})
const mapping: Map<string, string> = new Map<string, string>()
const supportedCurrencies: Set<string> = new Set(Intl.supportedValuesOf("currency"))
this.supportedCurrencies = supportedCurrencies
for (const currency of supportedCurrencies) {
const symbol = (0).toLocaleString(
locale,
{
const symbol = (0)
.toLocaleString(locale, {
style: "currency",
currency: currency,
minimumFractionDigits: 0,
maximumFractionDigits: 0
}
).replace(/\d/g, "").trim()
maximumFractionDigits: 0,
})
.replace(/\d/g, "")
.trim()
mapping.set(symbol.toLowerCase(), currency)
}
@ -44,8 +44,10 @@ export default class CurrencyValidator extends Validator {
return s
}
const parts = Array.from(this.segmenter.segment(s)).map(i => i.segment).filter(part => part.trim().length > 0)
if(parts.length !== 2){
const parts = Array.from(this.segmenter.segment(s))
.map((i) => i.segment)
.filter((part) => part.trim().length > 0)
if (parts.length !== 2) {
return s
}
const mapping = this.symbolToCurrencyMapping
@ -64,10 +66,10 @@ export default class CurrencyValidator extends Validator {
}
amount = part
}
if(amount === undefined || currency === undefined){
if (amount === undefined || currency === undefined) {
return s
}
return amount+" "+currency
return amount + " " + currency
}
}

View file

@ -3,7 +3,10 @@ import UrlValidator from "./UrlValidator"
export default class VeloparkValidator extends UrlValidator {
constructor() {
super("velopark", "A special URL-validator that checks the domain name and rewrites to the correct velopark format.")
super(
"velopark",
"A special URL-validator that checks the domain name and rewrites to the correct velopark format."
)
}
getFeedback(s: string): Translation {

View file

@ -30,41 +30,42 @@
}
> = UIEventSource.FromPromise(Utils.downloadJsonCached(source))
</script>
<main>
<h1>Contributed images with MapComplete: leaderboard</h1>
<h1>Contributed images with MapComplete: leaderboard</h1>
{#if $data}
<table>
<tr>
<th>Rank</th>
<th>Contributor</th>
<th>Number of images contributed</th>
</tr>
{#each $data.leaderboard as contributor}
{#if $data}
<table>
<tr>
<td>
{contributor.rank}
</td>
<td>
{#if $loggedInContributor === contributor.name}
<a class="thanks" href={contributor.account}>{contributor.name}</a>
{:else}
<a href={contributor.account}>{contributor.name}</a>
{/if}
</td>
<td>
<b>{contributor.nrOfImages}</b>
total images
</td>
<th>Rank</th>
<th>Contributor</th>
<th>Number of images contributed</th>
</tr>
{/each}
</table>
Statistics generated on {$data.date}
{:else}
<Loading />
{/if}
{#each $data.leaderboard as contributor}
<tr>
<td>
{contributor.rank}
</td>
<td>
{#if $loggedInContributor === contributor.name}
<a class="thanks" href={contributor.account}>{contributor.name}</a>
{:else}
<a href={contributor.account}>{contributor.name}</a>
{/if}
</td>
<td>
<b>{contributor.nrOfImages}</b>
total images
</td>
</tr>
{/each}
</table>
Statistics generated on {$data.date}
{:else}
<Loading />
{/if}
<div>
Logged in as {$loggedInContributor}
</div>
<div>
Logged in as {$loggedInContributor}
</div>
</main>

View file

@ -126,7 +126,6 @@
<LinkIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} />
{:else if icon === "popout"}
<LinkIcon style="--svg-color: {color}" />
{:else}
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" />
{/if}

View file

@ -154,7 +154,7 @@ class PointRenderingLayer {
if (this._onClick) {
const self = this
el.addEventListener("click", function(ev) {
el.addEventListener("click", function (ev) {
ev.preventDefault()
self._onClick(feature)
// Workaround to signal the MapLibreAdaptor to ignore this click
@ -200,7 +200,7 @@ class LineRenderingLayer {
"lineCap",
"offset",
"fill",
"fillColor"
"fillColor",
] as const
private static readonly lineConfigKeysColor = ["color", "fillColor"] as const
@ -264,8 +264,8 @@ class LineRenderingLayer {
"icon-rotation-alignment": "map",
"icon-pitch-alignment": "map",
"icon-image": imgId,
"icon-size": 0.055
}
"icon-size": 0.055,
},
}
const filter = img.if?.asMapboxExpression()
if (filter) {
@ -338,9 +338,9 @@ class LineRenderingLayer {
type: "geojson",
data: {
type: "FeatureCollection",
features
features,
},
promoteId: "id"
promoteId: "id",
})
const linelayer = this._layername + "_line"
const layer: AddLayerObject = {
@ -351,19 +351,21 @@ class LineRenderingLayer {
"line-color": ["feature-state", "color"],
"line-opacity": ["feature-state", "color-opacity"],
"line-width": ["feature-state", "width"],
"line-offset": ["feature-state", "offset"]
"line-offset": ["feature-state", "offset"],
},
layout: {
"line-cap": "round"
}
"line-cap": "round",
},
}
if (this._config.dashArray) {
try{
layer.paint["line-dasharray"] =
this._config.dashArray?.split(" ")?.map((s) => Number(s)) ?? null
}catch (e) {
console.error(`Invalid dasharray in layer ${this._layername}:`, this._config.dashArray)
try {
layer.paint["line-dasharray"] =
this._config.dashArray?.split(" ")?.map((s) => Number(s)) ?? null
} catch (e) {
console.error(
`Invalid dasharray in layer ${this._layername}:`,
this._config.dashArray
)
}
}
map.addLayer(layer)
@ -398,8 +400,8 @@ class LineRenderingLayer {
layout: {},
paint: {
"fill-color": ["feature-state", "fillColor"],
"fill-opacity": ["feature-state", "fillColor-opacity"]
}
"fill-opacity": ["feature-state", "fillColor-opacity"],
},
})
if (this._onClick) {
map.on("click", polylayer, (e) => {
@ -430,7 +432,7 @@ class LineRenderingLayer {
this.currentSourceData = features
src.setData({
type: "FeatureCollection",
features: this.currentSourceData
features: this.currentSourceData,
})
}
}
@ -513,15 +515,15 @@ export default class ShowDataLayer {
layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
features,
{
constructStore: (features, layer) => new SimpleFeatureSource(layer, features)
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
}
)
if (options?.zoomToFeatures) {
options.zoomToFeatures = false
features.features.addCallbackD(features => {
features.features.addCallbackD((features) => {
ShowDataLayer.zoomToCurrentFeatures(mlmap.data, features)
})
mlmap.addCallbackD(map => {
mlmap.addCallbackD((map) => {
ShowDataLayer.zoomToCurrentFeatures(map, features.features.data)
})
}
@ -530,7 +532,7 @@ export default class ShowDataLayer {
new ShowDataLayer(mlmap, {
layer: fs.layer.layerDef,
features: fs,
...(options ?? {})
...(options ?? {}),
})
})
}
@ -543,12 +545,11 @@ export default class ShowDataLayer {
return new ShowDataLayer(map, {
layer: ShowDataLayer.rangeLayer,
features,
doShowLayer
doShowLayer,
})
}
public destruct() {
}
public destruct() {}
private static zoomToCurrentFeatures(map: MlMap, features: Feature[]) {
if (!features || !map || features.length == 0) {
@ -560,7 +561,7 @@ export default class ShowDataLayer {
map.resize()
map.fitBounds(bbox.toLngLat(), {
padding: { top: 10, bottom: 10, left: 10, right: 10 },
animate: false
animate: false,
})
})
}
@ -573,8 +574,8 @@ export default class ShowDataLayer {
this._options.layer.title === undefined
? undefined
: (feature: Feature) => {
selectedElement?.setData(feature)
}
selectedElement?.setData(feature)
}
}
if (this._options.drawLines !== false) {
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
@ -606,7 +607,9 @@ export default class ShowDataLayer {
}
}
if (this._options.zoomToFeatures) {
features.features.addCallbackAndRunD((features) => ShowDataLayer.zoomToCurrentFeatures(map, features))
features.features.addCallbackAndRunD((features) =>
ShowDataLayer.zoomToCurrentFeatures(map, features)
)
}
}
}

View file

@ -6,17 +6,17 @@
</script>
<main>
<div class="flex flex-col">
<Tr t={Translations.t.general["404"]} />
<BackButton
clss="m-8"
on:click={() => {
window.location = "index.html"
}}
>
<div class="flex w-full justify-center">
<Tr t={Translations.t.general.backToIndex} />
</div>
</BackButton>
</div>
<div class="flex flex-col">
<Tr t={Translations.t.general["404"]} />
<BackButton
clss="m-8"
on:click={() => {
window.location = "index.html"
}}
>
<div class="flex w-full justify-center">
<Tr t={Translations.t.general.backToIndex} />
</div>
</BackButton>
</div>
</main>

View file

@ -21,16 +21,14 @@
}
let knownValues: UIEventSource<string[]> = new UIEventSource<string[]>([])
tags.addCallbackAndRunD(tags => {
tags.addCallbackAndRunD((tags) => {
knownValues.setData(Object.keys(tags))
})
function reEvalKnownValues(){
function reEvalKnownValues() {
knownValues.setData(Object.keys(tags.data))
}
const metaKeys: string[] = [].concat(...SimpleMetaTaggers.metatags.map((k) => k.keys))
let allCalculatedTags = new Set<string>([...calculatedTags, ...metaKeys])
</script>
@ -54,7 +52,7 @@
{:else if $tags[key] === ""}
<i>Empty string</i>
{:else if typeof $tags[key] === "object"}
<div class="literal-code" >{JSON.stringify($tags[key])}</div>
<div class="literal-code">{JSON.stringify($tags[key])}</div>
{:else}
{$tags[key]}
{/if}

View file

@ -17,13 +17,13 @@ export class MinimapViz implements SpecialVisualization {
{
doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close",
name: "zoomlevel",
defaultValue: "18"
defaultValue: "18",
},
{
doc: "(Matches all resting arguments) This argument should be the key of a property of the feature. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap. (Note: if the key is 'id', list interpration is disabled)",
name: "idKey",
defaultValue: "id"
}
defaultValue: "id",
},
]
example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`"
@ -82,13 +82,12 @@ export class MinimapViz implements SpecialVisualization {
const mla = new MapLibreAdaptor(mlmap, {
rasterLayer: state.mapProperties.rasterLayer,
zoom: new UIEventSource<number>(17),
maxzoom: new UIEventSource<number>(17)
maxzoom: new UIEventSource<number>(17),
})
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
mla.location.setData({lon, lat})
mla.location.setData({ lon, lat })
if (args[0]) {
const parsed = Number(args[0])
@ -96,19 +95,19 @@ export class MinimapViz implements SpecialVisualization {
mla.zoom.setData(parsed)
}
}
mlmap.addCallbackAndRun(map => console.log("Map for minimap vis is now", map))
mlmap.addCallbackAndRun((map) => console.log("Map for minimap vis is now", map))
ShowDataLayer.showMultipleLayers(
mlmap,
new StaticFeatureSource(featuresToShow),
state.layout.layers,
{zoomToFeatures: true}
{ zoomToFeatures: true }
)
return new SvelteUIElement(MaplibreMap, {
interactive: false,
map: mlmap,
mapProperties: mla
mapProperties: mla,
})
.SetClass("h-40 rounded")
.SetStyle("overflow: hidden; pointer-events: none;")

View file

@ -50,7 +50,6 @@
let questionboxElem: HTMLDivElement
let questionsToAsk = tags.map(
(tags) => {
const questionsToAsk: TagRenderingConfig[] = []
for (const baseQuestion of baseQuestions) {
if (skippedQuestions.data.has(baseQuestion.id)) {
@ -164,7 +163,13 @@
{#if $showAllQuestionsAtOnce}
<div class="flex flex-col gap-y-1">
{#each $allQuestionsToAsk as question (question.id)}
<TagRenderingQuestionDynamic config={question} {tags} {selectedElement} {state} {layer} />
<TagRenderingQuestionDynamic
config={question}
{tags}
{selectedElement}
{state}
{layer}
/>
{/each}
</div>
{:else if $firstQuestion !== undefined}

View file

@ -4,7 +4,7 @@
import Locale from "../../i18n/Locale"
import type {
RenderingSpecification,
SpecialVisualizationState
SpecialVisualizationState,
} from "../../SpecialVisualization"
import { Utils } from "../../../Utils.js"
import type { Feature } from "geojson"
@ -67,7 +67,7 @@
{#each specs as specpart}
{#if typeof specpart === "string"}
<span>
{@html Utils.purify(Utils.SubstituteKeys(specpart, $tags)) }
{@html Utils.purify(Utils.SubstituteKeys(specpart, $tags))}
<WeblateLink context={t.context} />
</span>
{:else if $tags !== undefined}
@ -79,7 +79,7 @@
{#each specs as specpart}
{#if typeof specpart === "string"}
<span>
{@html Utils.purify(Utils.SubstituteKeys(specpart, $tags)) }
{@html Utils.purify(Utils.SubstituteKeys(specpart, $tags))}
<WeblateLink context={t.context} />
</span>
{:else if $tags !== undefined}

View file

@ -1,5 +1,7 @@
<script lang="ts">
import TagRenderingConfig, { TagRenderingConfigUtils } from "../../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingConfig, {
TagRenderingConfigUtils,
} from "../../../Models/ThemeConfig/TagRenderingConfig"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Feature } from "geojson"
import { UIEventSource } from "../../../Logic/UIEventSource"
@ -16,7 +18,14 @@
export let id: string = undefined
let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
</script>
<TagRenderingAnswer {selectedElement} {layer} config={$dynamicConfig} {extraClasses} {id} {tags} {state} />
<TagRenderingAnswer
{selectedElement}
{layer}
config={$dynamicConfig}
{extraClasses}
{id}
{tags}
{state}
/>

View file

@ -1,5 +1,7 @@
<script lang="ts">
import TagRenderingConfig, { TagRenderingConfigUtils } from "../../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingConfig, {
TagRenderingConfigUtils,
} from "../../../Models/ThemeConfig/TagRenderingConfig"
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
@ -19,8 +21,16 @@
export let editMode = !config.IsKnown(tags.data)
let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
</script>
<TagRenderingEditable config={$dynamicConfig} {editMode} {clss} {highlightedRendering} {editingEnabled} {layer} {state}
{selectedElement} {tags} />
<TagRenderingEditable
config={$dynamicConfig}
{editMode}
{clss}
{highlightedRendering}
{editingEnabled}
{layer}
{state}
{selectedElement}
{tags}
/>

View file

@ -44,7 +44,7 @@
(search) => {
search = search?.trim()
if (!search) {
if(hideUnlessSearched){
if (hideUnlessSearched) {
if (mapping.priorityIf?.matchesProperties(tags.data)) {
return true
}

View file

@ -167,7 +167,11 @@
onDestroy(
freeformInput.subscribe((freeformValue) => {
if (!config?.mappings || config?.mappings?.length == 0 || config.freeform?.key === undefined) {
if (
!config?.mappings ||
config?.mappings?.length == 0 ||
config.freeform?.key === undefined
) {
return
}
// If a freeform value is given, mark the 'mapping' as marked
@ -232,7 +236,9 @@
// Add the extraTags to the existing And
selectedTags = new And([...selectedTags.and, ...extraTagsArray])
} else {
console.error("selectedTags is not of type Tag or And, it is a "+JSON.stringify(selectedTags))
console.error(
"selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags)
)
}
}
}
@ -289,7 +295,8 @@
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined)
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0
let question = config.question
let hideMappingsUnlessSearchedFor = config.mappings.length > 8 && config.mappings.some(m => m.priorityIf)
let hideMappingsUnlessSearchedFor =
config.mappings.length > 8 && config.mappings.some((m) => m.priorityIf)
$: question = config.question
if (state?.osmConnection) {
onDestroy(
@ -343,15 +350,13 @@
/>
</div>
{#if hideMappingsUnlessSearchedFor}
<div class="rounded border border-black border-dashed p-1 px-2 m-1">
<Tr t={Translations.t.general.mappingsAreHidden}/>
<div class="m-1 rounded border border-dashed border-black p-1 px-2">
<Tr t={Translations.t.general.mappingsAreHidden} />
</div>
{/if}
{/if}
{#if config.freeform?.key && !(config?.mappings?.filter(m => m.hideInAnswer != true)?.length > 0)}
{#if config.freeform?.key && !(config?.mappings?.filter((m) => m.hideInAnswer != true)?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput
{config}
@ -505,7 +510,7 @@
<span class="flex flex-wrap">
{#if $featureSwitchIsTesting}
<button class="small" on:click={() => console.log("Configuration is ", config)}>
Testmode &nbsp;
Testmode &nbsp;
</button>
{/if}
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}

View file

@ -1,34 +1,36 @@
<script lang="ts">/**
* Wrapper around 'tagRenderingEditable' but might add mappings dynamically
*
* Note: does not forward the 'save-button'-slot
*/
import TagRenderingConfig, { TagRenderingConfigUtils } from "../../../Models/ThemeConfig/TagRenderingConfig"
import { UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
import type { UploadableTag } from "../../../Logic/Tags/TagUtils"
import { writable } from "svelte/store"
import Translations from "../../i18n/Translations"
import { twJoin } from "tailwind-merge"
import Tr from "../../Base/Tr.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
<script lang="ts">
/**
* Wrapper around 'tagRenderingEditable' but might add mappings dynamically
*
* Note: does not forward the 'save-button'-slot
*/
import TagRenderingConfig, {
TagRenderingConfigUtils,
} from "../../../Models/ThemeConfig/TagRenderingConfig"
import { UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
import type { UploadableTag } from "../../../Logic/Tags/TagUtils"
import { writable } from "svelte/store"
import Translations from "../../i18n/Translations"
import { twJoin } from "tailwind-merge"
import Tr from "../../Base/Tr.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature
export let state: SpecialVisualizationState
export let layer: LayerConfig | undefined
export let selectedTags: UploadableTag = undefined
export let extraTags: UIEventSource<Record<string, string>> = new UIEventSource({})
export let selectedElement: Feature
export let state: SpecialVisualizationState
export let layer: LayerConfig | undefined
export let selectedTags: UploadableTag = undefined
export let extraTags: UIEventSource<Record<string, string>> = new UIEventSource({})
export let allowDeleteOfFreeform: boolean = false
export let allowDeleteOfFreeform: boolean = false
let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
</script>
<TagRenderingQuestion

View file

@ -13,24 +13,25 @@
const osmConnection = new OsmConnection()
let state: SpecialVisualizationState = {
osmConnection,
userRelatedState: new UserRelatedState(osmConnection)
userRelatedState: new UserRelatedState(osmConnection),
}
</script>
<main>
<div class="flex h-screen flex-col overflow-hidden px-4">
<div class="flex justify-between">
<h2 class="flex items-center">
<EyeIcon class="w-6 pr-2" />
<Tr t={Translations.t.privacy.title} />
</h2>
<LanguagePicker availableLanguages={Translations.t.privacy.intro.SupportedLanguages()} />
<div class="flex h-screen flex-col overflow-hidden px-4">
<div class="flex justify-between">
<h2 class="flex items-center">
<EyeIcon class="w-6 pr-2" />
<Tr t={Translations.t.privacy.title} />
</h2>
<LanguagePicker availableLanguages={Translations.t.privacy.intro.SupportedLanguages()} />
</div>
<div class="h-full overflow-auto border border-gray-500 p-4">
<PrivacyPolicy {state} />
</div>
<a class="button flex" href={Utils.HomepageLink()}>
<Add class="h-6 w-6" />
<Tr t={Translations.t.general.backToIndex} />
</a>
</div>
<div class="h-full overflow-auto border border-gray-500 p-4">
<PrivacyPolicy {state} />
</div>
<a class="button flex" href={Utils.HomepageLink()}>
<Add class="h-6 w-6" />
<Tr t={Translations.t.general.backToIndex} />
</a>
</div>
</main>

View file

@ -1,7 +1,11 @@
import { Store, UIEventSource } from "../Logic/UIEventSource"
import BaseUIElement from "./BaseUIElement"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
import {
FeatureSource,
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { Changes } from "../Logic/Osm/Changes"
import { ExportableMap, MapProperties } from "../Models/MapProperties"
@ -64,7 +68,6 @@ export interface SpecialVisualizationState {
readonly currentView: FeatureSource<Feature<Polygon>>
readonly favourites: FavouritesFeatureSource
/**
* If data is currently being fetched from external sources
*/

File diff suppressed because it is too large Load diff

View file

@ -18,18 +18,26 @@
<div class="mt-12">
{#if deleteState === "init"}
<button on:click={() => {deleteState = "confirm"}} class="small">
<button
on:click={() => {
deleteState = "confirm"
}}
class="small"
>
<TrashIcon class="h-6 w-6" />
Delete this {objectType}
Delete this {objectType}
</button>
{:else if deleteState === "confirm"}
<div class="flex">
<BackButton on:click={() => {deleteState = "init"}}>
<BackButton
on:click={() => {
deleteState = "init"
}}
>
Don't delete
</BackButton>
<NextButton clss="primary" on:click={() => deleteLayer()}>
<div class="alert flex p-2">
<TrashIcon class="h-6 w-6" />
Do delete this {objectType}
</div>

View file

@ -81,8 +81,6 @@
})
let highlightedItem: UIEventSource<HighlightedTagRendering> = state.highlightedItem
</script>
<div class="flex h-screen flex-col">
@ -136,7 +134,7 @@
General properties
<ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
</div>
<div class="flex flex-col mb-8" slot="content0">
<div class="mb-8 flex flex-col" slot="content0">
<Region {state} configs={perRegion["Basic"]} />
<DeleteButton {state} {backToStudio} objectType="layer" />
</div>
@ -189,15 +187,15 @@
Below, you'll find the raw configuration file in `.json`-format. This is mostly for
debugging purposes, but you can also edit the file directly if you want.
</div>
<div class="literal-code overflow-y-auto h-full" style="min-height: 75%">
<div class="literal-code h-full overflow-y-auto" style="min-height: 75%">
<RawEditor {state} />
</div>
<ShowConversionMessages messages={$messages} />
<div class="flex w-full flex-col">
<div>
The testobject (which is used to render the questions in the 'information panel'
item has the following tags:
The testobject (which is used to render the questions in the 'information panel' item
has the following tags:
</div>
<AllTagsPanel tags={state.testTags} />

View file

@ -5,7 +5,7 @@ import {
Conversion,
ConversionMessage,
DesugaringContext,
Pipe
Pipe,
} from "../../Models/ThemeConfig/Conversion/Conversion"
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
import { ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation"
@ -69,7 +69,6 @@ export abstract class EditJsonState<T> {
this.category = category
this.expertMode = options?.expertMode ?? new UIEventSource<boolean>(false)
const layerId = this.getId()
this.configuration
.mapD((config) => {
@ -89,7 +88,6 @@ export abstract class EditJsonState<T> {
await this.server.update(id, config, this.category)
})
this.messages = this.createMessagesStore()
}
public startSavingUpdates(enabled = true) {
@ -158,10 +156,10 @@ export abstract class EditJsonState<T> {
path,
type: "translation",
hints: {
typehint: "translation"
typehint: "translation",
},
required: origConfig.required ?? false,
description: origConfig.description ?? "A translatable object"
description: origConfig.description ?? "A translatable object",
}
}
@ -233,19 +231,21 @@ export abstract class EditJsonState<T> {
protected abstract getId(): Store<string>
protected abstract validate(configuration: Partial<T>): Promise<ConversionMessage[]>;
protected abstract validate(configuration: Partial<T>): Promise<ConversionMessage[]>
/**
* Creates a store that validates the configuration and which contains all relevant (error)-messages
* @private
*/
private createMessagesStore(): Store<ConversionMessage[]> {
return this.configuration.mapAsyncD(async (config) => {
if(!this.validate){
return []
}
return await this.validate(config)
}).map(messages => messages ?? [])
return this.configuration
.mapAsyncD(async (config) => {
if (!this.validate) {
return []
}
return await this.validate(config)
})
.map((messages) => messages ?? [])
}
}
@ -311,7 +311,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
public readonly imageUploadManager = {
getCountsFor() {
return 0
}
},
}
public readonly layout: { getMatchingLayer: (key: any) => LayerConfig }
public readonly featureSwitches: {
@ -327,8 +327,8 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
properties: this.testTags.data,
geometry: {
type: "Point",
coordinates: [3.21, 51.2]
}
coordinates: [3.21, 51.2],
},
}
constructor(
@ -346,10 +346,10 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
} catch (e) {
return undefined
}
}
},
}
this.featureSwitches = {
featureSwitchIsDebugging: new UIEventSource<boolean>(true)
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
}
this.addMissingTagRenderingIds()
@ -426,8 +426,9 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
})
}
protected async validate(configuration: Partial<LayerConfigJson>): Promise<ConversionMessage[]> {
protected async validate(
configuration: Partial<LayerConfigJson>
): Promise<ConversionMessage[]> {
const layers = AllSharedLayers.getSharedLayersConfigs()
const questions = layers.get("questions")
@ -437,7 +438,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
}
const state: DesugaringContext = {
tagRenderings: sharedQuestions,
sharedLayers: layers
sharedLayers: layers,
}
const prepare = this.buildValidation(state)
const context = ConversionContext.construct([], ["prepare"])
@ -475,7 +476,7 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
/** Applies a few bandaids to get everything smoothed out in case of errors; a big bunch of hacks basically
*/
public setupFixers() {
this.configuration.addCallbackAndRunD(config => {
this.configuration.addCallbackAndRunD((config) => {
if (config.layers) {
// Remove 'null' and 'undefined' values from the layer array if any are found
for (let i = config.layers.length; i >= 0; i--) {
@ -488,17 +489,16 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
}
protected async validate(configuration: Partial<LayoutConfigJson>) {
const layers = AllSharedLayers.getSharedLayersConfigs()
for (const l of configuration.layers ?? []) {
if(typeof l !== "string"){
if (typeof l !== "string") {
continue
}
if (!l.startsWith("https://")) {
continue
}
const config = <LayerConfigJson> await Utils.downloadJsonCached(l, 1000*60*10)
const config = <LayerConfigJson>await Utils.downloadJsonCached(l, 1000 * 60 * 10)
layers.set(l, config)
}
@ -509,11 +509,11 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
}
const state: DesugaringContext = {
tagRenderings: sharedQuestions,
sharedLayers: layers
sharedLayers: layers,
}
const prepare = this.buildValidation(state)
const context = ConversionContext.construct([], ["prepare"])
if(configuration.layers){
if (configuration.layers) {
Utils.NoNullInplace(configuration.layers)
}
try {
@ -524,5 +524,4 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
}
return context.messages
}
}

View file

@ -22,40 +22,37 @@
export let selfLayers: { owner: number; id: string }[]
export let otherLayers: { owner: number; id: string }[]
{
/**
* We modify the schema and inject options for self-declared layers
*/
const layerSchema = schema.find(l => l.path.join(".") === "layers")
const suggestions: { if: string, then: string }[] = layerSchema.hints.suggestions
suggestions.unshift(...selfLayers.map(
l => ({
const layerSchema = schema.find((l) => l.path.join(".") === "layers")
const suggestions: { if: string; then: string }[] = layerSchema.hints.suggestions
suggestions.unshift(
...selfLayers.map((l) => ({
if: `value=https://studio.mapcomplete.org/${l.owner}/layers/${l.id}/${l.id}.json`,
then: `<b>${l.id}</b> (made by you)`
})
))
then: `<b>${l.id}</b> (made by you)`,
}))
)
for (let i = 0; i < otherLayers.length; i++) {
const l = otherLayers[i]
const mapping = {
if: `value=https://studio.mapcomplete.org/${l.owner}/layers/${l.id}/${l.id}.json`,
then: `<b>${l.id}</b> (made by ${l.owner})`
then: `<b>${l.id}</b> (made by ${l.owner})`,
}
/**
* This is a filthy hack which is time-sensitive and will break
* It downloads the username and patches the suggestion, assuming that the list with all layers will be shown a while _after_ loading the view.
* Caching in 'getInformationAboutUser' helps with this as well
*/
osmConnection.getInformationAboutUser(l.owner).then(userInfo => {
osmConnection.getInformationAboutUser(l.owner).then((userInfo) => {
mapping.then = `<b>${l.id}</b> (made by ${userInfo.display_name})`
})
suggestions.push(mapping)
}
}
let messages = state.messages
let hasErrors = messages.map(
(m: ConversionMessage[]) => m.filter((m) => m.level === "error").length
@ -102,8 +99,7 @@
<div slot="content0" class="mb-8">
<Region configs={perRegion["basic"]} path={[]} {state} title="Basic properties" />
<Region configs={perRegion["start_location"]} path={[]} {state} title="Start location" />
<DeleteButton {state} {backToStudio} objectType="theme"/>
<DeleteButton {state} {backToStudio} objectType="theme" />
</div>
<div slot="title1">Layers</div>
@ -126,10 +122,11 @@
Below, you'll find the raw configuration file in `.json`-format. This is mostly for
debugging purposes, but you can also edit the file directly if you want.
</div>
<div class="literal-code overflow-y-auto h-full" style="min-height: 75%">
<div class="literal-code h-full overflow-y-auto" style="min-height: 75%">
<RawEditor {state} />
</div>
<ShowConversionMessages messages={$messages} />
</div>
</TabbedGroup>
</div>
</div>

View file

@ -75,7 +75,7 @@
{/if}
</NextButton>
{#if description}
<Markdown src={description}/>
<Markdown src={description} />
{/if}
{#each $messages as message}
<ShowConversionMessage {message} />

View file

@ -97,7 +97,7 @@
<h3>{schema.path.at(-1)}</h3>
{#if subparts.length > 0}
<Markdown src={schema.description}/>
<Markdown src={schema.description} />
{/if}
{#if $currentValue === undefined}
No array defined

View file

@ -67,7 +67,7 @@
type = type.substring(0, type.length - 2)
}
const configJson: QuestionableTagRenderingConfigJson & {questionHintIsMd: boolean} = {
const configJson: QuestionableTagRenderingConfigJson & { questionHintIsMd: boolean } = {
id: path.join("_"),
render: rendervalue,
question: schema.hints.question,

View file

@ -40,9 +40,10 @@
if (lastIsString) {
types.splice(types.length - 1, 1)
}
const configJson: QuestionableTagRenderingConfigJson & {questionHintIsMd: boolean}= {
const configJson: QuestionableTagRenderingConfigJson & { questionHintIsMd: boolean } = {
id: "TYPE_OF:" + path.join("_"),
question: schema.hints.question ?? "Which subcategory is needed for " + schema.path.at(-1) + "?",
question:
schema.hints.question ?? "Which subcategory is needed for " + schema.path.at(-1) + "?",
questionHint: schema.description,
questionHintIsMd: true,
mappings: types

View file

@ -3,9 +3,11 @@ import Hash from "../../Logic/Web/Hash"
export default class StudioHashSetter {
constructor(mode: "layer" | "theme", tab: Store<number>, name: Store<string>) {
tab.mapD(tab => {
tab.mapD(
(tab) => {
Hash.hash.setData(mode + "/" + name.data + "/" + tab)
}
, [name])
},
[name]
)
}
}

View file

@ -11,9 +11,13 @@ import { LayoutConfigJson } from "../../Models/ThemeConfig/Json/LayoutConfigJson
export default class StudioServer {
private readonly url: string
private readonly _userId: Store<number>
private readonly overview: UIEventSource<{
success: { id: string; owner: number; category: "layers" | "themes" }[]
} | { error: any } | undefined>
private readonly overview: UIEventSource<
| {
success: { id: string; owner: number; category: "layers" | "themes" }[]
}
| { error: any }
| undefined
>
constructor(url: string, userId: Store<number>) {
this.url = url
@ -21,9 +25,13 @@ export default class StudioServer {
this.overview = UIEventSource.FromPromiseWithErr(this.fetchOverviewRaw())
}
public fetchOverview(): Store<{
success: { id: string; owner: number; category: "layers" | "themes" }[]
} | { error } | undefined> {
public fetchOverview(): Store<
| {
success: { id: string; owner: number; category: "layers" | "themes" }[]
}
| { error }
| undefined
> {
return this.overview
}
@ -80,11 +88,15 @@ export default class StudioServer {
return
}
await fetch(this.urlFor(id, category), {
method: "DELETE"
method: "DELETE",
})
const overview: { id: string; owner: number; category: "layers" | "themes" }[] = this.overview.data?.["success"]
const overview: { id: string; owner: number; category: "layers" | "themes" }[] =
this.overview.data?.["success"]
if (overview) {
const index = overview.findIndex(obj => obj.id === id && obj.category === category && obj.owner === this._userId.data)
const index = overview.findIndex(
(obj) =>
obj.id === id && obj.category === category && obj.owner === this._userId.data
)
if (index >= 0) {
overview.splice(index, 1)
this.overview.ping()
@ -99,9 +111,9 @@ export default class StudioServer {
await fetch(this.urlFor(id, category), {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8"
"Content-Type": "application/json;charset=utf-8",
},
body: config
body: config,
})
}

View file

@ -7,7 +7,7 @@
import type { ConfigMeta } from "./configMeta"
import type {
MappingConfigJson,
QuestionableTagRenderingConfigJson
QuestionableTagRenderingConfigJson,
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
@ -59,8 +59,8 @@
labelMapping = {
if: "value=" + label,
then: {
en: "Builtin collection <b>" + label + "</b>:"
}
en: "Builtin collection <b>" + label + "</b>:",
},
}
perLabel[label] = labelMapping
mappingsBuiltin.push(labelMapping)
@ -72,14 +72,14 @@
mappingsBuiltin.push({
if: "value=" + tr["id"],
then: {
en: "Builtin <b>" + tr["id"] + "</b> <div class='subtle'>" + description + "</div>"
}
en: "Builtin <b>" + tr["id"] + "</b> <div class='subtle'>" + description + "</div>",
},
})
}
const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson>{
question: "Which builtin element should be shown?",
mappings: mappingsBuiltin
mappings: mappingsBuiltin,
})
const tags = new UIEventSource({ value })
@ -112,7 +112,7 @@
"condition",
"metacondition",
"mappings",
"icon"
"icon",
])
const ignored = new Set(["labels", "description", "classes"])

View file

@ -44,11 +44,11 @@
)
let osmConnection = new OsmConnection({
oauth_token,
checkOnlineRegularly: true
checkOnlineRegularly: true,
})
const expertMode = UIEventSource.asBoolean(
osmConnection.GetPreference("studio-expert-mode", "false", {
documentation: "Indicates if more options are shown in mapcomplete studio"
documentation: "Indicates if more options are shown in mapcomplete studio",
})
)
expertMode.addCallbackAndRunD((expert) => console.log("Expert mode is", expert))
@ -165,18 +165,18 @@
marker: [
{
icon: "circle",
color: "white"
}
]
}
color: "white",
},
],
},
],
tagRenderings: ["images"],
lineRendering: [
{
width: 1,
color: "blue"
}
]
color: "blue",
},
],
}
editLayerState.configuration.setData(initialLayerConfig)
editLayerState.startSavingUpdates()
@ -194,10 +194,11 @@
const event = {
detail: {
id,
owner: uid.data
}
owner: uid.data,
},
}
const statePromise: Promise<EditJsonState<any>> = mode === "layer" ? editLayer(event) : editTheme(event)
const statePromise: Promise<EditJsonState<any>> =
mode === "layer" ? editLayer(event) : editTheme(event)
const state = await statePromise
state.selectedTab.setData(Number(tab))
}
@ -221,8 +222,8 @@
<li>Try again in a few minutes</li>
<li>
Contact <a href="https://app.element.io/#/room/#MapComplete:matrix.org">
the MapComplete community via the chat.
</a>
the MapComplete community via the chat.
</a>
Someone might be able to help you
</li>
<li>
@ -284,11 +285,7 @@
</div>
{:else if state === "edit_layer"}
<div class="m-4 flex flex-col">
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => backToStudio()}
>
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => backToStudio()}>
MapComplete Studio
</BackButton>
<h2>Choose a layer to edit</h2>
@ -331,11 +328,7 @@
</div>
{:else if state === "edit_theme"}
<div class="m-4 flex flex-col">
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => backToStudio()}
>
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => backToStudio()}>
MapComplete Studio
</BackButton>
<h2>Choose a theme to edit</h2>
@ -372,26 +365,20 @@
<Loading />
</div>
{:else if state === "editing_layer"}
<EditLayer
state={editLayerState}
{backToStudio}
>
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => backToStudio()}
>
<EditLayer state={editLayerState} {backToStudio}>
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => backToStudio()}>
MapComplete Studio
</BackButton>
</EditLayer>
{:else if state === "editing_theme"}
<EditTheme state={editThemeState} selfLayers={$selfLayers} otherLayers={$otherLayers} {osmConnection}
{backToStudio}>
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => backToStudio()}
>
<EditTheme
state={editThemeState}
selfLayers={$selfLayers}
otherLayers={$otherLayers}
{osmConnection}
{backToStudio}
>
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => backToStudio()}>
MapComplete Studio
</BackButton>
</EditTheme>

View file

@ -7,165 +7,165 @@
</script>
<main>
<div>
<h1>Stylesheet testing grounds</h1>
<div>
<h1>Stylesheet testing grounds</h1>
This document exists to explore the style hierarchy.
This document exists to explore the style hierarchy.
<div class="normal-background">
<h2>Normal background</h2>
There are a few styles, such as the
<span class="literal-code">normal-background</span>
-style which is used if there is nothing special going on. Some general information, with at most
<a href="https://example.com" target="_blank">a link to someplace</a>
<div class="subtle">Subtle</div>
<div class="normal-background">
<h2>Normal background</h2>
There are a few styles, such as the
<span class="literal-code">normal-background</span>
-style which is used if there is nothing special going on. Some general information, with at most
<a href="https://example.com" target="_blank">a link to someplace</a>
<div class="subtle">Subtle</div>
<div class="alert">Alert: something went wrong</div>
<div class="warning">Warning</div>
<div class="information">Some important information</div>
<div class="thanks">Thank you! Operation successful</div>
<div class="alert">Alert: something went wrong</div>
<div class="warning">Warning</div>
<div class="information">Some important information</div>
<div class="thanks">Thank you! Operation successful</div>
<Login class="h-12 w-12" />
<Loading>Loading...</Loading>
<Login class="h-12 w-12" />
<Loading>Loading...</Loading>
</div>
<div class="low-interaction flex flex-col">
<h2>Low interaction</h2>
<p>
There are <span class="literal-code">low-interaction</span>
areas, where some buttons might appear.
</p>
<div class="border-interactive interactive">
Highly interactive area (mostly: active question)
</div>
<div class="subtle">Subtle</div>
<div class="flex">
<button class="primary">
<Community class="h-6 w-6" />
Main action
</button>
<button class="primary disabled">
<Community class="h-6 w-6" />
Main action (disabled)
</button>
<button class="small">
<Community class="h-6 w-6" />
Small button
</button>
<button class="small primary">Small button</button>
<button class="small primary disabled">Small, disabled button</button>
</div>
<div class="flex">
<button>
<Community class="h-6 w-6" />
Secondary action
</button>
<button class="disabled">
<Community class="h-6 w-6" />
Secondary action (disabled)
</button>
</div>
<input type="text" />
<div class="flex flex-col">
<label class="checked" for="html">
<input id="html" name="fav_language" type="radio" value="HTML" />
HTML (mimicks a
<span class="literal-code">checked</span>
-element)
<Dropdown value={new UIEventSource("abc")}>
<option>abc</option>
<option>def</option>
</Dropdown>
</label>
<label for="css">
<input id="css" name="fav_language" type="radio" value="CSS" />
CSS
</label>
<label for="javascript">
<input id="javascript" name="fav_language" type="radio" value="JavaScript" />
<Community class="h-8 w-8" />
JavaScript
</label>
</div>
<div class="alert">Alert: something went wrong</div>
<div class="warning">Warning</div>
<div class="information">Some important information</div>
<div class="thanks">Thank you! Operation successful</div>
<Login class="h-12 w-12" />
<Loading>Loading...</Loading>
</div>
<div class="interactive flex flex-col">
<h2>Interactive area</h2>
<p>
There are <span class="literal-code">interactive</span>
areas, where many buttons and input elements will appear.
</p>
<div class="subtle">Subtle</div>
<div class="flex">
<button class="primary">
<Community class="h-6 w-6" />
Main action
</button>
<button class="primary disabled">
<Community class="h-6 w-6" />
Main action (disabled)
</button>
<button class="small">
<Community class="h-6 w-6" />
Small button
</button>
</div>
<div class="flex">
<button>
<Community class="h-6 w-6" />
Secondary action
</button>
<button class="disabled">
<Community class="h-6 w-6" />
Secondary action (disabled)
</button>
</div>
<div class="alert">Alert: something went wrong</div>
<div class="warning">Warning</div>
<div class="information">Some important information</div>
<div class="thanks">Thank you! Operation successful</div>
<Login class="h-12 w-12" />
<Loading>Loading...</Loading>
<div>
<label for="html0">
<input id="html0" name="fav_language" type="radio" value="HTML" />
HTML
</label>
<label for="css0">
<input id="css0" name="fav_language" type="radio" value="CSS" />
CSS
</label>
<label for="javascript0">
<input id="javascript0" name="fav_language" type="radio" value="JavaScript" />
JavaScript
</label>
</div>
<div class="border-interactive">
Area with extreme high interactivity due to `border-interactive`
</div>
<select>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
</div>
</div>
<div class="low-interaction flex flex-col">
<h2>Low interaction</h2>
<p>
There are <span class="literal-code">low-interaction</span>
areas, where some buttons might appear.
</p>
<div class="border-interactive interactive">
Highly interactive area (mostly: active question)
</div>
<div class="subtle">Subtle</div>
<div class="flex">
<button class="primary">
<Community class="h-6 w-6" />
Main action
</button>
<button class="primary disabled">
<Community class="h-6 w-6" />
Main action (disabled)
</button>
<button class="small">
<Community class="h-6 w-6" />
Small button
</button>
<button class="small primary">Small button</button>
<button class="small primary disabled">Small, disabled button</button>
</div>
<div class="flex">
<button>
<Community class="h-6 w-6" />
Secondary action
</button>
<button class="disabled">
<Community class="h-6 w-6" />
Secondary action (disabled)
</button>
</div>
<input type="text" />
<div class="flex flex-col">
<label class="checked" for="html">
<input id="html" name="fav_language" type="radio" value="HTML" />
HTML (mimicks a
<span class="literal-code">checked</span>
-element)
<Dropdown value={new UIEventSource("abc")}>
<option>abc</option>
<option>def</option>
</Dropdown>
</label>
<label for="css">
<input id="css" name="fav_language" type="radio" value="CSS" />
CSS
</label>
<label for="javascript">
<input id="javascript" name="fav_language" type="radio" value="JavaScript" />
<Community class="h-8 w-8" />
JavaScript
</label>
</div>
<div class="alert">Alert: something went wrong</div>
<div class="warning">Warning</div>
<div class="information">Some important information</div>
<div class="thanks">Thank you! Operation successful</div>
<Login class="h-12 w-12" />
<Loading>Loading...</Loading>
</div>
<div class="interactive flex flex-col">
<h2>Interactive area</h2>
<p>
There are <span class="literal-code">interactive</span>
areas, where many buttons and input elements will appear.
</p>
<div class="subtle">Subtle</div>
<div class="flex">
<button class="primary">
<Community class="h-6 w-6" />
Main action
</button>
<button class="primary disabled">
<Community class="h-6 w-6" />
Main action (disabled)
</button>
<button class="small">
<Community class="h-6 w-6" />
Small button
</button>
</div>
<div class="flex">
<button>
<Community class="h-6 w-6" />
Secondary action
</button>
<button class="disabled">
<Community class="h-6 w-6" />
Secondary action (disabled)
</button>
</div>
<div class="alert">Alert: something went wrong</div>
<div class="warning">Warning</div>
<div class="information">Some important information</div>
<div class="thanks">Thank you! Operation successful</div>
<Login class="h-12 w-12" />
<Loading>Loading...</Loading>
<div>
<label for="html0">
<input id="html0" name="fav_language" type="radio" value="HTML" />
HTML
</label>
<label for="css0">
<input id="css0" name="fav_language" type="radio" value="CSS" />
CSS
</label>
<label for="javascript0">
<input id="javascript0" name="fav_language" type="radio" value="JavaScript" />
JavaScript
</label>
</div>
<div class="border-interactive">
Area with extreme high interactivity due to `border-interactive`
</div>
<select>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
</div>
</div>
</main>

View file

@ -1,5 +1,4 @@
<script lang="ts">
</script>
<main>
</main>
<main />

View file

@ -11,7 +11,7 @@
</script>
<div class="link-underline flex h-full w-full flex-col justify-between">
<div class="overflow-y-auto h-full">
<div class="h-full overflow-y-auto">
<slot />
</div>

View file

@ -12,7 +12,7 @@ import Link from "../Base/Link"
import BaseUIElement from "../BaseUIElement"
import { Utils } from "../../Utils"
import SvelteUIElement from "../Base/SvelteUIElement"
import {default as Wikidata_icon} from "../../assets/svg/Wikidata.svelte"
import { default as Wikidata_icon } from "../../assets/svg/Wikidata.svelte"
import Gender_male from "../../assets/svg/Gender_male.svelte"
import Gender_female from "../../assets/svg/Gender_female.svelte"
import Gender_inter from "../../assets/svg/Gender_inter.svelte"

View file

@ -15,7 +15,9 @@
* Shows a wikipedia-article + wikidata preview for the given item
*/
export let wikipediaDetails: Store<FullWikipediaDetails>
let titleOnly = wikipediaDetails.mapD(details => Object.keys(details).length === 1 && details.title !== undefined)
let titleOnly = wikipediaDetails.mapD(
(details) => Object.keys(details).length === 1 && details.title !== undefined
)
</script>
{#if $titleOnly}

View file

@ -19,10 +19,14 @@ export class Translation extends BaseUIElement {
*/
private _strictLanguages: boolean
constructor(translations: string | Record<string, string>, context?: string, strictLanguages?: boolean) {
constructor(
translations: string | Record<string, string>,
context?: string,
strictLanguages?: boolean
) {
super()
this._strictLanguages = strictLanguages
if(strictLanguages){
if (strictLanguages) {
console.log(">>> strict:", translations)
}
if (translations === undefined) {
@ -85,7 +89,7 @@ export class Translation extends BaseUIElement {
* Indicates what language is effectively returned by `current`.
* In most cases, this will be the language of choice, but if no translation is available, this will probably be `en`
*/
get currentLang(): Store<string>{
get currentLang(): Store<string> {
if (!this._currentLanguage) {
this._currentLanguage = Locale.language.map(
(l) => this.actualLanguage(l),
@ -100,7 +104,7 @@ export class Translation extends BaseUIElement {
get current(): Store<string> {
if (!this._current) {
this._current = this.currentLang.map(l => this.translations[l])
this._current = this.currentLang.map((l) => this.translations[l])
}
return this._current
}
@ -164,10 +168,10 @@ export class Translation extends BaseUIElement {
return "*"
}
const txt = this.translations[language]
if(txt === undefined && this._strictLanguages){
if (txt === undefined && this._strictLanguages) {
return undefined
}
if (txt !== undefined ) {
if (txt !== undefined) {
return language
}
if (this.translations["en"] !== undefined) {
@ -186,7 +190,7 @@ export class Translation extends BaseUIElement {
InnerConstructElement(): HTMLElement {
const el = document.createElement("span")
const self = this
if(self.txt){
if (self.txt) {
el.innerHTML = self.txt
}
if (self.translations["*"] !== undefined) {
@ -197,9 +201,9 @@ export class Translation extends BaseUIElement {
if (self.isDestroyed) {
return true
}
if(self.txt === undefined){
if (self.txt === undefined) {
el.innerHTML = ""
}else{
} else {
el.innerHTML = self.txt
}
})