forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
ee77dd0fc9
288 changed files with 7485 additions and 28619 deletions
|
|
@ -21,7 +21,7 @@
|
|||
class={$classnames}
|
||||
>
|
||||
{#if $icon}
|
||||
<Icon clss="w-4 h-4" icon={$icon}/>
|
||||
{/if}
|
||||
<Icon clss="w-4 h-4" icon={$icon} />
|
||||
{/if}
|
||||
{@html $text}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Utils } from "../../Utils"
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
*/
|
||||
export default class Img extends BaseUIElement {
|
||||
private readonly _src: string
|
||||
private readonly _rawSvg: boolean
|
||||
|
|
|
|||
|
|
@ -12,14 +12,13 @@
|
|||
export let cls = "m-0.5 p-0.5 sm:p-1 md:m-1"
|
||||
export let enabled: Store<boolean> = new ImmutableStore(true)
|
||||
export let arialabel: Translation = undefined
|
||||
export let arialabelDynamic : Store<Translation> = new ImmutableStore(arialabel)
|
||||
let arialabelString = arialabelDynamic.bind(tr => tr?.current)
|
||||
export let arialabelDynamic: Store<Translation> = new ImmutableStore(arialabel)
|
||||
let arialabelString = arialabelDynamic.bind((tr) => tr?.current)
|
||||
export let htmlElem: UIEventSource<HTMLElement> = undefined
|
||||
let _htmlElem: HTMLElement
|
||||
$: {
|
||||
htmlElem?.setData(_htmlElem)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -40,10 +40,7 @@
|
|||
style={svelteElem.getStyle()}
|
||||
/>
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={svelteElem?._svelteComponent}
|
||||
{...svelteElem._props}
|
||||
/>
|
||||
<svelte:component this={svelteElem?._svelteComponent} {...svelteElem._props} />
|
||||
{/if}
|
||||
{:else}
|
||||
<span bind:this={elem} />
|
||||
|
|
|
|||
|
|
@ -21,14 +21,14 @@
|
|||
|
||||
{#if $txt}
|
||||
{#if cls}
|
||||
<span class={cls}>
|
||||
<span lang={$lang}>
|
||||
{@html Utils.purify($txt)}
|
||||
<span class={cls}>
|
||||
<span lang={$lang}>
|
||||
{@html Utils.purify($txt)}
|
||||
</span>
|
||||
<WeblateLink context={t?.context} />
|
||||
</span>
|
||||
<WeblateLink context={t?.context} />
|
||||
</span>
|
||||
{:else}
|
||||
<span lang={$lang}>
|
||||
<span lang={$lang}>
|
||||
{@html Utils.purify($txt)}
|
||||
</span>
|
||||
<WeblateLink context={t?.context} />
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
target="_blank"
|
||||
tabindex="-1"
|
||||
>
|
||||
<LanguageIcon class="font-gray"/>
|
||||
<LanguageIcon class="font-gray" />
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
import Bug from "../../assets/svg/Bug.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import DocumentChartBar from "@babeard/svelte-heroicons/outline/DocumentChartBar"
|
||||
import DocumentMagnifyingGlass from "@babeard/svelte-heroicons/outline/DocumentMagnifyingGlass"
|
||||
|
||||
export let state: ThemeViewState
|
||||
|
||||
|
|
@ -48,18 +49,27 @@
|
|||
<Tr t={Translations.t.general.attribution.openIssueTracker} />
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="flex"
|
||||
href={"https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Themes/" + layout.id + ".md"}
|
||||
target="_blank"
|
||||
>
|
||||
<DocumentChartBar class="h-6 w-6" />
|
||||
<Tr
|
||||
t={Translations.t.general.attribution.openThemeDocumentation.Subs({
|
||||
name: layout.title,
|
||||
})}
|
||||
/>
|
||||
</a>
|
||||
{#if layout.official}
|
||||
<a
|
||||
class="flex"
|
||||
href={"https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Themes/" +
|
||||
layout.id +
|
||||
".md"}
|
||||
target="_blank"
|
||||
>
|
||||
<DocumentMagnifyingGlass class="h-6 w-6" />
|
||||
<Tr
|
||||
t={Translations.t.general.attribution.openThemeDocumentation.Subs({
|
||||
name: layout.title,
|
||||
})}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a class="flex" href={Utils.OsmChaLinkFor(31, layout.id)}>
|
||||
<DocumentChartBar class="h-6 w-6" />
|
||||
<Tr t={Translations.t.general.attribution.openOsmcha.Subs({ theme: layout.title })} />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<a class="flex" href="https://en.osm.town/@MapComplete" target="_blank">
|
||||
<Mastodon class="h-6 w-6" />
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
import { TranslateIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Osm_logo from "../../assets/svg/Osm_logo.svelte"
|
||||
import Generic_map from "../../assets/svg/Generic_map.svelte"
|
||||
import { UserGroupIcon} from "@babeard/svelte-heroicons/solid"
|
||||
import { UserGroupIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import Marker from "../Map/Marker.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
|
|
|||
|
|
@ -81,13 +81,13 @@ export class GeolocationControlState {
|
|||
return
|
||||
}
|
||||
|
||||
// A location _is_ known! Let's move to this location
|
||||
const currentLocation = geolocationState.currentGPSLocation.data
|
||||
if (currentLocation === undefined) {
|
||||
// No location is known yet, not much we can do
|
||||
lastClick.setData(new Date())
|
||||
return
|
||||
}
|
||||
// A location _is_ known! Let's move to this location
|
||||
const inBounds = state.bounds.data.contains([
|
||||
currentLocation.longitude,
|
||||
currentLocation.latitude,
|
||||
|
|
|
|||
|
|
@ -27,10 +27,7 @@
|
|||
{:else if $pending.length === 1}
|
||||
<Tr cls="alert" t={Translations.t.general.uploadPendingSingle} />
|
||||
{:else if $pending.length > 1}
|
||||
<Tr
|
||||
cls="alert"
|
||||
t={Translations.t.general.uploadPending.Subs({ count: $pending.length })}
|
||||
/>
|
||||
<Tr cls="alert" t={Translations.t.general.uploadPending.Subs({ count: $pending.length })} />
|
||||
{/if}
|
||||
|
||||
{#each $errors as error}
|
||||
|
|
@ -43,24 +40,19 @@
|
|||
</button>
|
||||
|
||||
<ul>
|
||||
|
||||
{#each $pending as pending}
|
||||
<li>
|
||||
|
||||
{#if pending.changes !== undefined}
|
||||
Create {pending.type}/{pending.id} {JSON.stringify(TagUtils.KVObjtoProperties(pending.tags))}
|
||||
Create {pending.type}/{pending.id}
|
||||
{JSON.stringify(TagUtils.KVObjtoProperties(pending.tags))}
|
||||
{:else}
|
||||
Modify {pending.type}/{pending.id} {JSON.stringify(pending.tags)}
|
||||
{/if}
|
||||
{#if pending.type === "way" && pending.changes?.nodes}
|
||||
{pending.changes.nodes.join(" ")}
|
||||
{/if}
|
||||
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
export let highlightedRendering: UIEventSource<string> = undefined
|
||||
|
||||
export let tags: UIEventSource<Record<string, string>> = state?.featureProperties?.getStore(
|
||||
selectedElement.properties.id,
|
||||
selectedElement.properties.id
|
||||
)
|
||||
|
||||
let isAddNew = tags.mapD(
|
||||
(t) => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false,
|
||||
(t) => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false
|
||||
)
|
||||
|
||||
export let layer: LayerConfig
|
||||
|
|
@ -32,29 +32,32 @@
|
|||
onDestroy(
|
||||
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
|
||||
_metatags = tags
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
let knownTagRenderings: Store<TagRenderingConfig[]> = tags.mapD((tgs) =>
|
||||
layer?.tagRenderings?.filter(
|
||||
(config) => {
|
||||
if (mustMatchLabels !== undefined) {
|
||||
if (!mustMatchLabels.has(config.id) && !config?.labels?.some(l => mustMatchLabels.has(l))) {
|
||||
return false
|
||||
}
|
||||
} else if (dontMatchLabels) {
|
||||
if (dontMatchLabels.has(config.id) || config?.labels?.some(l => dontMatchLabels.has(l))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (!config.IsKnown(tgs)) {
|
||||
layer?.tagRenderings?.filter((config) => {
|
||||
if (mustMatchLabels !== undefined) {
|
||||
if (
|
||||
!mustMatchLabels.has(config.id) &&
|
||||
!config?.labels?.some((l) => mustMatchLabels.has(l))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return (config.condition?.matchesProperties(tgs) ?? true) &&
|
||||
(config.metacondition?.matchesProperties({ ...tgs, ..._metatags }) ?? true)
|
||||
},
|
||||
),
|
||||
} else if (dontMatchLabels) {
|
||||
if (dontMatchLabels.has(config.id) || config?.labels?.some((l) => dontMatchLabels.has(l))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (!config.IsKnown(tgs)) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
(config.condition?.matchesProperties(tgs) ?? true) &&
|
||||
(config.metacondition?.matchesProperties({ ...tgs, ..._metatags }) ?? true)
|
||||
)
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
<Copyable {state} text={linkToShare} />
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<img src={new Qr(linkToShare).toImageElement(125)} style="width: 125px"/>
|
||||
<img src={new Qr(linkToShare).toImageElement(125)} style="width: 125px" />
|
||||
</div>
|
||||
|
||||
<Tr t={tr.embedIntro} />
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
let gpsAvailable = geolocation.gpsAvailable
|
||||
|
||||
function jumpToCurrentLocation() {
|
||||
state.geolocationControl.handleClick()
|
||||
const glstate = state.geolocation.geolocationState
|
||||
if (glstate.currentGPSLocation.data !== undefined) {
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
|
||||
|
|
@ -77,9 +78,12 @@
|
|||
|
||||
<div class="flex w-full flex-wrap sm:flex-nowrap">
|
||||
<If condition={state.featureSwitches.featureSwitchGeolocation}>
|
||||
|
||||
<button disabled={!$gpsAvailable} class:disabled={!$gpsAvailable} class="flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
|
||||
|
||||
<button
|
||||
disabled={!$gpsAvailable}
|
||||
class:disabled={!$gpsAvailable}
|
||||
class="flex w-full items-center gap-x-2"
|
||||
on:click={jumpToCurrentLocation}
|
||||
>
|
||||
<GeolocationIndicator {state} />
|
||||
<Tr t={$gpsExplanation} />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<Accordion>
|
||||
<AccordionItem open={expanded} paddingDefault="p-0" inactiveClass="text-black">
|
||||
<span slot="header" class="p-2 text-base w-full">
|
||||
<span slot="header" class="w-full p-2 text-base">
|
||||
<slot name="header" />
|
||||
</span>
|
||||
<div class="low-interaction rounded-b p-2">
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export default class Toggle extends VariableUiElement {
|
|||
super(isEnabled?.map((isEnabled) => (isEnabled ? showEnabled : showDisabled)))
|
||||
this.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -34,5 +33,4 @@ export class ClickableToggle extends Toggle {
|
|||
super(showEnabled, showDisabled, isEnabled)
|
||||
this.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@
|
|||
<Airport {color} class={clss}/>
|
||||
{:else if Utils.isEmoji(icon)}
|
||||
<span style={`font-size: ${emojiHeight}px; line-height: ${emojiHeight}px`}>
|
||||
{icon}
|
||||
{icon}
|
||||
</span>
|
||||
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,12 @@ class SingleBackgroundHandler {
|
|||
"map moved and not been used for",
|
||||
SingleBackgroundHandler.DEACTIVATE_AFTER
|
||||
)
|
||||
if (map.getLayer(<string>this._targetLayer.properties.id)) {
|
||||
map.removeLayer(<string>this._targetLayer.properties.id)
|
||||
try {
|
||||
if (map.getLayer(<string>this._targetLayer.properties.id)) {
|
||||
map.removeLayer(<string>this._targetLayer.properties.id)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Could not (try to) remove the raster layer", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import TitledPanel from "../Base/TitledPanel.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
export let availableLayers: Store<RasterLayerPolygon[]>
|
||||
export let availableLayers: { store: Store<RasterLayerPolygon[]> }
|
||||
export let mapproperties: MapProperties
|
||||
export let userstate: UserRelatedState
|
||||
export let map: Store<MlMap>
|
||||
let _availableLayers = availableLayers.store
|
||||
/**
|
||||
* Used to toggle the background layers on/off
|
||||
*/
|
||||
|
|
@ -32,7 +34,7 @@
|
|||
|
||||
function availableForCategory(type: CategoryType): Store<RasterLayerPolygon[]> {
|
||||
const keywords = categories[type]
|
||||
return availableLayers.mapD((available) =>
|
||||
return _availableLayers.mapD((available) =>
|
||||
available.filter((layer) => keywords.indexOf(<EliCategory>layer.properties.category) >= 0)
|
||||
)
|
||||
}
|
||||
|
|
@ -53,39 +55,42 @@
|
|||
|
||||
<TitledPanel>
|
||||
<Tr slot="title" t={Translations.t.general.backgroundMap} />
|
||||
|
||||
<div class="grid h-full w-full grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<RasterLayerPicker
|
||||
availableLayers={photoLayers}
|
||||
favourite={getPref("photo")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
<RasterLayerPicker
|
||||
availableLayers={mapLayers}
|
||||
favourite={getPref("map")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
<RasterLayerPicker
|
||||
availableLayers={osmbasedmapLayers}
|
||||
favourite={getPref("osmbasedmap")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
<RasterLayerPicker
|
||||
availableLayers={otherLayers}
|
||||
favourite={getPref("other")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
</div>
|
||||
{#if $_availableLayers?.length < 1}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="grid h-full w-full grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<RasterLayerPicker
|
||||
availableLayers={$photoLayers}
|
||||
favourite={getPref("photo")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
<RasterLayerPicker
|
||||
availableLayers={$mapLayers}
|
||||
favourite={getPref("map")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
<RasterLayerPicker
|
||||
availableLayers={$osmbasedmapLayers}
|
||||
favourite={getPref("osmbasedmap")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
<RasterLayerPicker
|
||||
availableLayers={$otherLayers}
|
||||
favourite={getPref("other")}
|
||||
{map}
|
||||
{mapproperties}
|
||||
on:appliedLayer={onApply}
|
||||
{visible}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</TitledPanel>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
/***
|
||||
* Chooses a background-layer out of available options
|
||||
*/
|
||||
export let availableLayers: Store<RasterLayerPolygon[]>
|
||||
export let availableLayers: RasterLayerPolygon[]
|
||||
export let mapproperties: MapProperties
|
||||
export let map: Store<MlMap>
|
||||
|
||||
|
|
@ -19,23 +19,19 @@
|
|||
|
||||
export let favourite: UIEventSource<string> | undefined = undefined
|
||||
|
||||
let rasterLayer = new UIEventSource<RasterLayerPolygon>(availableLayers.data?.[0])
|
||||
let hasLayers = true
|
||||
onDestroy(
|
||||
availableLayers.addCallbackAndRun((layers) => {
|
||||
if (layers === undefined || layers.length === 0) {
|
||||
hasLayers = false
|
||||
return
|
||||
}
|
||||
hasLayers = true
|
||||
rasterLayer.setData(layers[0])
|
||||
})
|
||||
let rasterLayer = new UIEventSource<RasterLayerPolygon>(availableLayers[0])
|
||||
let rasterLayerId = rasterLayer.sync(
|
||||
(l) => l?.properties?.id,
|
||||
[],
|
||||
(id) => availableLayers.find((l) => l.properties.id === id)
|
||||
)
|
||||
rasterLayer.setData(availableLayers[0])
|
||||
$: rasterLayer.setData(availableLayers[0])
|
||||
|
||||
if (favourite) {
|
||||
onDestroy(
|
||||
favourite.addCallbackAndRunD((favourite) => {
|
||||
const fav = availableLayers.data?.find((l) => l.properties.id === favourite)
|
||||
const fav = availableLayers?.find((l) => l.properties.id === favourite)
|
||||
if (!fav) {
|
||||
return
|
||||
}
|
||||
|
|
@ -56,13 +52,14 @@
|
|||
onDestroy(
|
||||
visible?.addCallbackAndRunD((visible) => {
|
||||
if (visible) {
|
||||
rasterLayerOnMap.setData(rasterLayer.data ?? availableLayers.data[0])
|
||||
rasterLayerOnMap.setData(rasterLayer.data ?? availableLayers[0])
|
||||
} else {
|
||||
rasterLayerOnMap.setData(undefined)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function apply() {
|
||||
mapproperties.rasterLayer.setData(rasterLayer.data)
|
||||
dispatch("appliedLayer")
|
||||
|
|
@ -75,7 +72,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#if hasLayers}
|
||||
{#if availableLayers?.length > 0}
|
||||
<form class="flex h-full w-full flex-col" on:submit|preventDefault={() => {}}>
|
||||
<button
|
||||
tabindex="-1"
|
||||
|
|
@ -92,9 +89,9 @@
|
|||
/>
|
||||
</span>
|
||||
</button>
|
||||
<select bind:value={$rasterLayer} class="w-full" on:keydown={handleKeyPress}>
|
||||
{#each $availableLayers as availableLayer}
|
||||
<option value={availableLayer}>
|
||||
<select bind:value={$rasterLayerId} class="w-full" on:keydown={handleKeyPress}>
|
||||
{#each availableLayers as availableLayer}
|
||||
<option value={availableLayer.properties.id}>
|
||||
{availableLayer.properties.name}
|
||||
</option>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
<script lang="ts">
|
||||
|
||||
|
||||
import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
function clearCaches(){
|
||||
function clearCaches() {
|
||||
IdbLocalStorage.clearAll()
|
||||
Utils.download("./service-worker-clear")
|
||||
}
|
||||
export let msg : string
|
||||
export let msg: string
|
||||
</script>
|
||||
|
||||
<button on:click={() => clearCaches()} class="flex gap-x-2">
|
||||
|
|
|
|||
|
|
@ -15,13 +15,12 @@
|
|||
export let header: string
|
||||
export let layer: LayerConfig
|
||||
|
||||
let headerTr = layer.tagRenderings.find(tr => tr.id === header)
|
||||
|
||||
let headerTr = layer.tagRenderings.find((tr) => tr.id === header)
|
||||
</script>
|
||||
|
||||
<AccordionSingle>
|
||||
<div slot="header">
|
||||
<TagRenderingAnswer {tags} {layer} config={headerTr} {state} {selectedElement} />
|
||||
</div>
|
||||
<SelectedElementView mustMatchLabels={new Set(labels)} {state} {layer} {tags} {selectedElement}/>
|
||||
<SelectedElementView mustMatchLabels={new Set(labels)} {state} {layer} {tags} {selectedElement} />
|
||||
</AccordionSingle>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
location: new UIEventSource({ lon, lat }),
|
||||
minzoom: new UIEventSource($reason.minZoom),
|
||||
rasterLayer: state.mapProperties.rasterLayer,
|
||||
zoom: new UIEventSource($reason?.startZoom ?? 16)
|
||||
zoom: new UIEventSource($reason?.startZoom ?? 16),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<AccordionSingle>
|
||||
<span slot="header" class="flex">
|
||||
{#if moveWizardState.reasons.length === 1}
|
||||
<Icon icon={moveWizardState.reasons[0].icon} clss="w-6 h-6"/>
|
||||
<Icon icon={moveWizardState.reasons[0].icon} clss="w-6 h-6" />
|
||||
<Tr t={Translations.T(moveWizardState.reasons[0].invitingText)} />
|
||||
{:else}
|
||||
<Move class="h-6 w-6" />
|
||||
|
|
@ -87,7 +87,7 @@
|
|||
currentStep = "pick_location"
|
||||
}}
|
||||
>
|
||||
<Icon icon={reasonSpec.icon} clss="w-12 h-12"/>
|
||||
<Icon icon={reasonSpec.icon} clss="w-12 h-12" />
|
||||
<Tr t={Translations.T(reasonSpec.text)} />
|
||||
</button>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
export let minzoom: number
|
||||
export let zoomMoreMessage: string
|
||||
|
||||
|
||||
let curZoom = state.mapProperties.zoom
|
||||
const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "")
|
||||
|
||||
|
|
@ -29,7 +28,6 @@
|
|||
tags.data["closed_at"] = new Date().toISOString()
|
||||
tags.ping()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
|
@ -37,7 +35,6 @@
|
|||
<Tr t={t.loginToClose} />
|
||||
</div>
|
||||
|
||||
|
||||
{#if $isClosed}
|
||||
<Tr cls="thanks" t={t.isClosed} />
|
||||
{:else if minzoom <= $curZoom}
|
||||
|
|
@ -50,5 +47,4 @@
|
|||
{:else if zoomMoreMessage}
|
||||
{zoomMoreMessage}
|
||||
{/if}
|
||||
|
||||
</LoginToggle>
|
||||
|
|
|
|||
|
|
@ -16,18 +16,14 @@
|
|||
|
||||
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
|
||||
const includeLayout = window.location.pathname
|
||||
.split("/")
|
||||
.at(-1)
|
||||
.startsWith("theme")
|
||||
const layout = includeLayout
|
||||
? "layout=" + state.layout.id + "&"
|
||||
: ""
|
||||
let id: Store<string> = tags.mapD(tags => tags.id)
|
||||
let url = id.mapD(id => `${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
|
||||
`#${id}`)
|
||||
|
||||
|
||||
const includeLayout = window.location.pathname.split("/").at(-1).startsWith("theme")
|
||||
const layout = includeLayout ? "layout=" + state.layout.id + "&" : ""
|
||||
let id: Store<string> = tags.mapD((tags) => tags.id)
|
||||
let url = id.mapD(
|
||||
(id) =>
|
||||
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
|
||||
`#${id}`
|
||||
)
|
||||
|
||||
function toggleSize() {
|
||||
if (size.data !== bigSize) {
|
||||
|
|
@ -42,5 +38,9 @@
|
|||
<!-- Not yet uploaded, doesn't have a fixed ID -->
|
||||
<Loading />
|
||||
{:else}
|
||||
<img on:click={() => toggleSize()} src={new Qr($url).toImageElement($size)} style={`width: ${$size}px; height: ${$size}px`} />
|
||||
<img
|
||||
on:click={() => toggleSize()}
|
||||
src={new Qr($url).toImageElement($size)}
|
||||
style={`width: ${$size}px; height: ${$size}px`}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@
|
|||
*/
|
||||
export let notForLabels: string[] | undefined = undefined
|
||||
const _notForLabels = new Set(notForLabels)
|
||||
let showAllQuestionsAtOnce : Store<boolean>= state.userRelatedState?.showAllQuestionsAtOnce ?? new ImmutableStore(false)
|
||||
let showAllQuestionsAtOnce: Store<boolean> =
|
||||
state.userRelatedState?.showAllQuestionsAtOnce ?? new ImmutableStore(false)
|
||||
|
||||
function allowed(labels: string[]) {
|
||||
if (onlyForLabels?.length > 0 && !labels.some((l) => _onlyForLabels.has(l))) {
|
||||
|
|
|
|||
|
|
@ -32,19 +32,18 @@
|
|||
onDestroy(
|
||||
tags.addCallbackD((tags) => {
|
||||
const knownNow = config.IsKnown(tags)
|
||||
if(!knownNow){
|
||||
if (!knownNow) {
|
||||
editMode = true
|
||||
return
|
||||
}
|
||||
if(knownNow && !knownAtTheStart) {
|
||||
if (knownNow && !knownAtTheStart) {
|
||||
// Some other question might have set this to 'known', so we close the 'editMode'
|
||||
editMode = false
|
||||
|
||||
// well, not really 'known at the start', but it is known at this point in time, so it should not set 'editMode' to false automatically anymore
|
||||
knownAtTheStart = true
|
||||
}
|
||||
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -306,7 +306,8 @@
|
|||
let hideMappingsUnlessSearchedFor =
|
||||
config.mappings.length > 8 && config.mappings.some((m) => m.priorityIf !== undefined)
|
||||
$: question = config.question
|
||||
$: hideMappingsUnlessSearchedFor = config.mappings.length > 8 && config.mappings.some((m) => m.priorityIf !== undefined)
|
||||
$: hideMappingsUnlessSearchedFor =
|
||||
config.mappings.length > 8 && config.mappings.some((m) => m.priorityIf !== undefined)
|
||||
|
||||
if (state?.osmConnection) {
|
||||
onDestroy(
|
||||
|
|
@ -362,7 +363,7 @@
|
|||
/>
|
||||
</div>
|
||||
{#if hideMappingsUnlessSearchedFor}
|
||||
<div class="m-1 rounded border border-dashed border-black p-1 px-2 flex items-center">
|
||||
<div class="m-1 flex items-center rounded border border-dashed border-black p-1 px-2">
|
||||
<Tr t={Translations.t.general.mappingsAreHidden} />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
import { RenderingSpecification, SpecialVisualization } from "./SpecialVisualization"
|
||||
|
||||
export default class SpecialVisualisationUtils {
|
||||
/**
|
||||
* Seeded by 'SpecialVisualisations' when that static class is initialized
|
||||
* This is to avoid some pesky circular imports
|
||||
*/
|
||||
public static specialVisualizations: SpecialVisualization[]
|
||||
|
||||
/**
|
||||
*
|
||||
* For a given string, returns a specification what parts are fixed and what parts are special renderings.
|
||||
|
|
@ -15,24 +9,28 @@ export default class SpecialVisualisationUtils {
|
|||
* import SpecialVisualisations from "./SpecialVisualizations"
|
||||
*
|
||||
* // Return empty list on empty input
|
||||
* SpecialVisualisationUtils.specialVisualizations = SpecialVisualisations.specialVisualizations
|
||||
* SpecialVisualisationUtils.constructSpecification("") // => []
|
||||
* SpecialVisualisationUtils.constructSpecification("", SpecialVisualisations.specialVisualisationsDict) // => []
|
||||
*
|
||||
* // Simple case
|
||||
* SpecialVisualisationUtils.specialVisualizations = SpecialVisualisations.specialVisualizations
|
||||
* const oh = SpecialVisualisationUtils.constructSpecification("The opening hours with value {opening_hours} can be seen in the following table: <br/> {opening_hours_table()}")
|
||||
* const oh = SpecialVisualisationUtils.constructSpecification("The opening hours with value {opening_hours} can be seen in the following table: <br/> {opening_hours_table()}", SpecialVisualisations.specialVisualisationsDict)
|
||||
* oh[0] // => "The opening hours with value {opening_hours} can be seen in the following table: <br/> "
|
||||
* oh[1].func.funcName // => "opening_hours_table"
|
||||
*
|
||||
* // Advanced cases with commas, braces and newlines should be handled without problem
|
||||
* SpecialVisualisationUtils.specialVisualizations = SpecialVisualisations.specialVisualizations
|
||||
* const templates = SpecialVisualisationUtils.constructSpecification("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.org/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}")
|
||||
* const templates = SpecialVisualisationUtils.constructSpecification("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.org/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}", SpecialVisualisations.specialVisualisationsDict)
|
||||
* const templ = <Exclude<RenderingSpecification, string>> templates[0]
|
||||
* templ.func.funcName // => "send_email"
|
||||
* templ.args[0] = "{email}"
|
||||
*
|
||||
* // Regression test - multiple special functions should all be found
|
||||
* const spec = "{create_review()}{list_reviews()}"
|
||||
* const parsed = SpecialVisualisationUtils.constructSpecification(spec, SpecialVisualisations.specialVisualisationsDict)
|
||||
* parsed[0].func.funcName // => "create_review"
|
||||
* parsed[1].func.funcName // => "list_reviews"
|
||||
*/
|
||||
public static constructSpecification(
|
||||
template: string,
|
||||
specialVisualisations: Map<string, SpecialVisualization>,
|
||||
extraMappings: SpecialVisualization[] = []
|
||||
): RenderingSpecification[] {
|
||||
if (template === "") {
|
||||
|
|
@ -46,52 +44,59 @@ export default class SpecialVisualisationUtils {
|
|||
)
|
||||
throw "Got a non-expanded template while constructing the specification"
|
||||
}
|
||||
const allKnownSpecials = extraMappings.concat(
|
||||
SpecialVisualisationUtils.specialVisualizations
|
||||
)
|
||||
for (const knownSpecial of allKnownSpecials) {
|
||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||
const matched = template.match(
|
||||
new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s")
|
||||
)
|
||||
if (matched != null) {
|
||||
// We found a special component that should be brought to live
|
||||
const partBefore = SpecialVisualisationUtils.constructSpecification(
|
||||
matched[1],
|
||||
extraMappings
|
||||
)
|
||||
const argument =
|
||||
matched[2] /* .trim() // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/
|
||||
const style = matched[3]?.substring(1) ?? ""
|
||||
const partAfter = SpecialVisualisationUtils.constructSpecification(
|
||||
matched[4],
|
||||
extraMappings
|
||||
)
|
||||
const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
|
||||
if (argument.length > 0) {
|
||||
const realArgs = argument
|
||||
.split(",")
|
||||
.map((str) => SpecialVisualisationUtils.undoEncoding(str))
|
||||
for (let i = 0; i < realArgs.length; i++) {
|
||||
if (args.length <= i) {
|
||||
args.push(realArgs[i])
|
||||
} else {
|
||||
args[i] = realArgs[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const element: RenderingSpecification = {
|
||||
args,
|
||||
style,
|
||||
func: knownSpecial,
|
||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||
const matched = template.match(new RegExp(`(.*?){\([a-zA-Z_]+\)\\((.*?)\\)(:.*)?}(.*)`, "s"))
|
||||
if (matched === null) {
|
||||
// IF we end up here, no changes have to be made - except to remove any resting {}
|
||||
return [template]
|
||||
}
|
||||
|
||||
const fName = matched[2]
|
||||
let knownSpecial = specialVisualisations.get(fName)
|
||||
if (!knownSpecial && extraMappings?.length > 0) {
|
||||
knownSpecial = extraMappings.find((em) => em.funcName === fName)
|
||||
}
|
||||
if (!knownSpecial) {
|
||||
throw "Didn't find a special visualisation: " + fName + " in " + template
|
||||
}
|
||||
|
||||
// Always a boring string
|
||||
const partBefore: string = matched[1]
|
||||
const argument: string =
|
||||
matched[3] /* .trim() // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/
|
||||
const style: string = matched[4]?.substring(1) ?? ""
|
||||
const partAfter: RenderingSpecification[] =
|
||||
SpecialVisualisationUtils.constructSpecification(
|
||||
matched[5],
|
||||
specialVisualisations,
|
||||
extraMappings
|
||||
)
|
||||
|
||||
const args: string[] = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
|
||||
if (argument.length > 0) {
|
||||
const realArgs = argument
|
||||
.split(",")
|
||||
.map((str) => SpecialVisualisationUtils.undoEncoding(str))
|
||||
for (let i = 0; i < realArgs.length; i++) {
|
||||
if (args.length <= i) {
|
||||
args.push(realArgs[i])
|
||||
} else {
|
||||
args[i] = realArgs[i]
|
||||
}
|
||||
return [...partBefore, element, ...partAfter]
|
||||
}
|
||||
}
|
||||
|
||||
// IF we end up here, no changes have to be made - except to remove any resting {}
|
||||
return [template]
|
||||
const element: RenderingSpecification = {
|
||||
args,
|
||||
style,
|
||||
func: knownSpecial,
|
||||
}
|
||||
partAfter.unshift(element)
|
||||
if (partBefore.length > 0) {
|
||||
partAfter.unshift(partBefore)
|
||||
}
|
||||
return partAfter
|
||||
}
|
||||
|
||||
private static undoEncoding(str: string) {
|
||||
|
|
|
|||
|
|
@ -90,8 +90,6 @@ export interface SpecialVisualizationState {
|
|||
readonly recentlyVisitedThemes: Store<string[]>
|
||||
}
|
||||
|
||||
readonly availableLayers: Store<RasterLayerPolygon[]>
|
||||
|
||||
readonly imageUploadManager: ImageUploadManager
|
||||
|
||||
readonly previewedImage: UIEventSource<ProvidedImage>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.sve
|
|||
import QrCode from "./Popup/QrCode.svelte"
|
||||
import ClearCaches from "./Popup/ClearCaches.svelte"
|
||||
import GroupedView from "./Popup/GroupedView.svelte"
|
||||
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
|
@ -119,7 +120,6 @@ class NearbyImageVis implements SpecialVisualization {
|
|||
"A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature"
|
||||
funcName = "nearby_images"
|
||||
needsUrls = CombinedFetcher.apiUrls
|
||||
svelteBased = true
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
|
|
@ -251,17 +251,17 @@ class CloseNoteViz implements SpecialVisualization {
|
|||
},
|
||||
]
|
||||
|
||||
public constr(state: SpecialVisualizationState, tags: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): SvelteUIElement {
|
||||
|
||||
const {
|
||||
text,
|
||||
icon,
|
||||
idkey,
|
||||
comment,
|
||||
minZoom,
|
||||
zoomButton
|
||||
} = Utils.ParseVisArgs(this.args, args)
|
||||
|
||||
public constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): SvelteUIElement {
|
||||
const { text, icon, idkey, comment, minZoom, zoomButton } = Utils.ParseVisArgs(
|
||||
this.args,
|
||||
args
|
||||
)
|
||||
|
||||
return new SvelteUIElement(CloseNoteButton, {
|
||||
state,
|
||||
|
|
@ -271,7 +271,7 @@ class CloseNoteViz implements SpecialVisualization {
|
|||
message: comment,
|
||||
text: Translations.T(text),
|
||||
minzoom: minZoom,
|
||||
zoomMoreMessage: zoomButton
|
||||
zoomMoreMessage: zoomButton,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -324,6 +324,19 @@ export class QuestionViz implements SpecialVisualization {
|
|||
|
||||
export default class SpecialVisualizations {
|
||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
||||
public static specialVisualisationsDict: Map<string, SpecialVisualization> = new Map<
|
||||
string,
|
||||
SpecialVisualization
|
||||
>()
|
||||
|
||||
static {
|
||||
for (const specialVisualization of SpecialVisualizations.specialVisualizations) {
|
||||
SpecialVisualizations.specialVisualisationsDict.set(
|
||||
specialVisualization.funcName,
|
||||
specialVisualization
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static DocumentationFor(viz: string | SpecialVisualization): string {
|
||||
if (typeof viz === "string") {
|
||||
|
|
@ -359,7 +372,11 @@ export default class SpecialVisualizations {
|
|||
template: string,
|
||||
extraMappings: SpecialVisualization[] = []
|
||||
): RenderingSpecification[] {
|
||||
return SpecialVisualisationUtils.constructSpecification(template, extraMappings)
|
||||
return SpecialVisualisationUtils.constructSpecification(
|
||||
template,
|
||||
SpecialVisualizations.specialVisualisationsDict,
|
||||
extraMappings
|
||||
)
|
||||
}
|
||||
|
||||
public static HelpMessage(): string {
|
||||
|
|
@ -778,8 +795,6 @@ export default class SpecialVisualizations {
|
|||
funcName: "list_reviews",
|
||||
docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten",
|
||||
needsUrls: [MangroveReviews.ORIGINAL_API],
|
||||
example:
|
||||
"`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used",
|
||||
args: [
|
||||
{
|
||||
name: "subjectKey",
|
||||
|
|
@ -807,6 +822,47 @@ export default class SpecialVisualizations {
|
|||
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "reviews",
|
||||
example:
|
||||
"`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used",
|
||||
docs: "A pragmatic combination of `create_review` and `list_reviews`",
|
||||
args: [
|
||||
{
|
||||
name: "subjectKey",
|
||||
defaultValue: "name",
|
||||
doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>",
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
|
||||
},
|
||||
],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
return new Combine([
|
||||
SpecialVisualizations.specialVisualisationsDict["create_review"].constr(
|
||||
state,
|
||||
tagSource,
|
||||
args,
|
||||
feature,
|
||||
layer
|
||||
),
|
||||
SpecialVisualizations.specialVisualisationsDict["list_reviews"].constr(
|
||||
state,
|
||||
tagSource,
|
||||
args,
|
||||
feature,
|
||||
layer
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "import_mangrove_key",
|
||||
docs: "Only makes sense in the usersettings. Allows to import a mangrove public key and to use this to make reviews",
|
||||
|
|
@ -1300,7 +1356,7 @@ export default class SpecialVisualizations {
|
|||
{
|
||||
name: "icon",
|
||||
doc: "If set, show this icon next to the link. You might want to combine this with `class: button`",
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
constr(
|
||||
|
|
@ -1328,7 +1384,7 @@ export default class SpecialVisualizations {
|
|||
download: tagSource.map((tags) => Utils.SubstituteKeys(download, tags)),
|
||||
ariaLabel: tagSource.map((tags) => Utils.SubstituteKeys(ariaLabel, tags)),
|
||||
newTab: new ImmutableStore(newTab),
|
||||
icon: tagSource.map((tags) => Utils.SubstituteKeys(icon, tags))
|
||||
icon: tagSource.map((tags) => Utils.SubstituteKeys(icon, tags)),
|
||||
}).setSpan()
|
||||
},
|
||||
},
|
||||
|
|
@ -1677,7 +1733,6 @@ export default class SpecialVisualizations {
|
|||
{
|
||||
funcName: "qr_code",
|
||||
args: [],
|
||||
|
||||
docs: "Generates a QR-code to share the selected object",
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
|
|
@ -1685,8 +1740,8 @@ export default class SpecialVisualizations {
|
|||
argument: string[],
|
||||
feature: Feature
|
||||
): SvelteUIElement {
|
||||
return new SvelteUIElement(QrCode , {state, tags, feature} )
|
||||
}
|
||||
return new SvelteUIElement(QrCode, { state, tags, feature })
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "direction_absolute",
|
||||
|
|
@ -1977,25 +2032,74 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
},
|
||||
{
|
||||
funcName:"pending_changes",
|
||||
funcName: "preset_type_select",
|
||||
docs: "An editable tag rendering which allows to change the type",
|
||||
args: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
selectedElement: Feature,
|
||||
layer: LayerConfig
|
||||
): SvelteUIElement {
|
||||
const t = Translations.t.preset_type
|
||||
const question: QuestionableTagRenderingConfigJson = {
|
||||
id: layer.id + "-type",
|
||||
question: t.question.translations,
|
||||
mappings: layer.presets.map((pr) => {
|
||||
return {
|
||||
if: new And(pr.tags).asJson(),
|
||||
then: (pr.description ? t.typeDescription : t.typeTitle).Subs({
|
||||
title: pr.title,
|
||||
description: pr.description,
|
||||
}).translations,
|
||||
}
|
||||
}),
|
||||
}
|
||||
const config = new TagRenderingConfig(question)
|
||||
return new SvelteUIElement(TagRenderingEditable, {
|
||||
config,
|
||||
tags,
|
||||
selectedElement,
|
||||
state,
|
||||
layer,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "pending_changes",
|
||||
docs: "A module showing the pending changes, with the option to clear the pending changes",
|
||||
args:[],
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
|
||||
return new SvelteUIElement(PendingChangesIndicator, {state, compact: false})
|
||||
args: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
return new SvelteUIElement(PendingChangesIndicator, { state, compact: false })
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "clear_caches",
|
||||
docs: "A button which clears the locally downloaded data and the service worker. Login status etc will be kept",
|
||||
args:[
|
||||
args: [
|
||||
{
|
||||
name: "text",
|
||||
required: true,
|
||||
doc: "The text to show on the button"
|
||||
}
|
||||
doc: "The text to show on the button",
|
||||
},
|
||||
],
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): SvelteUIElement {
|
||||
return new SvelteUIElement<any, any, any>(ClearCaches, {msg: argument[0] ?? "Clear local caches"})
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): SvelteUIElement {
|
||||
return new SvelteUIElement<any, any, any>(ClearCaches, {
|
||||
msg: argument[0] ?? "Clear local caches",
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -2004,37 +2108,152 @@ export default class SpecialVisualizations {
|
|||
args: [
|
||||
{
|
||||
name: "header",
|
||||
doc: "The _identifier_ of a single tagRendering. This will be used as header"
|
||||
doc: "The _identifier_ of a single tagRendering. This will be used as header",
|
||||
},
|
||||
{
|
||||
name: "labels",
|
||||
doc: "A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion"
|
||||
}
|
||||
doc: "A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion",
|
||||
},
|
||||
],
|
||||
constr(state: SpecialVisualizationState, tags: UIEventSource<Record<string, string>>, argument: string[], selectedElement: Feature, layer: LayerConfig): SvelteUIElement {
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
selectedElement: Feature,
|
||||
layer: LayerConfig
|
||||
): SvelteUIElement {
|
||||
const [header, labelsStr] = argument
|
||||
const labels = labelsStr.split(";").map(x => x.trim())
|
||||
const labels = labelsStr.split(";").map((x) => x.trim())
|
||||
return new SvelteUIElement<any, any, any>(GroupedView, {
|
||||
state, tags, selectedElement, layer, header, labels
|
||||
state,
|
||||
tags,
|
||||
selectedElement,
|
||||
layer,
|
||||
header,
|
||||
labels,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "preset_type_select",
|
||||
docs: "An editable tag rendering which allows to change the type",
|
||||
args: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
selectedElement: Feature,
|
||||
layer: LayerConfig
|
||||
): SvelteUIElement {
|
||||
const t = Translations.t.preset_type
|
||||
const question: QuestionableTagRenderingConfigJson = {
|
||||
id: layer.id + "-type",
|
||||
question: t.question.translations,
|
||||
mappings: layer.presets.map((pr) => {
|
||||
return {
|
||||
if: new And(pr.tags).asJson(),
|
||||
then: (pr.description ? t.typeDescription : t.typeTitle).Subs({
|
||||
title: pr.title,
|
||||
description: pr.description,
|
||||
}).translations,
|
||||
}
|
||||
}),
|
||||
}
|
||||
const config = new TagRenderingConfig(question)
|
||||
return new SvelteUIElement(TagRenderingEditable, {
|
||||
config,
|
||||
tags,
|
||||
selectedElement,
|
||||
state,
|
||||
layer,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "pending_changes",
|
||||
docs: "A module showing the pending changes, with the option to clear the pending changes",
|
||||
args: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
return new SvelteUIElement(PendingChangesIndicator, { state, compact: false })
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "clear_caches",
|
||||
docs: "A button which clears the locally downloaded data and the service worker. Login status etc will be kept",
|
||||
args: [
|
||||
{
|
||||
name: "text",
|
||||
required: true,
|
||||
doc: "The text to show on the button",
|
||||
},
|
||||
],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): SvelteUIElement {
|
||||
return new SvelteUIElement<any, any, any>(ClearCaches, {
|
||||
msg: argument[0] ?? "Clear local caches",
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "group",
|
||||
docs: "A collapsable group (accordion)",
|
||||
args: [
|
||||
{
|
||||
name: "header",
|
||||
doc: "The _identifier_ of a single tagRendering. This will be used as header",
|
||||
},
|
||||
{
|
||||
name: "labels",
|
||||
doc: "A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion",
|
||||
},
|
||||
],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
selectedElement: Feature,
|
||||
layer: LayerConfig
|
||||
): SvelteUIElement {
|
||||
const [header, labelsStr] = argument
|
||||
const labels = labelsStr.split(";").map((x) => x.trim())
|
||||
return new SvelteUIElement<any, any, any>(GroupedView, {
|
||||
state,
|
||||
tags,
|
||||
selectedElement,
|
||||
layer,
|
||||
header,
|
||||
labels,
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
||||
|
||||
const regex = /[a-zA-Z_]+/
|
||||
const invalid = specialVisualizations
|
||||
.map((sp, i) => ({ sp, i }))
|
||||
.filter((sp) => sp.sp.funcName === undefined)
|
||||
.filter((sp) => sp.sp.funcName === undefined || !sp.sp.funcName.match(regex))
|
||||
if (invalid.length > 0) {
|
||||
throw (
|
||||
"Invalid special visualisation found: funcName is undefined for " +
|
||||
"Invalid special visualisation found: funcName is undefined or doesn't match " +
|
||||
regex +
|
||||
invalid.map((sp) => sp.i).join(", ") +
|
||||
'. Did you perhaps type \n funcName: "funcname" // type declaration uses COLON\ninstead of:\n funcName = "funcName" // value definition uses EQUAL'
|
||||
)
|
||||
}
|
||||
|
||||
SpecialVisualisationUtils.specialVisualizations = Utils.NoNull(specialVisualizations)
|
||||
return specialVisualizations
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { ConfigMeta } from "./configMeta"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
|
|
@ -11,9 +10,7 @@
|
|||
import QuestionPreview from "./QuestionPreview.svelte"
|
||||
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
|
||||
import { EditJsonState } from "./EditLayerState"
|
||||
import type {
|
||||
QuestionableTagRenderingConfigJson,
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import { AccordionItem } from "flowbite-svelte"
|
||||
|
||||
export let state: EditJsonState<any>
|
||||
|
|
@ -33,14 +30,16 @@
|
|||
.getSchemaStartingWith(schema.path)
|
||||
.filter((part) => part.path.length - 1 === schema.path.length)
|
||||
|
||||
let usesOverride = value["builtin"] !== undefined
|
||||
|
||||
function schemaForMultitype() {
|
||||
const sch = { ...schema }
|
||||
sch.hints.typehint = undefined
|
||||
return sch
|
||||
}
|
||||
|
||||
function fusePath(i: number, subpartPath: string[]): (string | number)[] {
|
||||
const newPath = [...path, i]
|
||||
function fusePath(subpartPath: string[]): (string | number)[] {
|
||||
const newPath = [...path]
|
||||
const toAdd = [...subpartPath]
|
||||
for (const part of path) {
|
||||
if (toAdd[0] === part) {
|
||||
|
|
@ -84,10 +83,10 @@
|
|||
} catch (e) {
|
||||
console.log(
|
||||
"Warning: could not translate a title for " +
|
||||
`${singular} ${i} with function ` +
|
||||
schema.hints.title +
|
||||
" and value " +
|
||||
JSON.stringify(value),
|
||||
`${singular} ${i} with function ` +
|
||||
schema.hints.title +
|
||||
" and value " +
|
||||
JSON.stringify(value)
|
||||
)
|
||||
}
|
||||
return Translations.T(`${singular} ${i}`)
|
||||
|
|
@ -109,11 +108,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
|
||||
<AccordionItem open={$expanded} paddingDefault="p-0" inactiveClass="text-black m-0" >
|
||||
<div slot="header" class="p-1 text-base w-full m-0 text-black">
|
||||
<AccordionItem open={$expanded} paddingDefault="p-0" inactiveClass="text-black m-0">
|
||||
<div slot="header" class="m-0 w-full p-1 text-base text-black">
|
||||
{#if !isTagRenderingBlock}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div class="m-0 flex">
|
||||
{#if schema.hints.icon}
|
||||
<Icon clss="w-6 h-6" icon={genIcon(value)} color={genColor(value)} />
|
||||
|
|
@ -132,27 +130,30 @@
|
|||
<button
|
||||
class="h-fit w-fit rounded-full border border-black p-1"
|
||||
on:click={() => {
|
||||
del(i)
|
||||
}}
|
||||
del(i)
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{:else if typeof value === "string"}
|
||||
Builtin: <b>{value}</b>
|
||||
{:else if value["builtin"]}
|
||||
{:else if value["builtin"]}
|
||||
reused tagrendering <span class="font-bold">{JSON.stringify(value["builtin"])}</span>
|
||||
{:else}
|
||||
<Tr cls="font-bold" t={Translations.T(value?.question ?? value?.render)} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="normal-background p-2">
|
||||
{#if isTagRenderingBlock}
|
||||
<div class="normal-background border border-gray-300 p-2">
|
||||
{#if usesOverride}
|
||||
This block uses an builtin/override construction and cannot be edited in Studio. Edit the code
|
||||
directly
|
||||
{:else if isTagRenderingBlock}
|
||||
<QuestionPreview {state} {path} {schema}>
|
||||
<button
|
||||
on:click={() => {
|
||||
del(i)
|
||||
}}
|
||||
del(i)
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="h-4 w-4" />
|
||||
Delete this question
|
||||
|
|
@ -161,16 +162,16 @@
|
|||
{#if i > 0}
|
||||
<button
|
||||
on:click={() => {
|
||||
moveTo(i, 0)
|
||||
}}
|
||||
moveTo(i, 0)
|
||||
}}
|
||||
>
|
||||
Move to front
|
||||
</button>
|
||||
|
||||
<button
|
||||
on:click={() => {
|
||||
swap(i, i - 1)
|
||||
}}
|
||||
swap(i, i - 1)
|
||||
}}
|
||||
>
|
||||
Move up
|
||||
</button>
|
||||
|
|
@ -178,28 +179,25 @@
|
|||
{#if i + 1 < $currentValue.length}
|
||||
<button
|
||||
on:click={() => {
|
||||
swap(i, i + 1)
|
||||
}}
|
||||
swap(i, i + 1)
|
||||
}}
|
||||
>
|
||||
Move down
|
||||
</button>
|
||||
<button
|
||||
on:click={() => {
|
||||
moveTo(i, $currentValue.length - 1)
|
||||
}}
|
||||
moveTo(i, $currentValue.length - 1)
|
||||
}}
|
||||
>
|
||||
Move to back
|
||||
</button>
|
||||
{/if}
|
||||
</QuestionPreview>
|
||||
{:else if schema.hints.types}
|
||||
<SchemaBasedMultiType {state} {path} schema={schemaForMultitype()} />
|
||||
<SchemaBasedMultiType {state} path={[...path, i]} schema={schemaForMultitype()} />
|
||||
{:else}
|
||||
{#each subparts as subpart}
|
||||
<SchemaBasedInput
|
||||
{state}
|
||||
{path}
|
||||
/>
|
||||
<SchemaBasedInput {state} path={fusePath(subpart.path)} schema={subpart} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { EditJsonState } from "./EditLayerState"
|
||||
import BackButton from "../Base/BackButton.svelte"
|
||||
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
|
||||
let deleteState: "init" | "confirm" = "init"
|
||||
export let backToStudio: () => void
|
||||
export let state: EditJsonState
|
||||
|
||||
|
|
@ -16,32 +15,24 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-12">
|
||||
{#if deleteState === "init"}
|
||||
<button
|
||||
on:click={() => {
|
||||
deleteState = "confirm"
|
||||
}}
|
||||
class="small"
|
||||
>
|
||||
<TrashIcon class="h-6 w-6" />
|
||||
Delete this {objectType}
|
||||
</button>
|
||||
{:else if deleteState === "confirm"}
|
||||
<div class="flex">
|
||||
<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>
|
||||
</NextButton>
|
||||
<AccordionSingle>
|
||||
<div slot="header" class="flex gap-x-2">
|
||||
<TrashIcon class="h-6 w-6" />
|
||||
Delete this {objectType}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
Deleting this layer will delete your version. If you clicked a layer made by someone else,
|
||||
their version will remain. If you ever accidentally delete a layer, contact Pietervdvn. He
|
||||
might have a backup
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<NextButton clss="primary" on:click={() => deleteLayer()}>
|
||||
<div class="alert flex p-2">
|
||||
<TrashIcon class="h-6 w-6" />
|
||||
Do delete this {objectType}
|
||||
</div>
|
||||
</NextButton>
|
||||
</div>
|
||||
</AccordionSingle>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
let highlightedItem: UIEventSource<HighlightedTagRendering> = state.highlightedItem
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen flex-col">
|
||||
<div class="link-underline flex h-screen flex-col">
|
||||
<div class="my-2 flex w-full flex-wrap justify-between">
|
||||
<slot />
|
||||
{#if $title === undefined}
|
||||
|
|
@ -124,7 +124,7 @@
|
|||
|
||||
{#if $currentlyMissing.length > 0}
|
||||
{#each requiredFields as required}
|
||||
<SchemaBasedInput {state} schema={configForRequiredField(required)} path={[required]} />
|
||||
<SchemaBasedInput {state} path={[required]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="m4 h-full overflow-y-auto">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
Pipe,
|
||||
} from "../../Models/ThemeConfig/Conversion/Conversion"
|
||||
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
|
||||
import { PrevalidateTheme, ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation"
|
||||
import { PrevalidateTheme, ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation"
|
||||
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
|
||||
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
|
|
@ -23,6 +23,7 @@ import { PrepareTheme } from "../../Models/ThemeConfig/Conversion/PrepareTheme"
|
|||
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||
import { ValidateTheme } from "../../Models/ThemeConfig/Conversion/ValidateTheme"
|
||||
|
||||
export interface HighlightedTagRendering {
|
||||
path: ReadonlyArray<string | number>
|
||||
|
|
@ -53,7 +54,7 @@ export abstract class EditJsonState<T> {
|
|||
* The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out
|
||||
*/
|
||||
public readonly highlightedItem: UIEventSource<HighlightedTagRendering> = new UIEventSource(
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
private sendingUpdates = false
|
||||
private readonly _stores = new Map<string, UIEventSource<any>>()
|
||||
|
|
@ -65,7 +66,7 @@ export abstract class EditJsonState<T> {
|
|||
osmConnection: OsmConnection,
|
||||
options?: {
|
||||
expertMode?: UIEventSource<boolean>
|
||||
},
|
||||
}
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
this.schema = schema
|
||||
|
|
@ -92,14 +93,20 @@ export abstract class EditJsonState<T> {
|
|||
await this.server.update(id, config, this.category)
|
||||
})
|
||||
this.messages = this.createMessagesStore()
|
||||
|
||||
|
||||
}
|
||||
|
||||
public startSavingUpdates(enabled = true) {
|
||||
this.sendingUpdates = enabled
|
||||
this.register(["credits"], this.osmConnection.userDetails.mapD(u => u.name), false)
|
||||
this.register(["credits:uid"], this.osmConnection.userDetails.mapD(u => u.uid), false)
|
||||
this.register(
|
||||
["credits"],
|
||||
this.osmConnection.userDetails.mapD((u) => u.name),
|
||||
false
|
||||
)
|
||||
this.register(
|
||||
["credits:uid"],
|
||||
this.osmConnection.userDetails.mapD((u) => u.uid),
|
||||
false
|
||||
)
|
||||
if (enabled) {
|
||||
this.configuration.ping()
|
||||
}
|
||||
|
|
@ -140,7 +147,7 @@ export abstract class EditJsonState<T> {
|
|||
public register(
|
||||
path: ReadonlyArray<string | number>,
|
||||
value: Store<any>,
|
||||
noInitialSync: boolean = true,
|
||||
noInitialSync: boolean = true
|
||||
): () => void {
|
||||
const unsync = value.addCallback((v) => {
|
||||
this.setValueAt(path, v)
|
||||
|
|
@ -154,7 +161,7 @@ export abstract class EditJsonState<T> {
|
|||
public getSchemaStartingWith(path: string[]) {
|
||||
return this.schema.filter(
|
||||
(sch) =>
|
||||
!path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part)),
|
||||
!path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -171,11 +178,12 @@ export abstract class EditJsonState<T> {
|
|||
}
|
||||
}
|
||||
|
||||
public getSchema(path: string[]): ConfigMeta[] {
|
||||
public getSchema(path: (string | number)[]): ConfigMeta[] {
|
||||
path = path.filter((p) => typeof p === "string")
|
||||
const schemas = this.schema.filter(
|
||||
(sch) =>
|
||||
sch !== undefined &&
|
||||
!path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part)),
|
||||
!path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part))
|
||||
)
|
||||
if (schemas.length == 0) {
|
||||
console.warn("No schemas found for path", path.join("."))
|
||||
|
|
@ -265,12 +273,12 @@ class ContextRewritingStep<T> extends Conversion<LayerConfigJson, T> {
|
|||
constructor(
|
||||
state: DesugaringContext,
|
||||
step: Conversion<LayerConfigJson, T>,
|
||||
getTagRenderings: (t: T) => TagRenderingConfigJson[],
|
||||
getTagRenderings: (t: T) => TagRenderingConfigJson[]
|
||||
) {
|
||||
super(
|
||||
"When validating a layer, the tagRenderings are first expanded. Some builtin tagRendering-calls (e.g. `contact`) will introduce _multiple_ tagRenderings, causing the count to be off. This class rewrites the error messages to fix this",
|
||||
[],
|
||||
"ContextRewritingStep",
|
||||
"ContextRewritingStep"
|
||||
)
|
||||
this._state = state
|
||||
this._step = step
|
||||
|
|
@ -280,7 +288,7 @@ class ContextRewritingStep<T> extends Conversion<LayerConfigJson, T> {
|
|||
convert(json: LayerConfigJson, context: ConversionContext): T {
|
||||
const converted = this._step.convert(json, context)
|
||||
const originalIds = json.tagRenderings?.map(
|
||||
(tr) => (<QuestionableTagRenderingConfigJson>tr)["id"],
|
||||
(tr) => (<QuestionableTagRenderingConfigJson>tr)["id"]
|
||||
)
|
||||
if (!originalIds) {
|
||||
return converted
|
||||
|
|
@ -342,7 +350,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
|||
schema: ConfigMeta[],
|
||||
server: StudioServer,
|
||||
osmConnection: OsmConnection,
|
||||
options: { expertMode: UIEventSource<boolean> },
|
||||
options: { expertMode: UIEventSource<boolean> }
|
||||
) {
|
||||
super(schema, server, "layers", osmConnection, options)
|
||||
this.layout = {
|
||||
|
|
@ -399,7 +407,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
|||
return new ContextRewritingStep(
|
||||
state,
|
||||
new Pipe(new PrepareLayer(state), new ValidateLayer("dynamic", false, undefined, true)),
|
||||
(t) => <TagRenderingConfigJson[]>t.raw.tagRenderings,
|
||||
(t) => <TagRenderingConfigJson[]>t.raw.tagRenderings
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -433,7 +441,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
|||
}
|
||||
|
||||
protected async validate(
|
||||
configuration: Partial<LayerConfigJson>,
|
||||
configuration: Partial<LayerConfigJson>
|
||||
): Promise<ConversionMessage[]> {
|
||||
const layers = AllSharedLayers.getSharedLayersConfigs()
|
||||
|
||||
|
|
@ -463,18 +471,20 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
|
|||
schema: ConfigMeta[],
|
||||
server: StudioServer,
|
||||
osmConnection: OsmConnection,
|
||||
options: { expertMode: UIEventSource<boolean> },
|
||||
options: { expertMode: UIEventSource<boolean> }
|
||||
) {
|
||||
super(schema, server, "themes", osmConnection, options)
|
||||
this.setupFixers()
|
||||
}
|
||||
|
||||
protected buildValidation(state: DesugaringContext): Conversion<LayoutConfigJson, any> {
|
||||
return new Pipe(new PrevalidateTheme(),
|
||||
return new Pipe(
|
||||
new PrevalidateTheme(),
|
||||
new Pipe(
|
||||
new PrepareTheme(state),
|
||||
new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys())),
|
||||
), true,
|
||||
new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys()))
|
||||
),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@
|
|||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import type {
|
||||
QuestionableTagRenderingConfigJson,
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"
|
||||
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"
|
||||
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||
import FromHtml from "../Base/FromHtml.svelte"
|
||||
import ShowConversionMessage from "./ShowConversionMessage.svelte"
|
||||
|
|
@ -44,11 +42,12 @@
|
|||
if (config["builtin"]) {
|
||||
let override = ""
|
||||
if (config["override"]) {
|
||||
override = ". Some items are changed with an override. Editing this is not yet supported with Studio."
|
||||
override =
|
||||
". Some items are changed with an override. Editing this is not yet supported with Studio."
|
||||
}
|
||||
return new TagRenderingConfig({
|
||||
render: {
|
||||
"en": "This question reuses <b>" + JSON.stringify(config["builtin"]) + "</b>" + override,
|
||||
en: "This question reuses <b>" + JSON.stringify(config["builtin"]) + "</b>" + override,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -69,10 +68,10 @@
|
|||
|
||||
<div class="flex">
|
||||
<div class="m-4 flex w-full flex-col">
|
||||
{#if $configJson.some(config => config["builtin"] !== undefined)}
|
||||
<div class="interactive p-2 rounded-2xl">
|
||||
This question uses an advanced 'builtin'+'override' construction in the source code.
|
||||
Editing this with Studio is not supported.
|
||||
{#if $configJson.some((config) => config["builtin"] !== undefined)}
|
||||
<div class="interactive rounded-2xl p-2">
|
||||
This question uses an advanced 'builtin'+'override' construction in the source code. Editing
|
||||
this with Studio is not supported.
|
||||
</div>
|
||||
{:else}
|
||||
<NextButton clss="primary" on:click={() => state.highlightedItem.setData({ path, schema })}>
|
||||
|
|
|
|||
|
|
@ -5,17 +5,22 @@
|
|||
import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api"
|
||||
import layerSchemaJSON from "../../../Docs/Schemas/LayerConfigJson.schema.json"
|
||||
import layoutSchemaJSON from "../../../Docs/Schemas/LayoutConfigJson.schema.json"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
export let state: EditLayerState | EditThemeState
|
||||
|
||||
let rawConfig = state.configuration.sync(f => JSON.stringify(f, null, " "), [], json => {
|
||||
try {
|
||||
return JSON.parse(json)
|
||||
} catch (e) {
|
||||
console.error("Could not parse", json)
|
||||
return undefined
|
||||
let rawConfig = state.configuration.sync(
|
||||
(f) => JSON.stringify(f, null, " "),
|
||||
[],
|
||||
(json) => {
|
||||
try {
|
||||
return JSON.parse(json)
|
||||
} catch (e) {
|
||||
console.error("Could not parse", json)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
let container: HTMLDivElement
|
||||
let monaco: typeof Monaco
|
||||
|
|
@ -44,6 +49,7 @@
|
|||
})
|
||||
|
||||
let useFallback = false
|
||||
let isLoaded = false
|
||||
onMount(async () => {
|
||||
const monacoEditor = await import("monaco-editor")
|
||||
loader.config({
|
||||
|
|
@ -86,7 +92,7 @@
|
|||
model = monaco?.editor?.createModel(
|
||||
JSON.stringify(state.configuration.data, null, " "),
|
||||
"json",
|
||||
modelUri,
|
||||
modelUri
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Could not create model in MOnaco Editor", e)
|
||||
|
|
@ -106,6 +112,7 @@
|
|||
save()
|
||||
}, 500)
|
||||
})
|
||||
isLoaded = true
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
|
|
@ -121,5 +128,11 @@
|
|||
{#if useFallback}
|
||||
<textarea class="w-full" rows="25" bind:value={$rawConfig} />
|
||||
{:else}
|
||||
<div bind:this={container} class="h-full w-full" />
|
||||
<div bind:this={container} class="h-full w-full">
|
||||
{#if !isLoaded}
|
||||
<div class="align-center flex h-full w-full items-center">
|
||||
<Loading />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,14 @@
|
|||
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import ShowConversionMessage from "./ShowConversionMessage.svelte"
|
||||
import Markdown from "../Base/Markdown.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import CollapsedTagRenderingPreview from "./CollapsedTagRenderingPreview.svelte"
|
||||
import { Accordion } from "flowbite-svelte"
|
||||
|
||||
export let state: EditJsonState<any>
|
||||
export let path: (string | number)[] = []
|
||||
export let schema: ConfigMeta
|
||||
let schema: ConfigMeta = state.getSchema(path)[0]
|
||||
|
||||
schema = Utils.Clone(schema)
|
||||
let title = schema.path.at(-1)
|
||||
let singular = title
|
||||
if (title?.endsWith("s")) {
|
||||
|
|
@ -37,7 +35,8 @@
|
|||
.filter((part) => part.path.length - 1 === schema.path.length)
|
||||
let messages = state.messagesFor(path)
|
||||
|
||||
const currentValue = state.getStoreFor<(string | QuestionableTagRenderingConfigJson)[]>(path)
|
||||
let datapath = path
|
||||
const currentValue = state.getStoreFor<(string | QuestionableTagRenderingConfigJson)[]>(datapath)
|
||||
if (currentValue.data === undefined) {
|
||||
currentValue.setData([])
|
||||
}
|
||||
|
|
@ -54,27 +53,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
function fusePath(i: number, subpartPath: string[]): (string | number)[] {
|
||||
const newPath = [...path, i]
|
||||
const toAdd = [...subpartPath]
|
||||
for (const part of path) {
|
||||
if (toAdd[0] === part) {
|
||||
toAdd.splice(0, 1)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
newPath.push(...toAdd)
|
||||
return newPath
|
||||
function fusePath(i: number): (string | number)[] {
|
||||
return [...path, i]
|
||||
}
|
||||
|
||||
function del(i: number) {
|
||||
currentValue.data.splice(i, 1)
|
||||
currentValue.ping()
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="pl-2">
|
||||
|
|
@ -85,18 +71,22 @@
|
|||
{/if}
|
||||
{#if $currentValue === undefined}
|
||||
No array defined
|
||||
{:else if $currentValue.length === 0}
|
||||
{:else if !Array.isArray($currentValue)}
|
||||
Not an array: {typeof $currentValue}
|
||||
{JSON.stringify(path)}
|
||||
{JSON.stringify($currentValue).slice(0, 120)}
|
||||
{:else if $currentValue?.length === 0}
|
||||
No values are defined
|
||||
{#if $messages.length > 0}
|
||||
{#each $messages as message}
|
||||
<ShowConversionMessage {message} />
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if subparts.length === 0}
|
||||
{:else if subparts.length === 0}
|
||||
<!-- We need an array of values, so we use the typehint of the _parent_ element as field -->
|
||||
{#each $currentValue as value, i}
|
||||
<div class="flex w-full">
|
||||
<SchemaBasedField {state} {schema} path={fusePath(i, [])} />
|
||||
<SchemaBasedField {state} {schema} path={fusePath(i)} />
|
||||
<button
|
||||
class="h-fit w-fit rounded-full border border-black p-1"
|
||||
on:click={() => {
|
||||
|
|
@ -109,9 +99,18 @@
|
|||
{/each}
|
||||
{:else}
|
||||
<Accordion>
|
||||
{#each $currentValue as value, i (value)}
|
||||
<CollapsedTagRenderingPreview {state} {isTagRenderingBlock} {schema} {currentValue} {value} {i} {singular} path={fusePath(i, [])}/>
|
||||
{/each}
|
||||
{#each $currentValue as value, i (value)}
|
||||
<CollapsedTagRenderingPreview
|
||||
{state}
|
||||
{isTagRenderingBlock}
|
||||
{schema}
|
||||
{currentValue}
|
||||
{value}
|
||||
{i}
|
||||
{singular}
|
||||
path={fusePath(i)}
|
||||
/>
|
||||
{/each}
|
||||
</Accordion>
|
||||
{/if}
|
||||
<div class="flex">
|
||||
|
|
|
|||
|
|
@ -8,19 +8,20 @@
|
|||
|
||||
export let state: EditJsonState<any>
|
||||
export let path: (string | number)[] = []
|
||||
console.log("Fetched schema:", path, state.getSchema(<any> path))
|
||||
let schema: ConfigMeta = state.getSchema(<any> path)[0]
|
||||
export let schema: ConfigMeta = state.getSchema(<any>path)[0]
|
||||
let expertMode = state.expertMode
|
||||
</script>
|
||||
|
||||
{#if (schema.hints?.group !== "expert" || $expertMode) && schema.hints.group !== "hidden"}
|
||||
{#if schema === undefined}
|
||||
<div>ERROR: no schema found for {path.join(".")}</div>
|
||||
{:else if (schema.hints?.group !== "expert" || $expertMode) && schema.hints.group !== "hidden"}
|
||||
{#if schema.hints?.typehint?.endsWith("[]")}
|
||||
<!-- We cheat a bit here by matching this 'magical' type... -->
|
||||
<SchemaBasedArray {path} {state} {schema} />
|
||||
<SchemaBasedArray {path} {state} />
|
||||
{:else if schema.type === "array" && schema.hints.multianswer === "true"}
|
||||
<ArrayMultiAnswer {path} {state} {schema} />
|
||||
{:else if schema.type === "array"}
|
||||
<SchemaBasedArray {path} {state} {schema} />
|
||||
<SchemaBasedArray {path} {state} />
|
||||
{:else if schema.hints?.types}
|
||||
<SchemaBasedMultiType {path} {state} {schema} />
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -220,10 +220,7 @@
|
|||
{#if chosenOption !== undefined}
|
||||
{#each subSchemas as subschema}
|
||||
{#if $expertMode || subschema.hints?.group !== "expert"}
|
||||
<SchemaBasedInput
|
||||
{state}
|
||||
path={[...subpath, subschema?.path?.at(-1) ?? "???"]}
|
||||
/>
|
||||
<SchemaBasedInput {state} path={[...subpath, subschema?.path?.at(-1) ?? "???"]} />
|
||||
{:else if window.location.hostname === "127.0.0.1"}
|
||||
<span class="subtle">Omitted expert question {subschema.path.join(".")}</span>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
const store = state.getStoreFor(path)
|
||||
let value = store.data
|
||||
let hasSeenIntro = UIEventSource.asBoolean(
|
||||
LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false"),
|
||||
LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false")
|
||||
)
|
||||
onMount(() => {
|
||||
if (!hasSeenIntro.data) {
|
||||
|
|
@ -42,24 +42,24 @@
|
|||
* Should only be enabled for 'tagrenderings' in the theme, if the source is OSM
|
||||
*/
|
||||
let allowQuestions: Store<boolean> = state.configuration.mapD(
|
||||
(config) => path.at(0) === "tagRenderings" && config.source?.["geoJson"] === undefined,
|
||||
(config) => path.at(0) === "tagRenderings" && config.source?.["geoJson"] === undefined
|
||||
)
|
||||
|
||||
|
||||
let mappingsBuiltin: MappingConfigJson[] = []
|
||||
let perLabel: Record<string, MappingConfigJson> = {}
|
||||
{ // Build the list of options that one can choose for builtin questions
|
||||
const forbidden = new Set( ["ignore_docs","all_tags"])
|
||||
{
|
||||
// Build the list of options that one can choose for builtin questions
|
||||
const forbidden = new Set(["ignore_docs", "all_tags"])
|
||||
|
||||
for (const tr of questions.tagRenderings) {
|
||||
if(forbidden.has(tr.id)){
|
||||
if (forbidden.has(tr.id)) {
|
||||
continue
|
||||
}
|
||||
let description = tr["description"] ?? tr["question"] ?? "No description available"
|
||||
description = description["en"] ?? description
|
||||
if (tr["labels"]) {
|
||||
const labels: string[] = tr["labels"]
|
||||
if(labels.some(l => forbidden.has(l))){
|
||||
if (labels.some((l) => forbidden.has(l))) {
|
||||
continue
|
||||
}
|
||||
for (const label of labels) {
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
|
||||
const freeformSchemaAll = <ConfigMeta[]>(
|
||||
questionableTagRenderingSchemaRaw.filter(
|
||||
(schema) => schema.path.length == 2 && schema.path[0] === "freeform" && $allowQuestions,
|
||||
(schema) => schema.path.length == 2 && schema.path[0] === "freeform" && $allowQuestions
|
||||
)
|
||||
)
|
||||
let freeformSchema = $expertMode
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
const missing: string[] = questionableTagRenderingSchemaRaw
|
||||
.filter(
|
||||
(schema) =>
|
||||
schema.path.length >= 1 && !items.has(schema.path[0]) && !ignored.has(schema.path[0]),
|
||||
schema.path.length >= 1 && !items.has(schema.path[0]) && !ignored.has(schema.path[0])
|
||||
)
|
||||
.map((schema) => schema.path.join("."))
|
||||
console.log({ state })
|
||||
|
|
@ -146,7 +146,14 @@
|
|||
|
||||
{#if typeof $store === "string"}
|
||||
<div class="low-interaction flex">
|
||||
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} {state} {tags} editMode={true} clss="w-full" />
|
||||
<TagRenderingEditable
|
||||
config={configBuiltin}
|
||||
selectedElement={undefined}
|
||||
{state}
|
||||
{tags}
|
||||
editMode={true}
|
||||
clss="w-full"
|
||||
/>
|
||||
<slot name="upper-right" />
|
||||
</div>
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -42,11 +42,13 @@
|
|||
undefined,
|
||||
"Used to complete the login"
|
||||
)
|
||||
const fakeUser = UIEventSource.asBoolean( QueryParameters.GetQueryParameter("fake-user", "Test switch for fake login"))
|
||||
const fakeUser = UIEventSource.asBoolean(
|
||||
QueryParameters.GetQueryParameter("fake-user", "Test switch for fake login")
|
||||
)
|
||||
let osmConnection = new OsmConnection({
|
||||
oauth_token,
|
||||
checkOnlineRegularly: true,
|
||||
fakeUser: fakeUser.data
|
||||
fakeUser: fakeUser.data,
|
||||
})
|
||||
const expertMode = UIEventSource.asBoolean(
|
||||
osmConnection.GetPreference("studio-expert-mode", "false", {
|
||||
|
|
|
|||
|
|
@ -124,11 +124,11 @@
|
|||
state.mapProperties.installCustomKeyboardHandler(viewport)
|
||||
let canZoomIn = mapproperties.maxzoom.map(
|
||||
(mz) => mapproperties.zoom.data < mz,
|
||||
[mapproperties.zoom],
|
||||
[mapproperties.zoom]
|
||||
)
|
||||
let canZoomOut = mapproperties.minzoom.map(
|
||||
(mz) => mapproperties.zoom.data > mz,
|
||||
[mapproperties.zoom],
|
||||
[mapproperties.zoom]
|
||||
)
|
||||
|
||||
function updateViewport() {
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
onDestroy(
|
||||
rasterLayer.addCallbackAndRunD((l) => {
|
||||
rasterLayerName = l.properties.name
|
||||
}),
|
||||
})
|
||||
)
|
||||
let previewedImage = state.previewedImage
|
||||
|
||||
|
|
@ -196,7 +196,7 @@
|
|||
let openMapButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
|
||||
let openMenuButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
|
||||
let openCurrentViewLayerButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
let _openNewElementButton: HTMLButtonElement
|
||||
let openNewElementButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue