forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
f0823f4c4d
524 changed files with 18747 additions and 8546 deletions
|
|
@ -19,7 +19,7 @@
|
|||
import { LayoutInformation } from "../Models/ThemeConfig/LayoutConfig"
|
||||
import * as themeOverview from "../assets/generated/theme_overview.json"
|
||||
import UnofficialThemeList from "./BigComponents/UnofficialThemeList.svelte"
|
||||
import Eye from "@babeard/svelte-heroicons/mini/Eye"
|
||||
import Eye from "../assets/svg/Eye.svelte"
|
||||
|
||||
const featureSwitches = new OsmConnectionFeatureSwitches()
|
||||
const osmConnection = new OsmConnection({
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
.filter((key) => key.startsWith(prefix))
|
||||
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
|
||||
)
|
||||
return hiddenThemes.filter((theme) => knownIds.has(theme.id))
|
||||
return hiddenThemes.filter((theme) => knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet")
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "./VariableUIElement"
|
||||
import { Stores } from "../../Logic/UIEventSource"
|
||||
import Loading from "./Loading"
|
||||
|
||||
export default class AsyncLazy extends BaseUIElement {
|
||||
private readonly _f: () => Promise<BaseUIElement>
|
||||
|
||||
constructor(f: () => Promise<BaseUIElement>) {
|
||||
super()
|
||||
this._f = f
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
// The caching of the BaseUIElement will guarantee that _f will only be called once
|
||||
|
||||
return new VariableUiElement(
|
||||
Stores.FromPromise(this._f()).map((el) => {
|
||||
if (el === undefined) {
|
||||
return new Loading()
|
||||
}
|
||||
return el
|
||||
})
|
||||
).ConstructElement()
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
import { ariaLabelStore } from "../../Utils/ariaLabel"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Center from "../../assets/svg/Center.svelte"
|
||||
import Tr from "./Tr.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let feature: Feature
|
||||
|
|
@ -38,6 +39,13 @@
|
|||
|
||||
let relativeDirections = Translations.t.general.visualFeedback.directionsRelative
|
||||
let absoluteDirections = Translations.t.general.visualFeedback.directionsAbsolute
|
||||
|
||||
function round10(n :number){
|
||||
if(n < 50){
|
||||
return n
|
||||
}
|
||||
return Math.round(n / 10) * 10
|
||||
}
|
||||
|
||||
let closeToCurrentLocation = state.geolocation.geolocationState.currentGPSLocation.map(
|
||||
(gps) => {
|
||||
|
|
@ -53,7 +61,7 @@
|
|||
)
|
||||
let labelFromCenter: Store<string> = bearingAndDist.mapD(
|
||||
({ bearing, dist }) => {
|
||||
const distHuman = GeoOperations.distanceToHuman(dist)
|
||||
const distHuman = GeoOperations.distanceToHuman(round10(dist))
|
||||
const lang = Locale.language.data
|
||||
const t = absoluteDirections[GeoOperations.bearingToHuman(bearing)]
|
||||
const mainTr = Translations.t.general.visualFeedback.fromMapCenter.Subs({
|
||||
|
|
@ -75,7 +83,7 @@
|
|||
> = state.geolocation.geolocationState.currentGPSLocation.mapD(({ longitude, latitude }) => {
|
||||
let gps = [longitude, latitude]
|
||||
let bearing = Math.round(GeoOperations.bearing(gps, fcenter))
|
||||
let dist = Math.round(GeoOperations.distanceBetween(fcenter, gps))
|
||||
let dist = round10(Math.round(GeoOperations.distanceBetween(fcenter, gps)))
|
||||
return { bearing, dist }
|
||||
})
|
||||
let labelFromGps: Store<string | undefined> = bearingAndDistGps.mapD(
|
||||
|
|
@ -84,7 +92,6 @@
|
|||
const lang = Locale.language.data
|
||||
let bearingHuman: string
|
||||
if (compass.data !== undefined) {
|
||||
console.log("compass:", compass.data)
|
||||
const bearingRelative = bearing - compass.data
|
||||
const t = relativeDirections[GeoOperations.bearingToHumanRelative(bearingRelative)]
|
||||
bearingHuman = t.textFor(lang)
|
||||
|
|
@ -119,26 +126,36 @@
|
|||
</script>
|
||||
|
||||
{#if $bearingAndDistGps === undefined}
|
||||
<button
|
||||
class={twMerge("soft relative rounded-full p-1", size)}
|
||||
<!--
|
||||
Important: one would expect this to be a button - it certainly behaves as one
|
||||
However, this breaks the live-reading functionality (at least with Orca+FF),
|
||||
so we use a 'div' and add on:click manually
|
||||
-->
|
||||
<div
|
||||
class={twMerge("soft relative flex justify-center items-center border border-black rounded-full cursor-pointer p-1", size)}
|
||||
on:click={() => focusMap()}
|
||||
use:ariaLabelStore={label}
|
||||
>
|
||||
<Center class="h-7 w-7" />
|
||||
</button>
|
||||
<Center class=" h-6 w-6" />
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class={twMerge("soft relative rounded-full", size)}
|
||||
<div
|
||||
class={twMerge("soft relative rounded-full border-black border", size)}
|
||||
on:click={() => focusMap()}
|
||||
use:ariaLabelStore={label}
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
"absolute top-0 left-0 flex items-center justify-center break-words text-sm",
|
||||
"absolute top-0 left-0 flex items-center justify-center break-words text-xs cursor-pointer",
|
||||
size
|
||||
)}
|
||||
>
|
||||
<div aria-hidden="true">
|
||||
{GeoOperations.distanceToHuman($bearingAndDistGps?.dist)}
|
||||
</div>
|
||||
<div class="offscreen">
|
||||
{$label}
|
||||
</div>
|
||||
</div>
|
||||
{#if $bearingFromGps !== undefined}
|
||||
<div class={twMerge("absolute top-0 left-0 rounded-full", size)}>
|
||||
|
|
@ -148,5 +165,16 @@
|
|||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.offscreen {
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
white-space: nowrap; /* added line */
|
||||
width: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
<label
|
||||
class={twMerge(cls, drawAttention ? "glowing-shadow" : "")}
|
||||
for={id}
|
||||
on:click={() => {
|
||||
on:click|preventDefault={() => {
|
||||
inputElement.click()
|
||||
}}
|
||||
style="margin-left:0"
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { VariableUiElement } from "./VariableUIElement"
|
||||
import Combine from "./Combine"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class FilteredCombine extends VariableUiElement {
|
||||
/**
|
||||
* Only shows item matching the search
|
||||
* If predicate of an item is undefined, it will be filtered out as soon as a non-null or non-empty search term is given
|
||||
* @param entries
|
||||
* @param searchedValue
|
||||
* @param options
|
||||
*/
|
||||
constructor(
|
||||
entries: {
|
||||
element: BaseUIElement | string
|
||||
predicate?: (s: string) => boolean
|
||||
}[],
|
||||
searchedValue: UIEventSource<string>,
|
||||
options?: {
|
||||
onEmpty?: BaseUIElement | string
|
||||
innerClasses: string
|
||||
}
|
||||
) {
|
||||
entries = Utils.NoNull(entries)
|
||||
super(
|
||||
searchedValue.map(
|
||||
(searchTerm) => {
|
||||
if (searchTerm === undefined || searchTerm === "") {
|
||||
return new Combine(entries.map((e) => e.element)).SetClass(
|
||||
options?.innerClasses ?? ""
|
||||
)
|
||||
}
|
||||
const kept = entries.filter(
|
||||
(entry) => entry?.predicate !== undefined && entry.predicate(searchTerm)
|
||||
)
|
||||
if (kept.length === 0) {
|
||||
return options?.onEmpty
|
||||
}
|
||||
return new Combine(kept.map((entry) => entry.element)).SetClass(
|
||||
options?.innerClasses ?? ""
|
||||
)
|
||||
},
|
||||
[Locale.language]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { VariableUiElement } from "./VariableUIElement"
|
||||
import Locale from "../i18n/Locale"
|
||||
import Link from "./Link"
|
||||
import Svg from "../../Svg"
|
||||
import SvelteUIElement from "./SvelteUIElement"
|
||||
import Translate from "../../assets/svg/Translate.svelte"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
<script lang="ts">
|
||||
import ToSvelte from "./ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import Loading from "../../assets/svg/Loading.svelte"
|
||||
|
||||
export let cls: string = undefined
|
||||
</script>
|
||||
|
||||
<div class={twMerge("flex p-1 pl-2", cls)}>
|
||||
<div class="min-w-6 h-6 w-6 shrink-0 animate-spin self-center">
|
||||
<ToSvelte construct={Svg.loading_svg()} />
|
||||
<Loading />
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<slot />
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@
|
|||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Translations from "../i18n/Translations.js"
|
||||
import Tr from "./Tr.svelte"
|
||||
import ToSvelte from "./ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
import Login from "../../assets/svg/Login.svelte"
|
||||
|
||||
export let osmConnection: OsmConnection
|
||||
export let clss: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<button class={clss} on:click={() => osmConnection.AttemptLogin()} style="margin-left: 0">
|
||||
<ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")} />
|
||||
<Login class="w-12 m-1" />
|
||||
<slot>
|
||||
<Tr t={Translations.t.general.loginWithOpenStreetMap} />
|
||||
</slot>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { UIElement } from "../UIElement"
|
||||
import { VariableUiElement } from "./VariableUIElement"
|
||||
import Lazy from "./Lazy"
|
||||
import Loading from "./Loading"
|
||||
import SvelteUIElement from "./SvelteUIElement"
|
||||
import SubtleLink from "./SubtleLink.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import Svg from "../../Svg"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import OverlayMap from "../Map/OverlayMap.svelte"
|
||||
import RasterLayerPicker from "../Map/RasterLayerPicker.svelte"
|
||||
|
||||
export let mapproperties: MapProperties
|
||||
export let normalMap: UIEventSource<MlMap>
|
||||
/**
|
||||
* The current background (raster) layer of the polygon.
|
||||
* This is undefined if a vector layer is used
|
||||
*/
|
||||
let rasterLayer: UIEventSource<RasterLayerPolygon | undefined> = mapproperties.rasterLayer
|
||||
let name = rasterLayer.data?.properties?.name
|
||||
let icon = Svg.satellite_svg()
|
||||
onDestroy(
|
||||
rasterLayer.addCallback((polygon) => {
|
||||
name = polygon.properties?.name
|
||||
})
|
||||
)
|
||||
/**
|
||||
* The layers that this component can offer as a choice.
|
||||
*/
|
||||
export let availableRasterLayers: Store<RasterLayerPolygon[]>
|
||||
|
||||
let raster0 = new UIEventSource<RasterLayerPolygon>(undefined)
|
||||
|
||||
let raster1 = new UIEventSource<RasterLayerPolygon>(undefined)
|
||||
|
||||
let currentLayer: RasterLayerPolygon
|
||||
|
||||
function updatedAltLayer() {
|
||||
const available = availableRasterLayers.data
|
||||
const current = rasterLayer.data
|
||||
const defaultLayer = AvailableRasterLayers.maptilerDefaultLayer
|
||||
const firstOther = available.find((l) => l !== defaultLayer)
|
||||
const secondOther = available.find((l) => l !== defaultLayer && l !== firstOther)
|
||||
raster0.setData(firstOther === current ? defaultLayer : firstOther)
|
||||
raster1.setData(secondOther === current ? defaultLayer : secondOther)
|
||||
}
|
||||
|
||||
updatedAltLayer()
|
||||
onDestroy(mapproperties.rasterLayer.addCallbackAndRunD(updatedAltLayer))
|
||||
onDestroy(availableRasterLayers.addCallbackAndRunD(updatedAltLayer))
|
||||
|
||||
function use(rasterLayer: UIEventSource<RasterLayerPolygon>): () => void {
|
||||
return () => {
|
||||
currentLayer = undefined
|
||||
mapproperties.rasterLayer.setData(rasterLayer.data)
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ copyright_clicked }>()
|
||||
</script>
|
||||
|
||||
<div class="flex items-end opacity-50 hover:opacity-100">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<button class="m-0 h-12 w-16 overflow-hidden p-0 md:h-16 md:w-16" on:click={use(raster0)}>
|
||||
<OverlayMap
|
||||
placedOverMap={normalMap}
|
||||
placedOverMapProperties={mapproperties}
|
||||
rasterLayer={raster0}
|
||||
/>
|
||||
</button>
|
||||
<button class="m-0 h-12 w-16 overflow-hidden p-0 md:h-16 md:w-16" on:click={use(raster1)}>
|
||||
<OverlayMap
|
||||
placedOverMap={normalMap}
|
||||
placedOverMapProperties={mapproperties}
|
||||
rasterLayer={raster1}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ml-1 flex h-fit flex-col gap-y-1 text-sm">
|
||||
<div class="low-interaction w-64 rounded p-1">
|
||||
<RasterLayerPicker
|
||||
availableLayers={availableRasterLayers}
|
||||
value={mapproperties.rasterLayer}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="small" on:click={() => dispatch("copyright_clicked")}>© OpenStreetMap</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -11,11 +11,13 @@
|
|||
import { placeholder } from "../../Utils/placeholder"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
||||
|
||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
||||
export let bounds: UIEventSource<BBox>
|
||||
export let selectedElement: UIEventSource<Feature> | undefined = undefined
|
||||
|
||||
export let geolocationState: GeoLocationState | undefined = undefined
|
||||
export let clearAfterView: boolean = true
|
||||
let searchContents: string = ""
|
||||
export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
|
|
@ -55,6 +57,8 @@
|
|||
async function performSearch() {
|
||||
try {
|
||||
isRunning = true
|
||||
geolocationState?.allowMoving.setData(true)
|
||||
geolocationState?.requestMoment.setData(undefined) // If the GPS is still searching for a fix, we say that we don't want tozoom to it anymore
|
||||
searchContents = searchContents?.trim() ?? ""
|
||||
|
||||
if (searchContents === "") {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import Combine from "../Base/Combine"
|
|||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { Utils } from "../../Utils"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Svg from "../../Svg"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import Up from "../../assets/svg/Up.svelte"
|
||||
import Circle from "../../assets/svg/Circle.svelte"
|
||||
|
||||
export default class Histogram<T> extends VariableUiElement {
|
||||
private static defaultPalette = [
|
||||
|
|
@ -34,11 +36,11 @@ export default class Histogram<T> extends VariableUiElement {
|
|||
sortMode.map((m) => {
|
||||
switch (m) {
|
||||
case "name":
|
||||
return Svg.up_svg()
|
||||
return new SvelteUIElement(Up)
|
||||
case "name-rev":
|
||||
return Svg.up_svg().SetStyle("transform: rotate(180deg)")
|
||||
return new SvelteUIElement(Up).SetStyle("transform: rotate(180deg)")
|
||||
default:
|
||||
return Svg.circle_svg()
|
||||
return new SvelteUIElement(Circle)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
@ -56,11 +58,11 @@ export default class Histogram<T> extends VariableUiElement {
|
|||
sortMode.map((m) => {
|
||||
switch (m) {
|
||||
case "count":
|
||||
return Svg.up_svg()
|
||||
return new SvelteUIElement(Up)
|
||||
case "count-rev":
|
||||
return Svg.up_svg().SetStyle("transform: rotate(180deg)")
|
||||
return new SvelteUIElement(Up).SetStyle("transform: rotate(180deg)")
|
||||
default:
|
||||
return Svg.circle_svg()
|
||||
return new SvelteUIElement(Circle)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
|
||||
export default class IndexText extends Combine {
|
||||
constructor() {
|
||||
super([])
|
||||
|
||||
this.SetClass("flex flex-row")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +1,120 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import type {
|
||||
FeatureSource,
|
||||
FeatureSourceForLayer,
|
||||
} from "../../Logic/FeatureSource/FeatureSource"
|
||||
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"
|
||||
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Move_arrows from "../../assets/svg/Move_arrows.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import type {
|
||||
FeatureSource,
|
||||
FeatureSourceForLayer,
|
||||
} from "../../Logic/FeatureSource/FeatureSource"
|
||||
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"
|
||||
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Move_arrows from "../../assets/svg/Move_arrows.svelte"
|
||||
|
||||
/**
|
||||
* An advanced location input, which has support to:
|
||||
* - Show more layers
|
||||
* - Snap to layers
|
||||
*
|
||||
* This one is mostly used to insert new points, including when importing
|
||||
*/
|
||||
export let state: SpecialVisualizationState
|
||||
/**
|
||||
* The start coordinate
|
||||
*/
|
||||
export let coordinate: { lon: number; lat: number }
|
||||
/**
|
||||
* An advanced location input, which has support to:
|
||||
* - Show more layers
|
||||
* - Snap to layers
|
||||
*
|
||||
* This one is mostly used to insert new points, including when importing
|
||||
*/
|
||||
export let state: SpecialVisualizationState
|
||||
/**
|
||||
* The start coordinate
|
||||
*/
|
||||
export let coordinate: { lon: number; lat: number }
|
||||
|
||||
/**
|
||||
* The center of the map at all times
|
||||
* If undefined at the beginning, 'coordinate' will be used
|
||||
*/
|
||||
export let value: UIEventSource<{ lon: number; lat: number }>
|
||||
if (value.data === undefined) {
|
||||
value.setData(coordinate)
|
||||
}
|
||||
if (coordinate === undefined) {
|
||||
coordinate = value.data
|
||||
}
|
||||
export let snapToLayers: string[] | undefined
|
||||
export let targetLayer: LayerConfig | undefined
|
||||
export let maxSnapDistance: number = undefined
|
||||
|
||||
export let snappedTo: UIEventSource<string | undefined>
|
||||
|
||||
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
|
||||
lon: number
|
||||
lat: number
|
||||
}>(undefined)
|
||||
|
||||
const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>()
|
||||
|
||||
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
|
||||
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let initialMapProperties: Partial<MapProperties> = {
|
||||
zoom: new UIEventSource<number>(19),
|
||||
maxbounds: new UIEventSource(undefined),
|
||||
/*If no snapping needed: the value is simply the map location;
|
||||
* If snapping is needed: the value will be set later on by the snapping feature source
|
||||
* */
|
||||
location:
|
||||
snapToLayers?.length > 0
|
||||
? new UIEventSource<{ lon: number; lat: number }>(coordinate)
|
||||
: value,
|
||||
bounds: new UIEventSource<BBox>(undefined),
|
||||
allowMoving: new UIEventSource<boolean>(true),
|
||||
allowZooming: new UIEventSource<boolean>(true),
|
||||
minzoom: new UIEventSource<number>(18),
|
||||
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
|
||||
}
|
||||
|
||||
if (targetLayer) {
|
||||
const featuresForLayer = state.perLayer.get(targetLayer.id)
|
||||
if (featuresForLayer) {
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
features: featuresForLayer,
|
||||
})
|
||||
/**
|
||||
* The center of the map at all times
|
||||
* If undefined at the beginning, 'coordinate' will be used
|
||||
*/
|
||||
export let value: UIEventSource<{ lon: number; lat: number }>
|
||||
if (value.data === undefined) {
|
||||
value.setData(coordinate)
|
||||
}
|
||||
}
|
||||
|
||||
if (snapToLayers?.length > 0) {
|
||||
const snapSources: FeatureSource[] = []
|
||||
for (const layerId of snapToLayers ?? []) {
|
||||
const layer: FeatureSourceForLayer = state.perLayer.get(layerId)
|
||||
snapSources.push(layer)
|
||||
if (layer.features === undefined) {
|
||||
continue
|
||||
}
|
||||
new ShowDataLayer(map, {
|
||||
layer: layer.layer.layerDef,
|
||||
zoomToFeatures: false,
|
||||
features: layer,
|
||||
})
|
||||
if (coordinate === undefined) {
|
||||
coordinate = value.data
|
||||
}
|
||||
const snappedLocation = new SnappingFeatureSource(
|
||||
new FeatureSourceMerger(...Utils.NoNull(snapSources)),
|
||||
// We snap to the (constantly updating) map location
|
||||
initialMapProperties.location,
|
||||
{
|
||||
maxDistance: maxSnapDistance ?? 15,
|
||||
allowUnsnapped: true,
|
||||
snappedTo,
|
||||
snapLocation: value,
|
||||
}
|
||||
)
|
||||
export let snapToLayers: string[] | undefined
|
||||
export let targetLayer: LayerConfig | undefined
|
||||
export let maxSnapDistance: number = undefined
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
features: snappedLocation,
|
||||
})
|
||||
}
|
||||
export let snappedTo: UIEventSource<string | undefined>
|
||||
|
||||
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
|
||||
lon: number
|
||||
lat: number
|
||||
}>(undefined)
|
||||
|
||||
const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>()
|
||||
|
||||
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
|
||||
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let initialMapProperties: Partial<MapProperties> & {location} = {
|
||||
zoom: new UIEventSource<number>(19),
|
||||
maxbounds: new UIEventSource(undefined),
|
||||
/*If no snapping needed: the value is simply the map location;
|
||||
* If snapping is needed: the value will be set later on by the snapping feature source
|
||||
* */
|
||||
location:
|
||||
snapToLayers?.length > 0
|
||||
? new UIEventSource<{ lon: number; lat: number }>(coordinate)
|
||||
: value,
|
||||
bounds: new UIEventSource<BBox>(undefined),
|
||||
allowMoving: new UIEventSource<boolean>(true),
|
||||
allowZooming: new UIEventSource<boolean>(true),
|
||||
minzoom: new UIEventSource<number>(18),
|
||||
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
|
||||
}
|
||||
|
||||
if (targetLayer) {
|
||||
const featuresForLayer = state.perLayer.get(targetLayer.id)
|
||||
if (featuresForLayer) {
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
features: featuresForLayer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (snapToLayers?.length > 0) {
|
||||
const snapSources: FeatureSource[] = []
|
||||
for (const layerId of snapToLayers ?? []) {
|
||||
const layer: FeatureSourceForLayer = state.perLayer.get(layerId)
|
||||
snapSources.push(layer)
|
||||
if (layer.features === undefined) {
|
||||
continue
|
||||
}
|
||||
new ShowDataLayer(map, {
|
||||
layer: layer.layer.layerDef,
|
||||
zoomToFeatures: false,
|
||||
features: layer,
|
||||
})
|
||||
}
|
||||
const snappedLocation = new SnappingFeatureSource(
|
||||
new FeatureSourceMerger(...Utils.NoNull(snapSources)),
|
||||
// We snap to the (constantly updating) map location
|
||||
initialMapProperties.location,
|
||||
{
|
||||
maxDistance: maxSnapDistance ?? 15,
|
||||
allowUnsnapped: true,
|
||||
snappedTo,
|
||||
snapLocation: value,
|
||||
}
|
||||
)
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
features: snappedLocation,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<LocationInput
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
mapProperties={initialMapProperties}
|
||||
value={preciseLocation}
|
||||
initialCoordinate={coordinate}
|
||||
maxDistanceInMeters="50"
|
||||
maxDistanceInMeters={50}
|
||||
>
|
||||
<slot name="image" slot="image">
|
||||
<Move_arrows class="h-full max-h-24" />
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Search_disable from "../../assets/svg/Search_disable.svelte"
|
||||
|
||||
export let search: UIEventSource<string>
|
||||
|
||||
|
|
@ -27,8 +28,8 @@
|
|||
<h5>{t.noMatchingThemes.toString()}</h5>
|
||||
<div class="flex justify-center">
|
||||
<button on:click={() => search.setData("")}>
|
||||
<ToSvelte construct={Svg.search_disable_svg().SetClass("w-6 mr-2")} />
|
||||
<Tr slot="message" t={t.noSearch} />
|
||||
<Search_disable class="w-6 mr-2" />
|
||||
<Tr t={t.noSearch} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
**/
|
||||
import Motion from "../../Sensors/Motion"
|
||||
import { Geocoding } from "../../Logic/Osm/Geocoding"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Locale from "../i18n/Locale"
|
||||
|
|
@ -21,7 +20,7 @@
|
|||
let result = await Geocoding.reverse(
|
||||
mapProperties.location.data,
|
||||
mapProperties.zoom.data,
|
||||
Locale.language.data
|
||||
Locale.language.data,
|
||||
)
|
||||
let properties = result.features[0].properties
|
||||
currentLocation = properties.display_name
|
||||
|
|
@ -45,7 +44,7 @@
|
|||
() => {
|
||||
displayLocation()
|
||||
},
|
||||
[Translations.t.hotkeyDocumentation.shakePhone]
|
||||
[Translations.t.hotkeyDocumentation.shakePhone],
|
||||
)
|
||||
|
||||
Motion.singleton.startListening()
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
</h3>
|
||||
|
||||
<div
|
||||
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 p-1 pt-0.5 sm:pt-1"
|
||||
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 pt-0.5 sm:pt-1"
|
||||
>
|
||||
{#each layer.titleIcons as titleIconConfig}
|
||||
{#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...$metatags, ...$tags }) ?? true) && titleIconConfig.IsKnown($tags)}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
import Svg from "../../Svg"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import { DocumentDuplicateIcon } from "@rgossiaux/svelte-heroicons/outline"
|
||||
import Share from "../../assets/svg/Share.svelte"
|
||||
|
||||
export let state: ThemeViewState
|
||||
const tr = Translations.t.general.sharescreen
|
||||
|
|
@ -73,7 +74,7 @@
|
|||
<div class="flex">
|
||||
{#if typeof navigator?.share === "function"}
|
||||
<button class="h-8 w-8 shrink-0 p-1" on:click={shareCurrentLink}>
|
||||
<ToSvelte construct={Svg.share_svg()} />
|
||||
<Share/>
|
||||
</button>
|
||||
{/if}
|
||||
{#if navigator.clipboard !== undefined}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let feature: Feature
|
||||
|
|
@ -30,6 +29,6 @@
|
|||
{state}
|
||||
{tags}
|
||||
/>
|
||||
<DirectionIndicator {feature} {state} />
|
||||
</a>
|
||||
<DirectionIndicator {feature} {state} />
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@
|
|||
perLayer={state.perLayer}
|
||||
{selectedElement}
|
||||
{triggerSearch}
|
||||
geolocationState={state.geolocation.geolocationState}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
|
|
|
|||
69
src/UI/Comparison/ComparisonAction.svelte
Normal file
69
src/UI/Comparison/ComparisonAction.svelte
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
export let key: string
|
||||
export let externalProperties: Record<string, string>
|
||||
|
||||
export let tags: UIEventSource<OsmTags>
|
||||
export let state: SpecialVisualizationState
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
|
||||
export let readonly = false
|
||||
|
||||
let currentStep: "init" | "applying" | "done" = "init"
|
||||
|
||||
/**
|
||||
* Copy the given key into OSM
|
||||
* @param key
|
||||
*/
|
||||
async function apply(key: string) {
|
||||
currentStep = "applying"
|
||||
const change = new ChangeTagAction(
|
||||
tags.data.id,
|
||||
new Tag(key, externalProperties[key]),
|
||||
tags.data,
|
||||
{
|
||||
theme: state.layout.id,
|
||||
changeType: "import",
|
||||
})
|
||||
await state.changes.applyChanges(await change.CreateChangeDescriptions())
|
||||
currentStep = "done"
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<td><b>{key}</b></td>
|
||||
|
||||
<td>
|
||||
{#if externalProperties[key].startsWith("http")}
|
||||
<a href={externalProperties[key]} target="_blank">
|
||||
{externalProperties[key]}
|
||||
</a>
|
||||
{:else}
|
||||
{externalProperties[key]}
|
||||
{/if}
|
||||
</td>
|
||||
{#if !readonly}
|
||||
<td>
|
||||
{#if currentStep === "init"}
|
||||
<button class="small" on:click={() => apply(key)}>
|
||||
Apply
|
||||
</button>
|
||||
{:else if currentStep === "applying"}
|
||||
<Loading />
|
||||
{:else if currentStep === "done"}
|
||||
<div class="thanks">Done</div>
|
||||
{:else }
|
||||
<div class="alert">Error</div>
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
144
src/UI/Comparison/ComparisonTable.svelte
Normal file
144
src/UI/Comparison/ComparisonTable.svelte
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<script lang="ts">
|
||||
|
||||
import LinkableImage from "../Image/LinkableImage.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ComparisonAction from "./ComparisonAction.svelte"
|
||||
import Party from "../../assets/svg/Party.svelte"
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import AttributedImage from "../Image/AttributedImage.svelte"
|
||||
|
||||
export let osmProperties: Record<string, string>
|
||||
export let externalProperties: Record<string, string>
|
||||
|
||||
export let tags: UIEventSource<OsmTags>
|
||||
export let state: SpecialVisualizationState
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
|
||||
export let readonly = false
|
||||
|
||||
let externalKeys: string[] = (Object.keys(externalProperties))
|
||||
.sort()
|
||||
|
||||
const imageKeyRegex = /image|image:[0-9]+/
|
||||
console.log("Calculating knwon images")
|
||||
let knownImages = new Set(Object.keys(osmProperties).filter(k => k.match(imageKeyRegex))
|
||||
.map(k => osmProperties[k]))
|
||||
console.log("Known images are:", knownImages)
|
||||
let unknownImages = externalKeys.filter(k => k.match(imageKeyRegex))
|
||||
.map(k => externalProperties[k])
|
||||
.filter(i => !knownImages.has(i))
|
||||
|
||||
let propertyKeysExternal = externalKeys.filter(k => k.match(imageKeyRegex) === null)
|
||||
let missing = propertyKeysExternal.filter(k => osmProperties[k] === undefined)
|
||||
let same = propertyKeysExternal.filter(key => osmProperties[key] === externalProperties[key])
|
||||
let different = propertyKeysExternal.filter(key => osmProperties[key] !== undefined && osmProperties[key] !== externalProperties[key])
|
||||
|
||||
let currentStep: "init" | "applying_all" | "all_applied" = "init"
|
||||
|
||||
async function applyAllMissing() {
|
||||
currentStep = "applying_all"
|
||||
const tagsToApply = missing.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",
|
||||
})
|
||||
await state.changes.applyChanges(await change.CreateChangeDescriptions())
|
||||
currentStep = "all_applied"
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if different.length > 0}
|
||||
<h3>Conflicting items</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>OSM</th>
|
||||
<th>External</th>
|
||||
</tr>
|
||||
{#each different as key}
|
||||
<tr>
|
||||
<td>{key}</td>
|
||||
<td>{osmProperties[key]}</td>
|
||||
<td>{externalProperties[key]}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
{#if missing.length > 0}
|
||||
{#if currentStep === "init"}
|
||||
<table class="w-full">
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>External</th>
|
||||
</tr>
|
||||
|
||||
{#each missing as key}
|
||||
<ComparisonAction {key} {state} {tags} {externalProperties} {layer} {feature} {readonly} />
|
||||
{/each}
|
||||
|
||||
</table>
|
||||
{#if !readonly}
|
||||
<button on:click={() => applyAllMissing()}>Apply all missing values</button>
|
||||
{/if}
|
||||
{:else if currentStep === "applying_all"}
|
||||
<Loading>Applying all missing values</Loading>
|
||||
{:else if currentStep === "all_applied"}
|
||||
<div class="thanks">
|
||||
All values are applied
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
{#if unknownImages.length === 0 && missing.length === 0 && different.length === 0}
|
||||
<div class="thanks flex items-center gap-x-2 px-2 m-0">
|
||||
<Party class="w-8 h-8" />
|
||||
All data from Velopark is also included into OpenStreetMap
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if unknownImages.length > 0}
|
||||
{#if readonly}
|
||||
<div class="w-full overflow-x-auto">
|
||||
|
||||
<div class="flex w-max gap-x-2 h-32">
|
||||
{#each unknownImages as image}
|
||||
<AttributedImage imgClass="h-32 w-max shrink-0" image={{url:image}} previewedImage={state.previewedImage}/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each unknownImages as image}
|
||||
|
||||
<LinkableImage
|
||||
{tags}
|
||||
{state}
|
||||
image={{
|
||||
pictureUrl: image,
|
||||
provider: "Velopark",
|
||||
thumbUrl: image,
|
||||
details: undefined,
|
||||
coordinates: undefined,
|
||||
osmTags : {image}
|
||||
} }
|
||||
{feature}
|
||||
{layer} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
|
||||
63
src/UI/Comparison/ComparisonTool.svelte
Normal file
63
src/UI/Comparison/ComparisonTool.svelte
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<script lang="ts">/**
|
||||
* The comparison tool loads json-data from a speficied URL, eventually post-processes it
|
||||
* and compares it with the current object
|
||||
*/
|
||||
import { onMount } from "svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import VeloparkLoader from "../../Logic/Web/VeloparkLoader"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import ComparisonTable from "./ComparisonTable.svelte"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import type { Feature } from "geojson"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
|
||||
export let url: string
|
||||
export let postprocessVelopark: boolean
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: UIEventSource<OsmTags>
|
||||
export let layer: LayerConfig
|
||||
export let feature: Feature
|
||||
export let readonly = false
|
||||
let data: any = undefined
|
||||
let error: any = undefined
|
||||
|
||||
onMount(async () => {
|
||||
const _url = tags.data[url]
|
||||
if (!_url) {
|
||||
error = "No URL found in attribute" + url
|
||||
}
|
||||
try {
|
||||
console.log("Attempting to download", _url)
|
||||
const downloaded = await Utils.downloadJsonAdvanced(_url)
|
||||
if (downloaded["error"]) {
|
||||
console.error(downloaded)
|
||||
error = downloaded["error"]
|
||||
return
|
||||
}
|
||||
if (postprocessVelopark) {
|
||||
data = VeloparkLoader.convert(downloaded["content"])
|
||||
return
|
||||
}
|
||||
data = downloaded["content"]
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
error = "" + e
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{#if error !== undefined}
|
||||
<div class="alert">
|
||||
Something went wrong: {error}
|
||||
</div>
|
||||
{:else if data === undefined}
|
||||
<Loading>
|
||||
Loading {$tags[url]}
|
||||
</Loading>
|
||||
{:else if data.properties !== undefined}
|
||||
<ComparisonTable externalProperties={data.properties} osmProperties={$tags} {state} {feature} {layer} {tags} {readonly} />
|
||||
{/if}
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
mapExtent: state.mapProperties.bounds.data,
|
||||
width: maindiv.offsetWidth,
|
||||
height: maindiv.offsetHeight,
|
||||
noSelfIntersectingLines: true,
|
||||
noSelfIntersectingLines,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
|
||||
|
||||
export let feature: Feature
|
||||
let properties: Record<string, string> = feature.properties
|
||||
|
|
@ -30,7 +29,7 @@
|
|||
center()
|
||||
}
|
||||
|
||||
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
||||
let titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
||||
</script>
|
||||
|
||||
{#if favLayer !== undefined}
|
||||
|
|
@ -52,7 +51,7 @@
|
|||
class="title-icons links-as-button flex flex-wrap items-center gap-x-0.5 self-end justify-self-end p-1 pt-0.5 sm:pt-1"
|
||||
>
|
||||
{#each favConfig.titleIcons as titleIconConfig}
|
||||
{#if titleIconBlacklist.indexOf(titleIconConfig.id) < 0 && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...properties, ...state.userRelatedState.preferencesAsTags.data } ) ?? true) && titleIconConfig.IsKnown(properties)}
|
||||
{#if titleIconBlacklist.indexOf(titleIconConfig.id) < 0 && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...properties, ...state.userRelatedState.preferencesAsTags.data }) ?? true) && titleIconConfig.IsKnown(properties)}
|
||||
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
|
||||
<TagRenderingAnswer
|
||||
config={titleIconConfig}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { Utils } from "../../Utils"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import type { Feature, LineString, Point } from "geojson"
|
||||
import type { Feature, Point } from "geojson"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import LoginButton from "../Base/LoginButton.svelte"
|
||||
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
"mapcomplete-favourites-" + new Date().toISOString() + ".geojson",
|
||||
{
|
||||
mimetype: "application/vnd.geo+json",
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
"mapcomplete-favourites-" + new Date().toISOString() + ".gpx",
|
||||
{
|
||||
mimetype: "{gpx=application/gpx+xml}",
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
<Tr t={Translations.t.favouritePoi.intro.Subs({ length: $favourites?.length ?? 0 })} />
|
||||
<Tr t={Translations.t.favouritePoi.priintroPrivacyvacy} />
|
||||
<Tr t={Translations.t.favouritePoi.introPrivacy} />
|
||||
|
||||
{#each $favourites as feature (feature.properties.id)}
|
||||
<FavouriteSummary {feature} {state} />
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
on:click={() => {
|
||||
previewedImage?.setData(image)
|
||||
}}
|
||||
on:error={(event) => {
|
||||
on:error={() => {
|
||||
if (fallbackImage) {
|
||||
imgEl.src = fallbackImage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ export class ImageCarousel extends Toggle {
|
|||
changes?: Changes
|
||||
layout: LayoutConfig
|
||||
previewedImage?: UIEventSource<ProvidedImage>
|
||||
},
|
||||
feature: Feature
|
||||
}
|
||||
) {
|
||||
const uiElements = images.map(
|
||||
(imageURLS: { key: string; url: string; provider: ImageProvider; id: string }[]) => {
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@
|
|||
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Zoomcontrol from "../Zoomcontrol"
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let image: ProvidedImage
|
||||
let panzoomInstance = undefined
|
||||
let panzoomEl: HTMLElement
|
||||
export let isLoaded: UIEventSource<boolean> = undefined
|
||||
|
||||
|
||||
onDestroy(Zoomcontrol.createLock())
|
||||
|
||||
|
||||
$: {
|
||||
if (panzoomEl) {
|
||||
panzoomInstance = panzoom(panzoomEl, {
|
||||
|
|
|
|||
|
|
@ -15,18 +15,15 @@
|
|||
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
|
||||
|
||||
export let tags: UIEventSource<OsmTags>
|
||||
export let lon: number
|
||||
export let lat: number
|
||||
export let state: SpecialVisualizationState
|
||||
export let image: P4CPicture
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
|
||||
export let linkable = true
|
||||
let isLinked = Object.values(tags.data).some((v) => image.pictureUrl === v)
|
||||
|
||||
let targetValue = Object.values(image.osmTags)[0]
|
||||
let isLinked = new UIEventSource(Object.values(tags.data).some((v) => targetValue === v))
|
||||
const t = Translations.t.image.nearby
|
||||
const c = [lon, lat]
|
||||
const providedImage: ProvidedImage = {
|
||||
url: image.thumbUrl ?? image.pictureUrl,
|
||||
url_hd: image.pictureUrl,
|
||||
|
|
@ -36,10 +33,11 @@
|
|||
id: Object.values(image.osmTags)[0],
|
||||
}
|
||||
|
||||
$: {
|
||||
function applyLink(isLinked :boolean) {
|
||||
console.log("Applying linked image", isLinked, targetValue)
|
||||
const currentTags = tags.data
|
||||
const key = Object.keys(image.osmTags)[0]
|
||||
const url = image.osmTags[key]
|
||||
const url = targetValue
|
||||
if (isLinked) {
|
||||
const action = new LinkImageAction(currentTags.id, key, url, tags, {
|
||||
theme: tags.data._orig_theme ?? state.layout.id,
|
||||
|
|
@ -59,6 +57,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
isLinked.addCallback(isLinked => applyLink(isLinked))
|
||||
</script>
|
||||
|
||||
<div class="flex w-fit shrink-0 flex-col">
|
||||
|
|
@ -71,7 +70,7 @@
|
|||
</div>
|
||||
{#if linkable}
|
||||
<label>
|
||||
<input bind:checked={isLinked} type="checkbox" />
|
||||
<input bind:checked={$isLinked} type="checkbox" />
|
||||
<SpecialTranslation t={t.link} {tags} {state} {layer} {feature} />
|
||||
</label>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
<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} {lon} {lat} {feature} {layer} {linkable} />
|
||||
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
This is the old, deprecated directory. New, SVelte-based items go into `InputElement`
|
||||
This is the old, deprecated directory. New, Svelte-based items go into `InputElement`
|
||||
|
|
|
|||
|
|
@ -1,304 +0,0 @@
|
|||
import { UIElement } from "../UIElement"
|
||||
import { InputElement } from "./InputElement"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Locale from "../i18n/Locale"
|
||||
import Combine from "../Base/Combine"
|
||||
import { TextField } from "./TextField"
|
||||
import Svg from "../../Svg"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
|
||||
/**
|
||||
* A single 'pill' which can hide itself if the search criteria is not met
|
||||
*/
|
||||
class SelfHidingToggle extends UIElement implements InputElement<boolean> {
|
||||
public readonly _selected: UIEventSource<boolean>
|
||||
public readonly isShown: Store<boolean> = new UIEventSource<boolean>(true)
|
||||
public readonly matchesSearchCriteria: Store<boolean>
|
||||
public readonly forceSelected: UIEventSource<boolean>
|
||||
private readonly _shown: BaseUIElement
|
||||
private readonly _squared: boolean
|
||||
|
||||
public constructor(
|
||||
shown: string | BaseUIElement,
|
||||
mainTerm: Record<string, string>,
|
||||
search: Store<string>,
|
||||
options?: {
|
||||
searchTerms?: Record<string, string[]>
|
||||
selected?: UIEventSource<boolean>
|
||||
forceSelected?: UIEventSource<boolean>
|
||||
squared?: boolean
|
||||
/* Hide, if not selected*/
|
||||
hide?: Store<boolean>
|
||||
}
|
||||
) {
|
||||
super()
|
||||
this._shown = Translations.W(shown)
|
||||
this._squared = options?.squared ?? false
|
||||
const searchTerms: Record<string, string[]> = {}
|
||||
for (const lng in options?.searchTerms ?? []) {
|
||||
if (lng === "_context") {
|
||||
continue
|
||||
}
|
||||
searchTerms[lng] = options?.searchTerms[lng]?.map(SelfHidingToggle.clean)
|
||||
}
|
||||
for (const lng in mainTerm) {
|
||||
if (lng === "_context") {
|
||||
continue
|
||||
}
|
||||
const main = SelfHidingToggle.clean(mainTerm[lng])
|
||||
searchTerms[lng] = [main].concat(searchTerms[lng] ?? [])
|
||||
}
|
||||
const selected = (this._selected = options?.selected ?? new UIEventSource<boolean>(false))
|
||||
const forceSelected = (this.forceSelected =
|
||||
options?.forceSelected ?? new UIEventSource<boolean>(false))
|
||||
this.matchesSearchCriteria = search.map((s) => {
|
||||
if (s === undefined || s.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
s = s?.trim()?.toLowerCase()
|
||||
if (searchTerms[Locale.language.data]?.some((t) => t.indexOf(s) >= 0)) {
|
||||
return true
|
||||
}
|
||||
if (searchTerms["*"]?.some((t) => t.indexOf(s) >= 0)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
this.isShown = this.matchesSearchCriteria.map(
|
||||
(matchesSearch) => {
|
||||
if (selected.data && !forceSelected.data) {
|
||||
return true
|
||||
}
|
||||
if (options?.hide?.data) {
|
||||
return false
|
||||
}
|
||||
return matchesSearch
|
||||
},
|
||||
[selected, Locale.language, options?.hide]
|
||||
)
|
||||
|
||||
const self = this
|
||||
this.isShown.addCallbackAndRun((shown) => {
|
||||
if (shown) {
|
||||
self.RemoveClass("hidden")
|
||||
} else {
|
||||
self.SetClass("hidden")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private static clean(s: string): string {
|
||||
return s?.trim()?.toLowerCase()?.replace(/[-]/, "")
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<boolean> {
|
||||
return this._selected
|
||||
}
|
||||
|
||||
IsValid(t: boolean): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
protected InnerRender(): string | BaseUIElement {
|
||||
let el: BaseUIElement = this._shown
|
||||
const selected = this._selected
|
||||
|
||||
selected.addCallbackAndRun((selected) => {
|
||||
if (selected) {
|
||||
el.SetClass("border-4")
|
||||
el.RemoveClass("border")
|
||||
el.SetStyle("margin: 0")
|
||||
} else {
|
||||
el.SetStyle("margin: 3px")
|
||||
el.SetClass("border")
|
||||
el.RemoveClass("border-4")
|
||||
}
|
||||
})
|
||||
|
||||
const forcedSelection = this.forceSelected
|
||||
el.onClick(() => {
|
||||
if (forcedSelection.data) {
|
||||
selected.setData(true)
|
||||
} else {
|
||||
selected.setData(!selected.data)
|
||||
}
|
||||
})
|
||||
|
||||
if (!this._squared) {
|
||||
el.SetClass("rounded-full")
|
||||
}
|
||||
return el.SetClass("border border-black p-1 px-4")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The searchable mappings selector is a selector which shows various pills from which one (or more) options can be chosen.
|
||||
* A searchfield can be used to filter the values
|
||||
*/
|
||||
export class SearchablePillsSelector<T> extends Combine implements InputElement<T[]> {
|
||||
public readonly someMatchFound: Store<boolean>
|
||||
private readonly selectedElements: UIEventSource<T[]>
|
||||
|
||||
/**
|
||||
*
|
||||
* @param values: the values that can be selected
|
||||
* @param options
|
||||
*/
|
||||
constructor(
|
||||
values: {
|
||||
show: BaseUIElement
|
||||
value: T
|
||||
mainTerm: Record<string, string>
|
||||
searchTerms?: Record<string, string[]>
|
||||
/* If there are more then 200 elements, should this element still be shown? */
|
||||
hasPriority?: Store<boolean>
|
||||
}[],
|
||||
options?: {
|
||||
/*
|
||||
* If one single value can be selected (like a radio button) or if many values can be selected (like checkboxes)
|
||||
*/
|
||||
mode?: "select-one" | "select-many"
|
||||
/**
|
||||
* The values of the selected elements.
|
||||
* Use this to tie input elements together
|
||||
*/
|
||||
selectedElements?: UIEventSource<T[]>
|
||||
/**
|
||||
* The search bar. Use this to seed the search value or to tie to another value
|
||||
*/
|
||||
searchValue?: UIEventSource<string>
|
||||
/**
|
||||
* What is shown if the search yielded no results.
|
||||
* By default: a translated "no search results"
|
||||
*/
|
||||
onNoMatches?: BaseUIElement
|
||||
/**
|
||||
* An element that is shown if no search is entered
|
||||
* Default behaviour is to show all options
|
||||
*/
|
||||
onNoSearchMade?: BaseUIElement
|
||||
/**
|
||||
* Extra element to show if there are many (>200) possible mappings and when non-priority mappings are hidden
|
||||
*
|
||||
*/
|
||||
onManyElements?: BaseUIElement
|
||||
searchAreaClass?: string
|
||||
hideSearchBar?: false | boolean
|
||||
}
|
||||
) {
|
||||
const search = new TextField({ value: options?.searchValue })
|
||||
|
||||
const searchBar = options?.hideSearchBar
|
||||
? undefined
|
||||
: new Combine([
|
||||
Svg.search_svg().SetClass("w-8 normal-background"),
|
||||
search.SetClass("w-full"),
|
||||
]).SetClass("flex items-center border-2 border-black m-2")
|
||||
|
||||
const searchValue = search.GetValue().map((s) => s?.trim()?.toLowerCase())
|
||||
const selectedElements = options?.selectedElements ?? new UIEventSource<T[]>([])
|
||||
const mode = options?.mode ?? "select-one"
|
||||
const onEmpty = options?.onNoMatches ?? Translations.t.general.noMatchingMapping
|
||||
const forceHide = new UIEventSource(false)
|
||||
const mappedValues: {
|
||||
show: SelfHidingToggle
|
||||
mainTerm: Record<string, string>
|
||||
value: T
|
||||
}[] = values.map((v) => {
|
||||
const vIsSelected = new UIEventSource(false)
|
||||
|
||||
selectedElements.addCallbackAndRunD((selectedElements) => {
|
||||
vIsSelected.setData(selectedElements.some((t) => t === v.value))
|
||||
})
|
||||
|
||||
vIsSelected.addCallback((selected) => {
|
||||
if (selected) {
|
||||
if (mode === "select-one") {
|
||||
selectedElements.setData([v.value])
|
||||
} else if (!selectedElements.data.some((t) => t === v.value)) {
|
||||
selectedElements.data.push(v.value)
|
||||
selectedElements.ping()
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < selectedElements.data.length; i++) {
|
||||
const t = selectedElements.data[i]
|
||||
if (t == v.value) {
|
||||
selectedElements.data.splice(i, 1)
|
||||
selectedElements.ping()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const toggle = new SelfHidingToggle(v.show, v.mainTerm, searchValue, {
|
||||
searchTerms: v.searchTerms,
|
||||
selected: vIsSelected,
|
||||
squared: mode === "select-many",
|
||||
hide:
|
||||
v.hasPriority === undefined
|
||||
? forceHide
|
||||
: forceHide.map((fh) => fh && !v.hasPriority?.data, [v.hasPriority]),
|
||||
})
|
||||
|
||||
return {
|
||||
...v,
|
||||
show: toggle,
|
||||
}
|
||||
})
|
||||
|
||||
// The total number of elements that would be displayed based on the search criteria alone
|
||||
let totalShown: Store<number>
|
||||
totalShown = searchValue.map(
|
||||
(_) => mappedValues.filter((mv) => mv.show.matchesSearchCriteria.data).length
|
||||
)
|
||||
const tooMuchElementsCutoff = 40
|
||||
totalShown.addCallbackAndRunD((shown) => forceHide.setData(tooMuchElementsCutoff < shown))
|
||||
|
||||
super([
|
||||
searchBar,
|
||||
new VariableUiElement(
|
||||
Locale.language.map(
|
||||
(lng) => {
|
||||
if (
|
||||
options?.onNoSearchMade !== undefined &&
|
||||
(searchValue.data === undefined || searchValue.data.length === 0)
|
||||
) {
|
||||
return options?.onNoSearchMade
|
||||
}
|
||||
if (totalShown.data == 0) {
|
||||
return onEmpty
|
||||
}
|
||||
|
||||
mappedValues.sort((a, b) => (a.mainTerm[lng] < b.mainTerm[lng] ? -1 : 1))
|
||||
let pills = new Combine(mappedValues.map((e) => e.show))
|
||||
.SetClass("flex flex-wrap w-full content-start")
|
||||
.SetClass(options?.searchAreaClass ?? "")
|
||||
|
||||
if (totalShown.data >= tooMuchElementsCutoff) {
|
||||
pills = new Combine([
|
||||
options?.onManyElements ?? Translations.t.general.useSearch,
|
||||
pills,
|
||||
])
|
||||
}
|
||||
return pills
|
||||
},
|
||||
[totalShown, searchValue]
|
||||
)
|
||||
),
|
||||
])
|
||||
this.selectedElements = selectedElements
|
||||
this.someMatchFound = totalShown.map((t) => t > 0)
|
||||
}
|
||||
|
||||
public GetValue(): UIEventSource<T[]> {
|
||||
return this.selectedElements
|
||||
}
|
||||
|
||||
IsValid(t: T[]): boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@
|
|||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor"
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import Svg from "../../../Svg.js"
|
||||
import Direction_stroke from "../../../assets/svg/Direction_stroke.svelte"
|
||||
|
||||
/**
|
||||
|
|
@ -28,6 +26,7 @@
|
|||
})
|
||||
|
||||
let mainElem: HTMLElement
|
||||
|
||||
function onPosChange(x: number, y: number) {
|
||||
const rect = mainElem.getBoundingClientRect()
|
||||
const dx = -(rect.left + rect.right) / 2 + x
|
||||
|
|
@ -64,7 +63,7 @@
|
|||
on:touchstart={(e) => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}
|
||||
>
|
||||
<div class="absolute top-0 left-0 h-full w-full cursor-pointer">
|
||||
<MaplibreMap {map} attribution={false} />
|
||||
<MaplibreMap attribution={false} {map} />
|
||||
</div>
|
||||
|
||||
<div bind:this={directionElem} class="absolute top-0 left-0 h-full w-full">
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
function update() {
|
||||
const v = currentVal.data
|
||||
const l = currentLang.data
|
||||
if (translations.data === "" || translations.data === undefined) {
|
||||
if (<any> translations.data === "" || translations.data === undefined) {
|
||||
translations.data = {}
|
||||
}
|
||||
if (translations.data[l] === v) {
|
||||
|
|
@ -35,7 +35,6 @@
|
|||
|
||||
onDestroy(
|
||||
currentLang.addCallbackAndRunD((currentLang) => {
|
||||
console.log("Applying current lang:", currentLang)
|
||||
if (!translations.data) {
|
||||
translations.data = {}
|
||||
}
|
||||
|
|
@ -45,7 +44,7 @@
|
|||
)
|
||||
|
||||
onDestroy(
|
||||
currentVal.addCallbackAndRunD((v) => {
|
||||
currentVal.addCallbackAndRunD(() => {
|
||||
update()
|
||||
})
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,12 @@
|
|||
* This is only copied to 'value' when appropriate so that no invalid values leak outside;
|
||||
* Additionally, the unit is added when copying
|
||||
*/
|
||||
let _value = new UIEventSource(value.data ?? "")
|
||||
export let unvalidatedText = new UIEventSource(value.data ?? "")
|
||||
|
||||
|
||||
if(unvalidatedText == /*Compare by reference!*/ value){
|
||||
throw "Value and unvalidatedText may not be the same store!"
|
||||
}
|
||||
let validator: Validator = Validators.get(type ?? "string")
|
||||
if (validator === undefined) {
|
||||
console.warn("Didn't find a validator for type", type)
|
||||
|
|
@ -41,13 +45,13 @@
|
|||
if (unit && value.data) {
|
||||
const [v, denom] = unit?.findDenomination(value.data, getCountry)
|
||||
if (denom) {
|
||||
_value.setData(v)
|
||||
unvalidatedText.setData(v)
|
||||
selectedUnit.setData(denom.canonical)
|
||||
} else {
|
||||
_value.setData(value.data ?? "")
|
||||
unvalidatedText.setData(value.data ?? "")
|
||||
}
|
||||
} else {
|
||||
_value.setData(value.data ?? "")
|
||||
unvalidatedText.setData(value.data ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,8 +71,8 @@
|
|||
validator = Validators.get(type ?? "string")
|
||||
|
||||
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
||||
if (_value.data?.length > 0) {
|
||||
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
|
||||
if (unvalidatedText.data?.length > 0) {
|
||||
feedback?.setData(validator?.getFeedback(unvalidatedText.data, getCountry))
|
||||
} else {
|
||||
feedback?.setData(undefined)
|
||||
}
|
||||
|
|
@ -78,7 +82,7 @@
|
|||
|
||||
function setValues() {
|
||||
// Update the value stores
|
||||
const v = _value.data
|
||||
const v = unvalidatedText.data
|
||||
if (v === "") {
|
||||
value.setData(undefined)
|
||||
feedback?.setData(undefined)
|
||||
|
|
@ -103,12 +107,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
onDestroy(_value.addCallbackAndRun((_) => setValues()))
|
||||
onDestroy(unvalidatedText.addCallbackAndRun((_) => setValues()))
|
||||
if (unit === undefined) {
|
||||
onDestroy(
|
||||
value.addCallbackAndRunD((fromUpstream) => {
|
||||
if (_value.data !== fromUpstream && fromUpstream !== "") {
|
||||
_value.setData(fromUpstream)
|
||||
if (unvalidatedText.data !== fromUpstream && fromUpstream !== "") {
|
||||
unvalidatedText.setData(fromUpstream)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
@ -131,7 +135,7 @@
|
|||
)
|
||||
}
|
||||
|
||||
const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true)
|
||||
const isValid = unvalidatedText.map((v) => validator?.isValid(v, getCountry) ?? true)
|
||||
|
||||
let htmlElem: HTMLInputElement | HTMLTextAreaElement
|
||||
|
||||
|
|
@ -149,7 +153,7 @@
|
|||
{#if validator?.textArea}
|
||||
<textarea
|
||||
class="w-full"
|
||||
bind:value={$_value}
|
||||
bind:value={$unvalidatedText}
|
||||
inputmode={validator?.inputmode ?? "text"}
|
||||
placeholder={_placeholder}
|
||||
bind:this={htmlElem}
|
||||
|
|
@ -159,7 +163,7 @@
|
|||
<div class={twMerge("inline-flex", cls)}>
|
||||
<input
|
||||
bind:this={htmlElem}
|
||||
bind:value={$_value}
|
||||
bind:value={$unvalidatedText}
|
||||
class="w-full"
|
||||
inputmode={validator?.inputmode ?? "text"}
|
||||
placeholder={_placeholder}
|
||||
|
|
@ -170,7 +174,7 @@
|
|||
{/if}
|
||||
|
||||
{#if unit !== undefined}
|
||||
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} {getCountry} />
|
||||
<UnitInput {unit} {selectedUnit} textValue={unvalidatedText} upstreamValue={value} {getCountry} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export abstract class Validator {
|
|||
return Translations.t.validation[this.name].description
|
||||
}
|
||||
|
||||
public isValid(key: string, getCountry?: () => string): boolean {
|
||||
public isValid(_: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import IconValidator from "./Validators/IconValidator"
|
|||
import TagValidator from "./Validators/TagValidator"
|
||||
import IdValidator from "./Validators/IdValidator"
|
||||
import SlopeValidator from "./Validators/SlopeValidator"
|
||||
import VeloparkValidator from "./Validators/VeloparkValidator"
|
||||
|
||||
export type ValidatorType = (typeof Validators.availableTypes)[number]
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ export default class Validators {
|
|||
"fediverse",
|
||||
"id",
|
||||
"slope",
|
||||
"velopark"
|
||||
] as const
|
||||
|
||||
public static readonly AllValidators: ReadonlyArray<Validator> = [
|
||||
|
|
@ -86,6 +88,7 @@ export default class Validators {
|
|||
new FediverseValidator(),
|
||||
new IdValidator(),
|
||||
new SlopeValidator(),
|
||||
new VeloparkValidator()
|
||||
]
|
||||
|
||||
private static _byType = Validators._byTypeConstructor()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Translation } from "../../i18n/Translation"
|
|||
import Translations from "../../i18n/Translations"
|
||||
|
||||
export default class FediverseValidator extends Validator {
|
||||
public static readonly usernameAtServer: RegExp = /^@?(\w+)@((\w|\.)+)$/
|
||||
public static readonly usernameAtServer: RegExp = /^@?(\w+)@((\w|-|\.)+)$/
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ export default class PhoneValidator extends Validator {
|
|||
if (country !== undefined) {
|
||||
countryCode = country()?.toUpperCase()
|
||||
}
|
||||
if (this.isShortCode(str, countryCode)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false
|
||||
}
|
||||
|
||||
|
|
@ -46,9 +50,28 @@ export default class PhoneValidator extends Validator {
|
|||
if (country) {
|
||||
countryCode = country()
|
||||
}
|
||||
if (this.isShortCode(str, countryCode?.toUpperCase())) {
|
||||
return str
|
||||
}
|
||||
return parsePhoneNumberFromString(
|
||||
str,
|
||||
countryCode?.toUpperCase() as any
|
||||
)?.formatInternational()
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the given string is a special 'short code' valid in the given country
|
||||
* see https://nl.wikipedia.org/wiki/Short_code
|
||||
* @param str a possible phone number
|
||||
* @param country the upper case, two-letter code for a country
|
||||
* @private
|
||||
*/
|
||||
private isShortCode(str: string, country: string) {
|
||||
if (country == "BE" && str.length === 4 && str.match(/[0-9]{4}/)) {
|
||||
return true
|
||||
}
|
||||
if (country == "NL" && str.length === 4 && str.match(/14[0-9]{3}/)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import { Validator } from "../Validator"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import TagKeyValidator from "./TagKeyValidator"
|
||||
import SimpleTagValidator from "./SimpleTagValidator"
|
||||
|
||||
/**
|
||||
* Checks that the input conforms a JSON-encoded tag expression or a simpleTag`key=value`,
|
||||
*/
|
||||
export default class TagValidator extends Validator {
|
||||
public readonly isMeta = true
|
||||
|
||||
constructor() {
|
||||
super("tag", "A simple tag of the format `key=value` OR a tagExpression")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default class TranslationValidator extends Validator {
|
|||
super("translation", "Makes sure the the string is of format `Record<string, string>` ")
|
||||
}
|
||||
|
||||
isValid(value: string, getCountry?: () => string): boolean {
|
||||
isValid(value: string): boolean {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
return true
|
||||
|
|
|
|||
37
src/UI/InputElement/Validators/VeloparkValidator.ts
Normal file
37
src/UI/InputElement/Validators/VeloparkValidator.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Translation } from "../../i18n/Translation"
|
||||
import UrlValidator from "./UrlValidator"
|
||||
|
||||
export default class VeloparkValidator extends UrlValidator {
|
||||
constructor() {
|
||||
super("velopark", "A custom element to allow copy-pasting velopark-pages")
|
||||
}
|
||||
|
||||
getFeedback(s: string): Translation {
|
||||
const superF = super.getFeedback(s)
|
||||
if (superF) {
|
||||
return superF
|
||||
}
|
||||
const url = new URL(s)
|
||||
if (url.hostname !== "velopark.be" && url.hostname !== "www.velopark.be" && url.hostname !== "data.velopark.be") {
|
||||
return new Translation({ "*": "Invalid hostname, expected velopark.be" })
|
||||
}
|
||||
|
||||
if(!s.startsWith("https://data.velopark.be/data/") && !s.startsWith("https://www.velopark.be/static/data/")){
|
||||
return new Translation({"*":"A valid URL should either start with https://data.velopark.be/data/ or https://www.velopark.be/static/data/"})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public isValid(str: string) {
|
||||
return super.isValid(str)
|
||||
}
|
||||
|
||||
reformat(str: string): string {
|
||||
const url = new URL(super.reformat(str))
|
||||
if(url.pathname.startsWith("/static/data/")){
|
||||
const id = str.substring(str.lastIndexOf("/")+1)
|
||||
return "https://data.velopark.be/data/"+id
|
||||
}
|
||||
return super.reformat(str)
|
||||
}
|
||||
}
|
||||
89
src/UI/Map/BingRasterLayer.ts
Normal file
89
src/UI/Map/BingRasterLayer.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { Polygon } from "geojson"
|
||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export class BingRasterLayerProperties implements Partial<RasterLayerProperties> {
|
||||
private static singleton: BingRasterLayerProperties | "error"
|
||||
name = "Bing Maps Aerial"
|
||||
id = "Bing"
|
||||
type = "bing"
|
||||
category = "photo"
|
||||
min_zoom = 1
|
||||
max_zoom = 22
|
||||
best = false
|
||||
attribution = {
|
||||
url: "https://www.bing.com/maps",
|
||||
}
|
||||
url: string
|
||||
|
||||
private constructor(url: string) {
|
||||
this.url = url
|
||||
}
|
||||
|
||||
public static async get(): Promise<BingRasterLayerProperties | "error"> {
|
||||
if (BingRasterLayerProperties.singleton === undefined) {
|
||||
try {
|
||||
const url = await this.getEndpoint()
|
||||
BingRasterLayerProperties.singleton = new BingRasterLayerProperties(url)
|
||||
} catch (e) {
|
||||
BingRasterLayerProperties.singleton = "error"
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
return BingRasterLayerProperties.singleton
|
||||
}
|
||||
|
||||
private static async getEndpoint() {
|
||||
console.log("Getting bing endpoint")
|
||||
// Key by 'pietervdvn@outlook.com' from https://www.bingmapsportal.com/Application
|
||||
// Inspired by https://github.com/zlant/parking-lanes/pull/159
|
||||
const key = "An0vKZ4r_PZx820sn3seuPKjd1Vyc5WE3s1b-qN4HCgI-Nr6QR83aLOQ-3fbFl08"
|
||||
const url = `https://dev.virtualearth.net/REST/v1/Imagery/Metadata/AerialOSM?include=ImageryProviders&uriScheme=https&key=${key}`
|
||||
|
||||
// Get the image tiles template:
|
||||
const metadata = await Utils.downloadJson(url)
|
||||
// FYI:
|
||||
// "imageHeight": 256, "imageWidth": 256,
|
||||
// "imageUrlSubdomains": ["t0","t1","t2","t3"],
|
||||
// "zoomMax": 21,
|
||||
const imageryResource = metadata.resourceSets[0].resources[0]
|
||||
const template = new URL(imageryResource.imageUrl)
|
||||
// Add tile image strictness param (n=)
|
||||
// • n=f -> (Fail) returns a 404
|
||||
// • n=z -> (Empty) returns a 200 with 0 bytes (no content)
|
||||
// • n=t -> (Transparent) returns a 200 with a transparent (png) tile
|
||||
if (!template.searchParams.has("n")) template.searchParams.append("n", "f")
|
||||
|
||||
// FYI: `template` looks like this but partly encoded
|
||||
// https://ecn.{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=14107&pr=odbl&n=z
|
||||
const subdomains = ["t0", "t1", "t2", "t3"]
|
||||
const index = Math.floor(Math.random() * subdomains.length)
|
||||
return template.toString().replace("{subdomain}", subdomains[index])
|
||||
}
|
||||
}
|
||||
|
||||
export class BingRasterLayer implements RasterLayerPolygon {
|
||||
private static singleton: RasterLayerPolygon | "error"
|
||||
readonly type: "Feature" = "Feature"
|
||||
readonly geometry: Polygon = BBox.global.asGeometry()
|
||||
readonly id = "bing"
|
||||
readonly properties: RasterLayerProperties
|
||||
|
||||
constructor(properties: RasterLayerProperties) {
|
||||
this.properties = properties
|
||||
}
|
||||
|
||||
public static async get(): Promise<RasterLayerPolygon | "error"> {
|
||||
if (BingRasterLayer.singleton === undefined) {
|
||||
const properties = await BingRasterLayerProperties.get()
|
||||
if (properties === "error") {
|
||||
BingRasterLayer.singleton = "error"
|
||||
} else {
|
||||
BingRasterLayer.singleton = new BingRasterLayer(properties)
|
||||
}
|
||||
}
|
||||
return BingRasterLayer.singleton
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,49 @@
|
|||
<script lang="ts">
|
||||
import Pin from "../../assets/svg/Pin.svelte"
|
||||
import Square from "../../assets/svg/Square.svelte"
|
||||
import Circle from "../../assets/svg/Circle.svelte"
|
||||
import Checkmark from "../../assets/svg/Checkmark.svelte"
|
||||
import Clock from "../../assets/svg/Clock.svelte"
|
||||
import Close from "../../assets/svg/Close.svelte"
|
||||
import Crosshair from "../../assets/svg/Crosshair.svelte"
|
||||
import Help from "../../assets/svg/Help.svelte"
|
||||
import Home from "../../assets/svg/Home.svelte"
|
||||
import Invalid from "../../assets/svg/Invalid.svelte"
|
||||
import Location from "../../assets/svg/Location.svelte"
|
||||
import Location_empty from "../../assets/svg/Location_empty.svelte"
|
||||
import Location_locked from "../../assets/svg/Location_locked.svelte"
|
||||
import Note from "../../assets/svg/Note.svelte"
|
||||
import Resolved from "../../assets/svg/Resolved.svelte"
|
||||
import Ring from "../../assets/svg/Ring.svelte"
|
||||
import Scissors from "../../assets/svg/Scissors.svelte"
|
||||
import Teardrop from "../../assets/svg/Teardrop.svelte"
|
||||
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
|
||||
import Triangle from "../../assets/svg/Triangle.svelte"
|
||||
import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte"
|
||||
import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte"
|
||||
import Gps_arrow from "../../assets/svg/Gps_arrow.svelte"
|
||||
import { HeartIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline"
|
||||
import Confirm from "../../assets/svg/Confirm.svelte"
|
||||
import Not_found from "../../assets/svg/Not_found.svelte"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import Direction_gradient from "../../assets/svg/Direction_gradient.svelte"
|
||||
import Mastodon from "../../assets/svg/Mastodon.svelte"
|
||||
import Party from "../../assets/svg/Party.svelte"
|
||||
import AddSmall from "../../assets/svg/AddSmall.svelte"
|
||||
import Pin from "../../assets/svg/Pin.svelte"
|
||||
import Square from "../../assets/svg/Square.svelte"
|
||||
import Circle from "../../assets/svg/Circle.svelte"
|
||||
import Checkmark from "../../assets/svg/Checkmark.svelte"
|
||||
import Clock from "../../assets/svg/Clock.svelte"
|
||||
import Close from "../../assets/svg/Close.svelte"
|
||||
import Crosshair from "../../assets/svg/Crosshair.svelte"
|
||||
import Help from "../../assets/svg/Help.svelte"
|
||||
import Home from "../../assets/svg/Home.svelte"
|
||||
import Invalid from "../../assets/svg/Invalid.svelte"
|
||||
import Location from "../../assets/svg/Location.svelte"
|
||||
import Location_empty from "../../assets/svg/Location_empty.svelte"
|
||||
import Location_locked from "../../assets/svg/Location_locked.svelte"
|
||||
import Note from "../../assets/svg/Note.svelte"
|
||||
import Resolved from "../../assets/svg/Resolved.svelte"
|
||||
import Ring from "../../assets/svg/Ring.svelte"
|
||||
import Scissors from "../../assets/svg/Scissors.svelte"
|
||||
import Teardrop from "../../assets/svg/Teardrop.svelte"
|
||||
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
|
||||
import Triangle from "../../assets/svg/Triangle.svelte"
|
||||
import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte"
|
||||
import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte"
|
||||
import Gps_arrow from "../../assets/svg/Gps_arrow.svelte"
|
||||
import { HeartIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline"
|
||||
import Confirm from "../../assets/svg/Confirm.svelte"
|
||||
import Not_found from "../../assets/svg/Not_found.svelte"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import Direction_gradient from "../../assets/svg/Direction_gradient.svelte"
|
||||
import Mastodon from "../../assets/svg/Mastodon.svelte"
|
||||
import Party from "../../assets/svg/Party.svelte"
|
||||
import AddSmall from "../../assets/svg/AddSmall.svelte"
|
||||
import { LinkIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import Square_rounded from "../../assets/svg/Square_rounded.svelte"
|
||||
import Bug from "../../assets/svg/Bug.svelte"
|
||||
|
||||
/**
|
||||
* Renders a single icon.
|
||||
*
|
||||
* Icons -placed on top of each other- form a 'Marker' together
|
||||
*/
|
||||
/**
|
||||
* Renders a single icon.
|
||||
*
|
||||
* Icons -placed on top of each other- form a 'Marker' together
|
||||
*/
|
||||
|
||||
export let icon: string | undefined
|
||||
export let color: string | undefined = undefined
|
||||
export let clss: string | undefined = undefined
|
||||
export let icon: string | undefined
|
||||
export let color: string | undefined = undefined
|
||||
export let clss: string | undefined = undefined
|
||||
|
||||
</script>
|
||||
|
||||
|
|
@ -49,6 +52,11 @@
|
|||
<Pin {color} class={clss} />
|
||||
{:else if icon === "square"}
|
||||
<Square {color} class={clss} />
|
||||
{:else if icon === "square_rounded"}
|
||||
<Square_rounded {color} class={clss} />
|
||||
{:else if icon === "bug"}
|
||||
<Bug {color} class={clss} />
|
||||
|
||||
{:else if icon === "circle"}
|
||||
<Circle {color} class={clss} />
|
||||
{:else if icon === "checkmark"}
|
||||
|
|
@ -115,6 +123,8 @@
|
|||
<Party {color} class={clss} />
|
||||
{:else if icon === "addSmall"}
|
||||
<AddSmall {color} class={clss} />
|
||||
{:else if icon === "link"}
|
||||
<LinkIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} />
|
||||
{:else}
|
||||
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MLMap } from "maplibre-gl"
|
||||
import { Map as MlMap, SourceSpecification } from "maplibre-gl"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { ExportableMap, KeyNavigationEvent, MapProperties } from "../../Models/MapProperties"
|
||||
|
|
@ -9,6 +9,8 @@ import SvelteUIElement from "../Base/SvelteUIElement"
|
|||
import MaplibreMap from "./MaplibreMap.svelte"
|
||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
import * as htmltoimage from "html-to-image"
|
||||
import RasterLayerHandler from "./RasterLayerHandler"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
/**
|
||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||
|
|
@ -41,7 +43,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
readonly minzoom: UIEventSource<number>
|
||||
readonly maxzoom: UIEventSource<number>
|
||||
readonly rotation: UIEventSource<number>
|
||||
readonly animationRunning = new UIEventSource(false)
|
||||
readonly pitch: UIEventSource<number>
|
||||
readonly useTerrain: Store<boolean>
|
||||
|
||||
/**
|
||||
* Functions that are called when one of those actions has happened
|
||||
|
|
@ -50,11 +53,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
private _onKeyNavigation: ((event: KeyNavigationEvent) => void | boolean)[] = []
|
||||
|
||||
private readonly _maplibreMap: Store<MLMap>
|
||||
/**
|
||||
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
||||
* @private
|
||||
*/
|
||||
private _currentRasterLayer: string
|
||||
|
||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>) {
|
||||
this._maplibreMap = maplibreMap
|
||||
|
|
@ -83,6 +81,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
this.allowZooming = state?.allowZooming ?? new UIEventSource(true)
|
||||
this.bounds = state?.bounds ?? new UIEventSource(undefined)
|
||||
this.rotation = state?.rotation ?? new UIEventSource<number>(0)
|
||||
this.pitch = state?.pitch ?? new UIEventSource<number>(0)
|
||||
this.useTerrain = state?.useTerrain ?? new ImmutableStore<boolean>(false)
|
||||
this.rasterLayer =
|
||||
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
|
||||
|
||||
|
|
@ -90,6 +90,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
this.lastClickLocation = lastClickLocation
|
||||
const self = this
|
||||
|
||||
const rasterLayerHandler = new RasterLayerHandler(this._maplibreMap, this.rasterLayer)
|
||||
|
||||
function handleClick(e) {
|
||||
if (e.originalEvent["consumed"]) {
|
||||
// Workaround, 'ShowPointLayer' sets this flag
|
||||
|
|
@ -111,7 +113,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
self.setMinzoom(self.minzoom.data)
|
||||
self.setMaxzoom(self.maxzoom.data)
|
||||
self.setBounds(self.bounds.data)
|
||||
self.setBackground()
|
||||
self.setTerrain(self.useTerrain.data)
|
||||
rasterLayerHandler.setBackground()
|
||||
this.updateStores(true)
|
||||
})
|
||||
self.MoveMapToCurrentLoc(self.location.data)
|
||||
|
|
@ -124,7 +127,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
self.setMaxzoom(self.maxzoom.data)
|
||||
self.setBounds(self.bounds.data)
|
||||
self.SetRotation(self.rotation.data)
|
||||
self.setBackground()
|
||||
self.setTerrain(self.useTerrain.data)
|
||||
rasterLayerHandler.setBackground()
|
||||
this.updateStores(true)
|
||||
map.on("moveend", () => this.updateStores())
|
||||
map.on("click", (e) => {
|
||||
|
|
@ -136,7 +140,10 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
map.on("dblclick", (e) => {
|
||||
handleClick(e)
|
||||
})
|
||||
map.on("rotateend", (e) => {
|
||||
map.on("rotateend", (_) => {
|
||||
this.updateStores()
|
||||
})
|
||||
map.on("pitchend", () => {
|
||||
this.updateStores()
|
||||
})
|
||||
map.getContainer().addEventListener("keydown", (event) => {
|
||||
|
|
@ -170,11 +177,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
})
|
||||
})
|
||||
|
||||
this.rasterLayer.addCallbackAndRun((_) =>
|
||||
self.setBackground().catch((_) => {
|
||||
console.error("Could not set background")
|
||||
})
|
||||
)
|
||||
this.rasterLayer.addCallbackAndRun((_) => rasterLayerHandler.setBackground())
|
||||
this.location.addCallbackAndRunD((loc) => {
|
||||
self.MoveMapToCurrentLoc(loc)
|
||||
})
|
||||
|
|
@ -190,6 +193,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
)
|
||||
this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming))
|
||||
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
|
||||
this.useTerrain?.addCallbackAndRun(useTerrain => self.setTerrain(useTerrain))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -211,48 +215,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
}
|
||||
|
||||
public static prepareWmsSource(layer: RasterLayerProperties): SourceSpecification {
|
||||
return {
|
||||
type: "raster",
|
||||
// use the tiles option to specify a 256WMS tile source URL
|
||||
// https://maplibre.org/maplibre-gl-js-docs/style-spec/sources/
|
||||
tiles: [MapLibreAdaptor.prepareWmsURL(layer.url, layer["tile-size"] ?? 256)],
|
||||
tileSize: layer["tile-size"] ?? 256,
|
||||
minzoom: layer["min_zoom"] ?? 1,
|
||||
maxzoom: layer["max_zoom"] ?? 25,
|
||||
// Bit of a hack, but seems to work
|
||||
scheme: layer.url.includes("{-y}") ? "tms" : "xyz",
|
||||
}
|
||||
return RasterLayerHandler.prepareWmsSource(layer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares an ELI-URL to be compatible with mapbox
|
||||
*/
|
||||
private static prepareWmsURL(url: string, size: number = 256): string {
|
||||
// ELI: LAYERS=OGWRGB13_15VL&STYLES=&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap
|
||||
// PROD: SERVICE=WMS&REQUEST=GetMap&LAYERS=OGWRGB13_15VL&STYLES=&FORMAT=image/jpeg&TRANSPARENT=false&VERSION=1.3.0&WIDTH=256&HEIGHT=256&CRS=EPSG:3857&BBOX=488585.4847988467,6590094.830634755,489196.9810251281,6590706.32686104
|
||||
|
||||
const toReplace = {
|
||||
"{bbox}": "{bbox-epsg-3857}",
|
||||
"{proj}": "EPSG:3857",
|
||||
"{width}": "" + size,
|
||||
"{height}": "" + size,
|
||||
"{zoom}": "{z}",
|
||||
"{-y}": "{y}",
|
||||
}
|
||||
|
||||
for (const key in toReplace) {
|
||||
url = url.replace(new RegExp(key), toReplace[key])
|
||||
}
|
||||
|
||||
const subdomains = url.match(/\{switch:([a-zA-Z0-9,]*)}/)
|
||||
if (subdomains !== null) {
|
||||
const options = subdomains[1].split(",")
|
||||
const option = options[Math.floor(Math.random() * options.length)]
|
||||
url = url.replace(subdomains[0], option)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
private static async toBlob(canvas: HTMLCanvasElement): Promise<Blob> {
|
||||
return await new Promise<Blob>((resolve) => canvas.toBlob((blob) => resolve(blob)))
|
||||
|
|
@ -291,7 +259,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
const w = map.getContainer().getBoundingClientRect().width
|
||||
const h = map.getContainer().getBoundingClientRect().height
|
||||
|
||||
let dpi = map.getPixelRatio()
|
||||
const dpi = map.getPixelRatio()
|
||||
// The 'css'-size stays constant...
|
||||
drawOn.style.width = w + "px"
|
||||
drawOn.style.height = h + "px"
|
||||
|
|
@ -466,6 +434,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
this.bounds.setData(bbox)
|
||||
}
|
||||
this.rotation.setData(map.getBearing())
|
||||
this.pitch.setData(map.getPitch())
|
||||
}
|
||||
|
||||
private SetZoom(z: number): void {
|
||||
|
|
@ -502,15 +471,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
}
|
||||
}
|
||||
|
||||
private async awaitStyleIsLoaded(): Promise<void> {
|
||||
const map = this._maplibreMap.data
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
while (!map?.isStyleLoaded()) {
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
}
|
||||
public installCustomKeyboardHandler(viewportStore: UIEventSource<HTMLDivElement>) {
|
||||
viewportStore.mapD(
|
||||
(viewport) => {
|
||||
|
|
@ -534,111 +494,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
[this._maplibreMap]
|
||||
)
|
||||
}
|
||||
private removeCurrentLayer(map: MLMap): void {
|
||||
if (this._currentRasterLayer) {
|
||||
// hide the previous layer
|
||||
try {
|
||||
if (map.getLayer(this._currentRasterLayer)) {
|
||||
map.removeLayer(this._currentRasterLayer)
|
||||
}
|
||||
if (map.getSource(this._currentRasterLayer)) {
|
||||
map.removeSource(this._currentRasterLayer)
|
||||
}
|
||||
this._currentRasterLayer = undefined
|
||||
} catch (e) {
|
||||
console.warn("Could not remove the previous layer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async setBackground(retryAttempts: number = 3): Promise<void> {
|
||||
const map = this._maplibreMap.data
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
const background: RasterLayerProperties = this.rasterLayer?.data?.properties
|
||||
if (!background) {
|
||||
return
|
||||
}
|
||||
if (this._currentRasterLayer === background.id) {
|
||||
// already the correct background layer, nothing to do
|
||||
return
|
||||
}
|
||||
|
||||
if (!background?.url) {
|
||||
// no background to set
|
||||
this.removeCurrentLayer(map)
|
||||
return
|
||||
}
|
||||
|
||||
if (background.type === "vector") {
|
||||
this.removeCurrentLayer(map)
|
||||
map.setStyle(background.url)
|
||||
return
|
||||
}
|
||||
|
||||
let addLayerBeforeId = "aeroway_fill" // this is the first non-landuse item in the stylesheet, we add the raster layer before the roads but above the landuse
|
||||
if (background.category === "osmbasedmap" || background.category === "map") {
|
||||
// The background layer is already an OSM-based map or another map, so we don't want anything from the baselayer
|
||||
addLayerBeforeId = undefined
|
||||
this.removeCurrentLayer(map)
|
||||
} else {
|
||||
// Make sure that the default maptiler style is loaded as it gives an overlay with roads
|
||||
const maptiler = AvailableRasterLayers.maptilerDefaultLayer.properties
|
||||
try {
|
||||
|
||||
await this.awaitStyleIsLoaded()
|
||||
if (!map.getSource(maptiler.id)) {
|
||||
this.removeCurrentLayer(map)
|
||||
map.addSource(maptiler.id, MapLibreAdaptor.prepareWmsSource(maptiler))
|
||||
map.setStyle(maptiler.url)
|
||||
await this.awaitStyleIsLoaded()
|
||||
}
|
||||
}catch (e) {
|
||||
if(retryAttempts > 0){
|
||||
window.requestAnimationFrame(() => {
|
||||
console.log("Retrying to set the background ("+retryAttempts+" attempts remaining)... Failed because",e)
|
||||
this.setBackground(retryAttempts-1)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!map.getLayer(addLayerBeforeId)) {
|
||||
addLayerBeforeId = undefined
|
||||
}
|
||||
await this.awaitStyleIsLoaded()
|
||||
if (!map.getSource(background.id)) {
|
||||
map.addSource(background.id, MapLibreAdaptor.prepareWmsSource(background))
|
||||
}
|
||||
if (!map.getLayer(background.id)) {
|
||||
addLayerBeforeId ??= map
|
||||
.getStyle()
|
||||
.layers.find((l) => l.id.startsWith("mapcomplete_"))?.id
|
||||
console.log(
|
||||
"Adding background layer",
|
||||
background.id,
|
||||
"beforeId",
|
||||
addLayerBeforeId,
|
||||
"; all layers are",
|
||||
map.getStyle().layers.map((l) => l.id)
|
||||
)
|
||||
map.addLayer(
|
||||
{
|
||||
id: background.id,
|
||||
type: "raster",
|
||||
source: background.id,
|
||||
paint: {},
|
||||
},
|
||||
addLayerBeforeId
|
||||
)
|
||||
}
|
||||
await this.awaitStyleIsLoaded()
|
||||
if (this._currentRasterLayer !== background?.id) {
|
||||
this.removeCurrentLayer(map)
|
||||
}
|
||||
this._currentRasterLayer = background?.id
|
||||
}
|
||||
|
||||
private setMaxBounds(bbox: undefined | BBox) {
|
||||
const map = this._maplibreMap.data
|
||||
|
|
@ -736,4 +591,32 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
}
|
||||
map.fitBounds(bounds.toLngLat())
|
||||
}
|
||||
|
||||
private async setTerrain(useTerrain: boolean) {
|
||||
const map = this._maplibreMap.data
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
const id = "maptiler-terrain-data"
|
||||
if (useTerrain) {
|
||||
if(map.getTerrain()){
|
||||
return
|
||||
}
|
||||
map.addSource(id, {
|
||||
"type": "raster-dem",
|
||||
"url": "https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" + Constants.maptilerApiKey
|
||||
})
|
||||
try{
|
||||
while (!map?.isStyleLoaded()) {
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
map.setTerrain({
|
||||
source: id
|
||||
})
|
||||
}catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@
|
|||
let altproperties = new MapLibreAdaptor(altmap, {
|
||||
rasterLayer,
|
||||
zoom: UIEventSource.feedFrom(placedOverMapProperties.zoom),
|
||||
rotation: UIEventSource.feedFrom(placedOverMapProperties.rotation),
|
||||
pitch: UIEventSource.feedFrom(placedOverMapProperties.pitch)
|
||||
})
|
||||
altproperties.allowMoving.setData(false)
|
||||
altproperties.allowZooming.setData(false)
|
||||
|
|
|
|||
211
src/UI/Map/RasterLayerHandler.ts
Normal file
211
src/UI/Map/RasterLayerHandler.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { Map as MLMap, SourceSpecification } from "maplibre-gl"
|
||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
class SingleBackgroundHandler {
|
||||
// Value between 0 and 1.0
|
||||
public opacity = new UIEventSource<number>(0.0)
|
||||
private _map: Store<MLMap>
|
||||
private _background: UIEventSource<RasterLayerPolygon | undefined>
|
||||
private readonly _targetLayer: RasterLayerPolygon
|
||||
private _deactivationTime: Date = undefined
|
||||
|
||||
/**
|
||||
* Deactivate a layer after 60 seconds
|
||||
*/
|
||||
public static readonly DEACTIVATE_AFTER = 60
|
||||
private fadeStep = 0.1
|
||||
constructor(
|
||||
map: Store<MLMap>,
|
||||
targetLayer: RasterLayerPolygon,
|
||||
background: UIEventSource<RasterLayerPolygon | undefined>
|
||||
) {
|
||||
this._targetLayer = targetLayer
|
||||
this._map = map
|
||||
this._background = background
|
||||
|
||||
background.addCallback(async () => {
|
||||
await this.update()
|
||||
})
|
||||
map.addCallbackAndRunD(async (map) => {
|
||||
map.on("load", async () => {
|
||||
await this.update()
|
||||
})
|
||||
await this.update()
|
||||
map.on("moveend", () => this.onMove(map))
|
||||
map.on("zoomend", () => this.onMove(map))
|
||||
})
|
||||
}
|
||||
|
||||
private onMove(map: MLMap) {
|
||||
if (!this._deactivationTime) {
|
||||
return
|
||||
}
|
||||
// in seconds
|
||||
const timeSinceDeactivation =
|
||||
(new Date().getTime() - this._deactivationTime.getTime()) / 1000
|
||||
|
||||
if (timeSinceDeactivation < SingleBackgroundHandler.DEACTIVATE_AFTER) {
|
||||
return
|
||||
}
|
||||
|
||||
console.debug(
|
||||
"Removing raster layer",
|
||||
this._targetLayer.properties.id,
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
||||
private async update() {
|
||||
const newTarget: RasterLayerPolygon | undefined = this._background.data
|
||||
const targetLayer = this._targetLayer
|
||||
if (newTarget?.properties?.id !== targetLayer.properties.id) {
|
||||
this._deactivationTime = new Date()
|
||||
await this.awaitStyleIsLoaded()
|
||||
this.fadeOut()
|
||||
} else {
|
||||
this._deactivationTime = undefined
|
||||
this.enable()
|
||||
this.fadeIn()
|
||||
}
|
||||
}
|
||||
private async awaitStyleIsLoaded(): Promise<void> {
|
||||
const map = this._map.data
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
while (!map?.isStyleLoaded()) {
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
}
|
||||
|
||||
private enable() {
|
||||
const map: MLMap = this._map.data
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
const background = this._targetLayer.properties
|
||||
console.debug("Enabling", background.id)
|
||||
let addLayerBeforeId = "aeroway_fill" // this is the first non-landuse item in the stylesheet, we add the raster layer before the roads but above the landuse
|
||||
if (background.category === "osmbasedmap" || background.category === "map") {
|
||||
// The background layer is already an OSM-based map or another map, so we don't want anything from the baselayer
|
||||
addLayerBeforeId = undefined
|
||||
}
|
||||
if (!map.getSource(background.id)) {
|
||||
try {
|
||||
map.addSource(background.id, RasterLayerHandler.prepareWmsSource(background))
|
||||
} catch (e) {
|
||||
console.error("Could not add source", e)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!map.getLayer(background.id)) {
|
||||
addLayerBeforeId ??= map
|
||||
.getStyle()
|
||||
.layers.find((l) => l.id.startsWith("mapcomplete_"))?.id
|
||||
|
||||
map.addLayer(
|
||||
{
|
||||
id: background.id,
|
||||
type: "raster",
|
||||
source: background.id,
|
||||
paint: {
|
||||
"raster-opacity": 0,
|
||||
},
|
||||
},
|
||||
addLayerBeforeId
|
||||
)
|
||||
|
||||
this.opacity.addCallbackAndRun((o) => {
|
||||
map.setPaintProperty(background.id, "raster-opacity", o)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fadeOut() {
|
||||
Stores.Chronic(
|
||||
8,
|
||||
() => this.opacity.data > 0 && this._deactivationTime !== undefined
|
||||
).addCallback((_) => this.opacity.setData(Math.max(0, this.opacity.data - this.fadeStep)))
|
||||
}
|
||||
|
||||
private fadeIn() {
|
||||
Stores.Chronic(
|
||||
8,
|
||||
() => this.opacity.data < 1.0 && this._deactivationTime === undefined
|
||||
).addCallback((_) => this.opacity.setData(Math.min(1.0, this.opacity.data + this.fadeStep)))
|
||||
}
|
||||
}
|
||||
|
||||
export default class RasterLayerHandler {
|
||||
private _map: Store<MLMap>
|
||||
private _background: UIEventSource<RasterLayerPolygon | undefined>
|
||||
private _singleLayerHandlers: Record<string, SingleBackgroundHandler> = {}
|
||||
|
||||
constructor(map: Store<MLMap>, background: UIEventSource<RasterLayerPolygon | undefined>) {
|
||||
this._map = map
|
||||
this._background = background
|
||||
background.addCallbackAndRunD((l) => {
|
||||
const key = l.properties.id
|
||||
if (!this._singleLayerHandlers[key]) {
|
||||
this._singleLayerHandlers[key] = new SingleBackgroundHandler(map, l, background)
|
||||
}
|
||||
})
|
||||
map.addCallback((map) => {
|
||||
map.on("load", () => this.setBackground())
|
||||
this.setBackground()
|
||||
})
|
||||
}
|
||||
|
||||
public static prepareWmsSource(layer: RasterLayerProperties): SourceSpecification {
|
||||
return {
|
||||
type: "raster",
|
||||
// use the tiles option to specify a 256WMS tile source URL
|
||||
// https://maplibre.org/maplibre-gl-js-docs/style-spec/sources/
|
||||
tiles: [RasterLayerHandler.prepareWmsURL(layer.url, layer["tile-size"] ?? 256)],
|
||||
tileSize: layer["tile-size"] ?? 256,
|
||||
minzoom: layer["min_zoom"] ?? 1,
|
||||
maxzoom: layer["max_zoom"] ?? 25,
|
||||
// Bit of a hack, but seems to work
|
||||
scheme: layer.url.includes("{-y}") ? "tms" : "xyz",
|
||||
}
|
||||
}
|
||||
|
||||
private static prepareWmsURL(url: string, size: number = 256): string {
|
||||
// ELI: LAYERS=OGWRGB13_15VL&STYLES=&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap
|
||||
// PROD: SERVICE=WMS&REQUEST=GetMap&LAYERS=OGWRGB13_15VL&STYLES=&FORMAT=image/jpeg&TRANSPARENT=false&VERSION=1.3.0&WIDTH=256&HEIGHT=256&CRS=EPSG:3857&BBOX=488585.4847988467,6590094.830634755,489196.9810251281,6590706.32686104
|
||||
|
||||
const toReplace = {
|
||||
"{bbox}": "{bbox-epsg-3857}",
|
||||
"{proj}": "EPSG:3857",
|
||||
"{width}": "" + size,
|
||||
"{height}": "" + size,
|
||||
"{zoom}": "{z}",
|
||||
"{-y}": "{y}",
|
||||
}
|
||||
|
||||
for (const key in toReplace) {
|
||||
url = url.replace(new RegExp(key), toReplace[key])
|
||||
}
|
||||
|
||||
const subdomains = url.match(/\{switch:([a-zA-Z0-9,]*)}/)
|
||||
if (subdomains !== null) {
|
||||
const options = subdomains[1].split(",")
|
||||
const option = options[Math.floor(Math.random() * options.length)]
|
||||
url = url.replace(subdomains[0], option)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs all necessary updates
|
||||
*/
|
||||
public setBackground() {}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MlMap } from "maplibre-gl"
|
||||
import type { AddLayerObject, Map as MlMap } from "maplibre-gl"
|
||||
import { GeoJSONSource, Marker } from "maplibre-gl"
|
||||
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
|
|
@ -15,6 +15,7 @@ import * as range_layer from "../../../assets/layers/range/range.json"
|
|||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
|
||||
class PointRenderingLayer {
|
||||
private readonly _config: PointRenderingConfig
|
||||
|
|
@ -36,7 +37,7 @@ class PointRenderingLayer {
|
|||
visibility?: Store<boolean>,
|
||||
fetchStore?: (id: string) => Store<Record<string, string>>,
|
||||
onClick?: (feature: Feature) => void,
|
||||
selectedElement?: Store<{ properties: { id?: string } }>
|
||||
selectedElement?: Store<{ properties: { id?: string } }>,
|
||||
) {
|
||||
this._visibility = visibility
|
||||
this._config = config
|
||||
|
|
@ -89,7 +90,7 @@ class PointRenderingLayer {
|
|||
" while rendering",
|
||||
location,
|
||||
"of",
|
||||
this._config
|
||||
this._config,
|
||||
)
|
||||
}
|
||||
const id = feature.properties.id + "-" + location
|
||||
|
|
@ -97,7 +98,7 @@ class PointRenderingLayer {
|
|||
|
||||
const loc = GeoOperations.featureToCoordinateWithRenderingType(
|
||||
<any>feature,
|
||||
location
|
||||
location,
|
||||
)
|
||||
if (loc === undefined) {
|
||||
continue
|
||||
|
|
@ -153,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
|
||||
|
|
@ -221,7 +222,7 @@ class LineRenderingLayer {
|
|||
config: LineRenderingConfig,
|
||||
visibility?: Store<boolean>,
|
||||
fetchStore?: (id: string) => Store<Record<string, string>>,
|
||||
onClick?: (feature: Feature) => void
|
||||
onClick?: (feature: Feature) => void,
|
||||
) {
|
||||
this._layername = layername
|
||||
this._map = map
|
||||
|
|
@ -239,13 +240,56 @@ class LineRenderingLayer {
|
|||
this._map.removeLayer(this._layername + "_polygon")
|
||||
}
|
||||
|
||||
private async addSymbolLayer(sourceId: string, imageAlongWay: { if?: TagsFilter, then: string }[]) {
|
||||
const map = this._map
|
||||
await Promise.allSettled(imageAlongWay.map(async (img, i) => {
|
||||
const imgId = img.then.replaceAll(/[/.-]/g, "_")
|
||||
if (map.getImage(imgId) === undefined) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
map.loadImage(img.then, (err, image) => {
|
||||
if (err) {
|
||||
console.error("Could not add symbol layer to line due to", err)
|
||||
return
|
||||
}
|
||||
map.addImage(imgId, image)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const spec: AddLayerObject = {
|
||||
"id": "symbol-layer_" + this._layername + "-" + i,
|
||||
"type": "symbol",
|
||||
"source": sourceId,
|
||||
"layout": {
|
||||
"symbol-placement": "line",
|
||||
"symbol-spacing": 10,
|
||||
"icon-allow-overlap": true,
|
||||
"icon-rotation-alignment": "map",
|
||||
"icon-pitch-alignment": "map",
|
||||
"icon-image": imgId,
|
||||
"icon-size": 0.055,
|
||||
},
|
||||
}
|
||||
const filter = img.if?.asMapboxExpression()
|
||||
console.log(">>>", this._layername, imgId, img.if, "-->", filter)
|
||||
if (filter) {
|
||||
spec.filter = filter
|
||||
}
|
||||
map.addLayer(spec)
|
||||
}))
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the feature-state for maplibre
|
||||
* @param properties
|
||||
* @private
|
||||
*/
|
||||
private calculatePropsFor(
|
||||
properties: Record<string, string>
|
||||
properties: Record<string, string>,
|
||||
): Partial<Record<(typeof LineRenderingLayer.lineConfigKeys)[number], string>> {
|
||||
const config = this._config
|
||||
|
||||
|
|
@ -321,6 +365,11 @@ class LineRenderingLayer {
|
|||
},
|
||||
})
|
||||
|
||||
if (this._config.imageAlongWay) {
|
||||
this.addSymbolLayer(this._layername, this._config.imageAlongWay)
|
||||
}
|
||||
|
||||
|
||||
for (const feature of features) {
|
||||
if (!feature.properties.id) {
|
||||
console.warn("Feature without id:", feature)
|
||||
|
|
@ -328,7 +377,7 @@ class LineRenderingLayer {
|
|||
}
|
||||
map.setFeatureState(
|
||||
{ source: this._layername, id: feature.properties.id },
|
||||
this.calculatePropsFor(feature.properties)
|
||||
this.calculatePropsFor(feature.properties),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -371,7 +420,7 @@ class LineRenderingLayer {
|
|||
"Error while setting visibility of layers ",
|
||||
linelayer,
|
||||
polylayer,
|
||||
e
|
||||
e,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
@ -392,7 +441,7 @@ class LineRenderingLayer {
|
|||
console.trace(
|
||||
"Got a feature without ID; this causes rendering bugs:",
|
||||
feature,
|
||||
"from"
|
||||
"from",
|
||||
)
|
||||
LineRenderingLayer.missingIdTriggered = true
|
||||
}
|
||||
|
|
@ -404,7 +453,7 @@ class LineRenderingLayer {
|
|||
if (this._fetchStore === undefined) {
|
||||
map.setFeatureState(
|
||||
{ source: this._layername, id },
|
||||
this.calculatePropsFor(feature.properties)
|
||||
this.calculatePropsFor(feature.properties),
|
||||
)
|
||||
} else {
|
||||
const tags = this._fetchStore(id)
|
||||
|
|
@ -421,7 +470,7 @@ class LineRenderingLayer {
|
|||
}
|
||||
map.setFeatureState(
|
||||
{ source: this._layername, id },
|
||||
this.calculatePropsFor(properties)
|
||||
this.calculatePropsFor(properties),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -445,7 +494,7 @@ export default class ShowDataLayer {
|
|||
layer: LayerConfig
|
||||
drawMarkers?: true | boolean
|
||||
drawLines?: true | boolean
|
||||
}
|
||||
},
|
||||
) {
|
||||
this._options = options
|
||||
const self = this
|
||||
|
|
@ -456,7 +505,7 @@ export default class ShowDataLayer {
|
|||
mlmap: UIEventSource<MlMap>,
|
||||
features: FeatureSource,
|
||||
layers: LayerConfig[],
|
||||
options?: Partial<ShowDataLayerOptions>
|
||||
options?: Partial<ShowDataLayerOptions>,
|
||||
) {
|
||||
const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> =
|
||||
new PerLayerFeatureSourceSplitter(
|
||||
|
|
@ -464,7 +513,7 @@ export default class ShowDataLayer {
|
|||
features,
|
||||
{
|
||||
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
|
||||
}
|
||||
},
|
||||
)
|
||||
perLayer.forEach((fs) => {
|
||||
new ShowDataLayer(mlmap, {
|
||||
|
|
@ -478,7 +527,7 @@ export default class ShowDataLayer {
|
|||
public static showRange(
|
||||
map: Store<MlMap>,
|
||||
features: FeatureSource,
|
||||
doShowLayer?: Store<boolean>
|
||||
doShowLayer?: Store<boolean>,
|
||||
): ShowDataLayer {
|
||||
return new ShowDataLayer(map, {
|
||||
layer: ShowDataLayer.rangeLayer,
|
||||
|
|
@ -487,7 +536,8 @@ export default class ShowDataLayer {
|
|||
})
|
||||
}
|
||||
|
||||
public destruct() {}
|
||||
public destruct() {
|
||||
}
|
||||
|
||||
private zoomToCurrentFeatures(map: MlMap) {
|
||||
if (this._options.zoomToFeatures) {
|
||||
|
|
@ -508,9 +558,9 @@ export default class ShowDataLayer {
|
|||
(this._options.layer.title === undefined
|
||||
? undefined
|
||||
: (feature: Feature) => {
|
||||
selectedElement?.setData(feature)
|
||||
selectedLayer?.setData(this._options.layer)
|
||||
})
|
||||
selectedElement?.setData(feature)
|
||||
selectedLayer?.setData(this._options.layer)
|
||||
})
|
||||
if (this._options.drawLines !== false) {
|
||||
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
|
||||
const lineRenderingConfig = this._options.layer.lineRendering[i]
|
||||
|
|
@ -521,7 +571,7 @@ export default class ShowDataLayer {
|
|||
lineRenderingConfig,
|
||||
doShowLayer,
|
||||
fetchStore,
|
||||
onClick
|
||||
onClick,
|
||||
)
|
||||
this.onDestroy.push(l.destruct)
|
||||
}
|
||||
|
|
@ -536,7 +586,7 @@ export default class ShowDataLayer {
|
|||
doShowLayer,
|
||||
fetchStore,
|
||||
onClick,
|
||||
selectedElement
|
||||
selectedElement,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,63 +1,82 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loading from "../../assets/svg/Loading.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import Maproulette from "../../Logic/Maproulette"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loading from "../../assets/svg/Loading.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import Maproulette from "../../Logic/Maproulette"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
|
||||
/**
|
||||
* A UI-element to change the status of a maproulette-task
|
||||
*/
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let message: string
|
||||
export let image: string
|
||||
export let message_closed: string
|
||||
export let statusToSet: string
|
||||
export let maproulette_id_key: string
|
||||
/**
|
||||
* A UI-element to change the status of a maproulette-task
|
||||
*/
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let message: string
|
||||
export let image: string
|
||||
export let message_closed: string
|
||||
export let statusToSet: string
|
||||
export let maproulette_id_key: string
|
||||
|
||||
let applying = false
|
||||
let failed = false
|
||||
export let askFeedback: string = ""
|
||||
|
||||
/** Current status of the task*/
|
||||
let status: Store<number> = tags
|
||||
.map((tgs) => {
|
||||
if (tgs["status"]) {
|
||||
return tgs["status"]
|
||||
}
|
||||
return Maproulette.codeToIndex(tgs["mr_taskStatus"])
|
||||
})
|
||||
.map(Number)
|
||||
let applying = false
|
||||
let failed = false
|
||||
let feedback: string = ""
|
||||
|
||||
async function apply() {
|
||||
const maproulette_id = tags.data[maproulette_id_key] ?? tags.data.mr_taskId ?? tags.data.id
|
||||
try {
|
||||
await Maproulette.singleton.closeTask(Number(maproulette_id), Number(statusToSet), {
|
||||
tags: `MapComplete MapComplete:${state.layout.id}`,
|
||||
})
|
||||
tags.data["mr_taskStatus"] = Maproulette.STATUS_MEANING[Number(statusToSet)]
|
||||
tags.data.status = statusToSet
|
||||
tags.ping()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
failed = true
|
||||
/** Current status of the task*/
|
||||
let status: Store<number> = tags
|
||||
.map((tgs) => {
|
||||
if (tgs["status"]) {
|
||||
return tgs["status"]
|
||||
}
|
||||
return Maproulette.codeToIndex(tgs["mr_taskStatus"])
|
||||
})
|
||||
.map(Number)
|
||||
|
||||
async function apply() {
|
||||
const maproulette_id = tags.data[maproulette_id_key] ?? tags.data.mr_taskId ?? tags.data.id
|
||||
try {
|
||||
await Maproulette.singleton.closeTask(Number(maproulette_id), Number(statusToSet), {
|
||||
tags: `MapComplete MapComplete:${state.layout.id}`,
|
||||
comment: feedback
|
||||
})
|
||||
tags.data["mr_taskStatus"] = Maproulette.STATUS_MEANING[Number(statusToSet)]
|
||||
tags.data.status = statusToSet
|
||||
tags.ping()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if failed}
|
||||
<div class="alert">ERROR - could not close the MapRoulette task</div>
|
||||
{:else if applying}
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.loading} />
|
||||
</Loading>
|
||||
{:else if $status === Maproulette.STATUS_OPEN}
|
||||
<button class="no-image-background w-full p-4" on:click={() => apply()}>
|
||||
<Icon clss="w-8 h-8 mr-2" icon={image} />
|
||||
{message}
|
||||
</button>
|
||||
{:else}
|
||||
{message_closed}
|
||||
{/if}
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
{#if failed}
|
||||
<div class="alert">ERROR - could not close the MapRoulette task</div>
|
||||
{:else if applying}
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.loading} />
|
||||
</Loading>
|
||||
{:else if $status === Maproulette.STATUS_OPEN}
|
||||
{#if askFeedback !== "" && askFeedback !== undefined}
|
||||
<div class="flex flex-col p-1 gap-y-1 interactive border border-gray-500 border-dashed">
|
||||
<h3>{askFeedback}</h3>
|
||||
<textarea bind:value={feedback}></textarea>
|
||||
<button class="no-image-background w-full p-4 m-0" class:disabled={feedback===""} on:click={() => apply()}>
|
||||
<Icon clss="w-8 h-8 mr-2 shrink-0" icon={image} />
|
||||
{message}
|
||||
</button>
|
||||
{feedback}
|
||||
</div>
|
||||
{:else}
|
||||
<button class="no-image-background w-full p-4 m-0" on:click={() => apply()}>
|
||||
<Icon clss="w-8 h-8 mr-2 shrink-0" icon={image} />
|
||||
{message}
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
{message_closed}
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import Circle from "../../assets/svg/Circle.svelte"
|
||||
import Ring from "../../assets/svg/Ring.svelte"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: Store<Record<string, string>>
|
||||
|
|
@ -21,7 +20,7 @@
|
|||
tags,
|
||||
keyToUse,
|
||||
prefix,
|
||||
postfix
|
||||
postfix,
|
||||
)
|
||||
|
||||
let currentState = oh.mapD((oh) => (typeof oh === "string" ? undefined : oh.getState()))
|
||||
|
|
@ -30,12 +29,12 @@
|
|||
let nextChange = oh
|
||||
.mapD(
|
||||
(oh) => (typeof oh === "string" ? undefined : oh.getNextChange(new Date(), tomorrow)),
|
||||
[Stores.Chronic(5 * 60 * 1000)]
|
||||
[Stores.Chronic(5 * 60 * 1000)],
|
||||
)
|
||||
.mapD((date) => Utils.TwoDigits(date.getHours()) + ":" + Utils.TwoDigits(date.getMinutes()))
|
||||
|
||||
let size = nextChange.map((change) =>
|
||||
change === undefined ? "absolute h-7 w-7" : "absolute h-5 w-5 top-0 left-1/4"
|
||||
change === undefined ? "absolute h-7 w-7" : "absolute h-5 w-5 top-0 left-1/4",
|
||||
)
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -127,6 +127,11 @@ export class OH {
|
|||
* const oh1: OpeningHour = { weekday: 0, startHour: 10, startMinutes: 0, endHour: 11, endMinutes: 0 };
|
||||
* const oh0: OpeningHour = { weekday: 0, startHour: 11, startMinutes: 0, endHour: 12, endMinutes: 0 };
|
||||
* OH.MergeTimes([oh0, oh1]) // => [{ weekday: 0, startHour: 10, startMinutes: 0, endHour: 12, endMinutes: 0 }]
|
||||
*
|
||||
* // should merge touching opening hours spanning days
|
||||
* const oh0: OpeningHour = { weekday: 0, startHour: 10, startMinutes: 0, endHour: 24, endMinutes: 0 };
|
||||
* const oh1: OpeningHour = { weekday: 1, startHour: 0, startMinutes: 0, endHour: 12, endMinutes: 0 };
|
||||
* OH.MergeTimes([oh0, oh1]) // => [{ weekday: 0, startHour: 10, startMinutes: 0, endHour: 24, endMinutes: 0 }, { weekday: 1, startHour: 0, startMinutes: 0, endHour: 12, endMinutes: 0 }]
|
||||
*/
|
||||
public static MergeTimes(ohs: OpeningHour[]): OpeningHour[] {
|
||||
const queue = ohs.map((oh) => {
|
||||
|
|
@ -216,6 +221,7 @@ export class OH {
|
|||
// This means that the list is changed only if the lengths are different.
|
||||
// If the lengths are the same, we might just as well return the old list and be a bit more stable
|
||||
if (newList.length !== ohs.length) {
|
||||
newList.sort((a, b) => b.weekday - a.weekday)
|
||||
return newList
|
||||
} else {
|
||||
return ohs
|
||||
|
|
@ -308,6 +314,12 @@ export class OH {
|
|||
* rules[1].weekday // => 1
|
||||
* rules[1].startHour // => 0
|
||||
* rules[1].endHour // => 2
|
||||
*
|
||||
* const rules = OH.ParseRule("Mo 00:00-24:00")
|
||||
* rules.length // => 1
|
||||
* rules[0].weekday // => 0
|
||||
* rules[0].startHour // => 0
|
||||
* rules[0].endHour // => 24
|
||||
*/
|
||||
public static ParseRule(rule: string): OpeningHour[] {
|
||||
try {
|
||||
|
|
@ -781,6 +793,7 @@ This list will be sorted
|
|||
/**
|
||||
* OH.ParseHhmmRanges("20:00-22:15") // => [{startHour: 20, startMinutes: 0, endHour: 22, endMinutes: 15}]
|
||||
* OH.ParseHhmmRanges("20:00-02:15") // => [{startHour: 20, startMinutes: 0, endHour: 2, endMinutes: 15}]
|
||||
* OH.ParseHhmmRanges("00:00-24:00") // => [{startHour: 0, startMinutes: 0, endHour: 24, endMinutes: 0}]
|
||||
*/
|
||||
private static ParseHhmmRanges(hhmms: string): {
|
||||
startHour: number
|
||||
|
|
@ -897,6 +910,19 @@ This list will be sorted
|
|||
}
|
||||
|
||||
export class ToTextualDescription {
|
||||
/**
|
||||
* const oh = new opening_hours("mon 12:00-16:00")
|
||||
* const ranges = OH.createRangesForApplicableWeek(oh)
|
||||
* const tr = ToTextualDescription.createTextualDescriptionFor(oh, ranges.ranges)
|
||||
* tr.textFor("en") // => "On monday from 12:00 till 16:00"
|
||||
* tr.textFor("nl") // => "Op maandag van 12:00 tot 16:00"
|
||||
*
|
||||
* const oh = new opening_hours("mon 12:00-16:00; tu 13:00-14:00")
|
||||
* const ranges = OH.createRangesForApplicableWeek(oh)
|
||||
* const tr = ToTextualDescription.createTextualDescriptionFor(oh, ranges.ranges)
|
||||
* tr.textFor("en") // => "On monday from 12:00 till 16:00. On tuesday from 13:00 till 14:00"
|
||||
* tr.textFor("nl") // => "Op maandag van 12:00 tot 16:00. Op dinsdag van 13:00 tot 14:00"
|
||||
*/
|
||||
public static createTextualDescriptionFor(
|
||||
oh: opening_hours,
|
||||
ranges: OpeningRange[][]
|
||||
|
|
@ -972,10 +998,16 @@ export class ToTextualDescription {
|
|||
}
|
||||
|
||||
private static chain(trs: Translation[]): Translation {
|
||||
let chainer = new TypedTranslation<{ a; b }>({ "*": "{a}. {b}" })
|
||||
const languages: Record<string, string> = {}
|
||||
for (const tr1 of trs) {
|
||||
for (const supportedLanguage of tr1.SupportedLanguages()) {
|
||||
languages[supportedLanguage] = "{a}. {b}"
|
||||
}
|
||||
}
|
||||
let chainer = new TypedTranslation<{ a; b }>(languages)
|
||||
let tr = trs[0]
|
||||
for (let i = 1; i < trs.length; i++) {
|
||||
tr = chainer.Subs({ a: tr, b: trs[i] })
|
||||
tr = chainer.PartialSubsTr("a", tr).PartialSubsTr("b", trs[i])
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export default class OpeningHoursInput extends InputElement<string> {
|
|||
return this._value
|
||||
}
|
||||
|
||||
IsValid(t: string): boolean {
|
||||
IsValid(_: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
|
|||
return this._ohs
|
||||
}
|
||||
|
||||
IsValid(t: OpeningHour[]): boolean {
|
||||
IsValid(_: OpeningHour[]): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]>
|
|||
this.SetClass("w-full block")
|
||||
}
|
||||
|
||||
IsValid(t: OpeningHour[]): boolean {
|
||||
IsValid(_: OpeningHour[]): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Translation } from "../i18n/Translation"
|
|||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Loading from "../Base/Loading"
|
||||
import opening_hours from "opening_hours"
|
||||
import Locale from "../i18n/Locale"
|
||||
|
||||
export default class OpeningHoursVisualization extends Toggle {
|
||||
private static readonly weekdays: Translation[] = [
|
||||
|
|
@ -53,8 +54,9 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
applicableWeek.ranges,
|
||||
applicableWeek.startingMonday
|
||||
)
|
||||
textual.current.addCallbackAndRunD((descr) => {
|
||||
vis.ConstructElement().ariaLabel = descr
|
||||
Locale.language.mapD((lng) => {
|
||||
console.log("Setting OH description to", lng, textual)
|
||||
vis.ConstructElement().ariaLabel = textual.textFor(lng)
|
||||
})
|
||||
return vis
|
||||
})
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default class PublicHolidayInput extends InputElement<string> {
|
|||
return this._value
|
||||
}
|
||||
|
||||
IsValid(t: string): boolean {
|
||||
IsValid(_: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@
|
|||
import NextButton from "../Base/NextButton.svelte"
|
||||
import WikipediaPanel from "../Wikipedia/WikipediaPanel.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
import Plantnet_logo from "../../assets/svg/Plantnet_logo.svelte"
|
||||
|
||||
/**
|
||||
|
|
@ -28,7 +26,7 @@
|
|||
const dispatch = createEventDispatcher<{ selected: string }>()
|
||||
let collapsedMode = true
|
||||
let options: UIEventSource<PlantNetSpeciesMatch[]> = new UIEventSource<PlantNetSpeciesMatch[]>(
|
||||
undefined
|
||||
undefined,
|
||||
)
|
||||
|
||||
let error: string = undefined
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@
|
|||
import { onDestroy } from "svelte"
|
||||
import NextButton from "../../Base/NextButton.svelte"
|
||||
import BackButton from "../../Base/BackButton.svelte"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import Svg from "../../../Svg"
|
||||
import OpenBackgroundSelectorButton from "../../BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import Confirm from "../../../assets/svg/Confirm.svelte"
|
||||
import Close from "../../../assets/svg/Close.svelte"
|
||||
import Layers from "../../../assets/svg/Layers.svelte"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
|
||||
export let coordinate: { lon: number; lat: number }
|
||||
export let state: SpecialVisualizationState
|
||||
|
|
@ -41,8 +43,9 @@
|
|||
let selectedPreset: {
|
||||
preset: PresetConfig
|
||||
layer: LayerConfig
|
||||
icon: string
|
||||
tags: Record<string, string>
|
||||
icon: BaseUIElement
|
||||
tags: Record<string, string>,
|
||||
text: Translation
|
||||
} = undefined
|
||||
let checkedOfGlobalFilters: number = 0
|
||||
let confirmedCategory = false
|
||||
|
|
@ -142,7 +145,6 @@
|
|||
const feature = state.indexedFeatures.featuresById.data.get(newId)
|
||||
console.log("Selecting feature", feature, "and opening their popup")
|
||||
abort()
|
||||
state.selectedLayer.setData(selectedPreset.layer)
|
||||
state.selectedElement.setData(feature)
|
||||
tagsStore.ping()
|
||||
}
|
||||
|
|
@ -198,7 +200,7 @@
|
|||
state.guistate.openFilterView(selectedPreset.layer)
|
||||
}}
|
||||
>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
|
||||
<Layers class="w-12"/>
|
||||
<Tr t={Translations.t.general.add.openLayerControl} />
|
||||
</button>
|
||||
|
||||
|
|
@ -239,7 +241,7 @@
|
|||
state.guistate.openFilterView(selectedPreset.layer)
|
||||
}}
|
||||
>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
|
||||
<Layers class="w-12"/>
|
||||
<Tr t={Translations.t.general.add.openLayerControl} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -251,8 +253,6 @@
|
|||
/>
|
||||
</h2>
|
||||
|
||||
<Tr t={Translations.t.general.add.confirmIntro} />
|
||||
|
||||
{#if selectedPreset.preset.description}
|
||||
<Tr t={selectedPreset.preset.description} />
|
||||
{/if}
|
||||
|
|
@ -284,7 +284,7 @@
|
|||
|
||||
<NextButton on:click={() => (confirmedCategory = true)} clss="primary w-full">
|
||||
<div slot="image" class="relative">
|
||||
<ToSvelte construct={selectedPreset.icon} />
|
||||
<ToSvelte construct={selectedPreset.icon}/>
|
||||
<Confirm class="absolute bottom-0 right-0 h-4 w-4" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
|
|
@ -303,7 +303,7 @@
|
|||
<Tr
|
||||
slot="message"
|
||||
t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.confirmAddNew.Subs({
|
||||
preset: selectedPreset.preset,
|
||||
preset: selectedPreset.text
|
||||
})}
|
||||
/>
|
||||
</SubtleButton>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,10 @@
|
|||
import { ImmutableStore } from "../../../Logic/UIEventSource"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import FromHtml from "../../Base/FromHtml.svelte"
|
||||
import NextButton from "../../Base/NextButton.svelte"
|
||||
import { UIElement } from "../../UIElement"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import Combine from "../../Base/Combine"
|
||||
|
||||
/**
|
||||
* This component lists all the presets and allows the user to select one
|
||||
|
|
@ -24,6 +23,10 @@
|
|||
preset: PresetConfig
|
||||
layer: LayerConfig
|
||||
text: Translation
|
||||
/**
|
||||
* Same as `this.preset.description.firstSentence()`
|
||||
*/
|
||||
description: Translation,
|
||||
icon: BaseUIElement
|
||||
tags: Record<string, string>
|
||||
}[] = []
|
||||
|
|
@ -37,7 +40,7 @@
|
|||
"Not showing presets for layer",
|
||||
flayer.layerDef.id,
|
||||
"as not displayed and featureSwitchFilter.data is set",
|
||||
state.featureSwitches.featureSwitchFilter.data
|
||||
state.featureSwitches.featureSwitchFilter.data,
|
||||
)
|
||||
// ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
|
||||
continue
|
||||
|
|
@ -52,9 +55,9 @@
|
|||
for (const preset of layer.presets) {
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
|
||||
|
||||
const icon: BaseUIElement = layer.mapRendering[0]
|
||||
.RenderIcon(new ImmutableStore<any>(tags))
|
||||
.html.SetClass("w-12 h-12 block relative mr-4")
|
||||
const markers = layer.mapRendering.map((mr, i) => mr.RenderIcon(new ImmutableStore<any>(tags), {noSize: i == 0})
|
||||
.html.SetClass(i == 0 ? "w-full h-full" : ""))
|
||||
const icon: BaseUIElement = new Combine(markers.map(m => new Combine([m]).SetClass("absolute top-0 left-0 w-full h-full flex justify-around items-center"))).SetClass("w-12 h-12 block relative mr-4")
|
||||
|
||||
const description = preset.description?.FirstSentence()
|
||||
|
||||
|
|
@ -66,7 +69,7 @@
|
|||
tags,
|
||||
text: Translations.t.general.add.addNew.Subs(
|
||||
{ category: preset.title },
|
||||
preset.title["context"]
|
||||
preset.title["context"],
|
||||
),
|
||||
}
|
||||
presets.push(simplified)
|
||||
|
|
@ -74,7 +77,13 @@
|
|||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
select: { preset: PresetConfig; layer: LayerConfig; icon: string; tags: Record<string, string> }
|
||||
select: {
|
||||
preset: PresetConfig;
|
||||
layer: LayerConfig;
|
||||
icon: BaseUIElement;
|
||||
tags: Record<string, string>,
|
||||
text: Translation
|
||||
}
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import all_languages from "../../assets/language_translations.json"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
|
||||
export class AllLanguagesSelector extends SearchablePillsSelector<string> {
|
||||
constructor(options?: {
|
||||
mode?: "select-many" | "select-one"
|
||||
currentCountry?: Store<string>
|
||||
supportedLanguages?: Record<string, string> & { _meta?: { countries?: string[] } }
|
||||
}) {
|
||||
const possibleValues: {
|
||||
show: BaseUIElement
|
||||
value: string
|
||||
mainTerm: Record<string, string>
|
||||
searchTerms?: Record<string, string[]>
|
||||
hasPriority?: Store<boolean>
|
||||
}[] = []
|
||||
|
||||
const langs = options?.supportedLanguages ?? all_languages
|
||||
for (const ln in langs) {
|
||||
let languageInfo: Record<string, string> & { _meta?: { countries: string[] } } =
|
||||
all_languages[ln]
|
||||
const countries = languageInfo._meta?.countries?.map((c) => c.toLowerCase())
|
||||
languageInfo = { ...languageInfo }
|
||||
delete languageInfo._meta
|
||||
const term = {
|
||||
show: new Translation(languageInfo),
|
||||
value: ln,
|
||||
mainTerm: languageInfo,
|
||||
searchTerms: { "*": [ln] },
|
||||
hasPriority:
|
||||
countries === undefined
|
||||
? undefined
|
||||
: options?.currentCountry?.map(
|
||||
(country) => countries?.indexOf(country.toLowerCase()) >= 0
|
||||
),
|
||||
}
|
||||
possibleValues.push(term)
|
||||
}
|
||||
super(possibleValues, {
|
||||
mode: options?.mode ?? "select-many",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +1,100 @@
|
|||
<script lang="ts">
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Table from "../Base/Table"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import SimpleMetaTaggers from "../../Logic/SimpleMetaTagger"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { onDestroy } from "svelte"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
|
||||
//Svelte props
|
||||
export let tags: UIEventSource<any>
|
||||
export let state: { layoutToUse: LayoutConfig } = undefined
|
||||
export let tags: UIEventSource<Record<string, any>>
|
||||
export let tagKeys = tags.map(tgs => Object.keys(tgs))
|
||||
|
||||
const calculatedTags = [].concat(
|
||||
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])
|
||||
)
|
||||
export let layer: LayerConfig | undefined = undefined
|
||||
|
||||
const allTags = tags.mapD((tags) => {
|
||||
const parts: (string | BaseUIElement)[][] = []
|
||||
for (const key in tags) {
|
||||
let v = tags[key]
|
||||
if (v === "") {
|
||||
v = "<b>empty string</b>"
|
||||
}
|
||||
parts.push([key, v ?? "<b>undefined</b>"])
|
||||
/**
|
||||
* The names (keys) of the calculated tags. Each will normally start with an underscore (but in rare cases not)
|
||||
*/
|
||||
let calculatedTags: string[] = []
|
||||
for (const calculated of layer?.calculatedTags ?? []) {
|
||||
if(calculated){
|
||||
continue
|
||||
}
|
||||
const name = calculated[0]
|
||||
calculatedTags.push(name)
|
||||
}
|
||||
let knownValues: Store<string[]> = tags.map(tags => Object.keys(tags))
|
||||
|
||||
for (const key of calculatedTags) {
|
||||
const value = tags[key]
|
||||
if (value === undefined) {
|
||||
continue
|
||||
}
|
||||
let type = ""
|
||||
if (typeof value !== "string") {
|
||||
type = " <i>" + typeof value + "</i>"
|
||||
}
|
||||
parts.push(["<i>" + key + "</i>", value])
|
||||
}
|
||||
const metaKeys: string[] = [].concat(...SimpleMetaTaggers.metatags.map(k => k.keys))
|
||||
let allCalculatedTags = new Set<string>([...calculatedTags, ...metaKeys])
|
||||
|
||||
for (const metatag of SimpleMetaTaggers.metatags.filter((mt) => mt.isLazy)) {
|
||||
const title = "<i>" + metatag.keys.join(";") + "</i> (lazy)"
|
||||
const toggleState = new UIEventSource(false)
|
||||
const toggle: BaseUIElement = new Toggle(
|
||||
new Lazy(() => new FixedUiElement(metatag.keys.map((key) => tags[key]).join(";"))),
|
||||
new FixedUiElement("Evaluate").onClick(() => toggleState.setData(true)),
|
||||
toggleState
|
||||
)
|
||||
parts.push([title, toggle])
|
||||
}
|
||||
|
||||
return parts
|
||||
})
|
||||
|
||||
const tagsTable = new VariableUiElement(
|
||||
allTags.mapD((_allTags) =>
|
||||
new Table(["Key", "Value"], _allTags).SetClass("zebra-table break-all")
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<ToSvelte construct={tagsTable} />
|
||||
<table class="zebra-table break-all">
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="2">Normal tags</th>
|
||||
</tr>
|
||||
{#each $tagKeys as key}
|
||||
{#if !allCalculatedTags.has(key)}
|
||||
<tr>
|
||||
<td>{key}</td>
|
||||
<td>
|
||||
{#if $tags[key] === undefined}
|
||||
<i>undefined</i>
|
||||
{:else if $tags[key] === ""}
|
||||
<i>Empty string</i>
|
||||
{:else}
|
||||
{$tags[key]}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
<tr>
|
||||
<th colspan="2">Calculated tags</th>
|
||||
</tr>
|
||||
{#if calculatedTags.length === 0}
|
||||
<tr>
|
||||
<td colspan="2"><i>This layer does not use calculated tags</i></td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#each calculatedTags as key}
|
||||
<tr>
|
||||
<td>{key}</td>
|
||||
<td>
|
||||
{#if $tags[key] === undefined}
|
||||
<i>undefined</i>
|
||||
{:else if $tags[key] === ""}
|
||||
<i>Empty string</i>
|
||||
{:else if $tags[key] !== "string"}
|
||||
<span class="literal-code">{$tags[key]}</span>
|
||||
<i>{typeof $tags[key]}</i>
|
||||
{:else}
|
||||
{$tags[key]}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
<tr>
|
||||
<th colspan="2">Metatags tags</th>
|
||||
</tr>
|
||||
{#each metaKeys as key}
|
||||
<tr>
|
||||
<td>{key}</td>
|
||||
<td>
|
||||
{#if $knownValues.indexOf(key) < 0 }
|
||||
<button class="small" on:click={_ => {console.log($tags[key])}}>Evaluate</button>
|
||||
{:else if !$tags[key] === undefined}
|
||||
<i>Undefined</i>
|
||||
{:else if $tags[key] === ""}
|
||||
<i>Empty string</i>
|
||||
{:else}
|
||||
{$tags[key]}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@
|
|||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import type { UploadableTag } from "../../../Logic/Tags/TagUtils"
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction"
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
|
||||
|
|
@ -43,7 +42,7 @@
|
|||
|
||||
const t = Translations.t.delete
|
||||
|
||||
let selectedTags: TagsFilter
|
||||
let selectedTags: UploadableTag
|
||||
let changedProperties = undefined
|
||||
$: changedProperties = TagUtils.changeAsProperties(selectedTags?.asChange(tags?.data ?? {}) ?? [])
|
||||
let isHardDelete = undefined
|
||||
|
|
@ -66,7 +65,7 @@
|
|||
theme: state?.layout?.id ?? "unknown",
|
||||
specialMotivation: deleteReason,
|
||||
},
|
||||
canBeDeleted.data
|
||||
canBeDeleted.data,
|
||||
)
|
||||
} else {
|
||||
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
|||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Histogram from "../BigComponents/Histogram"
|
||||
import { Feature } from "geojson"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
export class HistogramViz implements SpecialVisualization {
|
||||
funcName = "histogram"
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ export default class ConflateImportButtonViz implements SpecialVisualization, Au
|
|||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
const canBeImported =
|
||||
feature.geometry.type === "LineString" ||
|
||||
|
|
|
|||
|
|
@ -1,42 +1,58 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
|
||||
* They show some default components
|
||||
*/
|
||||
import ImportFlow from "./ImportFlow"
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||
import BackButton from "../../Base/BackButton.svelte"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import NextButton from "../../Base/NextButton.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
import TagHint from "../TagHint.svelte"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import { Store } from "../../../Logic/UIEventSource"
|
||||
import Svg from "../../../Svg"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Confirm from "../../../assets/svg/Confirm.svelte"
|
||||
import type { ImportFlowArguments } from "./ImportFlow"
|
||||
/**
|
||||
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
|
||||
* They show some default components
|
||||
*/
|
||||
import ImportFlow from "./ImportFlow"
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||
import BackButton from "../../Base/BackButton.svelte"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import NextButton from "../../Base/NextButton.svelte"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
import TagHint from "../TagHint.svelte"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import { Store } from "../../../Logic/UIEventSource"
|
||||
import Svg from "../../../Svg"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import Confirm from "../../../assets/svg/Confirm.svelte"
|
||||
|
||||
export let importFlow: ImportFlow
|
||||
let state = importFlow.state
|
||||
export let importFlow: ImportFlow<ImportFlowArguments>
|
||||
let state = importFlow.state
|
||||
|
||||
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
|
||||
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
|
||||
|
||||
const isLoading = state.dataIsLoading
|
||||
const dispatch = createEventDispatcher<{ confirm }>()
|
||||
const canBeImported = importFlow.canBeImported()
|
||||
const tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags))
|
||||
const isLoading = state.dataIsLoading
|
||||
let dispatch = createEventDispatcher<{ confirm }>()
|
||||
let canBeImported = importFlow.canBeImported()
|
||||
let tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags))
|
||||
|
||||
const isDisplayed = importFlow.targetLayer.isDisplayed
|
||||
const hasFilter = importFlow.targetLayer.hasFilter
|
||||
|
||||
function abort() {
|
||||
state.selectedElement.setData(undefined)
|
||||
state.selectedLayer.setData(undefined)
|
||||
}
|
||||
let targetLayers = importFlow.targetLayer
|
||||
let filteredLayer: FilteredLayer
|
||||
let undisplayedLayer: FilteredLayer
|
||||
|
||||
function updateIsDisplayed() {
|
||||
filteredLayer = targetLayers.find(tl => tl.hasFilter.data)
|
||||
undisplayedLayer = targetLayers.find(tl => !tl.isDisplayed.data)
|
||||
}
|
||||
|
||||
updateIsDisplayed()
|
||||
|
||||
for (const tl of targetLayers) {
|
||||
onDestroy(
|
||||
tl.isDisplayed.addCallback(updateIsDisplayed),
|
||||
)
|
||||
}
|
||||
|
||||
function abort() {
|
||||
state.selectedElement.setData(undefined)
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
|
@ -45,13 +61,13 @@
|
|||
{#if $canBeImported.extraHelp}
|
||||
<Tr t={$canBeImported.extraHelp} />
|
||||
{/if}
|
||||
{:else if !$isDisplayed}
|
||||
{:else if undisplayedLayer !== undefined}
|
||||
<!-- Check that the layer is enabled, so that we don't add a duplicate -->
|
||||
<div class="alert flex items-center justify-center">
|
||||
<EyeOffIcon class="w-8" />
|
||||
<Tr
|
||||
t={Translations.t.general.add.layerNotEnabled.Subs({
|
||||
layer: importFlow.targetLayer.layerDef.name,
|
||||
layer: undisplayedLayer.layerDef.name,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -61,7 +77,7 @@
|
|||
class="flex w-full gap-x-1"
|
||||
on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(importFlow.targetLayer.layerDef)
|
||||
state.guistate.openFilterView(filteredLayer.layerDef)
|
||||
}}
|
||||
>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
|
||||
|
|
@ -71,19 +87,19 @@
|
|||
<button
|
||||
class="primary flex w-full gap-x-1"
|
||||
on:click={() => {
|
||||
isDisplayed.setData(true)
|
||||
undisplayedLayer.isDisplayed.setData(true)
|
||||
abort()
|
||||
}}
|
||||
>
|
||||
<EyeIcon class="w-12" />
|
||||
<Tr
|
||||
t={Translations.t.general.add.enableLayer.Subs({
|
||||
name: importFlow.targetLayer.layerDef.name,
|
||||
name: undisplayedLayer.layerDef.name,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{:else if $hasFilter}
|
||||
{:else if filteredLayer !== undefined}
|
||||
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
|
||||
<div class="alert flex items-center justify-center">
|
||||
<EyeOffIcon class="w-8" />
|
||||
|
|
@ -94,7 +110,7 @@
|
|||
class="primary flex w-full gap-x-1"
|
||||
on:click={() => {
|
||||
abort()
|
||||
importFlow.targetLayer.disableAllFilters()
|
||||
filteredLayer.disableAllFilters()
|
||||
}}
|
||||
>
|
||||
<EyeOffIcon class="w-12" />
|
||||
|
|
@ -104,7 +120,7 @@
|
|||
class="flex w-full gap-x-1"
|
||||
on:click={() => {
|
||||
abort()
|
||||
state.guistate.openFilterView(importFlow.targetLayer.layerDef)
|
||||
state.guistate.openFilterView(filteredLayer.layerDef)
|
||||
}}
|
||||
>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import TagApplyButton from "../TagApplyButton"
|
|||
import { PointImportFlowArguments } from "./PointImportFlowState"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
|
|
@ -25,7 +24,7 @@ export class ImportFlowUtils {
|
|||
public static readonly conflationLayer = new LayerConfig(
|
||||
<LayerConfigJson>conflation_json,
|
||||
"all_known_layers",
|
||||
true
|
||||
true,
|
||||
)
|
||||
|
||||
public static readonly documentationGeneral = `\n\n\nNote that the contributor must zoom to at least zoomlevel 18 to be able to use this functionality.
|
||||
|
|
@ -67,7 +66,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
*/
|
||||
public static getTagsToApply(
|
||||
originalFeatureTags: UIEventSource<any>,
|
||||
args: { tags: string }
|
||||
args: { tags: string },
|
||||
): Store<Tag[]> {
|
||||
if (originalFeatureTags === undefined) {
|
||||
return undefined
|
||||
|
|
@ -83,9 +82,9 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
const items: string = originalFeatureTags.data[tags]
|
||||
console.debug(
|
||||
"The import button is using tags from properties[" +
|
||||
tags +
|
||||
"] of this object, namely ",
|
||||
items
|
||||
tags +
|
||||
"] of this object, namely ",
|
||||
items,
|
||||
)
|
||||
|
||||
if (items.startsWith("{")) {
|
||||
|
|
@ -108,13 +107,12 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
* - targetLayer
|
||||
*
|
||||
* Others (e.g.: snapOnto-layers) are not to be handled here
|
||||
* @param argsRaw
|
||||
*/
|
||||
public static getLayerDependencies(argsRaw: string[], argSpec?) {
|
||||
public static getLayerDependencies(argsRaw: string[], argSpec?): string[] {
|
||||
const args: ImportFlowArguments = <any>(
|
||||
Utils.ParseVisArgs(argSpec ?? ImportFlowUtils.generalArguments, argsRaw)
|
||||
)
|
||||
return [args.targetLayer]
|
||||
return args.targetLayer.split(" ")
|
||||
}
|
||||
|
||||
public static getLayerDependenciesWithSnapOnto(
|
||||
|
|
@ -122,7 +120,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
name: string
|
||||
defaultValue?: string
|
||||
}[],
|
||||
argsRaw: string[]
|
||||
argsRaw: string[],
|
||||
): string[] {
|
||||
const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec)
|
||||
const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw)
|
||||
|
|
@ -130,30 +128,6 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
deps.push(...snapOntoLayers)
|
||||
return deps
|
||||
}
|
||||
|
||||
public static buildTagSpec(
|
||||
args: ImportFlowArguments,
|
||||
tagSource: Store<Record<string, string>>
|
||||
): Store<string> {
|
||||
let tagSpec = args.tags
|
||||
return tagSource.mapD((tags) => {
|
||||
if (
|
||||
tagSpec.indexOf(" ") < 0 &&
|
||||
tagSpec.indexOf(";") < 0 &&
|
||||
tags[args.tags] !== undefined
|
||||
) {
|
||||
// This is probably a key
|
||||
tagSpec = tags[args.tags]
|
||||
console.debug(
|
||||
"The import button is using tags from properties[" +
|
||||
args.tags +
|
||||
"] of this object, namely ",
|
||||
tagSpec
|
||||
)
|
||||
}
|
||||
return tagSpec
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -164,7 +138,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
||||
public readonly state: SpecialVisualizationState
|
||||
public readonly args: ArgT
|
||||
public readonly targetLayer: FilteredLayer
|
||||
public readonly targetLayer: FilteredLayer[]
|
||||
public readonly tagsToApply: Store<Tag[]>
|
||||
protected readonly _originalFeatureTags: UIEventSource<Record<string, string>>
|
||||
|
||||
|
|
@ -172,13 +146,19 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
|||
state: SpecialVisualizationState,
|
||||
args: ArgT,
|
||||
tagsToApply: Store<Tag[]>,
|
||||
originalTags: UIEventSource<Record<string, string>>
|
||||
originalTags: UIEventSource<Record<string, string>>,
|
||||
) {
|
||||
this.state = state
|
||||
this.args = args
|
||||
this.tagsToApply = tagsToApply
|
||||
this._originalFeatureTags = originalTags
|
||||
this.targetLayer = state.layerState.filteredLayers.get(args.targetLayer)
|
||||
this.targetLayer = args.targetLayer.split(" ").map(tl => {
|
||||
let found = state.layerState.filteredLayers.get(tl)
|
||||
if (!found) {
|
||||
throw "Layer " + tl + " not found"
|
||||
}
|
||||
return found
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -218,7 +198,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
|||
|
||||
return undefined
|
||||
},
|
||||
[state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags]
|
||||
[state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export class PointImportButtonViz implements SpecialVisualization {
|
|||
public readonly funcName: string
|
||||
public readonly docs: string | BaseUIElement
|
||||
public readonly example?: string
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string }[]
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string, split?: boolean }[]
|
||||
public needsUrls = []
|
||||
|
||||
constructor() {
|
||||
|
|
@ -51,8 +51,7 @@ export class PointImportButtonViz implements SpecialVisualization {
|
|||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
if (feature.geometry.type !== "Point") {
|
||||
return Translations.t.general.add.import.wrongType.SetClass("alert")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
const args = importFlow.args
|
||||
|
||||
// The following variables are used for the map
|
||||
const targetLayer: LayerConfig = state.layout.layers.find((l) => l.id === args.targetLayer)
|
||||
const targetLayers: LayerConfig[] = args.targetLayer.split(" ").map(tl => state.layout.layers.find((l) => l.id === tl))
|
||||
const snapToLayers: string[] | undefined =
|
||||
args.snap_onto_layers?.split(",")?.map((l) => l.trim()) ?? []
|
||||
const maxSnapDistance: number = Number(args.max_snap_distance ?? 25) ?? 25
|
||||
|
|
@ -33,21 +33,20 @@
|
|||
|
||||
async function onConfirm(): Promise<void> {
|
||||
const importedId = await importFlow.onConfirm(value.data, snappedTo.data)
|
||||
state.selectedLayer.setData(targetLayer)
|
||||
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(importedId))
|
||||
}
|
||||
</script>
|
||||
|
||||
<ImportFlow {importFlow} on:confirm={onConfirm}>
|
||||
<div class="relative" slot="map">
|
||||
<div class="h-32">
|
||||
<div class="h-64">
|
||||
<NewPointLocationInput
|
||||
coordinate={startCoordinate}
|
||||
{maxSnapDistance}
|
||||
{snapToLayers}
|
||||
{snappedTo}
|
||||
{state}
|
||||
{targetLayer}
|
||||
targetLayer={targetLayers}
|
||||
{value}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,254 +0,0 @@
|
|||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import all_languages from "../../assets/language_translations.json"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Combine from "../Base/Combine"
|
||||
import Title from "../Base/Title"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
||||
import List from "../Base/List"
|
||||
import { AllLanguagesSelector } from "./AllLanguagesSelector"
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { EditButton, SaveButton } from "./SaveButton"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
export class LanguageElement implements SpecialVisualization {
|
||||
funcName: string = "language_chooser"
|
||||
needsUrls = []
|
||||
|
||||
docs: string | BaseUIElement =
|
||||
"The language element allows to show and pick all known (modern) languages. The key can be set"
|
||||
|
||||
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
|
||||
{
|
||||
name: "key",
|
||||
required: true,
|
||||
doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `language:nl=yes` if nl is picked ",
|
||||
},
|
||||
{
|
||||
name: "question",
|
||||
required: true,
|
||||
doc: "What to ask if no questions are known",
|
||||
},
|
||||
{
|
||||
name: "render_list_item",
|
||||
doc: "How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain).",
|
||||
defaultValue: "{language()}",
|
||||
},
|
||||
{
|
||||
name: "render_single_language",
|
||||
doc: "What will be shown if the feature only supports a single language",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "render_all",
|
||||
doc: "The full rendering. Use `{list}` to show where the list of languages must come. Optional if mode=single",
|
||||
defaultValue: "{list()}",
|
||||
},
|
||||
{
|
||||
name: "no_known_languages",
|
||||
doc: "The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead",
|
||||
},
|
||||
{
|
||||
name: "mode",
|
||||
doc: "If one or many languages can be selected. Should be 'multi' or 'single'",
|
||||
defaultValue: "multi",
|
||||
},
|
||||
]
|
||||
|
||||
example: `
|
||||
\`\`\`json
|
||||
{"special":
|
||||
"type": "language_chooser",
|
||||
"key": "school:language",
|
||||
"question": {"en": "What are the main (and administrative) languages spoken in this school?"},
|
||||
"render_single_language": {"en": "{language()} is spoken on this school"},
|
||||
"render_list_item": {"en": "{language()}"},
|
||||
"render_all": {"en": "The following languages are spoken here:{list()}"}
|
||||
"mode":"multi"
|
||||
}
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] =
|
||||
argument
|
||||
if (mode === undefined || mode.length == 0) {
|
||||
mode = "multi"
|
||||
}
|
||||
if (item_render === undefined || item_render.trim() === "") {
|
||||
item_render = "{language()}"
|
||||
}
|
||||
if (all_render === undefined || all_render.length == 0) {
|
||||
all_render = "{list()}"
|
||||
}
|
||||
if (mode !== "single" && mode !== "multi") {
|
||||
throw (
|
||||
"Error while calling language_chooser: mode must be either 'single' or 'multi' but it is " +
|
||||
mode
|
||||
)
|
||||
}
|
||||
if (single_render.indexOf("{language()") < 0) {
|
||||
throw (
|
||||
"Error while calling language_chooser: render_single_language must contain '{language()}' but it is " +
|
||||
single_render
|
||||
)
|
||||
}
|
||||
if (item_render.indexOf("{language()") < 0) {
|
||||
throw (
|
||||
"Error while calling language_chooser: render_list_item must contain '{language()}' but it is " +
|
||||
item_render
|
||||
)
|
||||
}
|
||||
if (all_render.indexOf("{list()") < 0) {
|
||||
throw "Error while calling language_chooser: render_all must contain '{list()}'"
|
||||
}
|
||||
|
||||
const prefix = key + ":"
|
||||
const foundLanguages = tagSource.map((tags) => {
|
||||
const foundLanguages: string[] = []
|
||||
for (const k in tags) {
|
||||
const v = tags[k]
|
||||
if (v !== "yes") {
|
||||
continue
|
||||
}
|
||||
if (k.startsWith(prefix)) {
|
||||
foundLanguages.push(k.substring(prefix.length))
|
||||
}
|
||||
}
|
||||
return foundLanguages
|
||||
})
|
||||
const forceInputMode = new UIEventSource(false)
|
||||
const inputEl = new Lazy(() => {
|
||||
const selector = new AllLanguagesSelector({
|
||||
mode: mode === "single" ? "select-one" : "select-many",
|
||||
currentCountry: tagSource.map((tgs) => tgs["_country"]),
|
||||
})
|
||||
const cancelButton = Toggle.If(forceInputMode, () =>
|
||||
Translations.t.general.cancel
|
||||
.Clone()
|
||||
.SetClass("btn btn-secondary")
|
||||
.onClick(() => forceInputMode.setData(false))
|
||||
)
|
||||
|
||||
const saveButton = new SaveButton(
|
||||
selector.GetValue().map((lngs) => (lngs.length > 0 ? "true" : undefined)),
|
||||
state
|
||||
).onClick(() => {
|
||||
const selectedLanguages = selector.GetValue().data
|
||||
const currentLanguages = foundLanguages.data
|
||||
const selection: Tag[] = selectedLanguages.map((ln) => new Tag(prefix + ln, "yes"))
|
||||
|
||||
for (const currentLanguage of currentLanguages) {
|
||||
if (selectedLanguages.indexOf(currentLanguage) >= 0) {
|
||||
continue
|
||||
}
|
||||
// Erase language that is not spoken anymore
|
||||
selection.push(new Tag(prefix + currentLanguage, ""))
|
||||
}
|
||||
|
||||
if (state.featureSwitchIsTesting.data) {
|
||||
for (const tag of selection) {
|
||||
tagSource.data[tag.key] = tag.value
|
||||
}
|
||||
tagSource.ping()
|
||||
} else {
|
||||
;(state?.changes)
|
||||
.applyAction(
|
||||
new ChangeTagAction(
|
||||
tagSource.data.id,
|
||||
new And(selection),
|
||||
tagSource.data,
|
||||
{
|
||||
theme: state?.layout?.id ?? "unkown",
|
||||
changeType: "answer",
|
||||
}
|
||||
)
|
||||
)
|
||||
.then((_) => {
|
||||
console.log("Tagchanges applied")
|
||||
})
|
||||
}
|
||||
forceInputMode.setData(false)
|
||||
})
|
||||
|
||||
return new Combine([
|
||||
new Title(question),
|
||||
selector,
|
||||
new Combine([cancelButton, saveButton]).SetClass("flex justify-end"),
|
||||
]).SetClass("flex flex-col question disable-links")
|
||||
})
|
||||
|
||||
const editButton = new EditButton(state.osmConnection, () => forceInputMode.setData(true))
|
||||
|
||||
return new VariableUiElement(
|
||||
foundLanguages.map(
|
||||
(foundLanguages) => {
|
||||
if (forceInputMode.data) {
|
||||
return inputEl
|
||||
}
|
||||
|
||||
if (foundLanguages.length === 0) {
|
||||
// No languages found - we show the question and the input element
|
||||
if (
|
||||
on_no_known_languages !== undefined &&
|
||||
on_no_known_languages.length > 0
|
||||
) {
|
||||
return new Combine([on_no_known_languages, editButton]).SetClass(
|
||||
"flex justify-end"
|
||||
)
|
||||
}
|
||||
return inputEl
|
||||
}
|
||||
|
||||
let rendered: BaseUIElement
|
||||
if (foundLanguages.length === 1) {
|
||||
const ln = foundLanguages[0]
|
||||
let mapping = new Map<string, BaseUIElement>()
|
||||
mapping.set("language", new Translation(all_languages[ln]))
|
||||
rendered = new SubstitutedTranslation(
|
||||
new Translation({ "*": single_render }, undefined),
|
||||
tagSource,
|
||||
state,
|
||||
mapping
|
||||
)
|
||||
} else {
|
||||
let mapping = new Map<string, BaseUIElement>()
|
||||
const languagesList = new List(
|
||||
foundLanguages.map((ln) => {
|
||||
let mappingLn = new Map<string, BaseUIElement>()
|
||||
mappingLn.set("language", new Translation(all_languages[ln]))
|
||||
return new SubstitutedTranslation(
|
||||
new Translation({ "*": item_render }, undefined),
|
||||
tagSource,
|
||||
state,
|
||||
mappingLn
|
||||
)
|
||||
})
|
||||
)
|
||||
mapping.set("list", languagesList)
|
||||
rendered = new SubstitutedTranslation(
|
||||
new Translation({ "*": all_render }, undefined),
|
||||
tagSource,
|
||||
state,
|
||||
mapping
|
||||
)
|
||||
}
|
||||
return new Combine([rendered, editButton]).SetClass("flex justify-between")
|
||||
},
|
||||
[forceInputMode]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
46
src/UI/Popup/LanguageElement/LanguageAnswer.svelte
Normal file
46
src/UI/Popup/LanguageElement/LanguageAnswer.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import SpecialTranslation from "../TagRendering/SpecialTranslation.svelte"
|
||||
import { Translation, TypedTranslation } from "../../i18n/Translation"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import * as all_languages from "../../../assets/language_translations.json"
|
||||
|
||||
/**
|
||||
* Visualizes a list of the known languages
|
||||
*/
|
||||
export let languages: Store<string[]>
|
||||
export let single_render: string
|
||||
export let item_render: string
|
||||
export let render_all: string // Should contain one `{list()}`
|
||||
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig | undefined
|
||||
|
||||
let [beforeListing, afterListing] = (render_all ?? "{list()}").split("{list()}")
|
||||
</script>
|
||||
|
||||
{#if $languages.length === 1}
|
||||
<SpecialTranslation {state} {tags} {feature} {layer}
|
||||
t={new TypedTranslation({"*": single_render}).PartialSubsTr(
|
||||
"language()",
|
||||
new Translation(all_languages[$languages[0]], undefined)
|
||||
)}/>
|
||||
{:else}
|
||||
{beforeListing}
|
||||
<ul>
|
||||
{#each $languages as language}
|
||||
<li>
|
||||
<SpecialTranslation {state} {tags} {feature} {layer} t={
|
||||
new TypedTranslation({"*": item_render}).PartialSubsTr("language()",
|
||||
new Translation(all_languages[language], undefined) )}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{afterListing}
|
||||
{/if}
|
||||
69
src/UI/Popup/LanguageElement/LanguageElement.svelte
Normal file
69
src/UI/Popup/LanguageElement/LanguageElement.svelte
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import LanguageQuestion from "./LanguageQuestion.svelte"
|
||||
import LanguageAnswer from "./LanguageAnswer.svelte"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import EditButton from "../TagRendering/EditButton.svelte"
|
||||
|
||||
export let key: string
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig | undefined
|
||||
|
||||
export let question: string
|
||||
export let on_no_known_languages: string = undefined
|
||||
export let single_render: string
|
||||
export let item_render: string
|
||||
export let render_all: string // Should contain one `{list()}`
|
||||
let prefix = key + ":"
|
||||
let foundLanguages = tags.map((tags) => {
|
||||
const foundLanguages: string[] = []
|
||||
for (const k in tags) {
|
||||
const v = tags[k]
|
||||
if (v !== "yes") {
|
||||
continue
|
||||
}
|
||||
if (k.startsWith(prefix)) {
|
||||
foundLanguages.push(k.substring(prefix.length))
|
||||
}
|
||||
}
|
||||
return foundLanguages
|
||||
})
|
||||
|
||||
const forceInputMode = new UIEventSource(false)
|
||||
|
||||
</script>
|
||||
|
||||
{#if $foundLanguages.length === 0 && on_no_known_languages && !$forceInputMode}
|
||||
<div class="p-1 flex items-center justify-between low-interaction rounded">
|
||||
<div>
|
||||
{on_no_known_languages}
|
||||
</div>
|
||||
<EditButton on:click={_ => forceInputMode.setData(true)} />
|
||||
</div>
|
||||
{:else if $forceInputMode || $foundLanguages.length === 0}
|
||||
<LanguageQuestion {question} {foundLanguages} {prefix} {state} {tags} {feature} {layer}
|
||||
on:save={_ => forceInputMode.setData(false)}>
|
||||
<span slot="cancel-button">
|
||||
{#if $forceInputMode}
|
||||
<button on:click={_ => forceInputMode.setData(false)}>
|
||||
<Tr t={Translations.t.general.cancel} />
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
</LanguageQuestion>
|
||||
{:else}
|
||||
<div class="p-2 flex items-center justify-between low-interaction rounded">
|
||||
<div>
|
||||
<LanguageAnswer {single_render} {item_render} {render_all} languages={foundLanguages} {state} {tags} { feature}
|
||||
{layer} />
|
||||
</div>
|
||||
<EditButton on:click={_ => forceInputMode.setData(true)} />
|
||||
</div>
|
||||
{/if}
|
||||
107
src/UI/Popup/LanguageElement/LanguageElement.ts
Normal file
107
src/UI/Popup/LanguageElement/LanguageElement.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement"
|
||||
import { Feature } from "geojson"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { default as LanguageElementSvelte } from "./LanguageElement.svelte"
|
||||
|
||||
export class LanguageElement implements SpecialVisualization {
|
||||
funcName: string = "language_chooser"
|
||||
needsUrls = []
|
||||
|
||||
docs: string | BaseUIElement =
|
||||
"The language element allows to show and pick all known (modern) languages. The key can be set"
|
||||
|
||||
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
|
||||
{
|
||||
name: "key",
|
||||
required: true,
|
||||
doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `language:nl=yes` if nl is picked ",
|
||||
},
|
||||
{
|
||||
name: "question",
|
||||
required: true,
|
||||
doc: "What to ask if no questions are known",
|
||||
},
|
||||
{
|
||||
name: "render_list_item",
|
||||
doc: "How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain).",
|
||||
defaultValue: "{language()}",
|
||||
},
|
||||
{
|
||||
name: "render_single_language",
|
||||
doc: "What will be shown if the feature only supports a single language",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "render_all",
|
||||
doc: "The full rendering. Use `{list}` to show where the list of languages must come. Optional if mode=single",
|
||||
defaultValue: "{list()}",
|
||||
},
|
||||
{
|
||||
name: "no_known_languages",
|
||||
doc: "The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead",
|
||||
},
|
||||
]
|
||||
|
||||
example: `
|
||||
\`\`\`json
|
||||
{"special":
|
||||
"type": "language_chooser",
|
||||
"key": "school:language",
|
||||
"question": {"en": "What are the main (and administrative) languages spoken in this school?"},
|
||||
"render_single_language": {"en": "{language()} is spoken on this school"},
|
||||
"render_list_item": {"en": "{language()}"},
|
||||
"render_all": {"en": "The following languages are spoken here:{list()}"}
|
||||
}
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
let [key, question, item_render, single_render, all_render, on_no_known_languages] =
|
||||
argument
|
||||
if (item_render === undefined || item_render.trim() === "") {
|
||||
item_render = "{language()}"
|
||||
}
|
||||
if (all_render === undefined || all_render.length == 0) {
|
||||
all_render = "{list()}"
|
||||
}
|
||||
if (single_render.indexOf("{language()") < 0) {
|
||||
throw (
|
||||
"Error while calling language_chooser: render_single_language must contain '{language()}' but it is " +
|
||||
single_render
|
||||
)
|
||||
}
|
||||
if (item_render.indexOf("{language()") < 0) {
|
||||
throw (
|
||||
"Error while calling language_chooser: render_list_item must contain '{language()}' but it is " +
|
||||
item_render
|
||||
)
|
||||
}
|
||||
if (all_render.indexOf("{list()") < 0) {
|
||||
throw "Error while calling language_chooser: render_all must contain '{list()}'"
|
||||
}
|
||||
if (on_no_known_languages === "") {
|
||||
on_no_known_languages = undefined
|
||||
}
|
||||
|
||||
return new SvelteUIElement(LanguageElementSvelte, {
|
||||
key,
|
||||
tags: tagSource,
|
||||
state,
|
||||
feature,
|
||||
layer,
|
||||
question,
|
||||
on_no_known_languages,
|
||||
single_render,
|
||||
item_render,
|
||||
})
|
||||
}
|
||||
}
|
||||
133
src/UI/Popup/LanguageElement/LanguageOptions.svelte
Normal file
133
src/UI/Popup/LanguageElement/LanguageOptions.svelte
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<script lang="ts">/**
|
||||
* An input element which allows to select one or more langauges
|
||||
*/
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import all_languages from "../../../assets/language_translations.json"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import Translations from "../../i18n/Translations.js"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Locale from "../../i18n/Locale"
|
||||
|
||||
/**
|
||||
* Will contain one or more ISO-language codes
|
||||
*/
|
||||
export let selectedLanguages: UIEventSource<string[]>
|
||||
/**
|
||||
* The country (countries) that the point lies in.
|
||||
* Note that a single place might be claimed by multiple countries
|
||||
*/
|
||||
export let countries: Set<string>
|
||||
let searchValue: UIEventSource<string> = new UIEventSource<string>("")
|
||||
let searchLC = searchValue.mapD(search => search.toLowerCase())
|
||||
const knownLanguagecodes = Object.keys(all_languages)
|
||||
let probableLanguages = []
|
||||
let isChecked = {}
|
||||
for (const lng of knownLanguagecodes) {
|
||||
const lngInfo = all_languages[lng]
|
||||
if (lngInfo._meta?.countries?.some(l => countries.has(l))) {
|
||||
probableLanguages.push(lng)
|
||||
}
|
||||
isChecked[lng] = false
|
||||
}
|
||||
let newlyChecked: UIEventSource<string[]> = new UIEventSource<string[]>([])
|
||||
|
||||
function update(isChecked: Record<string, boolean>) {
|
||||
const currentlyChecked = new Set<string>(selectedLanguages.data)
|
||||
const languages: string[] = []
|
||||
for (const lng in isChecked) {
|
||||
if (isChecked[lng]) {
|
||||
languages.push(lng)
|
||||
if (!currentlyChecked.has(lng)) {
|
||||
newlyChecked.data.push(lng)
|
||||
newlyChecked.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedLanguages.setData(languages)
|
||||
}
|
||||
function matchesSearch(lng: string, searchLc: string | undefined): boolean {
|
||||
if(!searchLc){
|
||||
return
|
||||
}
|
||||
if(lng.indexOf(searchLc) >= 0){
|
||||
return true
|
||||
}
|
||||
|
||||
const languageInfo = all_languages[lng]
|
||||
const native : string = languageInfo[lng]?.toLowerCase()
|
||||
if(native?.indexOf(searchLc) >= 0){
|
||||
return true
|
||||
}
|
||||
const current : string = languageInfo[Locale.language.data]?.toLowerCase()
|
||||
if(current?.indexOf(searchLc) >= 0){
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
function onEnter(){
|
||||
// we select the first match which is not yet checked
|
||||
for (const lng of knownLanguagecodes) {
|
||||
if(lng === searchLC.data){
|
||||
isChecked[lng] = true
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
for (const lng of knownLanguagecodes) {
|
||||
if(matchesSearch(lng, searchLC.data)){
|
||||
isChecked[lng] = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
$: {
|
||||
update(isChecked)
|
||||
}
|
||||
searchValue.addCallback(_ => {
|
||||
newlyChecked.setData([])
|
||||
})
|
||||
</script>
|
||||
<form on:submit|preventDefault={() => onEnter()}>
|
||||
|
||||
{#each probableLanguages as lng}
|
||||
|
||||
<label class="no-image-background flex items-center gap-1">
|
||||
<input bind:checked={isChecked[lng]} type="checkbox" />
|
||||
<Tr t={new Translation(all_languages[lng])} />
|
||||
<span class="subtle">({lng})</span>
|
||||
</label>
|
||||
|
||||
{/each}
|
||||
|
||||
<label class="block relative neutral-label m-4 mx-16">
|
||||
<SearchIcon class="w-6 h-6 absolute right-0" />
|
||||
<input bind:value={$searchValue} type="text" />
|
||||
<Tr t={Translations.t.general.useSearch} />
|
||||
</label>
|
||||
|
||||
<div class="overflow-auto" style="max-height: 25vh">
|
||||
{#each knownLanguagecodes as lng}
|
||||
{#if (isChecked[lng]) && $newlyChecked.indexOf(lng) < 0 && probableLanguages.indexOf(lng) < 0}
|
||||
<label class="no-image-background flex items-center gap-1">
|
||||
<input bind:checked={isChecked[lng]} type="checkbox" />
|
||||
<Tr t={new Translation(all_languages[lng])} />
|
||||
<span class="subtle">({lng})</span>
|
||||
</label>
|
||||
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#each knownLanguagecodes as lng}
|
||||
{#if $searchLC.length > 0 && matchesSearch(lng, $searchLC) && (!isChecked[lng] || $newlyChecked.indexOf(lng) >= 0) && probableLanguages.indexOf(lng) < 0}
|
||||
<label class="no-image-background flex items-center gap-1">
|
||||
<input bind:checked={isChecked[lng]} type="checkbox" />
|
||||
<Tr t={new Translation(all_languages[lng])} />
|
||||
<span class="subtle">({lng})</span>
|
||||
</label>
|
||||
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</form>
|
||||
87
src/UI/Popup/LanguageElement/LanguageQuestion.svelte
Normal file
87
src/UI/Popup/LanguageElement/LanguageQuestion.svelte
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<script lang="ts">/**
|
||||
* The 'languageQuestion' is a special element which asks about the (possible) languages of a feature
|
||||
* (e.g. which speech output an ATM has, in what language(s) the braille writing is or what languages are spoken at a school)
|
||||
*
|
||||
* This is written into a `key`.
|
||||
*
|
||||
*/
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import SpecialTranslation from "../TagRendering/SpecialTranslation.svelte"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import type { Store } from "../../../Logic/UIEventSource"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import LanguageOptions from "./LanguageOptions.svelte"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
|
||||
export let question: string
|
||||
export let prefix: string
|
||||
|
||||
export let foundLanguages: Store<string[]>
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig | undefined
|
||||
let dispatch = createEventDispatcher<{ save }>()
|
||||
|
||||
let selectedLanguages: UIEventSource<string[]> = new UIEventSource<string[]>([])
|
||||
let countries: Store<Set<string>> = tags.mapD(tags => new Set<string>(tags["_country"]?.toUpperCase()?.split(";") ?? []))
|
||||
async function applySelectedLanguages() {
|
||||
const selectedLngs = selectedLanguages.data
|
||||
const selection: Tag[] = selectedLanguages.data.map((ln) => new Tag(prefix + ln, "yes"))
|
||||
if (selection.length === 0) {
|
||||
return
|
||||
}
|
||||
const currentLanguages = foundLanguages.data
|
||||
|
||||
for (const currentLanguage of currentLanguages) {
|
||||
if (selectedLngs.indexOf(currentLanguage) >= 0) {
|
||||
continue
|
||||
}
|
||||
// Erase languages that are not spoken anymore
|
||||
selection.push(new Tag(prefix + currentLanguage, ""))
|
||||
}
|
||||
|
||||
if (state === undefined || state?.featureSwitchIsTesting?.data) {
|
||||
for (const tag of selection) {
|
||||
tags.data[tag.key] = tag.value
|
||||
}
|
||||
tags.ping()
|
||||
} else if (state.changes) {
|
||||
await state.changes
|
||||
.applyAction(
|
||||
new ChangeTagAction(
|
||||
tags.data.id,
|
||||
new And(selection),
|
||||
tags.data,
|
||||
{
|
||||
theme: state?.layout?.id ?? "unkown",
|
||||
changeType: "answer",
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
dispatch("save")
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col disable-links interactive border-interactive p-2">
|
||||
<div class="interactive justify-between pt-1 font-bold">
|
||||
<SpecialTranslation {feature} {layer} {state} t={new Translation({"*":question})} {tags} />
|
||||
</div>
|
||||
<LanguageOptions {selectedLanguages} countries={$countries}/>
|
||||
|
||||
<div class="flex justify-end flex-wrap-reverse w-full">
|
||||
<slot name="cancel-button"></slot>
|
||||
<button class="primary" class:disabled={$selectedLanguages.length === 0} on:click={_ => applySelectedLanguages()}>
|
||||
<Tr t={Translations.t.general.save} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -6,7 +6,6 @@ import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
|||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ export class MinimapViz implements SpecialVisualization {
|
|||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
feature: Feature
|
||||
) {
|
||||
if (state === undefined || feature === undefined) {
|
||||
return undefined
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
import Geosearch from "../BigComponents/Geosearch.svelte"
|
||||
import If from "../Base/If.svelte"
|
||||
import Constants from "../../Models/Constants"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
|
|
@ -49,7 +50,7 @@
|
|||
let notAllowed = moveWizardState.moveDisallowedReason
|
||||
let currentMapProperties: MapProperties = undefined
|
||||
</script>
|
||||
|
||||
<LoginToggle {state}>
|
||||
{#if moveWizardState.reasons.length > 0}
|
||||
{#if $notAllowed}
|
||||
<div class="m-2 flex rounded-lg bg-gray-200 p-2">
|
||||
|
|
@ -165,3 +166,4 @@
|
|||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import { Tag } from "../../Logic/Tags/Tag"
|
|||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Feature, Point } from "geojson"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import Confirm from "../../assets/svg/Confirm.svelte"
|
||||
import Relocation from "../../assets/svg/Relocation.svelte"
|
||||
import Location from "../../assets/svg/Location.svelte"
|
||||
|
||||
export interface MoveReason {
|
||||
text: Translation | string
|
||||
|
|
@ -62,7 +62,7 @@ export class MoveWizardState {
|
|||
reasons.push({
|
||||
text: t.reasons.reasonInaccurate,
|
||||
invitingText: t.inviteToMove.reasonInaccurate,
|
||||
icon: new SvelteUIElement(Confirm),
|
||||
icon: new SvelteUIElement(Location),
|
||||
changesetCommentValue: "improve_accuracy",
|
||||
lockBounds: true,
|
||||
includeSearch: false,
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ export class AddNoteCommentViz implements SpecialVisualization {
|
|||
|
||||
public constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
tags: UIEventSource<Record<string, string>>
|
||||
) {
|
||||
return new SvelteUIElement(AddNoteComment, { state, tags })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import BaseUIElement from "../../BaseUIElement"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { Utils } from "../../../Utils"
|
||||
import Svg from "../../../Svg"
|
||||
import Img from "../../Base/Img"
|
||||
import { SubtleButton } from "../../Base/SubtleButton"
|
||||
import Toggle from "../../Input/Toggle"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import Combine from "../../Base/Combine"
|
||||
import BaseUIElement from "../../BaseUIElement"
|
||||
import Svg from "../../../Svg"
|
||||
import Link from "../../Base/Link"
|
||||
import { FixedUiElement } from "../../Base/FixedUiElement"
|
||||
import Translations from "../../i18n/Translations"
|
||||
|
|
@ -11,6 +10,10 @@ import { Stores, UIEventSource } from "../../../Logic/UIEventSource"
|
|||
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
|
||||
import { VariableUiElement } from "../../Base/VariableUIElement"
|
||||
import { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement"
|
||||
import Note from "../../../assets/svg/Note.svelte"
|
||||
import Resolved from "../../../assets/svg/Resolved.svelte"
|
||||
import Speech_bubble from "../../../assets/svg/Speech_bubble.svelte"
|
||||
|
||||
export default class NoteCommentElement extends Combine {
|
||||
constructor(
|
||||
|
|
@ -32,11 +35,11 @@ export default class NoteCommentElement extends Combine {
|
|||
|
||||
let actionIcon: BaseUIElement
|
||||
if (comment.action === "opened" || comment.action === "reopened") {
|
||||
actionIcon = Svg.note_svg()
|
||||
actionIcon = new SvelteUIElement(Note)
|
||||
} else if (comment.action === "closed") {
|
||||
actionIcon = Svg.resolved_svg()
|
||||
actionIcon = new SvelteUIElement(Resolved)
|
||||
} else {
|
||||
actionIcon = Svg.speech_bubble_svg()
|
||||
actionIcon = new SvelteUIElement(Speech_bubble)
|
||||
}
|
||||
|
||||
let user: BaseUIElement
|
||||
|
|
@ -72,6 +75,7 @@ export default class NoteCommentElement extends Combine {
|
|||
const extension = link.substring(lastDotIndex + 1, link.length)
|
||||
return Utils.imageExtensions.has(extension)
|
||||
})
|
||||
.filter(link => !link.startsWith("https://wiki.openstreetmap.org/wiki/File:"))
|
||||
let imagesEl: BaseUIElement = undefined
|
||||
if (images.length > 0) {
|
||||
const imageEls = images.map((i) =>
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import { LoginToggle } from "./LoginButton"
|
||||
|
||||
export class EditButton extends Toggle {
|
||||
constructor(osmConnection: OsmConnection, onClick: () => void) {
|
||||
super(
|
||||
new Combine([Svg.pencil_svg()])
|
||||
.SetClass("block relative h-10 w-10 p-2 float-right")
|
||||
.SetStyle("border: 1px solid black; border-radius: 0.7em")
|
||||
.onClick(onClick),
|
||||
undefined,
|
||||
osmConnection.isLoggedIn
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class SaveButton extends LoginToggle {
|
||||
constructor(
|
||||
value: Store<any>,
|
||||
state: {
|
||||
readonly osmConnection?: OsmConnection
|
||||
readonly featureSwitchUserbadge?: Store<boolean>
|
||||
},
|
||||
textEnabled?: BaseUIElement,
|
||||
textDisabled?: BaseUIElement
|
||||
) {
|
||||
if (value === undefined) {
|
||||
throw "No event source for savebutton, something is wrong"
|
||||
}
|
||||
|
||||
const isSaveable = value.map((v) => v !== false && (v ?? "") !== "")
|
||||
|
||||
const saveEnabled = (textEnabled ?? Translations.t.general.save.Clone()).SetClass(`btn`)
|
||||
const saveDisabled = (textDisabled ?? Translations.t.general.save.Clone()).SetClass(
|
||||
`btn btn-disabled`
|
||||
)
|
||||
|
||||
const save = new Toggle(saveEnabled, saveDisabled, isSaveable)
|
||||
super(save, Translations.t.general.loginToStart, state)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Envelope from "../../assets/svg/Envelope.svelte"
|
||||
|
||||
export let tags: Store<OsmTags>
|
||||
export let args: string[]
|
||||
|
|
@ -14,6 +15,6 @@
|
|||
</script>
|
||||
|
||||
<a class="button flex w-full items-center" href={url} style="margin-left: 0">
|
||||
<ToSvelte construct={Svg.envelope_svg().SetClass("w-8 h-8 mr-4 shrink-0")} />
|
||||
<Envelope class="w-8 h-8 mr-4 shrink-0"/>
|
||||
{button_text}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import Toggle from "../Input/Toggle"
|
||||
import Svg from "../../Svg"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Combine from "../Base/Combine"
|
||||
|
|
@ -19,6 +18,7 @@ import { Changes } from "../../Logic/Osm/Changes"
|
|||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
|
||||
import Scissors from "../../assets/svg/Scissors.svelte"
|
||||
|
||||
export default class SplitRoadWizard extends Combine {
|
||||
public dialogIsOpened: UIEventSource<boolean>
|
||||
|
|
@ -26,8 +26,8 @@ export default class SplitRoadWizard extends Combine {
|
|||
/**
|
||||
* A UI Element used for splitting roads
|
||||
*
|
||||
* @param id: The id of the road to remove
|
||||
* @param state: the state of the application
|
||||
* @param id The id of the road to remove
|
||||
* @param state the state of the application
|
||||
*/
|
||||
constructor(
|
||||
id: WayId,
|
||||
|
|
@ -68,7 +68,7 @@ export default class SplitRoadWizard extends Combine {
|
|||
|
||||
// Toggle between splitmap
|
||||
const splitButton = new SubtleButton(
|
||||
Svg.scissors_svg().SetStyle("height: 1.5rem; width: auto"),
|
||||
new SvelteUIElement(Scissors).SetClass("h-6 w-6"),
|
||||
new Toggle(
|
||||
t.splitAgain.Clone().SetClass("text-lg font-bold"),
|
||||
t.inviteToSplit.Clone().SetClass("text-lg font-bold"),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
|||
import { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Maproulette from "../../Logic/Maproulette"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import { Map } from "maplibre-gl"
|
||||
|
||||
export default class TagApplyButton implements AutoAction, SpecialVisualization {
|
||||
public readonly funcName = "tag_apply"
|
||||
|
|
@ -45,9 +48,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
doc: "If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element",
|
||||
},
|
||||
{
|
||||
name: "maproulette_task_id",
|
||||
name: "maproulette_id",
|
||||
defaultValue: undefined,
|
||||
doc: "If specified, this maproulette-challenge will be closed when the tags are applied",
|
||||
doc: "If specified, this maproulette-challenge will be closed when the tags are applied. This should be the ID of the task, _not_ the task_id.",
|
||||
},
|
||||
]
|
||||
public readonly example =
|
||||
|
|
@ -81,7 +84,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
for (const [key, value] of tgsSpec) {
|
||||
if (value.indexOf("$") >= 0) {
|
||||
let parts = value.split("$")
|
||||
// THe first of the split won't start with a '$', so no substitution needed
|
||||
// The first item of the split won't start with a '$', so no substitution needed
|
||||
let actualValue = parts[0]
|
||||
parts.shift()
|
||||
|
||||
|
|
@ -111,7 +114,6 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
|
||||
while (spec.length > 0) {
|
||||
const [part] = spec.match(/((\\;)|[^;])*/)
|
||||
console.log("Spec is", part, spec)
|
||||
spec = spec.substring(part.length + 1) // +1 to remove the pending ';' as well
|
||||
const kv = part.split("=").map((s) => s.trim().replace("\\;", ";"))
|
||||
if (kv.length == 2) {
|
||||
|
|
@ -133,12 +135,8 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
}
|
||||
|
||||
public async applyActionOn(
|
||||
feature: Feature,
|
||||
state: {
|
||||
layout: LayoutConfig
|
||||
changes: Changes
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
},
|
||||
_: Feature,
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<any>,
|
||||
args: string[]
|
||||
): Promise<void> {
|
||||
|
|
@ -156,14 +154,22 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
}
|
||||
)
|
||||
await state.changes.applyAction(changeAction)
|
||||
try {
|
||||
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(targetId))
|
||||
}catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
const maproulette_id_key = args[4]
|
||||
if (maproulette_id_key) {
|
||||
const maproulette_id = Number(tags.data[maproulette_id_key])
|
||||
await Maproulette.singleton.closeTask(maproulette_id, Maproulette.STATUS_FIXED, {
|
||||
const maproulette_id = tags.data[ maproulette_id_key]
|
||||
const maproulette_feature= state.indexedFeatures.featuresById.data.get(
|
||||
maproulette_id)
|
||||
const maproulette_task_id = Number(maproulette_feature.properties.mr_taskId)
|
||||
await Maproulette.singleton.closeTask(maproulette_task_id, Maproulette.STATUS_FIXED, {
|
||||
comment: "Tags are copied onto " + targetId + " with MapComplete",
|
||||
})
|
||||
tags.data["mr_taskStatus"] = "Fixed"
|
||||
tags.ping()
|
||||
maproulette_feature.properties["mr_taskStatus"] = "Fixed"
|
||||
state.featureProperties.getStore(maproulette_id).ping()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +186,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
if (image === "" || image === "undefined") {
|
||||
image = undefined
|
||||
}
|
||||
|
||||
const targetIdKey = args[3]
|
||||
const t = Translations.t.general.apply_button
|
||||
|
||||
|
|
@ -195,9 +202,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
})
|
||||
).SetClass("subtle")
|
||||
const self = this
|
||||
const applied = new UIEventSource(tags?.data?.["mr_taskStatus"] !== "Created") // This will default to 'false' for non-maproulette challenges
|
||||
const applied = new UIEventSource(tags?.data?.["mr_taskStatus"] !== undefined && tags?.data?.["mr_taskStatus"] !== "Created") // This will default to 'false' for non-maproulette challenges
|
||||
const applyButton = new SubtleButton(
|
||||
image,
|
||||
new SvelteUIElement(Icon, {icon: image}),
|
||||
new Combine([msg, tagsExplanation]).SetClass("flex flex-col")
|
||||
).onClick(async () => {
|
||||
applied.setData(true)
|
||||
|
|
|
|||
23
src/UI/Popup/TagRendering/EditButton.svelte
Normal file
23
src/UI/Popup/TagRendering/EditButton.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { PencilAltIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { ariaLabel } from "../../../Utils/ariaLabel.js";
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
|
||||
/**
|
||||
* A small, round button with an edit-icon (and aria-labels etc)
|
||||
*/
|
||||
/**
|
||||
* What arialabel to apply onto this button?
|
||||
*/
|
||||
export let arialabel : Translation = undefined;
|
||||
export let ariaLabelledBy: string = undefined
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click
|
||||
class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1"
|
||||
aria-labelledby={arialabel === undefined ? ariaLabelledBy : undefined}
|
||||
use:ariaLabel={arialabel}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</button>
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
export let unvalidatedText: UIEventSource<string> = new UIEventSource<string>(value.data)
|
||||
export let config: TagRenderingConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
||||
|
|
@ -64,6 +65,7 @@
|
|||
type={config.freeform.type}
|
||||
{placeholder}
|
||||
{value}
|
||||
{unvalidatedText}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
@ -74,5 +76,6 @@
|
|||
{value}
|
||||
{state}
|
||||
on:submit
|
||||
{unvalidatedText}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import type { Feature } from "geojson"
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { onDestroy } from "svelte"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
|
|
@ -24,7 +23,7 @@
|
|||
throw "Config is undefined in tagRenderingAnswer"
|
||||
}
|
||||
let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) =>
|
||||
Utils.NoNull(config?.GetRenderValues(tags))
|
||||
Utils.NoNull(config?.GetRenderValues(tags)),
|
||||
)
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
import { Utils } from "../../../Utils"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { ariaLabel } from "../../../Utils/ariaLabel"
|
||||
import EditButton from "./EditButton.svelte"
|
||||
import EditItemButton from "../../Studio/EditItemButton.svelte"
|
||||
|
||||
export let config: TagRenderingConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
|
@ -63,7 +65,6 @@
|
|||
if (config.id === highlighted) {
|
||||
htmlElem.classList.add("glowing-shadow")
|
||||
htmlElem.tabIndex = -1
|
||||
console.log("Scrolling to", htmlElem)
|
||||
htmlElem.scrollIntoView({ behavior: "smooth" })
|
||||
Utils.focusOnFocusableChild(htmlElem)
|
||||
} else {
|
||||
|
|
@ -88,6 +89,7 @@
|
|||
{state}
|
||||
{layer}
|
||||
on:saved={() => (editMode = false)}
|
||||
allowDeleteOfFreeform={true}
|
||||
>
|
||||
<button
|
||||
slot="cancel"
|
||||
|
|
@ -102,16 +104,12 @@
|
|||
{:else}
|
||||
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">
|
||||
<TagRenderingAnswer id={answerId} {config} {tags} {selectedElement} {state} {layer} />
|
||||
<button
|
||||
<EditButton
|
||||
arialabel={config.editButtonAriaLabel}
|
||||
ariaLabelledBy={answerId}
|
||||
on:click={() => {
|
||||
editMode = true
|
||||
}}
|
||||
class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1"
|
||||
aria-labelledby={config.editButtonAriaLabel === undefined ? answerId : undefined}
|
||||
use:ariaLabel={config.editButtonAriaLabel}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</button>
|
||||
editMode = true
|
||||
}}/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
{#if mapping.icon !== undefined}
|
||||
<div class="inline-flex items-center">
|
||||
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-2")} />
|
||||
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass ?? "small"}`, "mr-2")} />
|
||||
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} />
|
||||
</div>
|
||||
{:else if mapping.then !== undefined}
|
||||
|
|
|
|||
|
|
@ -22,17 +22,23 @@
|
|||
import { Unit } from "../../../Models/Unit"
|
||||
import UserRelatedState from "../../../Logic/State/UserRelatedState"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import type { UploadableTag } from "../../../Logic/Tags/TagUtils"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
|
||||
import Search from "../../../assets/svg/Search.svelte"
|
||||
import Login from "../../../assets/svg/Login.svelte"
|
||||
import { placeholder } from "../../../Utils/placeholder"
|
||||
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
|
||||
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: TagsFilter = undefined
|
||||
export let selectedTags: UploadableTag = undefined
|
||||
|
||||
export let allowDeleteOfFreeform: boolean = false
|
||||
|
||||
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
|
||||
|
||||
|
|
@ -40,13 +46,15 @@
|
|||
|
||||
// Will be bound if a freeform is available
|
||||
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
|
||||
let freeformInputUnvalidated = new UIEventSource<string>(freeformInput.data)
|
||||
|
||||
let selectedMapping: number = undefined
|
||||
/**
|
||||
* A list of booleans, used if multiAnswer is set
|
||||
*/
|
||||
let checkedMappings: boolean[]
|
||||
|
||||
let mappings: Mapping[] = config?.mappings
|
||||
let mappings: Mapping[] = config?.mappings ?? []
|
||||
let searchTerm: UIEventSource<string> = new UIEventSource("")
|
||||
|
||||
let dispatch = createEventDispatcher<{
|
||||
|
|
@ -128,7 +136,6 @@
|
|||
}
|
||||
|
||||
freeformInput.addCallbackAndRun((freeformValue) => {
|
||||
console.log("FreeformValue:", freeformValue)
|
||||
if (!mappings || mappings?.length == 0 || config.freeform?.key === undefined) {
|
||||
return
|
||||
}
|
||||
|
|
@ -146,20 +153,25 @@
|
|||
}
|
||||
})
|
||||
$: {
|
||||
try {
|
||||
selectedTags = config?.constructChangeSpecification(
|
||||
$freeformInput,
|
||||
selectedMapping,
|
||||
checkedMappings,
|
||||
tags.data,
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Could not calculate changeSpecification:", e)
|
||||
selectedTags = undefined
|
||||
if (allowDeleteOfFreeform && $freeformInput === undefined && $freeformInputUnvalidated === "" && (mappings?.length ?? 0) === 0) {
|
||||
selectedTags = new Tag(config.freeform.key, "")
|
||||
} else {
|
||||
|
||||
try {
|
||||
selectedTags = config?.constructChangeSpecification(
|
||||
$freeformInput,
|
||||
selectedMapping,
|
||||
checkedMappings,
|
||||
tags.data,
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Could not calculate changeSpecification:", e)
|
||||
selectedTags = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onSave(e) {
|
||||
function onSave(_ = undefined) {
|
||||
if (selectedTags === undefined) {
|
||||
return
|
||||
}
|
||||
|
|
@ -198,9 +210,9 @@
|
|||
|
||||
function onInputKeypress(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onSave(e)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onSave()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -231,136 +243,139 @@
|
|||
<fieldset>
|
||||
|
||||
<legend>
|
||||
<div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11">
|
||||
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
|
||||
</div>
|
||||
<div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11">
|
||||
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
|
||||
</div>
|
||||
|
||||
{#if config.questionhint}
|
||||
<div class="max-h-60 overflow-y-auto">
|
||||
<SpecialTranslation
|
||||
t={config.questionhint}
|
||||
{tags}
|
||||
{state}
|
||||
{layer}
|
||||
feature={selectedElement}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if config.questionhint}
|
||||
<div class="max-h-60 overflow-y-auto">
|
||||
<SpecialTranslation
|
||||
t={config.questionhint}
|
||||
{tags}
|
||||
{state}
|
||||
{layer}
|
||||
feature={selectedElement}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</legend>
|
||||
|
||||
{#if config.mappings?.length >= 8}
|
||||
<div class="sticky flex w-full" aria-hidden="true">
|
||||
<Search class="h-6 w-6" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$searchTerm}
|
||||
class="w-full"
|
||||
use:placeholder={Translations.t.general.searchAnswer}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if config.mappings?.length >= 8}
|
||||
<div class="sticky flex w-full" aria-hidden="true">
|
||||
<Search class="h-6 w-6" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$searchTerm}
|
||||
class="w-full"
|
||||
use:placeholder={Translations.t.general.searchAnswer}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if config.freeform?.key && !(mappings?.length > 0)}
|
||||
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
|
||||
<FreeformInput
|
||||
{config}
|
||||
{#if config.freeform?.key && !(mappings?.length > 0)}
|
||||
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
|
||||
<FreeformInput
|
||||
{config}
|
||||
{tags}
|
||||
{feedback}
|
||||
{unit}
|
||||
{state}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
on:submit={onSave}
|
||||
/>
|
||||
{:else if mappings !== undefined && !config.multiAnswer}
|
||||
<!-- Simple radiobuttons as mapping -->
|
||||
<div class="flex flex-col">
|
||||
{#each config.mappings as mapping, i (mapping.then)}
|
||||
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
|
||||
<TagRenderingMappingInput
|
||||
{mapping}
|
||||
{tags}
|
||||
{feedback}
|
||||
{unit}
|
||||
{state}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
on:submit={onSave}
|
||||
/>
|
||||
{:else if mappings !== undefined && !config.multiAnswer}
|
||||
<!-- Simple radiobuttons as mapping -->
|
||||
<div class="flex flex-col">
|
||||
{#each config.mappings as mapping, i (mapping.then)}
|
||||
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
|
||||
<TagRenderingMappingInput
|
||||
{mapping}
|
||||
{tags}
|
||||
{state}
|
||||
{selectedElement}
|
||||
{layer}
|
||||
{searchTerm}
|
||||
mappingIsSelected={selectedMapping === i}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={selectedMapping}
|
||||
name={"mappings-radio-" + config.id}
|
||||
value={i}
|
||||
on:keypress={(e) => onInputKeypress(e)}
|
||||
/>
|
||||
</TagRenderingMappingInput>
|
||||
{/each}
|
||||
{#if config.freeform?.key}
|
||||
<label class="flex gap-x-1">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={selectedMapping}
|
||||
name={"mappings-radio-" + config.id}
|
||||
value={config.mappings?.length}
|
||||
on:keypress={(e) => onInputKeypress(e)}
|
||||
/>
|
||||
<FreeformInput
|
||||
{config}
|
||||
{tags}
|
||||
{feedback}
|
||||
{unit}
|
||||
{state}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
on:selected={() => (selectedMapping = config.mappings?.length)}
|
||||
on:submit={onSave}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if mappings !== undefined && config.multiAnswer}
|
||||
<!-- Multiple answers can be chosen: checkboxes -->
|
||||
<div class="flex flex-col">
|
||||
{#each config.mappings as mapping, i (mapping.then)}
|
||||
<TagRenderingMappingInput
|
||||
{mapping}
|
||||
{tags}
|
||||
{state}
|
||||
{selectedElement}
|
||||
{layer}
|
||||
{searchTerm}
|
||||
mappingIsSelected={checkedMappings[i]}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={"mappings-checkbox-" + config.id + "-" + i}
|
||||
bind:checked={checkedMappings[i]}
|
||||
on:keypress={(e) => onInputKeypress(e)}
|
||||
/>
|
||||
</TagRenderingMappingInput>
|
||||
{/each}
|
||||
{#if config.freeform?.key}
|
||||
<label class="flex gap-x-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
|
||||
bind:checked={checkedMappings[config.mappings.length]}
|
||||
on:keypress={(e) => onInputKeypress(e)}
|
||||
/>
|
||||
<FreeformInput
|
||||
{config}
|
||||
{tags}
|
||||
{feedback}
|
||||
{unit}
|
||||
{state}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
on:submit={onSave}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{selectedElement}
|
||||
{layer}
|
||||
{searchTerm}
|
||||
mappingIsSelected={selectedMapping === i}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={selectedMapping}
|
||||
name={"mappings-radio-" + config.id}
|
||||
value={i}
|
||||
on:keypress={(e) => onInputKeypress(e)}
|
||||
/>
|
||||
</TagRenderingMappingInput>
|
||||
{/each}
|
||||
{#if config.freeform?.key}
|
||||
<label class="flex gap-x-1">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={selectedMapping}
|
||||
name={"mappings-radio-" + config.id}
|
||||
value={config.mappings?.length}
|
||||
on:keypress={(e) => onInputKeypress(e)}
|
||||
/>
|
||||
<FreeformInput
|
||||
{config}
|
||||
{tags}
|
||||
{feedback}
|
||||
{unit}
|
||||
{state}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
on:selected={() => (selectedMapping = config.mappings?.length)}
|
||||
on:submit={onSave}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if mappings !== undefined && config.multiAnswer}
|
||||
<!-- Multiple answers can be chosen: checkboxes -->
|
||||
<div class="flex flex-col">
|
||||
{#each config.mappings as mapping, i (mapping.then)}
|
||||
<TagRenderingMappingInput
|
||||
{mapping}
|
||||
{tags}
|
||||
{state}
|
||||
{selectedElement}
|
||||
{layer}
|
||||
{searchTerm}
|
||||
mappingIsSelected={checkedMappings[i]}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={"mappings-checkbox-" + config.id + "-" + i}
|
||||
bind:checked={checkedMappings[i]}
|
||||
on:keypress={(e) => onInputKeypress(e)}
|
||||
/>
|
||||
</TagRenderingMappingInput>
|
||||
{/each}
|
||||
{#if config.freeform?.key}
|
||||
<label class="flex gap-x-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
|
||||
bind:checked={checkedMappings[config.mappings.length]}
|
||||
on:keypress={(e) => onInputKeypress(e)}
|
||||
/>
|
||||
<FreeformInput
|
||||
{config}
|
||||
{tags}
|
||||
{feedback}
|
||||
{unit}
|
||||
{state}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
on:submit={onSave}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<LoginToggle {state}>
|
||||
<Loading slot="loading" />
|
||||
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
|
||||
|
|
@ -379,12 +394,19 @@
|
|||
<!-- TagRenderingQuestion-buttons -->
|
||||
<slot name="cancel" />
|
||||
<slot name="save-button" {selectedTags}>
|
||||
<button
|
||||
on:click={onSave}
|
||||
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")}
|
||||
>
|
||||
<Tr t={Translations.t.general.save} />
|
||||
</button>
|
||||
{#if allowDeleteOfFreeform && (mappings?.length ?? 0) === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""}
|
||||
<button class="primary flex" on:click|stopPropagation|preventDefault={onSave}>
|
||||
<TrashIcon class="w-6 h-6 text-red-500" />
|
||||
<Tr t={Translations.t.general.eraseValue}/>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
on:click={onSave}
|
||||
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")}
|
||||
>
|
||||
<Tr t={Translations.t.general.save} />
|
||||
</button>
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,18 @@
|
|||
import Translations from "./i18n/Translations"
|
||||
import { Utils } from "../Utils"
|
||||
import Add from "../assets/svg/Add.svelte"
|
||||
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen flex-col overflow-hidden p-4">
|
||||
<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 />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,12 @@
|
|||
import SingleReview from "./SingleReview.svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import ReviewForm from "./ReviewForm.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
import Mangrove_logo from "../../assets/svg/Mangrove_logo.svelte"
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,63 +1,88 @@
|
|||
<script lang="ts">
|
||||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Checkbox from "../Base/Checkbox.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import If from "../Base/If.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { Utils } from "../../Utils"
|
||||
import { placeholder } from "../../Utils/placeholder"
|
||||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Checkbox from "../Base/Checkbox.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import If from "../Base/If.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { Utils } from "../../Utils"
|
||||
import { placeholder } from "../../Utils/placeholder"
|
||||
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
/**
|
||||
* The form to create a new review.
|
||||
* This is multi-stepped.
|
||||
*/
|
||||
export let reviews: FeatureReviews
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let feature: Feature
|
||||
export let layer: LayerConfig
|
||||
/**
|
||||
* The form to create a new review.
|
||||
* This is multi-stepped.
|
||||
*/
|
||||
export let reviews: FeatureReviews
|
||||
|
||||
let score = 0
|
||||
let confirmedScore = undefined
|
||||
let isAffiliated = new UIEventSource(false)
|
||||
let opinion = new UIEventSource<string>(undefined)
|
||||
let score = 0
|
||||
let confirmedScore = undefined
|
||||
let isAffiliated = new UIEventSource(false)
|
||||
let opinion = new UIEventSource<string>(undefined)
|
||||
|
||||
const t = Translations.t.reviews
|
||||
const t = Translations.t.reviews
|
||||
|
||||
let _state: "ask" | "saving" | "done" = "ask"
|
||||
let _state: "ask" | "saving" | "done" = "ask"
|
||||
|
||||
const connection = state.osmConnection
|
||||
let connection = state.osmConnection
|
||||
|
||||
async function save() {
|
||||
_state = "saving"
|
||||
let nickname = undefined
|
||||
if (connection.isLoggedIn.data) {
|
||||
nickname = connection.userDetails.data.name
|
||||
let hasError: Store<undefined | "too_long"> = opinion.mapD(op => {
|
||||
const tooLong = op.length > FeatureReviews.REVIEW_OPINION_MAX_LENGTH
|
||||
if (tooLong) {
|
||||
return "too_long"
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
let uploadFailed: string = undefined
|
||||
|
||||
async function save() {
|
||||
if (hasError.data) {
|
||||
return
|
||||
}
|
||||
_state = "saving"
|
||||
let nickname = undefined
|
||||
if (connection.isLoggedIn.data) {
|
||||
nickname = connection.userDetails.data.name
|
||||
}
|
||||
const review: Omit<Review, "sub"> = {
|
||||
rating: confirmedScore,
|
||||
opinion: opinion.data,
|
||||
metadata: { nickname, is_affiliated: isAffiliated.data },
|
||||
}
|
||||
if (state.featureSwitchIsTesting?.data ?? true) {
|
||||
console.log("Testing - not actually saving review", review)
|
||||
await Utils.waitFor(1000)
|
||||
} else {
|
||||
try {
|
||||
|
||||
await reviews.createReview(review)
|
||||
} catch (e) {
|
||||
console.error("Could not create review due to", e)
|
||||
uploadFailed = "" + e
|
||||
}
|
||||
}
|
||||
_state = "done"
|
||||
}
|
||||
const review: Omit<Review, "sub"> = {
|
||||
rating: confirmedScore,
|
||||
opinion: opinion.data,
|
||||
metadata: { nickname, is_affiliated: isAffiliated.data },
|
||||
}
|
||||
if (state.featureSwitchIsTesting?.data ?? true) {
|
||||
console.log("Testing - not actually saving review", review)
|
||||
await Utils.waitFor(1000)
|
||||
} else {
|
||||
await reviews.createReview(review)
|
||||
}
|
||||
_state = "done"
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if _state === "done"}
|
||||
{#if uploadFailed}
|
||||
<div class="alert flex">
|
||||
<ExclamationTriangle class="w-6 h-6" />
|
||||
<Tr t={Translations.t.general.error}/>
|
||||
{uploadFailed}
|
||||
</div>
|
||||
{:else if _state === "done"}
|
||||
<Tr cls="thanks w-full" t={t.saved} />
|
||||
{:else if _state === "saving"}
|
||||
<Loading>
|
||||
|
|
@ -77,7 +102,7 @@
|
|||
on:hover={(e) => {
|
||||
score = e.detail.score
|
||||
}}
|
||||
on:mouseout={(e) => {
|
||||
on:mouseout={() => {
|
||||
score = null
|
||||
}}
|
||||
score={score ?? confirmedScore ?? 0}
|
||||
|
|
@ -95,6 +120,13 @@
|
|||
class="mb-1 w-full"
|
||||
use:placeholder={t.reviewPlaceholder}
|
||||
/>
|
||||
{#if $hasError === "too_long"}
|
||||
<div class="alert flex items-center px-2">
|
||||
<ExclamationTriangle class="w-12 h-12" />
|
||||
<Tr
|
||||
t={t.too_long.Subs({max: FeatureReviews.REVIEW_OPINION_MAX_LENGTH, amount: $opinion?.length ?? 0})}></Tr>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<Checkbox selected={isAffiliated}>
|
||||
|
|
@ -108,7 +140,8 @@
|
|||
<Tr t={t.reviewing_as.Subs({ nickname: state.osmConnection.userDetails.data.name })} />
|
||||
<Tr slot="else" t={t.reviewing_as_anonymous} />
|
||||
</If>
|
||||
<button class="primary" on:click={save}>
|
||||
<button class="primary" class:disabled={$hasError !== undefined}
|
||||
on:click={save}>
|
||||
<Tr t={t.save} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
40
src/UI/Reviews/ReviewsOverview.svelte
Normal file
40
src/UI/Reviews/ReviewsOverview.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import LoginButton from "../Base/LoginButton.svelte"
|
||||
import SingleReview from "./SingleReview.svelte"
|
||||
import Mangrove_logo from "../../assets/svg/Mangrove_logo.svelte"
|
||||
|
||||
/**
|
||||
* A panel showing all the reviews by the logged-in user
|
||||
*/
|
||||
export let state: SpecialVisualizationState
|
||||
let reviews = state.userRelatedState.mangroveIdentity.getAllReviews()
|
||||
const t = Translations.t.reviews
|
||||
</script>
|
||||
|
||||
<LoginToggle {state}>
|
||||
<div slot="not-logged-in">
|
||||
<LoginButton osmConnection={state.osmConnection}>
|
||||
<Tr t={Translations.t.favouritePoi.loginToSeeList} />
|
||||
</LoginButton>
|
||||
</div>
|
||||
|
||||
|
||||
{#if $reviews?.length > 0}
|
||||
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
{#each $reviews as review (review.sub)}
|
||||
<SingleReview {review} showSub={true} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Tr t={t.your_reviews_empty} />
|
||||
{/if}
|
||||
<a class="link-underline" href="https://github.com/pietervdvn/MapComplete/issues/1782" target="_blank" rel="noopener noreferrer"><Tr t={t.reviews_bug}/></a>
|
||||
<div class="flex justify-end">
|
||||
<Mangrove_logo class="h-12 w-12 shrink-0 p-1" />
|
||||
<Tr cls="text-sm subtle" t={t.attribution} />
|
||||
</div>
|
||||
</LoginToggle>
|
||||
|
|
@ -1,22 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export let review: Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> }
|
||||
export let state: SpecialVisualizationState = undefined
|
||||
export let review: Review & { kid: string; signature: string; madeByLoggedInUser?: Store<boolean> }
|
||||
let name = review.metadata.nickname
|
||||
name ??= ((review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "")).trim()
|
||||
let d = new Date()
|
||||
d.setTime(review.iat * 1000)
|
||||
let date = d.toDateString()
|
||||
let byLoggedInUser = review.madeByLoggedInUser
|
||||
let byLoggedInUser = review.madeByLoggedInUser ?? ImmutableStore.FALSE
|
||||
|
||||
export let showSub = false
|
||||
let subUrl = new URL(review.sub)
|
||||
let [lat, lon] = subUrl.pathname.split(",").map(l => Number(l))
|
||||
let sub = subUrl.searchParams.get("q")
|
||||
|
||||
function selectFeature(){
|
||||
console.log("Selecting and zooming to", {lon, lat})
|
||||
state?.mapProperties?.location?.setData({lon, lat})
|
||||
state?.mapProperties?.zoom?.setData(Math.max(16, state?.mapProperties?.zoom?.data))
|
||||
|
||||
state?.guistate?.closeAll()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class={"low-interaction rounded-lg p-1 px-2 " + ($byLoggedInUser ? "border-interactive" : "")}>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class={"low-interaction rounded-lg p-1 px-2 flex flex-col" + ($byLoggedInUser ? "border-interactive" : "")}>
|
||||
{#if showSub}
|
||||
<button class="link" on:click={() => selectFeature()}>
|
||||
<h3>{sub}</h3>
|
||||
</button>
|
||||
{/if}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div
|
||||
tabindex="0"
|
||||
use:ariaLabel={Translations.t.reviews.rated.Subs({
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import Star from "../../assets/svg/Star.svelte"
|
||||
import Star_half from "../../assets/svg/Star_half.svelte"
|
||||
import Star_outline from "../../assets/svg/Star_outline.svelte"
|
||||
import { ariaLabel, ariaLabelStore } from "../../Utils/ariaLabel"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export let score: number
|
||||
|
|
|
|||
106
src/UI/SpecialVisualisationUtils.ts
Normal file
106
src/UI/SpecialVisualisationUtils.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
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.
|
||||
* Note that _normal_ substitutions are ignored.
|
||||
*
|
||||
* import SpecialVisualisations from "./SpecialVisualizations"
|
||||
*
|
||||
* // Return empty list on empty input
|
||||
* SpecialVisualisationUtils.specialVisualizations = SpecialVisualisations.specialVisualizations
|
||||
* SpecialVisualisationUtils.constructSpecification("") // => []
|
||||
*
|
||||
* // 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()}")
|
||||
* 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 templ = <Exclude<RenderingSpecification, string>> templates[0]
|
||||
* templ.func.funcName // => "send_email"
|
||||
* templ.args[0] = "{email}"
|
||||
*/
|
||||
public static constructSpecification(
|
||||
template: string,
|
||||
extraMappings: SpecialVisualization[] = []
|
||||
): RenderingSpecification[] {
|
||||
if (template === "") {
|
||||
return []
|
||||
}
|
||||
|
||||
if (template["type"] !== undefined) {
|
||||
console.trace(
|
||||
"Got a non-expanded template while constructing the specification, it still has a 'special-key':",
|
||||
template
|
||||
)
|
||||
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: args,
|
||||
style: style,
|
||||
func: knownSpecial,
|
||||
}
|
||||
return [...partBefore, element, ...partAfter]
|
||||
}
|
||||
}
|
||||
|
||||
// IF we end up here, no changes have to be made - except to remove any resting {}
|
||||
return [template]
|
||||
}
|
||||
|
||||
private static undoEncoding(str: string) {
|
||||
return str
|
||||
.trim()
|
||||
.replace(/&LPARENS/g, "(")
|
||||
.replace(/&RPARENS/g, ")")
|
||||
.replace(/&LBRACE/g, "{")
|
||||
.replace(/&RBRACE/g, "}")
|
||||
.replace(/&COMMA/g, ",")
|
||||
}
|
||||
}
|
||||
|
|
@ -97,7 +97,7 @@ export interface SpecialVisualization {
|
|||
readonly funcName: string
|
||||
readonly docs: string | BaseUIElement
|
||||
readonly example?: string
|
||||
readonly needsUrls?: string[] | ((args: string[]) => string)
|
||||
readonly needsUrls?: string[] | ((args: string[]) => string | string[])
|
||||
|
||||
/**
|
||||
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@ import { FixedUiElement } from "./Base/FixedUiElement"
|
|||
import BaseUIElement from "./BaseUIElement"
|
||||
import Title from "./Base/Title"
|
||||
import Table from "./Base/Table"
|
||||
import {
|
||||
RenderingSpecification,
|
||||
SpecialVisualization,
|
||||
SpecialVisualizationState,
|
||||
} from "./SpecialVisualization"
|
||||
import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
|
||||
import { HistogramViz } from "./Popup/HistogramViz"
|
||||
import { MinimapViz } from "./Popup/MinimapViz"
|
||||
import { ShareLinkViz } from "./Popup/ShareLinkViz"
|
||||
|
|
@ -18,7 +14,7 @@ import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
|
|||
import TagApplyButton from "./Popup/TagApplyButton"
|
||||
import { CloseNoteButton } from "./Popup/Notes/CloseNoteButton"
|
||||
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
|
||||
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
|
||||
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
|
||||
import { ImageCarousel } from "./Image/ImageCarousel"
|
||||
|
|
@ -31,11 +27,10 @@ import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
|
|||
import { SubtleButton } from "./Base/SubtleButton"
|
||||
import Svg from "../Svg"
|
||||
import NoteCommentElement from "./Popup/Notes/NoteCommentElement"
|
||||
import { SubstitutedTranslation } from "./SubstitutedTranslation"
|
||||
import List from "./Base/List"
|
||||
import StatisticsPanel from "./BigComponents/StatisticsPanel"
|
||||
import AutoApplyButton from "./Popup/AutoApplyButton"
|
||||
import { LanguageElement } from "./Popup/LanguageElement"
|
||||
import { LanguageElement } from "./Popup/LanguageElement/LanguageElement"
|
||||
import FeatureReviews from "../Logic/Web/MangroveReviews"
|
||||
import Maproulette from "../Logic/Maproulette"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
|
|
@ -88,6 +83,11 @@ import MaprouletteSetStatus from "./MapRoulette/MaprouletteSetStatus.svelte"
|
|||
import DirectionIndicator from "./Base/DirectionIndicator.svelte"
|
||||
import Img from "./Base/Img"
|
||||
import Qr from "../Utils/Qr"
|
||||
import ComparisonTool from "./Comparison/ComparisonTool.svelte"
|
||||
import SpecialTranslation from "./Popup/TagRendering/SpecialTranslation.svelte"
|
||||
import SpecialVisualisationUtils from "./SpecialVisualisationUtils"
|
||||
import LoginButton from "./Base/LoginButton.svelte"
|
||||
import Toggle from "./Input/Toggle"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
|
@ -97,6 +97,11 @@ class NearbyImageVis implements SpecialVisualization {
|
|||
defaultValue: "closed",
|
||||
doc: "Either `open` or `closed`. If `open`, then the image carousel will always be shown",
|
||||
},
|
||||
{
|
||||
name: "readonly",
|
||||
required: false,
|
||||
doc: "If 'readonly', will not show the 'link'-button",
|
||||
},
|
||||
]
|
||||
docs =
|
||||
"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"
|
||||
|
|
@ -109,9 +114,10 @@ class NearbyImageVis implements SpecialVisualization {
|
|||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const isOpen = args[0] === "open"
|
||||
const readonly = args[1] === "readonly"
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, {
|
||||
tags,
|
||||
|
|
@ -120,6 +126,7 @@ class NearbyImageVis implements SpecialVisualization {
|
|||
lat,
|
||||
feature,
|
||||
layer,
|
||||
linkable: !readonly,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -174,7 +181,7 @@ class StealViz implements SpecialVisualization {
|
|||
selectedElement: otherFeature,
|
||||
state,
|
||||
layer,
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (elements.length === 1) {
|
||||
|
|
@ -182,8 +189,8 @@ class StealViz implements SpecialVisualization {
|
|||
}
|
||||
return new Combine(elements).SetClass("flex flex-col")
|
||||
},
|
||||
[state.indexedFeatures.featuresById]
|
||||
)
|
||||
[state.indexedFeatures.featuresById],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +229,7 @@ export class QuestionViz implements SpecialVisualization {
|
|||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const labels = args[0]
|
||||
?.split(";")
|
||||
|
|
@ -246,94 +253,6 @@ export class QuestionViz implements SpecialVisualization {
|
|||
export default class SpecialVisualizations {
|
||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
||||
|
||||
static undoEncoding(str: string) {
|
||||
return str
|
||||
.trim()
|
||||
.replace(/&LPARENS/g, "(")
|
||||
.replace(/&RPARENS/g, ")")
|
||||
.replace(/&LBRACE/g, "{")
|
||||
.replace(/&RBRACE/g, "}")
|
||||
.replace(/&COMMA/g, ",")
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* For a given string, returns a specification what parts are fixed and what parts are special renderings.
|
||||
* Note that _normal_ substitutions are ignored.
|
||||
*
|
||||
* // Return empty list on empty input
|
||||
* SpecialVisualizations.constructSpecification("") // => []
|
||||
*
|
||||
* // Simple case
|
||||
* const oh = SpecialVisualizations.constructSpecification("The opening hours with value {opening_hours} can be seen in the following table: <br/> {opening_hours_table()}")
|
||||
* 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
|
||||
* const templates = SpecialVisualizations.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 templ = <Exclude<RenderingSpecification, string>> templates[0]
|
||||
* templ.func.funcName // => "send_email"
|
||||
* templ.args[0] = "{email}"
|
||||
*/
|
||||
public static constructSpecification(
|
||||
template: string,
|
||||
extraMappings: SpecialVisualization[] = []
|
||||
): RenderingSpecification[] {
|
||||
if (template === "") {
|
||||
return []
|
||||
}
|
||||
|
||||
if (template["type"] !== undefined) {
|
||||
console.trace(
|
||||
"Got a non-expanded template while constructing the specification, it still has a 'special-key':",
|
||||
template
|
||||
)
|
||||
throw "Got a non-expanded template while constructing the specification"
|
||||
}
|
||||
const allKnownSpecials = extraMappings.concat(SpecialVisualizations.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 = SpecialVisualizations.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 = SpecialVisualizations.constructSpecification(
|
||||
matched[4],
|
||||
extraMappings
|
||||
)
|
||||
const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
|
||||
if (argument.length > 0) {
|
||||
const realArgs = argument.split(",").map((str) => this.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: args,
|
||||
style: style,
|
||||
func: knownSpecial,
|
||||
}
|
||||
return [...partBefore, element, ...partAfter]
|
||||
}
|
||||
}
|
||||
|
||||
// IF we end up here, no changes have to be made - except to remove any resting {}
|
||||
return [template]
|
||||
}
|
||||
|
||||
public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
|
||||
if (typeof viz === "string") {
|
||||
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
|
||||
|
|
@ -346,31 +265,38 @@ export default class SpecialVisualizations {
|
|||
viz.docs,
|
||||
viz.args.length > 0
|
||||
? new Table(
|
||||
["name", "default", "description"],
|
||||
viz.args.map((arg) => {
|
||||
let defaultArg = arg.defaultValue ?? "_undefined_"
|
||||
if (defaultArg == "") {
|
||||
defaultArg = "_empty string_"
|
||||
}
|
||||
return [arg.name, defaultArg, arg.doc]
|
||||
})
|
||||
)
|
||||
["name", "default", "description"],
|
||||
viz.args.map((arg) => {
|
||||
let defaultArg = arg.defaultValue ?? "_undefined_"
|
||||
if (defaultArg == "") {
|
||||
defaultArg = "_empty string_"
|
||||
}
|
||||
return [arg.name, defaultArg, arg.doc]
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
new Title("Example usage of " + viz.funcName, 4),
|
||||
new FixedUiElement(
|
||||
viz.example ??
|
||||
"`{" +
|
||||
viz.funcName +
|
||||
"(" +
|
||||
viz.args.map((arg) => arg.defaultValue).join(",") +
|
||||
")}`"
|
||||
"`{" +
|
||||
viz.funcName +
|
||||
"(" +
|
||||
viz.args.map((arg) => arg.defaultValue).join(",") +
|
||||
")}`",
|
||||
).SetClass("literal-code"),
|
||||
])
|
||||
}
|
||||
|
||||
public static constructSpecification(
|
||||
template: string,
|
||||
extraMappings: SpecialVisualization[] = [],
|
||||
): RenderingSpecification[] {
|
||||
return SpecialVisualisationUtils.constructSpecification(template, extraMappings)
|
||||
}
|
||||
|
||||
public static HelpMessage() {
|
||||
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
|
||||
SpecialVisualizations.DocumentationFor(viz)
|
||||
SpecialVisualizations.DocumentationFor(viz),
|
||||
)
|
||||
|
||||
return new Combine([
|
||||
|
|
@ -404,10 +330,10 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
},
|
||||
null,
|
||||
" "
|
||||
)
|
||||
" ",
|
||||
),
|
||||
).SetClass("code"),
|
||||
'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)',
|
||||
"In other words: use `{ \"before\": ..., \"after\": ..., \"special\": {\"type\": ..., \"argname\": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)",
|
||||
]).SetClass("flex flex-col"),
|
||||
...helpTexts,
|
||||
]).SetClass("flex flex-col")
|
||||
|
|
@ -416,20 +342,20 @@ export default class SpecialVisualizations {
|
|||
// noinspection JSUnusedGlobalSymbols
|
||||
public static renderExampleOfSpecial(
|
||||
state: SpecialVisualizationState,
|
||||
s: SpecialVisualization
|
||||
s: SpecialVisualization,
|
||||
): BaseUIElement {
|
||||
const examples =
|
||||
s.structuredExamples === undefined
|
||||
? []
|
||||
: s.structuredExamples().map((e) => {
|
||||
return s.constr(
|
||||
state,
|
||||
new UIEventSource<Record<string, string>>(e.feature.properties),
|
||||
e.args,
|
||||
e.feature,
|
||||
undefined
|
||||
)
|
||||
})
|
||||
return s.constr(
|
||||
state,
|
||||
new UIEventSource<Record<string, string>>(e.feature.properties),
|
||||
e.args,
|
||||
e.feature,
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
return new Combine([new Title(s.funcName), s.docs, ...examples])
|
||||
}
|
||||
|
||||
|
|
@ -469,7 +395,7 @@ export default class SpecialVisualizations {
|
|||
assignTo: state.userRelatedState.language,
|
||||
availableLanguages: state.layout.language,
|
||||
preferredLanguages: state.osmConnection.userDetails.map(
|
||||
(ud) => ud.languages
|
||||
(ud) => ud.languages,
|
||||
),
|
||||
})
|
||||
},
|
||||
|
|
@ -494,7 +420,7 @@ export default class SpecialVisualizations {
|
|||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
): BaseUIElement {
|
||||
return new VariableUiElement(
|
||||
tagSource
|
||||
|
|
@ -504,7 +430,7 @@ export default class SpecialVisualizations {
|
|||
return new SplitRoadWizard(<WayId>id, state)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -518,7 +444,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
if (feature.geometry.type !== "Point") {
|
||||
return undefined
|
||||
|
|
@ -541,7 +467,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
if (!layer.deletion) {
|
||||
return undefined
|
||||
|
|
@ -569,7 +495,7 @@ export default class SpecialVisualizations {
|
|||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature
|
||||
feature: Feature,
|
||||
): BaseUIElement {
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
return new SvelteUIElement(CreateNewNote, {
|
||||
|
|
@ -633,7 +559,7 @@ export default class SpecialVisualizations {
|
|||
.map((tags) => tags[args[0]])
|
||||
.map((wikidata) => {
|
||||
wikidata = Utils.NoEmpty(
|
||||
wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
|
||||
wikidata?.split(";")?.map((wd) => wd.trim()) ?? [],
|
||||
)[0]
|
||||
const entry = Wikidata.LoadWikidataEntry(wikidata)
|
||||
return new VariableUiElement(
|
||||
|
|
@ -643,9 +569,9 @@ export default class SpecialVisualizations {
|
|||
}
|
||||
const response = <WikidataResponse>e["success"]
|
||||
return Translation.fromMap(response.labels)
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
),
|
||||
},
|
||||
new MapillaryLinkVis(),
|
||||
|
|
@ -654,8 +580,13 @@ export default class SpecialVisualizations {
|
|||
funcName: "all_tags",
|
||||
docs: "Prints all key-value pairs of the object - used for debugging",
|
||||
args: [],
|
||||
constr: (state, tags: UIEventSource<any>) =>
|
||||
new SvelteUIElement(AllTagsPanel, { tags, state }),
|
||||
constr: (
|
||||
state,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
_,
|
||||
__,
|
||||
layer: LayerConfig,
|
||||
) => new SvelteUIElement(AllTagsPanel, { tags, layer }),
|
||||
},
|
||||
{
|
||||
funcName: "image_carousel",
|
||||
|
|
@ -677,7 +608,6 @@ export default class SpecialVisualizations {
|
|||
AllImageProviders.LoadImagesFor(tags, imagePrefixes),
|
||||
tags,
|
||||
state,
|
||||
feature
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -733,7 +663,7 @@ export default class SpecialVisualizations {
|
|||
{
|
||||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
}
|
||||
},
|
||||
)
|
||||
return new SvelteUIElement(StarsBarIcon, {
|
||||
score: reviews.average,
|
||||
|
|
@ -766,7 +696,7 @@ export default class SpecialVisualizations {
|
|||
{
|
||||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
}
|
||||
},
|
||||
)
|
||||
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
|
||||
},
|
||||
|
|
@ -798,7 +728,7 @@ export default class SpecialVisualizations {
|
|||
{
|
||||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
}
|
||||
},
|
||||
)
|
||||
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
|
||||
},
|
||||
|
|
@ -856,7 +786,7 @@ export default class SpecialVisualizations {
|
|||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): SvelteUIElement {
|
||||
const keyToUse = args[0]
|
||||
const prefix = args[1]
|
||||
|
|
@ -893,18 +823,17 @@ export default class SpecialVisualizations {
|
|||
return undefined
|
||||
}
|
||||
const allUnits: Unit[] = [].concat(
|
||||
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? [])
|
||||
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []),
|
||||
)
|
||||
const unit = allUnits.filter((unit) =>
|
||||
unit.isApplicableToKey(key)
|
||||
unit.isApplicableToKey(key),
|
||||
)[0]
|
||||
if (unit === undefined) {
|
||||
return value
|
||||
}
|
||||
const getCountry = () => tagSource.data._country
|
||||
const [v, denom] = unit.findDenomination(value, getCountry)
|
||||
return unit.asHumanLongValue(v, getCountry)
|
||||
})
|
||||
return unit.asHumanLongValue(value, getCountry)
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -921,7 +850,7 @@ export default class SpecialVisualizations {
|
|||
new Combine([
|
||||
t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
|
||||
t.downloadGeoJsonHelper.SetClass("subtle"),
|
||||
]).SetClass("flex flex-col")
|
||||
]).SetClass("flex flex-col"),
|
||||
)
|
||||
.onClick(() => {
|
||||
console.log("Exporting as Geojson")
|
||||
|
|
@ -934,7 +863,7 @@ export default class SpecialVisualizations {
|
|||
title + "_mapcomplete_export.geojson",
|
||||
{
|
||||
mimetype: "application/vnd.geo+json",
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.SetClass("w-full")
|
||||
|
|
@ -970,7 +899,7 @@ export default class SpecialVisualizations {
|
|||
constr: (state) => {
|
||||
return new SubtleButton(
|
||||
Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
|
||||
Translations.t.general.removeLocationHistory
|
||||
Translations.t.general.removeLocationHistory,
|
||||
).onClick(() => {
|
||||
state.historicalUserLocations.features.setData([])
|
||||
state.selectedElement.setData(undefined)
|
||||
|
|
@ -1008,10 +937,10 @@ export default class SpecialVisualizations {
|
|||
.filter((c) => c.text !== "")
|
||||
.map(
|
||||
(c, i) =>
|
||||
new NoteCommentElement(c, state, i, comments.length)
|
||||
)
|
||||
new NoteCommentElement(c, state, i, comments.length),
|
||||
),
|
||||
).SetClass("flex flex-col")
|
||||
})
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -1040,21 +969,30 @@ export default class SpecialVisualizations {
|
|||
docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'",
|
||||
example:
|
||||
"`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.",
|
||||
constr: (state, tagsSource) =>
|
||||
constr: (
|
||||
state: SpecialVisualizationState,
|
||||
tagsSource: UIEventSource<Record<string, string>>,
|
||||
_: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig,
|
||||
) =>
|
||||
new VariableUiElement(
|
||||
tagsSource.map((tags) => {
|
||||
if (state.layout === undefined) {
|
||||
return "<feature title>"
|
||||
}
|
||||
const layer = state.layout?.getMatchingLayer(tags)
|
||||
const title = layer?.title?.GetRenderValue(tags)
|
||||
if (title === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new SubstitutedTranslation(title, tagsSource, state).SetClass(
|
||||
"px-1"
|
||||
)
|
||||
})
|
||||
return new SvelteUIElement(SpecialTranslation, {
|
||||
t: title,
|
||||
tags: tagsSource,
|
||||
state,
|
||||
feature,
|
||||
layer,
|
||||
}).SetClass("px-1")
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -1070,8 +1008,8 @@ export default class SpecialVisualizations {
|
|||
let challenge = Stores.FromPromise(
|
||||
Utils.downloadJsonCached(
|
||||
`${Maproulette.defaultEndpoint}/challenge/${parentId}`,
|
||||
24 * 60 * 60 * 1000
|
||||
)
|
||||
24 * 60 * 60 * 1000,
|
||||
),
|
||||
)
|
||||
|
||||
return new VariableUiElement(
|
||||
|
|
@ -1096,7 +1034,7 @@ export default class SpecialVisualizations {
|
|||
} else {
|
||||
return [title, new List(listItems)]
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.",
|
||||
|
|
@ -1110,15 +1048,15 @@ export default class SpecialVisualizations {
|
|||
"\n" +
|
||||
"```json\n" +
|
||||
"{\n" +
|
||||
' "id": "mark_duplicate",\n' +
|
||||
' "render": {\n' +
|
||||
' "special": {\n' +
|
||||
' "type": "maproulette_set_status",\n' +
|
||||
' "message": {\n' +
|
||||
' "en": "Mark as not found or false positive"\n' +
|
||||
" \"id\": \"mark_duplicate\",\n" +
|
||||
" \"render\": {\n" +
|
||||
" \"special\": {\n" +
|
||||
" \"type\": \"maproulette_set_status\",\n" +
|
||||
" \"message\": {\n" +
|
||||
" \"en\": \"Mark as not found or false positive\"\n" +
|
||||
" },\n" +
|
||||
' "status": "2",\n' +
|
||||
' "image": "close"\n' +
|
||||
" \"status\": \"2\",\n" +
|
||||
" \"image\": \"close\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}\n" +
|
||||
|
|
@ -1147,16 +1085,22 @@ export default class SpecialVisualizations {
|
|||
doc: "The property name containing the maproulette id",
|
||||
defaultValue: "mr_taskId",
|
||||
},
|
||||
{
|
||||
name: "ask_feedback",
|
||||
doc: "If not an empty string, this will be used as question to ask some additional feedback. A text field will be added",
|
||||
defaultValue: ""
|
||||
}
|
||||
],
|
||||
|
||||
constr: (state, tagsSource, args) => {
|
||||
let [message, image, message_closed, statusToSet, maproulette_id_key] = args
|
||||
let [message, image, message_closed, statusToSet, maproulette_id_key, askFeedback] = args
|
||||
if (image === "") {
|
||||
image = "confirm"
|
||||
}
|
||||
if (maproulette_id_key === "" || maproulette_id_key === undefined) {
|
||||
maproulette_id_key = "mr_taskId"
|
||||
}
|
||||
statusToSet = statusToSet ?? "1"
|
||||
return new SvelteUIElement(MaprouletteSetStatus, {
|
||||
state,
|
||||
tags: tagsSource,
|
||||
|
|
@ -1165,6 +1109,7 @@ export default class SpecialVisualizations {
|
|||
message_closed,
|
||||
statusToSet,
|
||||
maproulette_id_key,
|
||||
askFeedback
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
@ -1184,8 +1129,8 @@ export default class SpecialVisualizations {
|
|||
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
|
||||
return new StatisticsPanel(fsBboxed)
|
||||
},
|
||||
[state.mapProperties.bounds]
|
||||
)
|
||||
[state.mapProperties.bounds],
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -1231,7 +1176,7 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
{
|
||||
name: "href",
|
||||
doc: "The URL to link to",
|
||||
doc: "The URL to link to. Note that this will be URI-encoded before ",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -1240,7 +1185,7 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
{
|
||||
name: "download",
|
||||
doc: "If set, this link will act as a download-button. The contents of `href` will be offered for download; this parameter will act as the proposed filename",
|
||||
doc: "Expects a string which denotes the filename to download the contents of `href` into. If set, this link will act as a download-button.",
|
||||
},
|
||||
{
|
||||
name: "arialabel",
|
||||
|
|
@ -1251,7 +1196,7 @@ export default class SpecialVisualizations {
|
|||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
args: string[],
|
||||
): BaseUIElement {
|
||||
let [text, href, classnames, download, ariaLabel] = args
|
||||
if (download === "") {
|
||||
|
|
@ -1263,20 +1208,19 @@ export default class SpecialVisualizations {
|
|||
(tags) =>
|
||||
new SvelteUIElement(Link, {
|
||||
text: Utils.SubstituteKeys(text, tags),
|
||||
href: Utils.SubstituteKeys(href, tags),
|
||||
href: Utils.SubstituteKeys(href, tags).replaceAll(/ /g, "%20") /* Chromium based browsers eat the spaces */,
|
||||
classnames,
|
||||
download: Utils.SubstituteKeys(download, tags),
|
||||
ariaLabel: Utils.SubstituteKeys(ariaLabel, tags),
|
||||
newTab,
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "multi",
|
||||
docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering",
|
||||
|
||||
example:
|
||||
"```json\n" +
|
||||
JSON.stringify(
|
||||
|
|
@ -1292,7 +1236,7 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
},
|
||||
null,
|
||||
" "
|
||||
" ",
|
||||
) +
|
||||
"\n```",
|
||||
args: [
|
||||
|
|
@ -1306,24 +1250,42 @@ export default class SpecialVisualizations {
|
|||
doc: "An entire tagRenderingConfig",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "classes",
|
||||
doc: "CSS-classes to apply on every individual item. Seperated by `space`",
|
||||
},
|
||||
],
|
||||
constr(state, featureTags, args) {
|
||||
const [key, tr] = args
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
featureTags: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig,
|
||||
) {
|
||||
const [key, tr, classesRaw] = args
|
||||
let classes = classesRaw ?? ""
|
||||
const translation = new Translation({ "*": tr })
|
||||
return new VariableUiElement(
|
||||
featureTags.map((tags) => {
|
||||
const properties: object[] = JSON.parse(tags[key])
|
||||
let properties: object[]
|
||||
if (typeof tags[key] === "string") {
|
||||
properties = JSON.parse(tags[key])
|
||||
} else {
|
||||
properties = <any>tags[key]
|
||||
}
|
||||
const elements = []
|
||||
for (const property of properties) {
|
||||
const subsTr = new SubstitutedTranslation(
|
||||
translation,
|
||||
new UIEventSource<any>(property),
|
||||
state
|
||||
)
|
||||
const subsTr = new SvelteUIElement(SpecialTranslation, {
|
||||
t: translation,
|
||||
tags: new ImmutableStore(property),
|
||||
state,
|
||||
feature,
|
||||
layer,
|
||||
}).SetClass(classes)
|
||||
elements.push(subsTr)
|
||||
}
|
||||
return new List(elements)
|
||||
})
|
||||
return elements
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -1343,7 +1305,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
return new VariableUiElement(
|
||||
tagSource.map((tags) => {
|
||||
|
|
@ -1355,7 +1317,7 @@ export default class SpecialVisualizations {
|
|||
console.error("Cannot create a translation for", v, "due to", e)
|
||||
return JSON.stringify(v)
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -1375,7 +1337,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const key = argument[0]
|
||||
const validator = new FediverseValidator()
|
||||
|
|
@ -1385,14 +1347,38 @@ export default class SpecialVisualizations {
|
|||
.map((fediAccount) => {
|
||||
fediAccount = validator.reformat(fediAccount)
|
||||
const [_, username, host] = fediAccount.match(
|
||||
FediverseValidator.usernameAtServer
|
||||
FediverseValidator.usernameAtServer,
|
||||
)
|
||||
return new SvelteUIElement(Link, {
|
||||
|
||||
const normalLink = new SvelteUIElement(Link, {
|
||||
text: fediAccount,
|
||||
url: "https://" + host + "/@" + username,
|
||||
href: "https://" + host + "/@" + username,
|
||||
newTab: true,
|
||||
})
|
||||
})
|
||||
|
||||
const loggedInContributorMastodon =
|
||||
state.userRelatedState?.preferencesAsTags?.data?.[
|
||||
"_mastodon_link"
|
||||
]
|
||||
console.log(
|
||||
"LoggedinContributorMastodon",
|
||||
loggedInContributorMastodon,
|
||||
)
|
||||
if (!loggedInContributorMastodon) {
|
||||
return normalLink
|
||||
}
|
||||
const homeUrl = new URL(loggedInContributorMastodon)
|
||||
const homeHost = homeUrl.protocol + "//" + homeUrl.hostname
|
||||
|
||||
return new Combine([
|
||||
normalLink,
|
||||
new SvelteUIElement(Link, {
|
||||
href: homeHost + "/" + fediAccount,
|
||||
text: Translations.t.validation.fediverse.onYourServer,
|
||||
newTab: true,
|
||||
}).SetClass("button"),
|
||||
])
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -1412,7 +1398,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
return new FixedUiElement("{" + args[0] + "}")
|
||||
},
|
||||
|
|
@ -1433,7 +1419,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const key = argument[0] ?? "value"
|
||||
return new VariableUiElement(
|
||||
|
|
@ -1451,12 +1437,12 @@ export default class SpecialVisualizations {
|
|||
} catch (e) {
|
||||
return new FixedUiElement(
|
||||
"Could not parse this tag: " +
|
||||
JSON.stringify(value) +
|
||||
" due to " +
|
||||
e
|
||||
JSON.stringify(value) +
|
||||
" due to " +
|
||||
e,
|
||||
).SetClass("alert")
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -1477,7 +1463,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const giggityUrl = argument[0]
|
||||
return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl })
|
||||
|
|
@ -1493,12 +1479,12 @@ export default class SpecialVisualizations {
|
|||
_: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const tags = (<ThemeViewState>(
|
||||
state
|
||||
)).geolocation.currentUserLocation.features.map(
|
||||
(features) => features[0]?.properties
|
||||
(features) => features[0]?.properties,
|
||||
)
|
||||
return new Combine([
|
||||
new SvelteUIElement(OrientationDebugPanel, {}),
|
||||
|
|
@ -1520,7 +1506,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
return new SvelteUIElement(MarkAsFavourite, {
|
||||
tags: tagSource,
|
||||
|
|
@ -1540,7 +1526,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
return new SvelteUIElement(MarkAsFavouriteMini, {
|
||||
tags: tagSource,
|
||||
|
|
@ -1560,7 +1546,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
return new SvelteUIElement(DirectionIndicator, { state, feature })
|
||||
},
|
||||
|
|
@ -1575,7 +1561,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
return new VariableUiElement(
|
||||
tagSource
|
||||
|
|
@ -1597,9 +1583,9 @@ export default class SpecialVisualizations {
|
|||
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
|
||||
`#${id}`
|
||||
return new Img(new Qr(url).toImageElement(75)).SetStyle(
|
||||
"width: 75px"
|
||||
"width: 75px",
|
||||
)
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
@ -1619,7 +1605,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
|
||||
return new VariableUiElement(
|
||||
|
|
@ -1630,14 +1616,72 @@ export default class SpecialVisualizations {
|
|||
})
|
||||
.mapD((value) => {
|
||||
const dir = GeoOperations.bearingToHuman(
|
||||
GeoOperations.parseBearing(value)
|
||||
GeoOperations.parseBearing(value),
|
||||
)
|
||||
console.log("Human dir", dir)
|
||||
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "compare_data",
|
||||
needsUrls: (args) => args[1].split(";"),
|
||||
args: [
|
||||
{
|
||||
name: "url",
|
||||
required: true,
|
||||
doc: "The attribute containing the url where to fetch more data",
|
||||
},
|
||||
{
|
||||
name: "host",
|
||||
required: true,
|
||||
doc: "The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. ",
|
||||
},
|
||||
{
|
||||
name: "postprocessing",
|
||||
required: false,
|
||||
doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value",
|
||||
},
|
||||
{
|
||||
name: "readonly",
|
||||
required: false,
|
||||
doc: "If 'yes', will not show 'apply'-buttons",
|
||||
},
|
||||
],
|
||||
docs: "Gives an interactive element which shows a tag comparison between the OSM-object and the upstream object. This allows to copy some or all tags into OSM",
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const url = args[0]
|
||||
const postprocessVelopark = args[2] === "velopark"
|
||||
const readonly = args[3] === "yes"
|
||||
return new SvelteUIElement(ComparisonTool, {
|
||||
url,
|
||||
postprocessVelopark,
|
||||
state,
|
||||
tags: tagSource,
|
||||
layer,
|
||||
feature,
|
||||
readonly,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "login_button",
|
||||
args: [
|
||||
],
|
||||
docs: "Show a login button",
|
||||
needsUrls: [],
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
|
||||
return new Toggle(undefined,
|
||||
new SvelteUIElement(LoginButton), state.osmConnection.isLoggedIn)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
||||
|
|
@ -1649,10 +1693,11 @@ export default class SpecialVisualizations {
|
|||
throw (
|
||||
"Invalid special visualisation found: funcName is undefined for " +
|
||||
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'
|
||||
". 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue