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()
}
new AllThemesGui().setup()

View file

@ -345,4 +345,4 @@
}
],
"deletion": true
}
}

View file

@ -315,4 +315,4 @@
"cs": "Vrstva zobrazující automaty na cyklistické duše (buď speciální automaty na cyklistické duše, nebo klasické automaty s cyklistickými dušemi a případně dalšími předměty souvisejícími s jízdními koly, jako jsou světla, rukavice, zámky, ...)",
"ca": "Una capa que mostra màquines expenedores per a tubs de bicicleta (ja siguin màquines expenedores de tubs de bicicleta o màquines expenedores clàssiques amb tubs de bicicleta i opcionalment objectes addicionals relacionats amb la bicicleta com ara llums, guants, panys, ...)"
}
}
}

View file

@ -1015,4 +1015,4 @@
"fr": "Une couche montrant les pompes à vélo et les centres de réparation",
"cs": "Vrstva zobrazující vzduchové kompresory na jízdní kola a stojany na nářadí pro opravu jízdních kol"
}
}
}

View file

@ -815,4 +815,4 @@
}
}
]
}
}

View file

@ -360,4 +360,4 @@
"da": "Et lag med caféer og pubber, hvor man kan samles omkring en drink. Laget stiller nogle relevante spørgsmål",
"fr": "Une couche montrants les cafés et pubs où lon peut prendre un verre. Cette couche pose des questions y afférentes."
}
}
}

View file

@ -5065,4 +5065,4 @@
},
"neededChangesets": 10
}
}
}

View file

@ -324,7 +324,6 @@
"hideInAnswer": true
}
]
},
{
"id": "max_bolts",
@ -404,4 +403,4 @@
}
],
"mapRendering": null
}
}

View file

@ -186,4 +186,4 @@
}
}
]
}
}

View file

@ -246,4 +246,4 @@
]
}
]
}
}

View file

@ -79,4 +79,4 @@
}
}
]
}
}

View file

@ -10,4 +10,4 @@
"color": "#cccc0088"
}
]
}
}

View file

@ -175,4 +175,4 @@
]
}
]
}
}

View file

@ -102,4 +102,4 @@
"filter": [
"open_now"
]
}
}

View file

@ -410,4 +410,4 @@
"filter": [
"open_now"
]
}
}

View file

@ -239,4 +239,4 @@
"nl": "Een laag die herdenkingsplaatsen voor verongelukte fietsers toont",
"de": "Eine Ebene mit Gedenkstätten für Radfahrer, die bei Verkehrsunfällen ums Leben gekommen sind"
}
}
}

View file

@ -36,4 +36,4 @@
]
}
]
}
}

View file

@ -15,4 +15,4 @@
"iconSize": "5,5,center"
}
]
}
}

View file

@ -44,4 +44,4 @@
}
],
"syncSelection": "global"
}
}

View file

@ -17,4 +17,4 @@
]
}
]
}
}

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