More refactoring
This commit is contained in:
parent
5d0fe31c41
commit
41e6a2c760
147 changed files with 1540 additions and 1797 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
52
Logic/FeatureSource/Sources/SnappingFeatureSource.ts
Normal file
52
Logic/FeatureSource/Sources/SnappingFeatureSource.ts
Normal 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]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 }]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import { RasterLayerPolygon } from "./RasterLayers"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { UIElement } from "../UIElement"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
|
||||
/**
|
||||
|
|
89
UI/Base/DragInvitation.svelte
Normal file
89
UI/Base/DragInvitation.svelte
Normal 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
19
UI/Base/FromHtml.svelte
Normal 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}
|
||||
|
|
@ -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
30
UI/Base/Tr.svelte
Normal 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>
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
1
UI/Input/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
This is the old, deprecated directory. New, SVelte-based items go into `InputElement`
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
70
UI/InputElement/Helpers/DirectionInput.svelte
Normal file
70
UI/InputElement/Helpers/DirectionInput.svelte
Normal 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>
|
42
UI/InputElement/Helpers/LocationInput.svelte
Normal file
42
UI/InputElement/Helpers/LocationInput.svelte
Normal 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>
|
13
UI/InputElement/InputHelper.svelte
Normal file
13
UI/InputElement/InputHelper.svelte
Normal 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>
|
16
UI/InputElement/InputHelpers.ts
Normal file
16
UI/InputElement/InputHelpers.ts
Normal 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
|
||||
*/
|
||||
}
|
119
UI/InputElement/ValidatedTextField.ts
Normal file
119
UI/InputElement/ValidatedTextField.ts
Normal 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
|
||||
}
|
||||
}
|
7
UI/InputElement/Validators/ColorValidator.ts
Normal file
7
UI/InputElement/Validators/ColorValidator.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class ColorValidator extends Validator {
|
||||
constructor() {
|
||||
super("color", "Shows a color picker")
|
||||
}
|
||||
}
|
23
UI/InputElement/Validators/DateValidator.ts
Normal file
23
UI/InputElement/Validators/DateValidator.ts
Normal 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("-")
|
||||
}
|
||||
}
|
17
UI/InputElement/Validators/DirectionValidator.ts
Normal file
17
UI/InputElement/Validators/DirectionValidator.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
39
UI/InputElement/Validators/EmailValidator.ts
Normal file
39
UI/InputElement/Validators/EmailValidator.ts
Normal 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)
|
||||
}
|
||||
}
|
27
UI/InputElement/Validators/FloatValidator.ts
Normal file
27
UI/InputElement/Validators/FloatValidator.ts
Normal 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
|
||||
}
|
||||
}
|
29
UI/InputElement/Validators/IntValidator.ts
Normal file
29
UI/InputElement/Validators/IntValidator.ts
Normal 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
|
||||
}
|
||||
}
|
16
UI/InputElement/Validators/LengthValidator.ts
Normal file
16
UI/InputElement/Validators/LengthValidator.ts
Normal 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)
|
||||
}
|
||||
}
|
30
UI/InputElement/Validators/NatValidator.ts
Normal file
30
UI/InputElement/Validators/NatValidator.ts
Normal 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
|
||||
}
|
||||
}
|
54
UI/InputElement/Validators/OpeningHoursValidator.ts
Normal file
54
UI/InputElement/Validators/OpeningHoursValidator.ts
Normal 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 )`",
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
23
UI/InputElement/Validators/PFloatValidator.ts
Normal file
23
UI/InputElement/Validators/PFloatValidator.ts
Normal 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
|
||||
}
|
||||
}
|
27
UI/InputElement/Validators/PNatValidator.ts
Normal file
27
UI/InputElement/Validators/PNatValidator.ts
Normal 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
|
||||
}
|
||||
}
|
32
UI/InputElement/Validators/PhoneValidator.ts
Normal file
32
UI/InputElement/Validators/PhoneValidator.ts
Normal 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()
|
||||
}
|
||||
}
|
8
UI/InputElement/Validators/StringValidator.ts
Normal file
8
UI/InputElement/Validators/StringValidator.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class StringValidator extends Validator {
|
||||
constructor() {
|
||||
super("string", "A simple piece of text")
|
||||
}
|
||||
|
||||
}
|
7
UI/InputElement/Validators/TextValidator.ts
Normal file
7
UI/InputElement/Validators/TextValidator.ts
Normal 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")
|
||||
}
|
||||
}
|
75
UI/InputElement/Validators/UrlValidator.ts
Normal file
75
UI/InputElement/Validators/UrlValidator.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
179
UI/InputElement/Validators/WikidataValidator.ts
Normal file
179
UI/InputElement/Validators/WikidataValidator.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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>>,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
34
UI/Popup/SpecialTranslation.svelte
Normal file
34
UI/Popup/SpecialTranslation.svelte
Normal 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}
|
|
@ -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}
|
|
@ -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?: {
|
||||
|
|
32
UI/Popup/TagRenderingMapping.svelte
Normal file
32
UI/Popup/TagRenderingMapping.svelte
Normal 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}
|
||||
|
|
@ -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"
|
||||
|
|
|
@ -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> }) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -324,7 +324,6 @@
|
|||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
"id": "max_bolts",
|
||||
|
|
|
@ -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
103
index.css
|
@ -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 *****/
|
||||
|
||||
|
|
16
index.html
16
index.html
|
@ -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>
|
||||
|
||||
|
|
1
index.ts
1
index.ts
|
@ -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"
|
||||
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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?"
|
||||
|
|
|
@ -614,14 +614,14 @@
|
|||
},
|
||||
"grb": {
|
||||
"layers": {
|
||||
"1": {
|
||||
"0": {
|
||||
"tagRenderings": {
|
||||
"building type": {
|
||||
"question": "Jaký druh budovy je toto?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"5": {
|
||||
"tagRenderings": {
|
||||
"Import-button": {
|
||||
"mappings": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -685,14 +685,14 @@
|
|||
"grb": {
|
||||
"description": "Ce thème tente d’aider l’import automatique GRB.",
|
||||
"layers": {
|
||||
"1": {
|
||||
"0": {
|
||||
"tagRenderings": {
|
||||
"building type": {
|
||||
"question": "De quel type de bâtiment s’agit-il ?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"5": {
|
||||
"tagRenderings": {
|
||||
"Import-button": {
|
||||
"mappings": {
|
||||
|
|
|
@ -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
11
package-lock.json
generated
|
@ -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
Loading…
Reference in a new issue