More refactoring

This commit is contained in:
Pieter Vander Vennet 2023-03-29 17:21:20 +02:00
parent 5d0fe31c41
commit 41e6a2c760
147 changed files with 1540 additions and 1797 deletions

View file

@ -2,7 +2,6 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import { Utils } from "../Utils"
import known_themes from "../assets/generated/known_layers.json"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
import { ALL } from "dns"
import { AllKnownLayouts } from "./AllKnownLayouts"
export class AllSharedLayers {
public static sharedLayers: Map<string, LayerConfig> = AllSharedLayers.getSharedLayers()

View file

@ -1,12 +1,13 @@
import { Store, UIEventSource } from "../UIEventSource"
import Locale from "../../UI/i18n/Locale"
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"
import Combine from "../../UI/Base/Combine"
import { Utils } from "../../Utils"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Feature } from "geojson"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer.svelte"
export default class TitleHandler {
constructor(
@ -32,7 +33,7 @@ export default class TitleHandler {
const tagsSource =
allElements.getStore(tags.id) ??
new UIEventSource<Record<string, string>>(tags)
const title = new TagRenderingAnswer(tagsSource, layer.title, {})
const title = new SvelteUIElement(TagRenderingAnswer, { tags: tagsSource })
return (
new Combine([defaultTitle, " | ", title]).ConstructElement()
?.textContent ?? defaultTitle

View file

@ -1,12 +1,4 @@
import FeatureSource, { Tiled } from "../FeatureSource"
import { Tiles } from "../../../Models/TileRange"
import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
import { UIEventSource } from "../../UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../BBox"
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import Loc from "../../../Models/Loc"
import FeatureSource from "../FeatureSource"
import { Feature } from "geojson"
import TileLocalStorage from "./TileLocalStorage"
import { GeoOperations } from "../../GeoOperations"

View file

@ -1,7 +1,6 @@
import { UIEventSource } from "../../UIEventSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
import { BBox } from "../../BBox"
import { FeatureSourceForLayer } from "../FeatureSource"
import { Feature } from "geojson"
export default class SimpleFeatureSource implements FeatureSourceForLayer {

View file

@ -0,0 +1,52 @@
import FeatureSource from "../FeatureSource"
import { Store } from "../../UIEventSource"
import { Feature, Point } from "geojson"
import { GeoOperations } from "../../GeoOperations"
export interface SnappingOptions {
/**
* If the distance is bigger then this amount, don't snap.
* In meter
*/
maxDistance?: number
}
export default class SnappingFeatureSource implements FeatureSource {
public readonly features: Store<Feature<Point>[]>
constructor(
snapTo: FeatureSource,
location: Store<{ lon: number; lat: number }>,
options?: SnappingOptions
) {
const simplifiedFeatures = snapTo.features.mapD((features) =>
features
.filter((feature) => feature.geometry.type !== "Point")
.map((f) => GeoOperations.forceLineString(<any>f))
)
location.mapD(
({ lon, lat }) => {
const features = snapTo.features.data
const loc: [number, number] = [lon, lat]
const maxDistance = (options?.maxDistance ?? 1000) * 1000
let bestSnap: Feature<Point, { "snapped-to": string; dist: number }> = undefined
for (const feature of features) {
const snapped = GeoOperations.nearestPoint(<any>feature, loc)
if (snapped.properties.dist > maxDistance) {
continue
}
if (
bestSnap === undefined ||
bestSnap.properties.dist > snapped.properties.dist
) {
snapped.properties["snapped-to"] = feature.properties.id
bestSnap = <any>snapped
}
}
return bestSnap
},
[snapTo.features]
)
}
}

View file

@ -2,7 +2,6 @@ import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource"
import StaticFeatureSource from "./StaticFeatureSource"
import { GeoOperations } from "../../GeoOperations"
import { BBox } from "../../BBox"
import exp from "constants"
import FilteredLayer from "../../../Models/FilteredLayer"
/**

View file

@ -7,6 +7,7 @@ import {
GeoJSON,
Geometry,
LineString,
MultiLineString,
MultiPolygon,
Point,
Polygon,
@ -272,17 +273,42 @@ export class GeoOperations {
* @param point Point defined as [lon, lat]
*/
public static nearestPoint(
way: Feature<LineString | Polygon>,
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
point: [number, number]
): Feature<Point> {
): Feature<
Point,
{
index: number
dist: number
location: number
}
> {
return <any>(
turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" })
)
}
/**
* Helper method to reuse the coordinates of the way as LineString.
* Mostly used as helper for 'nearestPoint'
* @param way
*/
public static forceLineString(
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
): Feature<LineString | MultiLineString> {
if (way.geometry.type === "Polygon") {
way = { ...way }
way.geometry = { ...way.geometry }
way.geometry.type = "LineString"
way.geometry.coordinates = (<Polygon>way.geometry).coordinates[0]
} else if (way.geometry.type === "MultiPolygon") {
way = { ...way }
way.geometry = { ...way.geometry }
way.geometry.type = "MultiLineString"
way.geometry.coordinates = (<MultiPolygon>way.geometry).coordinates[0]
}
return turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" })
return <any>way
}
public static toCSV(features: any[]): string {

View file

@ -5,6 +5,7 @@ import GenericImageProvider from "./GenericImageProvider"
import { Store, UIEventSource } from "../UIEventSource"
import ImageProvider, { ProvidedImage } from "./ImageProvider"
import { WikidataImageProvider } from "./WikidataImageProvider"
import { OsmTags } from "../../Models/OsmFeature"
/**
* A generic 'from the interwebz' image picker, without attribution
@ -44,7 +45,7 @@ export default class AllImageProviders {
UIEventSource<ProvidedImage[]>
>()
public static LoadImagesFor(tags: Store<any>, tagKey?: string[]): Store<ProvidedImage[]> {
public static LoadImagesFor(tags: Store<OsmTags>, tagKey?: string[]): Store<ProvidedImage[]> {
if (tags.data.id === undefined) {
return undefined
}

View file

@ -24,7 +24,7 @@ export default class ChangeLocationAction extends OsmChangeAction {
this._meta = meta
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
protected async CreateChangeDescriptions(): Promise<ChangeDescription[]> {
const d: ChangeDescription = {
changes: {
lat: this._newLonLat[1],

View file

@ -71,7 +71,7 @@ export default class ChangeTagAction extends OsmChangeAction {
return { k: key.trim(), v: value.trim() }
}
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
async CreateChangeDescriptions(): Promise<ChangeDescription[]> {
const changedTags: { k: string; v: string }[] = this._tagsFilter
.asChange(this._currentTags)
.map(ChangeTagAction.checkChange)

View file

@ -3,7 +3,6 @@ import { OsmConnection } from "../Osm/OsmConnection"
import { MangroveIdentity } from "../Web/MangroveReviews"
import { Store, Stores, UIEventSource } from "../UIEventSource"
import Locale from "../../UI/i18n/Locale"
import { Changes } from "../Osm/Changes"
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
import FeatureSource from "../FeatureSource/FeatureSource"
import { Feature } from "geojson"

View file

@ -122,7 +122,7 @@ export class Tag extends TagsFilter {
return [this]
}
asChange(properties: any): { k: string; v: string }[] {
asChange(): { k: string; v: string }[] {
return [{ k: this.key, v: this.value }]
}

View file

@ -1,4 +1,4 @@
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { UIEventSource } from "../Logic/UIEventSource"
import { BBox } from "../Logic/BBox"
import { RasterLayerPolygon } from "./RasterLayers"

View file

@ -26,7 +26,6 @@ import Table from "../../UI/Base/Table"
import FilterConfigJson from "./Json/FilterConfigJson"
import { And } from "../../Logic/Tags/And"
import { Overpass } from "../../Logic/Osm/Overpass"
import Constants from "../Constants"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import Svg from "../../Svg"
import { ImmutableStore } from "../../Logic/UIEventSource"

View file

@ -108,7 +108,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
const indexedElements = new LayoutSource(
this.indexedFeatures = new LayoutSource(
layout.layers,
this.featureSwitches,
new StaticFeatureSource([]),
@ -116,6 +116,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.osmConnection.Backend(),
(id) => this.layerState.filteredLayers.get(id).isDisplayed
)
const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements)
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()),

View file

@ -15,7 +15,6 @@ import { OsmConnection } from "../Logic/Osm/OsmConnection"
export default class AllThemesGui {
setup() {
try {
new FixedUiElement("").AttachTo("centermessage")
const osmConnection = new OsmConnection()
const state = new UserRelatedState(osmConnection)
const intro = new Combine([
@ -38,15 +37,14 @@ export default class AllThemesGui {
new FixedUiElement("v" + Constants.vNumber),
])
.SetClass("block m-5 lg:w-3/4 lg:ml-40")
.SetStyle("pointer-events: all;")
.AttachTo("top-left")
.AttachTo("main")
} catch (e) {
console.error(">>>> CRITICAL", e)
new FixedUiElement(
"Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!"
)
.SetClass("alert")
.AttachTo("centermessage")
.AttachTo("main")
}
}
}

View file

@ -1,6 +1,6 @@
import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "./VariableUIElement"
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
import { Stores } from "../../Logic/UIEventSource"
import Loading from "./Loading"
export default class AsyncLazy extends BaseUIElement {

View file

@ -1,4 +1,3 @@
import { UIElement } from "../UIElement"
import BaseUIElement from "../BaseUIElement"
/**

View file

@ -0,0 +1,89 @@
<script lang="ts">
/**
* This overlay element will regularly show a hand that swipes over the underlying element.
* This element will hide as soon as the Store 'hideSignal' receives a change (which is not undefined)
*/
import ToSvelte from "./ToSvelte.svelte";
import Svg from "../../Svg";
import { Store } from "../../Logic/UIEventSource";
import { onDestroy } from "svelte";
let mainElem: HTMLElement;
export let hideSignal: Store<any>;
function hide(){
console.trace("Hiding...")
mainElem.style.visibility = "hidden";
}
if (hideSignal) {
onDestroy(hideSignal.addCallbackD(() => {
console.trace("Hiding invitation")
return true;
}));
}
$: {
console.log("Binding listeners on", mainElem)
mainElem?.addEventListener("click",_ => hide())
mainElem?.addEventListener("touchstart",_ => hide())
}
</script>
<div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full">
<div id="hand-container">
<ToSvelte construct={Svg.hand_ui}></ToSvelte>
</div>
</div>
<style>
@keyframes hand-drag-animation {
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
0% {
opacity: 0;
transform: rotate(-30deg);
}
6% {
opacity: 1;
transform: rotate(-30deg);
}
12% {
opacity: 1;
transform: rotate(-45deg);
}
24% {
opacity: 1;
transform: rotate(-00deg);
}
30% {
opacity: 1;
transform: rotate(-30deg);
}
36% {
opacity: 0;
transform: rotate(-30deg);
}
100% {
opacity: 0;
transform: rotate(-30deg);
}
}
#hand-container {
position: absolute;
width: 2rem;
left: calc(50% + 4rem);
top: calc(50%);
opacity: 0.7;
animation: hand-drag-animation 4s ease-in-out infinite;
transform-origin: 50% 125%;
}
</style>

19
UI/Base/FromHtml.svelte Normal file
View file

@ -0,0 +1,19 @@
<script lang="ts">
/**
* Given an HTML string, properly shows this
*/
export let src: string;
let htmlElem: HTMLElement;
$: {
if(htmlElem !== undefined){
htmlElem.innerHTML = src
}
}
</script>
{#if src !== undefined}
<span bind:this={htmlElem}></span>
{/if}

View file

@ -1,6 +1,6 @@
import Translations from "../i18n/Translations"
import BaseUIElement from "../BaseUIElement"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Store } from "../../Logic/UIEventSource"
export default class Link extends BaseUIElement {
private readonly _href: string | Store<string>

30
UI/Base/Tr.svelte Normal file
View file

@ -0,0 +1,30 @@
<script lang="ts">
/**
* Properly renders a translation
*/
import { Translation } from "../i18n/Translation";
import { onDestroy } from "svelte";
import Locale from "../i18n/Locale";
import { Utils } from "../../Utils";
import FromHtml from "./FromHtml.svelte";
export let t: Translation;
export let tags: Record<string, string> | undefined;
// Text for the current language
let txt: string | undefined;
onDestroy(Locale.language.addCallbackAndRunD(l => {
const translation = t?.textFor(l)
if(translation === undefined){
return
}
if(tags){
txt = Utils.SubstituteKeys(txt, tags)
}else{
txt = translation
}
}));
</script>
<FromHtml src={txt}></FromHtml>

View file

@ -13,7 +13,6 @@ import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
import Toggle from "../Input/Toggle"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { DefaultGuiState } from "../DefaultGuiState"
import DefaultGUI from "../DefaultGUI"
export class BackToThemeOverview extends Toggle {
constructor(

View file

@ -14,7 +14,6 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { BBox } from "../../Logic/BBox"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import geojson2svg from "geojson2svg"
import Constants from "../../Models/Constants"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
export class DownloadPanel extends Toggle {

View file

@ -17,7 +17,6 @@ import UserRelatedState from "../../Logic/State/UserRelatedState"
import Loc from "../../Models/Loc"
import BaseLayer from "../../Models/BaseLayer"
import FilteredLayer from "../../Models/FilteredLayer"
import CopyrightPanel from "./CopyrightPanel"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import PrivacyPolicy from "./PrivacyPolicy"
import Hotkeys from "../Base/Hotkeys"

View file

@ -15,8 +15,6 @@
Translations.t;
export let bounds: UIEventSource<BBox>
export let layout: LayoutConfig;
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
export let selectedElement: UIEventSource<Feature>;
export let selectedLayer: UIEventSource<LayerConfig>;

View file

@ -1,23 +1,15 @@
<script lang="ts">
import type { Feature } from "geojson";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import TagRenderingAnswer from "../Popup/TagRenderingAnswer";
import { UIEventSource } from "../../Logic/UIEventSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import { VariableUiElement } from "../Base/VariableUIElement.js";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { onDestroy } from "svelte";
import TagRenderingAnswer from "../Popup/TagRenderingAnswer.svelte";
export let selectedElement: UIEventSource<Feature>;
export let layer: UIEventSource<LayerConfig>;
export let tags: Store<UIEventSource<Record<string, string>>>;
let _tags: UIEventSource<Record<string, string>>;
onDestroy(tags.subscribe(tags => {
_tags = tags;
return false
}));
export let selectedElement: Feature;
export let layer: LayerConfig;
export let tags: UIEventSource<Record<string, string>>;
export let specialVisState: SpecialVisualizationState;
export let state: SpecialVisualizationState;
/**
* const title = new TagRenderingAnswer(
@ -46,30 +38,27 @@
</script>
<div>
<div on:click={() =>selectedElement.setData(undefined)}>close</div>
<div class="flex flex-col sm:flex-row flex-grow justify-between">
<!-- Title element-->
<ToSvelte
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, layer.data.title, specialVisState), [layer]))}></ToSvelte>
<h3>
<TagRenderingAnswer config={layer.title} {tags} {selectedElement}></TagRenderingAnswer>
</h3>
<div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2">
{#each $layer.titleIcons as titleIconConfig (titleIconConfig.id)}
{#each layer.titleIcons as titleIconConfig (titleIconConfig.id)}
<div class="w-8 h-8">
<ToSvelte
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, titleIconConfig, specialVisState)))}></ToSvelte>
<TagRenderingAnswer config={titleIconConfig} {tags} {selectedElement}></TagRenderingAnswer>
</div>
{/each}
</div>
</div>
<ul>
{#each Object.keys($_tags) as key}
<li><b>{key}</b>=<b>{$_tags[key]}</b></li>
<div class="flex flex-col">
{#each layer.tagRenderings as config (config.id)}
<TagRenderingAnswer {tags} {config} {state}></TagRenderingAnswer>
{/each}
</ul>
</div>
</div>

View file

@ -7,6 +7,7 @@
import Constants from "../../Models/Constants"
import type Loc from "../../Models/Loc"
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig";
import Tr from "../Base/Tr.svelte";
export let theme: LayoutInformation
export let isCustom: boolean = false
@ -16,8 +17,8 @@
$: title = new Translation(
theme.title,
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
).toString()
$: description = new Translation(theme.shortDescription).toString()
)
$: description = new Translation(theme.shortDescription)
// TODO: Improve this function
function createUrl(
@ -83,8 +84,10 @@
<img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt="" />
<span slot="message" class="message">
<span>
<span>{title}</span>
<span>{description}</span>
<Tr t={title}></Tr>
<span class="subtle">
<Tr t={description}></Tr>
</span>
</span>
</span>
</SubtleButton>

View file

@ -5,7 +5,6 @@ import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs"
import MapControlButton from "./MapControlButton"
import Svg from "../Svg"
import Toggle from "./Input/Toggle"
import SearchAndGo from "./BigComponents/SearchAndGo"
import BaseUIElement from "./BaseUIElement"
import LeftControls from "./BigComponents/LeftControls"
import RightControls from "./BigComponents/RightControls"
@ -26,7 +25,6 @@ import UserInformationPanel from "./BigComponents/UserInformation"
import { LoginToggle } from "./Popup/LoginButton"
import { FixedUiElement } from "./Base/FixedUiElement"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { GeoLocationState } from "../Logic/State/GeoLocationState"
import Hotkeys from "./Base/Hotkeys"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import SvelteUIElement from "./Base/SvelteUIElement"

View file

@ -1,4 +1,4 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Store } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement"
import { Utils } from "../../Utils"
import Combine from "../Base/Combine"

View file

@ -17,7 +17,6 @@ import Minimap from "../Base/Minimap"
import BaseLayer from "../../Models/BaseLayer"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import Loc from "../../Models/Loc"
import Attribution from "../BigComponents/Attribution"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import ValidatedTextField from "../Input/ValidatedTextField"

View file

@ -7,7 +7,6 @@ import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
import Toggle from "../Input/Toggle"
import { UIElement } from "../UIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
export interface FlowStep<T> extends BaseUIElement {
readonly IsValid: Store<boolean>

View file

@ -4,7 +4,6 @@ import { BBox } from "../../Logic/BBox"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import Translations from "../i18n/Translations"
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
import Constants from "../../Models/Constants"
import { DropDown } from "../Input/DropDown"
import { Utils } from "../../Utils"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"

View file

@ -1,118 +0,0 @@
import { InputElement } from "./InputElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine"
import Svg from "../../Svg"
import BaseUIElement from "../BaseUIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
import { Utils } from "../../Utils"
import Loc from "../../Models/Loc"
import Minimap from "../Base/Minimap"
/**
* Selects a direction in degrees
*/
export default class DirectionInput extends InputElement<string> {
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false)
private readonly _location: UIEventSource<Loc>
private readonly value: UIEventSource<string>
private background
constructor(
mapBackground: UIEventSource<any>,
location: UIEventSource<Loc>,
value?: UIEventSource<string>
) {
super()
this._location = location
this.value = value ?? new UIEventSource<string>(undefined)
this.background = mapBackground
}
GetValue(): UIEventSource<string> {
return this.value
}
IsValid(str: string): boolean {
const t = Number(str)
return !isNaN(t) && t >= 0 && t <= 360
}
protected InnerConstructElement(): HTMLElement {
let map: BaseUIElement = new FixedUiElement("")
if (!Utils.runningFromConsole) {
map = Minimap.createMiniMap({
background: this.background,
allowMoving: false,
location: this._location,
})
}
const element = new Combine([
Svg.direction_stroke_svg()
.SetStyle(
`position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${
this.value.data ?? 0
}deg);`
)
.SetClass("direction-svg relative")
.SetStyle("z-index: 1000"),
map.SetStyle(`position: absolute;top: 0;left: 0;width: 100%;height: 100%;`),
])
.SetStyle("width: min(100%, 25em); height: 0; padding-bottom: 100%") // A bit a weird CSS , see https://stackoverflow.com/questions/13851940/pure-css-solution-square-elements#19448481
.SetClass("relative block bg-white border border-black overflow-hidden rounded-full")
.ConstructElement()
this.value.addCallbackAndRunD((rotation) => {
const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement
cone.style.transform = `rotate(${rotation}deg)`
})
this.RegisterTriggers(element)
element.style.overflow = "hidden"
element.style.display = "block"
return element
}
private RegisterTriggers(htmlElement: HTMLElement) {
const self = this
function onPosChange(x: number, y: number) {
const rect = htmlElement.getBoundingClientRect()
const dx = -(rect.left + rect.right) / 2 + x
const dy = (rect.top + rect.bottom) / 2 - y
const angle = (180 * Math.atan2(dy, dx)) / Math.PI
const angleGeo = Math.floor((450 - angle) % 360)
self.value.setData("" + angleGeo)
}
htmlElement.ontouchmove = (ev: TouchEvent) => {
onPosChange(ev.touches[0].clientX, ev.touches[0].clientY)
ev.preventDefault()
}
htmlElement.ontouchstart = (ev: TouchEvent) => {
onPosChange(ev.touches[0].clientX, ev.touches[0].clientY)
}
let isDown = false
htmlElement.onmousedown = (ev: MouseEvent) => {
isDown = true
onPosChange(ev.clientX, ev.clientY)
ev.preventDefault()
}
htmlElement.onmouseup = (ev) => {
isDown = false
ev.preventDefault()
}
htmlElement.onmousemove = (ev: MouseEvent) => {
if (isDown) {
onPosChange(ev.clientX, ev.clientY)
}
ev.preventDefault()
}
}
}

View file

@ -1,5 +1,5 @@
import { InputElement } from "./InputElement"
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine"
import Slider from "./Slider"
import { ClickableToggle } from "./Toggle"

View file

@ -1,24 +1,17 @@
import { ReadonlyInputElement } from "./InputElement"
import Loc from "../../Models/Loc"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Minimap, { MinimapObj } from "../Base/Minimap"
import BaseLayer from "../../Models/BaseLayer"
import Combine from "../Base/Combine"
import Svg from "../../Svg"
import { GeoOperations } from "../../Logic/GeoOperations"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../Logic/BBox"
import { FixedUiElement } from "../Base/FixedUiElement"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import BaseUIElement from "../BaseUIElement"
import Toggle from "./Toggle"
import matchpoint from "../../assets/layers/matchpoint/matchpoint.json"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import FilteredLayer from "../../Models/FilteredLayer"
import { ElementStorage } from "../../Logic/ElementStorage"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import { RelationId, WayId } from "../../Models/OsmFeature"
import { Feature, LineString, Polygon } from "geojson"
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
@ -313,10 +306,6 @@ export default class LocationInput
[this.map.leafletMap]
)
const animatedHand = Svg.hand_ui()
.SetStyle("width: 2rem; height: unset;")
.SetClass("hand-drag-animation block pointer-events-none")
return new Combine([
new Combine([
Svg.move_arrows_ui()
@ -328,10 +317,6 @@ export default class LocationInput
"background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%; opacity: 0.5"
),
new Toggle(undefined, animatedHand, hasMoved)
.SetClass("block w-0 h-0 z-10 relative")
.SetStyle("left: calc(50% + 3rem); top: calc(50% + 2rem); opacity: 0.7"),
this.map.SetClass("z-0 relative block w-full h-full bg-gray-100"),
]).ConstructElement()
} catch (e) {
@ -341,11 +326,4 @@ export default class LocationInput
.ConstructElement()
}
}
TakeScreenshot(format: "image"): Promise<string>
TakeScreenshot(format: "blob"): Promise<Blob>
TakeScreenshot(format: "image" | "blob"): Promise<string | Blob>
TakeScreenshot(format: "image" | "blob"): Promise<string | Blob> {
return this.map.TakeScreenshot(format)
}
}

1
UI/Input/README.md Normal file
View file

@ -0,0 +1 @@
This is the old, deprecated directory. New, SVelte-based items go into `InputElement`

View file

@ -2,7 +2,6 @@ import { InputElement } from "./InputElement"
import { UIEventSource } from "../../Logic/UIEventSource"
export default class SimpleDatePicker extends InputElement<string> {
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false)
private readonly value: UIEventSource<string>
private readonly _element: HTMLElement

View file

@ -50,10 +50,6 @@ export class TextField extends InputElement<string> {
return this.value
}
GetRawValue(): UIEventSource<string> {
return this._rawValue
}
IsValid(t: string): boolean {
if (t === undefined || t === null) {
return false

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,70 @@
<script lang="ts">
import { UIEventSource } from "../../../Logic/UIEventSource";
import type { MapProperties } from "../../../Models/MapProperties";
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";
/**
* A visualisation to pick a direction on a map background
*/
export let value: UIEventSource<undefined | number>;
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> };
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
let mla = new MapLibreAdaptor(map, mapProperties);
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
let directionElem: HTMLElement | undefined;
$: value.addCallbackAndRunD(degrees => {
console.log("Degrees are", degrees, directionElem);
if (directionElem === undefined) {
return;
}
directionElem.style.rotate = degrees + "deg";
});
let mainElem : HTMLElement
function onPosChange(x: number, y: number) {
const rect = mainElem.getBoundingClientRect();
const dx = -(rect.left + rect.right) / 2 + x;
const dy = (rect.top + rect.bottom) / 2 - y;
const angle = (180 * Math.atan2(dy, dx)) / Math.PI;
const angleGeo = Math.floor((450 - angle) % 360);
value.setData(angleGeo);
}
let isDown = false;
</script>
<div bind:this={mainElem} class="relative w-48 h-48 cursor-pointer overflow-hidden"
on:click={e => onPosChange(e.x, e.y)}
on:mousedown={e => {
isDown = true
onPosChange(e.clientX, e.clientY)
} }
on:mousemove={e => {
if(isDown){
onPosChange(e.clientX, e.clientY)
}}}
on:mouseup={() => {
isDown = false
} }
on:touchmove={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}
on:touchstart={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}>
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
<MaplibreMap {map} attribution={false}></MaplibreMap>
</div>
<div bind:this={directionElem} class="absolute w-full h-full top-0 left-0 border border-red-500">
<ToSvelte construct={ Svg.direction_stroke_svg}>
</ToSvelte>
</div>
</div>

View file

@ -0,0 +1,42 @@
<script lang="ts">
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
import type { MapProperties } from "../../../Models/MapProperties";
import { Map as MlMap } from "maplibre-gl";
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor";
import MaplibreMap from "../../Map/MaplibreMap.svelte";
import Svg from "../../../Svg";
import ToSvelte from "../../Base/ToSvelte.svelte";
import DragInvitation from "../../Base/DragInvitation.svelte";
/**
* A visualisation to pick a direction on a map background
*/
export let value: UIEventSource<{lon: number, lat: number}>;
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> };
/**
* Called when setup is done, cna be used to add layrs to the map
*/
export let onCreated : (value: Store<{lon: number, lat: number}> , map: Store<MlMap>, mapProperties: MapProperties ) => void
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
let mla = new MapLibreAdaptor(map, mapProperties);
mla.allowMoving.setData(true)
mla.allowZooming.setData(true)
if(onCreated){
onCreated(value, map, mla)
}
</script>
<div class="relative h-32 cursor-pointer overflow-hidden">
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
<MaplibreMap {map} attribution={false}></MaplibreMap>
</div>
<div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50">
<ToSvelte construct={() => Svg.move_arrows_svg().SetClass("h-full")}></ToSvelte>
</div>
<DragInvitation></DragInvitation>
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
/**
* Constructs an input helper element for the given type.
* Note that all values are stringified
*/
import { AvailableInputHelperType } from "./InputHelpers";
import { UIEventSource } from "../../Logic/UIEventSource";
export let type : AvailableInputHelperType
export let value : UIEventSource<string>
</script>

View file

@ -0,0 +1,16 @@
import { AvailableRasterLayers } from "../../Models/RasterLayers"
export type AvailableInputHelperType = typeof InputHelpers.AvailableInputHelpers[number]
export default class InputHelpers {
public static readonly AvailableInputHelpers = [] as const
/**
* To port
* direction
* opening_hours
* color
* length
* date
* wikidata
*/
}

View file

@ -0,0 +1,119 @@
import BaseUIElement from "../BaseUIElement"
import Combine from "../Base/Combine"
import Title from "../Base/Title"
import Translations from "../i18n/Translations"
import { Translation } from "../i18n/Translation"
import WikidataValidator from "./Validators/WikidataValidator"
import StringValidator from "./Validators/StringValidator"
import TextValidator from "./Validators/TextValidator"
import DateValidator from "./Validators/DateValidator"
import LengthValidator from "./Validators/LengthValidator"
import IntValidator from "./Validators/IntValidator"
import EmailValidator from "./Validators/EmailValidator"
import DirectionValidator from "./Validators/DirectionValidator"
import NatValidator from "./Validators/NatValidator"
import OpeningHoursValidator from "./Validators/OpeningHoursValidator"
import PFloatValidator from "./Validators/PFloatValidator"
import ColorValidator from "./Validators/ColorValidator"
import PhoneValidator from "./Validators/PhoneValidator"
import UrlValidator from "./Validators/UrlValidator"
import FloatValidator from "./Validators/FloatValidator"
import PNatValidator from "./Validators/PNatValidator"
/**
* A 'TextFieldValidator' contains various methods to check and cleanup an entered value or to give feedback.
* They also double as an index of supported types for textfields in MapComplete
*/
export abstract class Validator {
public readonly name: string
/*
* An explanation for the theme builder.
* This can indicate which special input element is used, ...
* */
public readonly explanation: string
/**
* What HTML-inputmode to use
*/
public readonly inputmode?: string
constructor(name: string, explanation: string | BaseUIElement, inputmode?: string) {
this.name = name
this.inputmode = inputmode
if (this.name.endsWith("textfield")) {
this.name = this.name.substr(0, this.name.length - "TextField".length)
}
if (this.name.endsWith("textfielddef")) {
this.name = this.name.substr(0, this.name.length - "TextFieldDef".length)
}
if (typeof explanation === "string") {
this.explanation = explanation
} else {
this.explanation = explanation.AsMarkdown()
}
}
/**
* Gets a piece of feedback. By default, validation.<type> will be used, resulting in a generic 'not a valid <type>'.
* However, inheritors might overwrite this to give more specific feedback
* @param s
*/
public getFeedback(s: string): Translation {
const tr = Translations.t.validation[this.name]
if (tr !== undefined) {
return tr["feedback"]
}
}
public isValid(string: string, requestCountry: () => string): boolean {
return true
}
public reformat(s: string, country?: () => string): string {
return s
}
}
export default class Validators {
private static readonly AllValidators: ReadonlyArray<Validator> = [
new StringValidator(),
new TextValidator(),
new DateValidator(),
new NatValidator(),
new IntValidator(),
new LengthValidator(),
new DirectionValidator(),
new WikidataValidator(),
new PNatValidator(),
new FloatValidator(),
new PFloatValidator(),
new EmailValidator(),
new UrlValidator(),
new PhoneValidator(),
new OpeningHoursValidator(),
new ColorValidator(),
]
public static allTypes: Map<string, Validator> = Validators.allTypesDict()
public static HelpText(): BaseUIElement {
const explanations: BaseUIElement[] = Validators.AllValidators.map((type) =>
new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")
)
return new Combine([
new Title("Available types for text fields", 1),
"The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them",
...explanations,
]).SetClass("flex flex-col")
}
public static AvailableTypes(): string[] {
return Validators.AllValidators.map((tp) => tp.name)
}
private static allTypesDict(): Map<string, Validator> {
const types = new Map<string, Validator>()
for (const tp of Validators.AllValidators) {
types.set(tp.name, tp)
}
return types
}
}

View file

@ -0,0 +1,7 @@
import { Validator } from "../ValidatedTextField"
export default class ColorValidator extends Validator {
constructor() {
super("color", "Shows a color picker")
}
}

View file

@ -0,0 +1,23 @@
import { Validator } from "../ValidatedTextField"
export default class DateValidator extends Validator {
constructor() {
super("date", "A date with date picker")
}
isValid(str: string): boolean {
return !isNaN(new Date(str).getTime())
}
reformat(str: string) {
const d = new Date(str)
let month = "" + (d.getMonth() + 1)
let day = "" + d.getDate()
const year = d.getFullYear()
if (month.length < 2) month = "0" + month
if (day.length < 2) day = "0" + day
return [year, month, day].join("-")
}
}

View file

@ -0,0 +1,17 @@
import { Validator } from "../ValidatedTextField"
import IntValidator from "./IntValidator";
export default class DirectionValidator extends IntValidator {
constructor() {
super(
"direction",
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)"
)
}
reformat(str): string {
const n = Number(str) % 360
return "" + n
}
}

View file

@ -0,0 +1,39 @@
import { Validator } from "../ValidatedTextField.js"
import { Translation } from "../../i18n/Translation.js"
import Translations from "../../i18n/Translations.js"
import * as emailValidatorLibrary from "email-validator"
export default class EmailValidator extends Validator {
constructor() {
super("email", "An email adress", "email")
}
isValid = (str) => {
if (str === undefined) {
return false
}
str = str.trim()
if (str.startsWith("mailto:")) {
str = str.substring("mailto:".length)
}
return emailValidatorLibrary.validate(str)
}
reformat = (str) => {
if (str === undefined) {
return undefined
}
str = str.trim()
if (str.startsWith("mailto:")) {
str = str.substring("mailto:".length)
}
return str
}
getFeedback(s: string): Translation {
if (s.indexOf("@") < 0) {
return Translations.t.validation.email.noAt
}
return super.getFeedback(s)
}
}

View file

@ -0,0 +1,27 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../ValidatedTextField"
export default class FloatValidator extends Validator {
inputmode = "decimal"
constructor(name?: string, explanation?: string) {
super(name ?? "float", explanation ?? "A decimal number", "decimal")
}
isValid(str) {
return !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(",")
}
reformat(str): string {
return "" + Number(str)
}
getFeedback(s: string): Translation {
if (isNaN(Number(s))) {
return Translations.t.validation.nat.notANumber
}
return undefined
}
}

View file

@ -0,0 +1,29 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../ValidatedTextField"
export default class IntValidator extends Validator {
constructor(name?: string, explanation?: string) {
super(
name ?? "int",
explanation ?? "A whole number, either positive, negative or zero",
"numeric"
)
}
isValid(str): boolean {
str = "" + str
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))
}
getFeedback(s: string): Translation {
const n = Number(s)
if (isNaN(n)) {
return Translations.t.validation.nat.notANumber
}
if (Math.floor(n) !== n) {
return Translations.t.validation.nat.mustBeWhole
}
return undefined
}
}

View file

@ -0,0 +1,16 @@
import { Validator } from "../ValidatedTextField"
export default class LengthValidator extends Validator {
constructor() {
super(
"distance",
'A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]',
"decimal"
)
}
isValid = (str) => {
const t = Number(str)
return !isNaN(t)
}
}

View file

@ -0,0 +1,30 @@
import IntValidator from "./IntValidator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class NatValidator extends IntValidator {
constructor(name?: string, explanation?: string) {
super(name ?? "nat", explanation ?? "A whole, positive number or zero")
}
isValid(str): boolean {
if (str === undefined) {
return false
}
str = "" + str
return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
}
getFeedback(s: string): Translation {
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
const n = Number(s)
if (n < 0) {
return Translations.t.validation.nat.mustBePositive
}
return undefined
}
}

View file

@ -0,0 +1,54 @@
import { Validator } from "../ValidatedTextField"
import Combine from "../../Base/Combine"
import Title from "../../Base/Title"
import Table from "../../Base/Table"
export default class OpeningHoursValidator extends Validator {
constructor() {
super(
"opening_hours",
new Combine([
"Has extra elements to easily input when a POI is opened.",
new Title("Helper arguments"),
new Table(
["name", "doc"],
[
[
"options",
new Combine([
"A JSON-object of type `{ prefix: string, postfix: string }`. ",
new Table(
["subarg", "doc"],
[
[
"prefix",
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse.",
],
[
"postfix",
"Piece of text that will always be added to the end of the generated opening hours",
],
]
),
]),
],
]
),
new Title("Example usage"),
"To add a conditional (based on time) access restriction:\n\n```\n" +
`
"freeform": {
"key": "access:conditional",
"type": "opening_hours",
"helperArgs": [
{
"prefix":"no @ (",
"postfix":")"
}
]
}` +
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`",
])
)
}
}

View file

@ -0,0 +1,23 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../ValidatedTextField"
export default class PFloatValidator extends Validator {
constructor() {
super("pfloat", "A positive decimal number or zero")
}
isValid = (str) =>
!isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(",")
getFeedback(s: string): Translation {
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
if (Number(s) < 0) {
return Translations.t.validation.nat.mustBePositive
}
return undefined
}
}

View file

@ -0,0 +1,27 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import NatValidator from "./NatValidator"
export default class PNatValidator extends NatValidator {
constructor() {
super("pnat", "A strict positive number")
}
getFeedback(s: string): Translation {
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
if (Number(s) === 0) {
return Translations.t.validation.pnat.noZero
}
return undefined
}
isValid = (str) => {
if (!super.isValid(str)) {
return false
}
return Number(str) > 0
}
}

View file

@ -0,0 +1,32 @@
import { Validator } from "../ValidatedTextField"
import { parsePhoneNumberFromString } from "libphonenumber-js"
export default class PhoneValidator extends Validator {
constructor() {
super("phone", "A phone number", "tel")
}
isValid(str, country: () => string): boolean {
if (str === undefined) {
return false
}
if (str.startsWith("tel:")) {
str = str.substring("tel:".length)
}
let countryCode = undefined
if (country !== undefined) {
countryCode = country()?.toUpperCase()
}
return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false
}
reformat = (str, country: () => string) => {
if (str.startsWith("tel:")) {
str = str.substring("tel:".length)
}
return parsePhoneNumberFromString(
str,
country()?.toUpperCase() as any
)?.formatInternational()
}
}

View file

@ -0,0 +1,8 @@
import { Validator } from "../ValidatedTextField"
export default class StringValidator extends Validator {
constructor() {
super("string", "A simple piece of text")
}
}

View file

@ -0,0 +1,7 @@
import { Validator } from "../ValidatedTextField"
export default class TextValidator extends Validator {
constructor() {
super("text", "A longer piece of text. Uses an textArea instead of a textField", "text")
}
}

View file

@ -0,0 +1,75 @@
import { Validator } from "../ValidatedTextField"
export default class UrlValidator extends Validator {
constructor() {
super(
"url",
"The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user. Furthermore, some tracking parameters will be removed",
"url"
)
}
reformat(str: string): string {
try {
let url: URL
// str = str.toLowerCase() // URLS are case sensitive. Lowercasing them might break some URLS. See #763
if (
!str.startsWith("http://") &&
!str.startsWith("https://") &&
!str.startsWith("http:")
) {
url = new URL("https://" + str)
} else {
url = new URL(str)
}
const blacklistedTrackingParams = [
"fbclid", // Oh god, how I hate the fbclid. Let it burn, burn in hell!
"gclid",
"cmpid",
"agid",
"utm",
"utm_source",
"utm_medium",
"campaignid",
"campaign",
"AdGroupId",
"AdGroup",
"TargetId",
"msclkid",
]
for (const dontLike of blacklistedTrackingParams) {
url.searchParams.delete(dontLike.toLowerCase())
}
let cleaned = url.toString()
if (cleaned.endsWith("/") && !str.endsWith("/")) {
// Do not add a trailing '/' if it wasn't typed originally
cleaned = cleaned.substr(0, cleaned.length - 1)
}
if (!str.startsWith("http") && cleaned.startsWith("https://")) {
cleaned = cleaned.substr("https://".length)
}
return cleaned
} catch (e) {
console.error(e)
return undefined
}
}
isValid(str: string): boolean {
try {
if (
!str.startsWith("http://") &&
!str.startsWith("https://") &&
!str.startsWith("http:")
) {
str = "https://" + str
}
const url = new URL(str)
const dotIndex = url.host.indexOf(".")
return dotIndex > 0 && url.host[url.host.length - 1] !== "."
} catch (e) {
return false
}
}
}

View file

@ -0,0 +1,179 @@
import Combine from "../../Base/Combine"
import Title from "../../Base/Title"
import Table from "../../Base/Table"
import Wikidata from "../../../Logic/Web/Wikidata"
import { UIEventSource } from "../../../Logic/UIEventSource"
import Locale from "../../i18n/Locale"
import { Utils } from "../../../Utils"
import WikidataSearchBox from "../../Wikipedia/WikidataSearchBox"
import { Validator } from "../ValidatedTextField"
export default class WikidataValidator extends Validator {
constructor() {
super(
"wikidata",
new Combine([
"A wikidata identifier, e.g. Q42.",
new Title("Helper arguments"),
new Table(
["name", "doc"],
[
["key", "the value of this tag will initialize search (default: name)"],
[
"options",
new Combine([
"A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.",
new Table(
["subarg", "doc"],
[
[
"removePrefixes",
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
],
[
"removePostfixes",
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
],
[
"instanceOf",
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans",
],
[
"notInstanceof",
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results",
],
]
),
]),
],
]
),
new Title("Example usage"),
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
\`\`\`json
"freeform": {
"key": "name:etymology:wikidata",
"type": "wikidata",
"helperArgs": [
"name",
{
"removePostfixes": {"en": [
"street",
"boulevard",
"path",
"square",
"plaza",
],
"nl": ["straat","plein","pad","weg",laan"],
"fr":["route (de|de la|de l'| de le)"]
},
"#": "Remove streets and parks from the search results:"
"notInstanceOf": ["Q79007","Q22698"]
}
]
}
\`\`\`
Another example is to search for species and trees:
\`\`\`json
"freeform": {
"key": "species:wikidata",
"type": "wikidata",
"helperArgs": [
"species",
{
"instanceOf": [10884, 16521]
}]
}
\`\`\`
`,
])
)
}
public isValid(str): boolean {
if (str === undefined) {
return false
}
if (str.length <= 2) {
return false
}
return !str.split(";").some((str) => Wikidata.ExtractKey(str) === undefined)
}
public reformat(str) {
if (str === undefined) {
return undefined
}
let out = str
.split(";")
.map((str) => Wikidata.ExtractKey(str))
.join("; ")
if (str.endsWith(";")) {
out = out + ";"
}
return out
}
public inputHelper(currentValue, inputHelperOptions) {
const args = inputHelperOptions.args ?? []
const searchKey = args[0] ?? "name"
const searchFor = <string>(
(inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "")
)
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
const options: any = args[1]
if (searchFor !== undefined && options !== undefined) {
const prefixes = <string[] | Record<string, string[]>>options["removePrefixes"] ?? []
const postfixes = <string[] | Record<string, string[]>>options["removePostfixes"] ?? []
const defaultValueCandidate = Locale.language.map((lg) => {
const prefixesUnrwapped: RegExp[] = (
Array.isArray(prefixes) ? prefixes : prefixes[lg] ?? []
).map((s) => new RegExp("^" + s, "i"))
const postfixesUnwrapped: RegExp[] = (
Array.isArray(postfixes) ? postfixes : postfixes[lg] ?? []
).map((s) => new RegExp(s + "$", "i"))
let clipped = searchFor
for (const postfix of postfixesUnwrapped) {
const match = searchFor.match(postfix)
if (match !== null) {
clipped = searchFor.substring(0, searchFor.length - match[0].length)
break
}
}
for (const prefix of prefixesUnrwapped) {
const match = searchFor.match(prefix)
if (match !== null) {
clipped = searchFor.substring(match[0].length)
break
}
}
return clipped
})
defaultValueCandidate.addCallbackAndRun((clipped) => searchForValue.setData(clipped))
}
let instanceOf: number[] = Utils.NoNull(
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let notInstanceOf: number[] = Utils.NoNull(
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
return new WikidataSearchBox({
value: currentValue,
searchText: searchForValue,
instanceOf,
notInstanceOf,
})
}
}

View file

@ -43,7 +43,7 @@ export class MapLibreAdaptor implements MapProperties {
*/
private _currentRasterLayer: string
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapProperties, "bounds">>) {
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>) {
this._maplibreMap = maplibreMap
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })

View file

@ -16,6 +16,7 @@
*/
export let map: Writable<MaplibreMap>
export let attribution = true
let center = {};
onMount(() => {
@ -28,6 +29,9 @@
<main>
<Map bind:center={center}
bind:map={$map}
{attribution}
css="./maplibre-gl.css"
id="map" location={{lng: 0, lat: 0, zoom: 0}} maxzoom=24 style={styleUrl} />
</main>

View file

@ -106,7 +106,7 @@ class PointRenderingLayer {
store = new ImmutableStore(<OsmTags>feature.properties)
}
const { html, iconAnchor } = this._config.RenderIcon(store, true)
html.SetClass("marker")
html.SetClass("marker cursor-pointer")
const el = html.ConstructElement()
if (this._onClick) {
@ -244,7 +244,7 @@ class LineRenderingLayer {
},
})
this._visibility.addCallbackAndRunD((visible) => {
this._visibility?.addCallbackAndRunD((visible) => {
map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none")
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none")
})

View file

@ -1,6 +1,5 @@
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { OsmTags } from "../../Models/OsmFeature"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Feature } from "geojson"

View file

@ -1,12 +1,13 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import Histogram from "../BigComponents/Histogram"
import { Feature } from "geojson"
export class HistogramViz implements SpecialVisualization {
funcName = "histogram"
docs = "Create a histogram for a list of given values, read from the properties."
example =
"`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram"
'`{histogram(\'some_key\')}` with properties being `{some_key: ["a","b","a","c"]} to create a histogram'
args = [
{
name: "key",
@ -29,6 +30,22 @@ export class HistogramViz implements SpecialVisualization {
},
]
structuredExamples(): { feature: Feature; args: string[] }[] {
return [
{
feature: <Feature>{
type: "Feature",
properties: { values: `["a","b","a","b","b","b","c","c","c","d","d"]` },
geometry: {
type: "Point",
coordinates: [0, 0],
},
},
args: ["values"],
},
]
}
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,

View file

@ -5,10 +5,7 @@ import { Feature } from "geojson"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import SvelteUIElement from "../Base/SvelteUIElement"
import MaplibreMap from "../Map/MaplibreMap.svelte"
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteredLayer from "../../Models/FilteredLayer"
import ShowDataLayer from "../Map/ShowDataLayer"
import { stat } from "fs"
export class MinimapViz implements SpecialVisualization {
funcName = "minimap"

View file

@ -54,11 +54,6 @@ export default class MoveWizard extends Toggle {
options: MoveConfig
) {
const t = Translations.t.move
const loginButton = new Toggle(
t.loginToMove.SetClass("btn").onClick(() => state.osmConnection.AttemptLogin()),
undefined,
state.featureSwitchUserbadge
)
const reasons: MoveReason[] = []
if (options.enableRelocation) {

View file

@ -0,0 +1,34 @@
<script lang="ts">
import { Translation } from "../i18n/Translation";
import SpecialVisualizations from "../SpecialVisualizations";
import { onDestroy } from "svelte";
import Locale from "../i18n/Locale";
import type { RenderingSpecification, SpecialVisualizationState } from "../SpecialVisualization";
import { Utils } from "../../Utils.js";
import type { Feature } from "geojson";
import { UIEventSource } from "../../Logic/UIEventSource.js";
import ToSvelte from "../Base/ToSvelte.svelte";
import FromHtml from "../Base/FromHtml.svelte";
/**
* The 'specialTranslation' renders a `Translation`-object, but interprets the special values as well
*/
export let t: Translation;
export let state: SpecialVisualizationState;
export let tags: UIEventSource<Record<string, string>>;
export let feature: Feature;
let txt: string;
onDestroy(Locale.language.addCallbackAndRunD(l => {
txt = t.textFor(l);
}));
let specs: RenderingSpecification[];
specs = SpecialVisualizations.constructSpecification(txt);
</script>
{#each specs as specpart}
{#if typeof specpart === "string"}
<FromHtml src= {Utils.SubstituteKeys(specpart, $tags)}></FromHtml>
{:else if $tags !== undefined }
<ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature)}></ToSvelte>
{/if}
{/each}

View file

@ -0,0 +1,34 @@
<script lang="ts">
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import { Utils } from "../../Utils";
import { Translation } from "../i18n/Translation";
import TagRenderingMapping from "./TagRenderingMapping.svelte";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import type { Feature } from "geojson";
import { UIEventSource } from "../../Logic/UIEventSource";
import { onDestroy } from "svelte";
export let tags: UIEventSource<Record<string, string> | undefined>;
let _tags : Record<string, string>
onDestroy(tags.addCallbackAndRun(tags => {
_tags = tags
}))
export let state: SpecialVisualizationState
export let selectedElement: Feature
export let config: TagRenderingConfig;
let trs: { then: Translation; icon?: string; iconClass?: string }[];
$: trs = Utils.NoNull(config?.GetRenderValues(_tags));
</script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(tags))}
<div>
{#if trs.length === 1}
<TagRenderingMapping mapping={trs[0]} {tags} {state} feature={selectedElement}></TagRenderingMapping>
{/if}
{#if trs.length > 1}
{#each trs as mapping}
<TagRenderingMapping mapping={trs} {tags} {state} feature=""{selectedElement}></TagRenderingMapping>
{/each}
{/if}
</div>
{/if}

View file

@ -6,7 +6,7 @@ import { SubstitutedTranslation } from "../SubstitutedTranslation"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import Combine from "../Base/Combine"
import Img from "../Base/Img"
import { SpecialVisualisationState } from "../SpecialVisualization"
import { SpecialVisualizationState } from "../SpecialVisualization"
/***
* Displays the correct value for a known tagrendering
@ -15,7 +15,7 @@ export default class TagRenderingAnswer extends VariableUiElement {
constructor(
tagsSource: UIEventSource<any>,
configuration: TagRenderingConfig,
state: SpecialVisualisationState,
state: SpecialVisualizationState,
contentClasses: string = "",
contentStyle: string = "",
options?: {

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { Translation } from "../i18n/Translation";
import SpecialTranslation from "./SpecialTranslation.svelte";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import type { Feature } from "geojson";
import { UIEventSource } from "../../Logic/UIEventSource";
export let selectedElement: Feature
export let tags: UIEventSource<Record<string, string>>;
export let state: SpecialVisualizationState
export let mapping: {
then: Translation; icon?: string; iconClass?: | "small"
| "medium"
| "large"
| "small-height"
| "medium-height"
| "large-height"
};
let iconclass = "mapping-icon-" + mapping.iconClass;
</script>
{#if mapping.icon !== undefined}
<div class="flex">
<img class={iconclass+" mr-1"} src={mapping.icon}>
<SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation>
</div>
{:else if mapping.then !== undefined}
<SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation>
{/if}

View file

@ -1,7 +1,6 @@
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine"
import { InputElement, ReadonlyInputElement } from "../Input/InputElement"
import ValidatedTextField from "../Input/ValidatedTextField"
import { FixedInputElement } from "../Input/FixedInputElement"
import { RadioButton } from "../Input/RadioButton"
import { Utils } from "../../Utils"

View file

@ -6,7 +6,6 @@ import BaseUIElement from "../BaseUIElement"
import Img from "../Base/Img"
import { Review } from "mangrove-reviews-typescript"
import { Store } from "../../Logic/UIEventSource"
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"
export default class SingleReview extends Combine {
constructor(review: Review & { madeByLoggedInUser: Store<boolean> }) {

View file

@ -2,17 +2,13 @@ import { Store, UIEventSource } from "../Logic/UIEventSource"
import BaseUIElement from "./BaseUIElement"
import { DefaultGuiState } from "./DefaultGuiState"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import FeatureSource, {
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { Changes } from "../Logic/Osm/Changes"
import { MapProperties } from "../Models/MapProperties"
import LayerState from "../Logic/State/LayerState"
import { Feature } from "geojson"
import { Feature, Geometry } from "geojson"
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import UserRelatedState from "../Logic/State/UserRelatedState"
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
@ -58,6 +54,8 @@ export interface SpecialVisualization {
funcName: string
docs: string | BaseUIElement
example?: string
structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[]
args: { name: string; defaultValue?: string; doc: string; required?: false | boolean }[]
getLayerDependencies?: (argument: string[]) => string[]
@ -68,3 +66,11 @@ export interface SpecialVisualization {
feature: Feature
): BaseUIElement
}
export type RenderingSpecification =
| string
| {
func: SpecialVisualization
args: string[]
style: string
}

View file

@ -3,7 +3,11 @@ import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title"
import Table from "./Base/Table"
import { SpecialVisualization } from "./SpecialVisualization"
import {
RenderingSpecification,
SpecialVisualization,
SpecialVisualizationState,
} from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { StealViz } from "./Popup/StealViz"
import { MinimapViz } from "./Popup/MinimapViz"
@ -51,10 +55,97 @@ import FeatureReviews from "../Logic/Web/MangroveReviews"
import Maproulette from "../Logic/Maproulette"
import SvelteUIElement from "./Base/SvelteUIElement"
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import { Feature } from "geojson"
export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
/**
*
* 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
* SubstitutedTranslation.ExtractSpecialComponents("") // => []
*
* // Advanced cases with commas, braces and newlines should be handled without problem
* const templates = SubstitutedTranslation.ExtractSpecialComponents("{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.osm.be/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 = templates[0]
* templ.special.func.funcName // => "send_email"
* templ.special.args[0] = "{email}"
*/
public static constructSpecification(
template: string,
extraMappings: SpecialVisualization[] = []
): RenderingSpecification[] {
if (template === "") {
return []
}
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()
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) =>
str
.trim()
.replace(/&LPARENS/g, "(")
.replace(/&RPARENS/g, ")")
.replace(/&LBRACE/g, "{")
.replace(/&RBRACE/g, "}")
.replace(/&COMMA/g, ",")
)
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]
}
}
// Let's to a small sanity check to help the theme designers:
if (template.search(/{[^}]+\([^}]*\)}/) >= 0) {
// Hmm, we might have found an invalid rendering name
console.warn(
"Found a suspicious special rendering value in: ",
template,
" did you mean one of: "
/*SpecialVisualizations.specialVisualizations
.map((sp) => sp.funcName + "()")
.join(", ")*/
)
}
// 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)
@ -649,7 +740,7 @@ export default class SpecialVisualizations {
defaultValue: "mr_taskId",
},
],
constr: (state, tagsSource, args, guistate) => {
constr: (state, tagsSource, args) => {
let [message, image, message_closed, status, maproulette_id_key] = args
if (image === "") {
image = "confirm"
@ -720,7 +811,7 @@ export default class SpecialVisualizations {
funcName: "statistics",
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
args: [],
constr: (state, tagsSource, args, guiState) => {
constr: (state) => {
return new Combine(
state.layout.layers
.filter((l) => l.name !== null)
@ -852,4 +943,23 @@ export default class SpecialVisualizations {
return specialVisualizations
}
// noinspection JSUnusedGlobalSymbols
public static renderExampleOfSpecial(
state: SpecialVisualizationState,
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
)
})
return new Combine([new Title(s.funcName), s.docs, ...examples])
}
}

View file

@ -7,10 +7,10 @@ import { Utils } from "../Utils"
import { VariableUiElement } from "./Base/VariableUIElement"
import Combine from "./Base/Combine"
import BaseUIElement from "./BaseUIElement"
import { DefaultGuiState } from "./DefaultGuiState"
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
import LinkToWeblate from "./Base/LinkToWeblate"
import { SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
import SpecialVisualizations from "./SpecialVisualizations"
import { Feature } from "geojson"
export class SubstitutedTranslation extends VariableUiElement {
public constructor(
@ -21,10 +21,10 @@ export class SubstitutedTranslation extends VariableUiElement {
string,
| BaseUIElement
| ((
state: FeaturePipelineState,
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
guistate: DefaultGuiState
feature: Feature
) => BaseUIElement)
> = undefined
) {
@ -55,19 +55,23 @@ export class SubstitutedTranslation extends VariableUiElement {
txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`)
})
const allElements = SubstitutedTranslation.ExtractSpecialComponents(
const allElements = SpecialVisualizations.constructSpecification(
txt,
extraMappings
).map((proto) => {
if (proto.fixed !== undefined) {
if (typeof proto === "string") {
if (tagsSource === undefined) {
return Utils.SubstituteKeys(proto.fixed, undefined)
return Utils.SubstituteKeys(proto, undefined)
}
return new VariableUiElement(
tagsSource.map((tags) => Utils.SubstituteKeys(proto.fixed, tags))
tagsSource.map((tags) => Utils.SubstituteKeys(proto, tags))
)
}
const viz = proto.special
const viz: {
func: SpecialVisualization
args: string[]
style: string
} = proto
if (viz === undefined) {
console.error(
"SPECIALRENDERING UNDEFINED for",
@ -77,9 +81,12 @@ export class SubstitutedTranslation extends VariableUiElement {
return undefined
}
try {
const feature = state.indexedFeatures.featuresById.data.get(
tagsSource.data.id
)
return viz.func
.constr(state, tagsSource, proto.special.args)
?.SetStyle(proto.special.style)
.constr(state, tagsSource, proto.args, feature)
?.SetStyle(proto.style)
} catch (e) {
console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e)
return new FixedUiElement(
@ -97,98 +104,4 @@ export class SubstitutedTranslation extends VariableUiElement {
this.SetClass("w-full")
}
/**
*
* // Return empty list on empty input
* SubstitutedTranslation.ExtractSpecialComponents("") // => []
*
* // Advanced cases with commas, braces and newlines should be handled without problem
* const templates = SubstitutedTranslation.ExtractSpecialComponents("{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.osm.be/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 = templates[0]
* templ.special.func.funcName // => "send_email"
* templ.special.args[0] = "{email}"
*/
public static ExtractSpecialComponents(
template: string,
extraMappings: SpecialVisualization[] = []
): {
fixed?: string
special?: {
func: SpecialVisualization
args: string[]
style: string
}
}[] {
if (template === "") {
return []
}
for (const knownSpecial of extraMappings.concat(
[] // TODO enable SpecialVisualizations.specialVisualizations
)) {
// 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 = SubstitutedTranslation.ExtractSpecialComponents(
matched[1],
extraMappings
)
const argument = matched[2].trim()
const style = matched[3]?.substring(1) ?? ""
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(
matched[4],
extraMappings
)
const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
if (argument.length > 0) {
const realArgs = argument.split(",").map((str) =>
str
.trim()
.replace(/&LPARENS/g, "(")
.replace(/&RPARENS/g, ")")
.replace(/&LBRACE/g, "{")
.replace(/&RBRACE/g, "}")
.replace(/&COMMA/g, ",")
)
for (let i = 0; i < realArgs.length; i++) {
if (args.length <= i) {
args.push(realArgs[i])
} else {
args[i] = realArgs[i]
}
}
}
let element
element = {
special: {
args: args,
style: style,
func: knownSpecial,
},
}
return [...partBefore, element, ...partAfter]
}
}
// Let's to a small sanity check to help the theme designers:
if (template.search(/{[^}]+\([^}]*\)}/) >= 0) {
// Hmm, we might have found an invalid rendering name
console.warn(
"Found a suspicious special rendering value in: ",
template,
" did you mean one of: "
/*SpecialVisualizations.specialVisualizations
.map((sp) => sp.funcName + "()")
.join(", ")*/
)
}
// IF we end up here, no changes have to be made - except to remove any resting {}
return [{ fixed: template }]
}
}

View file

@ -20,6 +20,7 @@
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
import Translations from "./i18n/Translations";
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid";
import Tr from "./Base/Tr.svelte";
export let layout: LayoutConfig;
const state = new ThemeViewState(layout);
@ -48,7 +49,7 @@
<div class="flex mr-2 items-center">
<img class="w-8 h-8 block mr-2" src={layout.icon}>
<b>
{layout.title}
<Tr t={layout.title}></Tr>
</b>
</div>
</MapControlButton>
@ -58,9 +59,7 @@
</div>
<div class="absolute bottom-0 left-0 mb-4 ml-4">
<MapControlButton on:click={() => state.guistate.filterViewIsOpened.setData(true)}>
<ToSvelte class="w-7 h-7 block" construct={Svg.layers_ui}></ToSvelte>
</MapControlButton>
</div>
<div class="absolute bottom-0 right-0 mb-4 mr-4">
@ -86,17 +85,6 @@
</If>
</div>
<If condition={state.guistate.filterViewIsOpened}>
<div class="normal-background absolute bottom-0 left-0 flex flex-col">
<div on:click={() => state.guistate.filterViewIsOpened.setData(false)}>Close</div>
<!-- Filter panel -- TODO move to actual location-->
{#each layout.layers as layer}
<Filterview filteredLayer={state.layerState.filteredLayers.get(layer.id)}></Filterview>
{/each}
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
</div>
</If>
<If condition={state.guistate.welcomeMessageIsOpened}>
<!-- Theme page -->
@ -105,31 +93,47 @@
<div on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>Close</div>
<TabGroup>
<TabList>
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>About</Tab>
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 2</Tab>
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
<Tr t={layout.title}/>
</Tab>
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
<Tr t={Translations.t.general.menu.filter}/>
</Tab>
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 3</Tab>
</TabList>
<TabPanels>
<TabPanel class="flex flex-col">
<ToSvelte construct={() => layout.description}></ToSvelte>
{Translations.t.general.welcomeExplanation.general}
<Tr t={layout.description}></Tr>
<Tr t={Translations.t.general.welcomeExplanation.general}/>
{#if layout.layers.some((l) => l.presets?.length > 0)}
<If condition={state.featureSwitches.featureSwitchAddNew}>
{Translations.t.general.welcomeExplanation.addNew}
<Tr t={Translations.t.general.welcomeExplanation.addNew}/>
</If>
{/if}
<!--toTheMap,
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
-->
<ToSvelte construct= {() => layout.descriptionTail}></ToSvelte>
<Tr t={layout.descriptionTail}></Tr>
<div class="m-x-8">
<button class="subtle-background rounded w-full p-4">Explore the map</button>
<button class="subtle-background rounded w-full p-4"
on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>
<Tr t={Translations.t.general.openTheMap} />
</button>
</div>
</TabPanel>
<TabPanel>Content 2</TabPanel>
<TabPanel>
<div class="flex flex-col">
<!-- Filter panel -- TODO move to actual location-->
{#each layout.layers as layer}
<Filterview filteredLayer={state.layerState.filteredLayers.get(layer.id)}></Filterview>
{/each}
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
</div>
</TabPanel>
<TabPanel>Content 3</TabPanel>
</TabPanels>
</TabGroup>
@ -163,15 +167,14 @@
</div>
</If>
<If condition={selectedElement}>
{#if $selectedElement !== undefined && $selectedLayer !== undefined}
<div class="absolute top-0 right-0 normal-background">
<SelectedElementView layer={selectedLayer} {selectedElement}
tags={selectedElementTags}></SelectedElementView>
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
tags={$selectedElementTags} state={state}></SelectedElementView>
</div>
</If>
{/if}
<style>
/* WARNING: This is just for demonstration.
Using :global() in this way can be risky. */

View file

@ -1,5 +1,5 @@
import { VariableUiElement } from "../Base/VariableUIElement"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Store } from "../../Logic/UIEventSource"
import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata"
import { Translation, TypedTranslation } from "../i18n/Translation"
import { FixedUiElement } from "../Base/FixedUiElement"

View file

@ -51,10 +51,6 @@ export class Translation extends BaseUIElement {
return this.textFor(Translation.forcedLanguage ?? Locale.language.data)
}
public toString() {
return this.txt
}
static ExtractAllTranslationsFrom(
object: any,
context = ""
@ -91,6 +87,10 @@ export class Translation extends BaseUIElement {
return new Translation(translations)
}
public toString() {
return this.txt
}
Destroy() {
super.Destroy()
this.isDestroyed = true

View file

@ -1,9 +1,6 @@
import { Utils } from "./Utils"
import AllThemesGui from "./UI/AllThemesGui"
import { QueryParameters } from "./Logic/Web/QueryParameters"
import StatisticsGUI from "./UI/StatisticsGUI"
import { FixedUiElement } from "./UI/Base/FixedUiElement"
import { PdfExportGui } from "./UI/BigComponents/PdfExportGui"
const layout = QueryParameters.GetQueryParameter("layout", undefined).data ?? ""
const customLayout = QueryParameters.GetQueryParameter("userlayout", undefined).data ?? ""
@ -32,23 +29,4 @@ if (layout !== "") {
}
Utils.DisableLongPresses()
document.getElementById("decoration-desktop").remove()
const mode = QueryParameters.GetQueryParameter(
"mode",
"map",
"The mode the application starts in, e.g. 'statistics'"
)
if (mode.data === "statistics") {
console.log("Statistics mode!")
new FixedUiElement("").AttachTo("centermessage")
new StatisticsGUI().SetClass("w-full h-full pointer-events-auto").AttachTo("topleft-tools")
} else if (mode.data === "pdf") {
new FixedUiElement("").AttachTo("centermessage")
const div = document.createElement("div")
div.id = "extra_div_for_maps"
new PdfExportGui(div.id).SetClass("pointer-events-auto").AttachTo("topleft-tools")
document.getElementById("topleft-tools").appendChild(div)
} else {
new AllThemesGui().setup()
}

View file

@ -324,7 +324,6 @@
"hideInAnswer": true
}
]
},
{
"id": "max_bolts",

View file

@ -1183,6 +1183,10 @@ video {
width: auto;
}
.w-48 {
width: 12rem;
}
.min-w-min {
min-width: -webkit-min-content;
min-width: min-content;
@ -1509,6 +1513,11 @@ video {
border-color: rgb(107 114 128 / var(--tw-border-opacity));
}
.border-red-500 {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.border-opacity-50 {
--tw-border-opacity: 0.5;
}
@ -1580,10 +1589,6 @@ video {
padding: 0.5rem;
}
.p-8 {
padding: 2rem;
}
.p-3 {
padding: 0.75rem;
}
@ -1596,6 +1601,10 @@ video {
padding: 0.125rem;
}
.p-8 {
padding: 2rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
@ -1921,6 +1930,10 @@ video {
transition-duration: 150ms;
}
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.z-above-map {
z-index: 10000;
}
@ -1971,8 +1984,6 @@ video {
--shadow-color: #00000066;
--return-to-the-map-height: 2em;
--image-carousel-height: 350px;
/* The border colour of the leaflet popup */
--popup-border: white;
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
--variable-title-height: 0px;
}
@ -1989,31 +2000,6 @@ body {
font-family: "Helvetica Neue", Arial, sans-serif;
}
.leaflet-overlay-pane .leaflet-zoom-animated {
/* Another workaround to keep leaflet working */
width: initial !important;
height: initial !important;
box-sizing: initial !important;
}
.leaflet-marker-icon img {
-webkit-touch-callout: none;
/* prevent callout to copy image, etc when tap to hold */
}
.leaflet-control-attribution {
display: flex;
}
.badge {
}
.badge svg {
/*Workaround for leaflet*/
width: unset !important;
height: 100% !important;
}
svg,
img {
box-sizing: content-box;
@ -2255,28 +2241,6 @@ li::marker {
fill: var(--catch-detail-color) !important;
}
#leafletDiv {
height: 100%;
}
.leaflet-popup-content-wrapper {
background-color: var(--background-color);
color: var(--foreground-color);
border: 2px solid var(--popup-border);
box-shadow: 0 3px 14px var(--shadow-color) !important;
}
.leaflet-container {
font: unset !important;
background-color: var(--background-color) !important;
}
.leaflet-popup-tip {
background-color: var(--popup-border) !important;
color: var(--popup-border) !important;
box-shadow: 0 3px 14px var(--shadow-color) !important;
}
.single-layer-selection-toggle {
position: relative;
width: 2em;
@ -2408,131 +2372,17 @@ li::marker {
}
}
.hand-drag-animation {
-webkit-animation: hand-drag-animation 6s ease-in-out infinite;
animation: hand-drag-animation 6s ease-in-out infinite;
-webkit-transform-origin: 50% 125%;
transform-origin: 50% 125%;
}
@-webkit-keyframes hand-drag-animation {
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
0% {
opacity: 0;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
6% {
opacity: 1;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
12% {
opacity: 1;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
24% {
opacity: 1;
-webkit-transform: rotate(-00deg);
transform: rotate(-00deg);
}
30% {
opacity: 1;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
36% {
opacity: 0;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
100% {
opacity: 0;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
}
@keyframes hand-drag-animation {
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
0% {
opacity: 0;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
6% {
opacity: 1;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
12% {
opacity: 1;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
24% {
opacity: 1;
-webkit-transform: rotate(-00deg);
transform: rotate(-00deg);
}
30% {
opacity: 1;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
36% {
opacity: 0;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
100% {
opacity: 0;
-webkit-transform: rotate(-30deg);
transform: rotate(-30deg);
}
}
/***************** Info box (box containing features and questions ******************/
input {
color: var(--foreground-color);
}
.leaflet-popup-content {
width: 45em !important;
margin: 0.25rem !important;
}
.leaflet-div-icon {
background-color: unset !important;
border: unset !important;
}
.floating-element-width {
max-width: calc(100vw - 5em);
width: 40em;
}
.leaflet-div-icon svg {
width: calc(100%);
height: calc(100%);
}
/****** ShareScreen *****/
.literal-code {

103
index.css
View file

@ -87,9 +87,6 @@
--return-to-the-map-height: 2em;
--image-carousel-height: 350px;
/* The border colour of the leaflet popup */
--popup-border: white;
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
--variable-title-height: 0px;
}
@ -106,29 +103,6 @@ body {
font-family: "Helvetica Neue", Arial, sans-serif;
}
.leaflet-overlay-pane .leaflet-zoom-animated {
/* Another workaround to keep leaflet working */
width: initial !important;
height: initial !important;
box-sizing: initial !important;
}
.leaflet-marker-icon img {
-webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */
}
.leaflet-control-attribution {
display: flex;
}
.badge {
}
.badge svg {
/*Workaround for leaflet*/
width: unset !important;
height: 100% !important;
}
svg,
img {
@ -365,27 +339,6 @@ li::marker {
fill: var(--catch-detail-color) !important;
}
#leafletDiv {
height: 100%;
}
.leaflet-popup-content-wrapper {
background-color: var(--background-color);
color: var(--foreground-color);
border: 2px solid var(--popup-border);
box-shadow: 0 3px 14px var(--shadow-color) !important;
}
.leaflet-container {
font: unset !important;
background-color: var(--background-color) !important;
}
.leaflet-popup-tip {
background-color: var(--popup-border) !important;
color: var(--popup-border) !important;
box-shadow: 0 3px 14px var(--shadow-color) !important;
}
.single-layer-selection-toggle {
position: relative;
@ -499,48 +452,6 @@ li::marker {
}
}
.hand-drag-animation {
animation: hand-drag-animation 6s ease-in-out infinite;
transform-origin: 50% 125%;
}
@keyframes hand-drag-animation {
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
0% {
opacity: 0;
transform: rotate(-30deg);
}
6% {
opacity: 1;
transform: rotate(-30deg);
}
12% {
opacity: 1;
transform: rotate(-45deg);
}
24% {
opacity: 1;
transform: rotate(-00deg);
}
30% {
opacity: 1;
transform: rotate(-30deg);
}
36% {
opacity: 0;
transform: rotate(-30deg);
}
100% {
opacity: 0;
transform: rotate(-30deg);
}
}
/***************** Info box (box containing features and questions ******************/
@ -549,25 +460,11 @@ input {
color: var(--foreground-color);
}
.leaflet-popup-content {
width: 45em !important;
margin: 0.25rem !important;
}
.leaflet-div-icon {
background-color: unset !important;
border: unset !important;
}
.floating-element-width {
max-width: calc(100vw - 5em);
width: 40em;
}
.leaflet-div-icon svg {
width: calc(100%);
height: calc(100%);
}
/****** ShareScreen *****/

View file

@ -51,22 +51,8 @@
</head>
<body>
<div id="decoration-desktop" style="position: fixed; left: 1em; bottom: 1em; width:35vh; height:35vh;">
<!-- A nice decoration while loading or on errors -->
<!-- DECORATION 0 START -->
<img src="./assets/svg/add.svg"/>
<!-- DECORATION 0 END -->
</div>
<div id="top-left">
</div>
<div class="clutter absolute h-24 left-24 right-24 top-56 text-xl text-center"
id="centermessage" style="z-index: 4000">
Loading MapComplete, hang on...
</div>
<div id="main"></div>
<script type="module" src="./all_themes_index.ts"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js"></script>

View file

@ -4,7 +4,6 @@ import { Utils } from "./Utils"
import AllThemesGui from "./UI/AllThemesGui"
import DetermineLayout from "./Logic/DetermineLayout"
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
import DefaultGUI from "./UI/DefaultGUI"
import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"
import { DefaultGuiState } from "./UI/DefaultGuiState"

View file

@ -88,7 +88,7 @@
},
"general": {
"about": "Easily edit and add OpenStreetMap for a certain theme",
"aboutMapcomplete": "<h3>About</h3><p>Use MapComplete to add OpenStreetMap info on a <b>single theme.</b> Answer questions, and within minutes your contributions are available everywhere. In most themes you can add pictures or even leave a review. The <b>theme maintainer</b> defines elements, questions and languages for it.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete.</li><li>The fullscreen version offers info about OpenStreetMap.</li><li>Viewing works without login, but editing requires an OSM account.</li><li>If you are not logged in, you are asked to do so</li><li>Once you answered a single question, you can add new features to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='{osmcha_link}' target='_blank' >OsmCha</a>.</p>",
"aboutMapcomplete": "<p>Use MapComplete to add OpenStreetMap info on a <b>single theme.</b> Answer questions, and within minutes your contributions are available everywhere. In most themes you can add pictures or even leave a review. The <b>theme maintainer</b> defines elements, questions and languages for it.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete.</li><li>The fullscreen version offers info about OpenStreetMap.</li><li>Viewing works without login, but editing requires an OSM account.</li><li>If you are not logged in, you are asked to do so</li><li>Once you answered a single question, you can add new features to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='{osmcha_link}' target='_blank' >OsmCha</a>.</p>",
"add": {
"addNew": "Add {category}",
"addNewMapLabel": "Click here to add a new item",
@ -203,6 +203,10 @@
"loginToStart": "Log in to answer this question",
"loginWithOpenStreetMap": "Login with OpenStreetMap",
"logout": "Log out",
"menu": {
"aboutMapComplete": "About MapComplete",
"filter": "Filter data"
},
"morescreen": {
"createYourOwnTheme": "Create your own MapComplete theme from scratch",
"hiddenExplanation": "These themes are only accessible to those with the link. You have discovered {hidden_discovered} of {total_hidden} hidden themes.",

View file

@ -1809,9 +1809,6 @@
"gps_track": {
"name": "La teva traça recorreguda"
},
"grass_in_parks": {
"description": "Cerques per a tots els camins d'herba accessibles dins dels parcs públics - aquests són «groenzones»"
},
"hackerspace": {
"presets": {
"1": {

View file

@ -4750,9 +4750,6 @@
}
}
},
"grass_in_parks": {
"description": "Sucht nach allen zugänglichen Grasflächen in öffentlichen Parks - dies sind 'Grünzonen'"
},
"hackerspace": {
"description": "Hackerspace",
"name": "Hackerspaces",

View file

@ -4750,9 +4750,6 @@
}
}
},
"grass_in_parks": {
"description": "Searches for all accessible grass patches within public parks - these are 'groenzones'"
},
"hackerspace": {
"description": "Hackerspace",
"name": "Hackerspace",

View file

@ -4657,18 +4657,6 @@
}
}
},
"grass_in_parks": {
"description": "Dit zoekt naar alle toegankelijke grasvelden binnen publieke parken - dit zijn 'groenzones'",
"name": "Toegankelijke grasvelden in parken",
"title": {
"mappings": {
"0": {
"then": "{name}"
}
},
"render": "Speelweide in een park"
}
},
"hackerspace": {
"description": "Hackerspace",
"name": "Hackerspace",

View file

@ -661,7 +661,7 @@
"grb": {
"description": "Aquest tema és un intent d'automatitzar la importació GRB.",
"layers": {
"1": {
"0": {
"tagRenderings": {
"building type": {
"question": "Quin tipus d'edifici és aquest?"

View file

@ -614,14 +614,14 @@
},
"grb": {
"layers": {
"1": {
"0": {
"tagRenderings": {
"building type": {
"question": "Jaký druh budovy je toto?"
}
}
},
"6": {
"5": {
"tagRenderings": {
"Import-button": {
"mappings": {

View file

@ -552,14 +552,14 @@
"grb": {
"description": "Dette tema er et forsøg på at hjælpe med at automatisere GRB-importen.",
"layers": {
"1": {
"0": {
"tagRenderings": {
"building type": {
"question": "Hvad er det for en bygning?"
}
}
},
"6": {
"5": {
"tagRenderings": {
"Import-button": {
"mappings": {

View file

@ -689,14 +689,14 @@
"grb": {
"description": "Dieses Thema ist ein Versuch, die Automatisierung des GRB-Imports zu unterstützen.",
"layers": {
"1": {
"0": {
"tagRenderings": {
"building type": {
"question": "Was ist das für ein Gebäude?"
}
}
},
"6": {
"5": {
"tagRenderings": {
"Import-button": {
"mappings": {

View file

@ -689,14 +689,14 @@
"grb": {
"description": "This theme is an attempt to help automating the GRB import.",
"layers": {
"1": {
"0": {
"tagRenderings": {
"building type": {
"question": "What kind of building is this?"
}
}
},
"6": {
"5": {
"tagRenderings": {
"Import-button": {
"mappings": {

View file

@ -689,14 +689,14 @@
"grb": {
"description": "Este tema es un intento de automatizar la importación GRB.",
"layers": {
"1": {
"0": {
"tagRenderings": {
"building type": {
"question": "¿Qué tipo de edificio es este?"
}
}
},
"6": {
"5": {
"tagRenderings": {
"Import-button": {
"mappings": {

View file

@ -685,14 +685,14 @@
"grb": {
"description": "Ce thème tente daider limport automatique GRB.",
"layers": {
"1": {
"0": {
"tagRenderings": {
"building type": {
"question": "De quel type de bâtiment sagit-il ?"
}
}
},
"6": {
"5": {
"tagRenderings": {
"Import-button": {
"mappings": {

View file

@ -739,7 +739,7 @@
"grb": {
"description": "Dit thema helpt het GRB importeren.",
"layers": {
"1": {
"0": {
"tagRenderings": {
"building type": {
"question": "Wat voor soort gebouw is dit?"
@ -775,7 +775,7 @@
}
}
},
"5": {
"4": {
"override": {
"tagRenderings+": {
"0": {
@ -800,7 +800,7 @@
}
}
},
"6": {
"5": {
"tagRenderings": {
"Import-button": {
"mappings": {
@ -1130,7 +1130,7 @@
"speelplekken": {
"description": "<h3>Welkom bij de Groendoener!</h3>De Zuidrand dat is spelen, ravotten, chillen, wandelen,… in het groen. Meer dan <b>200 grote en kleine speelplekken</b> liggen er in parken, in bossen en op pleintjes te wachten om ontdekt te worden. De verschillende speelplekken werden getest én goedgekeurd door kinder- en jongerenreporters uit de Zuidrand. Met leuke challenges dagen de reporters jou uit om ook op ontdekking te gaan. Klik op een speelplek op de kaart, bekijk het filmpje en ga op verkenning!<br/><br/>Het project groendoener kadert binnen het strategisch project <a href='https://www.provincieantwerpen.be/aanbod/dlm/samenwerkingsverbanden/zuidrand/projecten/strategisch-project-beleefbare-open-ruimte.html' target='_blank'>Beleefbare Open Ruimte in de Antwerpse Zuidrand</a> en is een samenwerking tussen het departement Leefmilieu van provincie Antwerpen, Sportpret vzw, een OpenStreetMap-België Consultent en Createlli vzw. Het project kwam tot stand met steun van Departement Omgeving van de Vlaamse Overheid.<br/><img class='w-full md:w-1/2' src='./assets/themes/speelplekken/provincie_antwerpen.jpg'/><img class='w-full md:w-1/2' src='./assets/themes/speelplekken/Departement_Omgeving_Vlaanderen.png'/>",
"layers": {
"7": {
"6": {
"name": "Wandelroutes van provincie Antwerpen",
"tagRenderings": {
"walk-description": {

11
package-lock.json generated
View file

@ -28,6 +28,7 @@
"escape-html": "^1.0.3",
"fake-dom": "^1.0.4",
"geojson2svg": "^1.3.1",
"html-to-markdown": "^1.0.0",
"i18next-client": "^1.11.4",
"idb-keyval": "^6.0.3",
"jest-mock": "^29.4.1",
@ -6355,6 +6356,11 @@
"node": ">=12"
}
},
"node_modules/html-to-markdown": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/html-to-markdown/-/html-to-markdown-1.0.0.tgz",
"integrity": "sha512-QWHVycyZXQyotyyh5Zdh65L0mUvlAtcoi6R7Fqz4W+CbcWKt2TekVXZbFG5RUh2XBkNmSWechMyQSxvJOwXrHw=="
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
@ -16731,6 +16737,11 @@
"whatwg-encoding": "^2.0.0"
}
},
"html-to-markdown": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/html-to-markdown/-/html-to-markdown-1.0.0.tgz",
"integrity": "sha512-QWHVycyZXQyotyyh5Zdh65L0mUvlAtcoi6R7Fqz4W+CbcWKt2TekVXZbFG5RUh2XBkNmSWechMyQSxvJOwXrHw=="
},
"html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",

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