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 { Utils } from "../Utils"
|
||||||
import known_themes from "../assets/generated/known_layers.json"
|
import known_themes from "../assets/generated/known_layers.json"
|
||||||
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
|
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
|
||||||
import { ALL } from "dns"
|
|
||||||
import { AllKnownLayouts } from "./AllKnownLayouts"
|
import { AllKnownLayouts } from "./AllKnownLayouts"
|
||||||
export class AllSharedLayers {
|
export class AllSharedLayers {
|
||||||
public static sharedLayers: Map<string, LayerConfig> = AllSharedLayers.getSharedLayers()
|
public static sharedLayers: Map<string, LayerConfig> = AllSharedLayers.getSharedLayers()
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { Store, UIEventSource } from "../UIEventSource"
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
import Locale from "../../UI/i18n/Locale"
|
import Locale from "../../UI/i18n/Locale"
|
||||||
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"
|
|
||||||
import Combine from "../../UI/Base/Combine"
|
import Combine from "../../UI/Base/Combine"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
|
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
|
||||||
|
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer.svelte"
|
||||||
|
|
||||||
export default class TitleHandler {
|
export default class TitleHandler {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -32,7 +33,7 @@ export default class TitleHandler {
|
||||||
const tagsSource =
|
const tagsSource =
|
||||||
allElements.getStore(tags.id) ??
|
allElements.getStore(tags.id) ??
|
||||||
new UIEventSource<Record<string, string>>(tags)
|
new UIEventSource<Record<string, string>>(tags)
|
||||||
const title = new TagRenderingAnswer(tagsSource, layer.title, {})
|
const title = new SvelteUIElement(TagRenderingAnswer, { tags: tagsSource })
|
||||||
return (
|
return (
|
||||||
new Combine([defaultTitle, " | ", title]).ConstructElement()
|
new Combine([defaultTitle, " | ", title]).ConstructElement()
|
||||||
?.textContent ?? defaultTitle
|
?.textContent ?? defaultTitle
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
import FeatureSource, { Tiled } from "../FeatureSource"
|
import FeatureSource 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 { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import TileLocalStorage from "./TileLocalStorage"
|
import TileLocalStorage from "./TileLocalStorage"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { UIEventSource } from "../../UIEventSource"
|
import { UIEventSource } from "../../UIEventSource"
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
import { FeatureSourceForLayer } from "../FeatureSource"
|
||||||
import { BBox } from "../../BBox"
|
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
export default class SimpleFeatureSource implements FeatureSourceForLayer {
|
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 StaticFeatureSource from "./StaticFeatureSource"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
import exp from "constants"
|
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
GeoJSON,
|
GeoJSON,
|
||||||
Geometry,
|
Geometry,
|
||||||
LineString,
|
LineString,
|
||||||
|
MultiLineString,
|
||||||
MultiPolygon,
|
MultiPolygon,
|
||||||
Point,
|
Point,
|
||||||
Polygon,
|
Polygon,
|
||||||
|
@ -272,17 +273,42 @@ export class GeoOperations {
|
||||||
* @param point Point defined as [lon, lat]
|
* @param point Point defined as [lon, lat]
|
||||||
*/
|
*/
|
||||||
public static nearestPoint(
|
public static nearestPoint(
|
||||||
way: Feature<LineString | Polygon>,
|
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
||||||
point: [number, number]
|
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") {
|
if (way.geometry.type === "Polygon") {
|
||||||
way = { ...way }
|
way = { ...way }
|
||||||
way.geometry = { ...way.geometry }
|
way.geometry = { ...way.geometry }
|
||||||
way.geometry.type = "LineString"
|
way.geometry.type = "LineString"
|
||||||
way.geometry.coordinates = (<Polygon>way.geometry).coordinates[0]
|
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 {
|
public static toCSV(features: any[]): string {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import GenericImageProvider from "./GenericImageProvider"
|
||||||
import { Store, UIEventSource } from "../UIEventSource"
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||||
import { WikidataImageProvider } from "./WikidataImageProvider"
|
import { WikidataImageProvider } from "./WikidataImageProvider"
|
||||||
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A generic 'from the interwebz' image picker, without attribution
|
* A generic 'from the interwebz' image picker, without attribution
|
||||||
|
@ -44,7 +45,7 @@ export default class AllImageProviders {
|
||||||
UIEventSource<ProvidedImage[]>
|
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) {
|
if (tags.data.id === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default class ChangeLocationAction extends OsmChangeAction {
|
||||||
this._meta = meta
|
this._meta = meta
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
protected async CreateChangeDescriptions(): Promise<ChangeDescription[]> {
|
||||||
const d: ChangeDescription = {
|
const d: ChangeDescription = {
|
||||||
changes: {
|
changes: {
|
||||||
lat: this._newLonLat[1],
|
lat: this._newLonLat[1],
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default class ChangeTagAction extends OsmChangeAction {
|
||||||
return { k: key.trim(), v: value.trim() }
|
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
|
const changedTags: { k: string; v: string }[] = this._tagsFilter
|
||||||
.asChange(this._currentTags)
|
.asChange(this._currentTags)
|
||||||
.map(ChangeTagAction.checkChange)
|
.map(ChangeTagAction.checkChange)
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { OsmConnection } from "../Osm/OsmConnection"
|
||||||
import { MangroveIdentity } from "../Web/MangroveReviews"
|
import { MangroveIdentity } from "../Web/MangroveReviews"
|
||||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||||
import Locale from "../../UI/i18n/Locale"
|
import Locale from "../../UI/i18n/Locale"
|
||||||
import { Changes } from "../Osm/Changes"
|
|
||||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||||
import FeatureSource from "../FeatureSource/FeatureSource"
|
import FeatureSource from "../FeatureSource/FeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
|
@ -122,7 +122,7 @@ export class Tag extends TagsFilter {
|
||||||
return [this]
|
return [this]
|
||||||
}
|
}
|
||||||
|
|
||||||
asChange(properties: any): { k: string; v: string }[] {
|
asChange(): { k: string; v: string }[] {
|
||||||
return [{ k: this.key, v: this.value }]
|
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 { BBox } from "../Logic/BBox"
|
||||||
import { RasterLayerPolygon } from "./RasterLayers"
|
import { RasterLayerPolygon } from "./RasterLayers"
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ import Table from "../../UI/Base/Table"
|
||||||
import FilterConfigJson from "./Json/FilterConfigJson"
|
import FilterConfigJson from "./Json/FilterConfigJson"
|
||||||
import { And } from "../../Logic/Tags/And"
|
import { And } from "../../Logic/Tags/And"
|
||||||
import { Overpass } from "../../Logic/Osm/Overpass"
|
import { Overpass } from "../../Logic/Osm/Overpass"
|
||||||
import Constants from "../Constants"
|
|
||||||
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
|
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg"
|
||||||
import { ImmutableStore } from "../../Logic/UIEventSource"
|
import { ImmutableStore } from "../../Logic/UIEventSource"
|
||||||
|
|
|
@ -108,7 +108,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
|
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
|
||||||
|
|
||||||
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
|
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
|
||||||
const indexedElements = new LayoutSource(
|
this.indexedFeatures = new LayoutSource(
|
||||||
layout.layers,
|
layout.layers,
|
||||||
this.featureSwitches,
|
this.featureSwitches,
|
||||||
new StaticFeatureSource([]),
|
new StaticFeatureSource([]),
|
||||||
|
@ -116,6 +116,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.osmConnection.Backend(),
|
this.osmConnection.Backend(),
|
||||||
(id) => this.layerState.filteredLayers.get(id).isDisplayed
|
(id) => this.layerState.filteredLayers.get(id).isDisplayed
|
||||||
)
|
)
|
||||||
|
const indexedElements = this.indexedFeatures
|
||||||
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
||||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||||
Array.from(this.layerState.filteredLayers.values()),
|
Array.from(this.layerState.filteredLayers.values()),
|
||||||
|
|
|
@ -15,7 +15,6 @@ import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
export default class AllThemesGui {
|
export default class AllThemesGui {
|
||||||
setup() {
|
setup() {
|
||||||
try {
|
try {
|
||||||
new FixedUiElement("").AttachTo("centermessage")
|
|
||||||
const osmConnection = new OsmConnection()
|
const osmConnection = new OsmConnection()
|
||||||
const state = new UserRelatedState(osmConnection)
|
const state = new UserRelatedState(osmConnection)
|
||||||
const intro = new Combine([
|
const intro = new Combine([
|
||||||
|
@ -38,15 +37,14 @@ export default class AllThemesGui {
|
||||||
new FixedUiElement("v" + Constants.vNumber),
|
new FixedUiElement("v" + Constants.vNumber),
|
||||||
])
|
])
|
||||||
.SetClass("block m-5 lg:w-3/4 lg:ml-40")
|
.SetClass("block m-5 lg:w-3/4 lg:ml-40")
|
||||||
.SetStyle("pointer-events: all;")
|
.AttachTo("main")
|
||||||
.AttachTo("top-left")
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(">>>> CRITICAL", e)
|
console.error(">>>> CRITICAL", e)
|
||||||
new FixedUiElement(
|
new FixedUiElement(
|
||||||
"Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!"
|
"Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!"
|
||||||
)
|
)
|
||||||
.SetClass("alert")
|
.SetClass("alert")
|
||||||
.AttachTo("centermessage")
|
.AttachTo("main")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import { VariableUiElement } from "./VariableUIElement"
|
import { VariableUiElement } from "./VariableUIElement"
|
||||||
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
|
import { Stores } from "../../Logic/UIEventSource"
|
||||||
import Loading from "./Loading"
|
import Loading from "./Loading"
|
||||||
|
|
||||||
export default class AsyncLazy extends BaseUIElement {
|
export default class AsyncLazy extends BaseUIElement {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { UIElement } from "../UIElement"
|
|
||||||
import BaseUIElement from "../BaseUIElement"
|
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 Translations from "../i18n/Translations"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
|
|
||||||
export default class Link extends BaseUIElement {
|
export default class Link extends BaseUIElement {
|
||||||
private readonly _href: string | Store<string>
|
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 Toggle from "../Input/Toggle"
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||||
import { DefaultGuiState } from "../DefaultGuiState"
|
import { DefaultGuiState } from "../DefaultGuiState"
|
||||||
import DefaultGUI from "../DefaultGUI"
|
|
||||||
|
|
||||||
export class BackToThemeOverview extends Toggle {
|
export class BackToThemeOverview extends Toggle {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
@ -14,7 +14,6 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
import { BBox } from "../../Logic/BBox"
|
import { BBox } from "../../Logic/BBox"
|
||||||
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
|
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
|
||||||
import geojson2svg from "geojson2svg"
|
import geojson2svg from "geojson2svg"
|
||||||
import Constants from "../../Models/Constants"
|
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
|
|
||||||
export class DownloadPanel extends Toggle {
|
export class DownloadPanel extends Toggle {
|
||||||
|
|
|
@ -17,7 +17,6 @@ import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||||
import Loc from "../../Models/Loc"
|
import Loc from "../../Models/Loc"
|
||||||
import BaseLayer from "../../Models/BaseLayer"
|
import BaseLayer from "../../Models/BaseLayer"
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import CopyrightPanel from "./CopyrightPanel"
|
|
||||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||||
import PrivacyPolicy from "./PrivacyPolicy"
|
import PrivacyPolicy from "./PrivacyPolicy"
|
||||||
import Hotkeys from "../Base/Hotkeys"
|
import Hotkeys from "../Base/Hotkeys"
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
|
|
||||||
Translations.t;
|
Translations.t;
|
||||||
export let bounds: UIEventSource<BBox>
|
export let bounds: UIEventSource<BBox>
|
||||||
export let layout: LayoutConfig;
|
|
||||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
|
||||||
export let selectedElement: UIEventSource<Feature>;
|
export let selectedElement: UIEventSource<Feature>;
|
||||||
export let selectedLayer: UIEventSource<LayerConfig>;
|
export let selectedLayer: UIEventSource<LayerConfig>;
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Feature } from "geojson";
|
import type { Feature } from "geojson";
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||||
import TagRenderingAnswer from "../Popup/TagRenderingAnswer";
|
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement.js";
|
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||||
import { onDestroy } from "svelte";
|
import TagRenderingAnswer from "../Popup/TagRenderingAnswer.svelte";
|
||||||
|
|
||||||
export let selectedElement: UIEventSource<Feature>;
|
export let selectedElement: Feature;
|
||||||
export let layer: UIEventSource<LayerConfig>;
|
export let layer: LayerConfig;
|
||||||
export let tags: Store<UIEventSource<Record<string, string>>>;
|
export let tags: UIEventSource<Record<string, string>>;
|
||||||
let _tags: UIEventSource<Record<string, string>>;
|
|
||||||
onDestroy(tags.subscribe(tags => {
|
|
||||||
_tags = tags;
|
|
||||||
return false
|
|
||||||
}));
|
|
||||||
|
|
||||||
export let specialVisState: SpecialVisualizationState;
|
export let state: SpecialVisualizationState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* const title = new TagRenderingAnswer(
|
* const title = new TagRenderingAnswer(
|
||||||
|
@ -46,30 +38,27 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div on:click={() =>selectedElement.setData(undefined)}>close</div>
|
|
||||||
<div class="flex flex-col sm:flex-row flex-grow justify-between">
|
<div class="flex flex-col sm:flex-row flex-grow justify-between">
|
||||||
<!-- Title element-->
|
<!-- Title element-->
|
||||||
<ToSvelte
|
<h3>
|
||||||
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, layer.data.title, specialVisState), [layer]))}></ToSvelte>
|
<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">
|
<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">
|
<div class="w-8 h-8">
|
||||||
<ToSvelte
|
<TagRenderingAnswer config={titleIconConfig} {tags} {selectedElement}></TagRenderingAnswer>
|
||||||
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, titleIconConfig, specialVisState)))}></ToSvelte>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul>
|
<div class="flex flex-col">
|
||||||
|
{#each layer.tagRenderings as config (config.id)}
|
||||||
{#each Object.keys($_tags) as key}
|
<TagRenderingAnswer {tags} {config} {state}></TagRenderingAnswer>
|
||||||
<li><b>{key}</b>=<b>{$_tags[key]}</b></li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import type Loc from "../../Models/Loc"
|
import type Loc from "../../Models/Loc"
|
||||||
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig";
|
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig";
|
||||||
|
import Tr from "../Base/Tr.svelte";
|
||||||
|
|
||||||
export let theme: LayoutInformation
|
export let theme: LayoutInformation
|
||||||
export let isCustom: boolean = false
|
export let isCustom: boolean = false
|
||||||
|
@ -16,8 +17,8 @@
|
||||||
$: title = new Translation(
|
$: title = new Translation(
|
||||||
theme.title,
|
theme.title,
|
||||||
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
|
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
|
||||||
).toString()
|
)
|
||||||
$: description = new Translation(theme.shortDescription).toString()
|
$: description = new Translation(theme.shortDescription)
|
||||||
|
|
||||||
// TODO: Improve this function
|
// TODO: Improve this function
|
||||||
function createUrl(
|
function createUrl(
|
||||||
|
@ -83,8 +84,10 @@
|
||||||
<img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt="" />
|
<img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt="" />
|
||||||
<span slot="message" class="message">
|
<span slot="message" class="message">
|
||||||
<span>
|
<span>
|
||||||
<span>{title}</span>
|
<Tr t={title}></Tr>
|
||||||
<span>{description}</span>
|
<span class="subtle">
|
||||||
|
<Tr t={description}></Tr>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</SubtleButton>
|
</SubtleButton>
|
||||||
|
|
|
@ -5,7 +5,6 @@ import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs"
|
||||||
import MapControlButton from "./MapControlButton"
|
import MapControlButton from "./MapControlButton"
|
||||||
import Svg from "../Svg"
|
import Svg from "../Svg"
|
||||||
import Toggle from "./Input/Toggle"
|
import Toggle from "./Input/Toggle"
|
||||||
import SearchAndGo from "./BigComponents/SearchAndGo"
|
|
||||||
import BaseUIElement from "./BaseUIElement"
|
import BaseUIElement from "./BaseUIElement"
|
||||||
import LeftControls from "./BigComponents/LeftControls"
|
import LeftControls from "./BigComponents/LeftControls"
|
||||||
import RightControls from "./BigComponents/RightControls"
|
import RightControls from "./BigComponents/RightControls"
|
||||||
|
@ -26,7 +25,6 @@ import UserInformationPanel from "./BigComponents/UserInformation"
|
||||||
import { LoginToggle } from "./Popup/LoginButton"
|
import { LoginToggle } from "./Popup/LoginButton"
|
||||||
import { FixedUiElement } from "./Base/FixedUiElement"
|
import { FixedUiElement } from "./Base/FixedUiElement"
|
||||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||||
import { GeoLocationState } from "../Logic/State/GeoLocationState"
|
|
||||||
import Hotkeys from "./Base/Hotkeys"
|
import Hotkeys from "./Base/Hotkeys"
|
||||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
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 BaseUIElement from "../BaseUIElement"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
|
|
|
@ -17,7 +17,6 @@ import Minimap from "../Base/Minimap"
|
||||||
import BaseLayer from "../../Models/BaseLayer"
|
import BaseLayer from "../../Models/BaseLayer"
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
||||||
import Loc from "../../Models/Loc"
|
import Loc from "../../Models/Loc"
|
||||||
import Attribution from "../BigComponents/Attribution"
|
|
||||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||||
import ValidatedTextField from "../Input/ValidatedTextField"
|
import ValidatedTextField from "../Input/ValidatedTextField"
|
||||||
|
|
|
@ -7,7 +7,6 @@ import Translations from "../i18n/Translations"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
import { UIElement } from "../UIElement"
|
import { UIElement } from "../UIElement"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
|
||||||
|
|
||||||
export interface FlowStep<T> extends BaseUIElement {
|
export interface FlowStep<T> extends BaseUIElement {
|
||||||
readonly IsValid: Store<boolean>
|
readonly IsValid: Store<boolean>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { BBox } from "../../Logic/BBox"
|
||||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
|
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
|
||||||
import Constants from "../../Models/Constants"
|
|
||||||
import { DropDown } from "../Input/DropDown"
|
import { DropDown } from "../Input/DropDown"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
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 { InputElement } from "./InputElement"
|
||||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import Slider from "./Slider"
|
import Slider from "./Slider"
|
||||||
import { ClickableToggle } from "./Toggle"
|
import { ClickableToggle } from "./Toggle"
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
import { ReadonlyInputElement } from "./InputElement"
|
import { ReadonlyInputElement } from "./InputElement"
|
||||||
import Loc from "../../Models/Loc"
|
import Loc from "../../Models/Loc"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import Minimap, { MinimapObj } from "../Base/Minimap"
|
|
||||||
import BaseLayer from "../../Models/BaseLayer"
|
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
|
|
||||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import { BBox } from "../../Logic/BBox"
|
import { BBox } from "../../Logic/BBox"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import Toggle from "./Toggle"
|
|
||||||
import matchpoint from "../../assets/layers/matchpoint/matchpoint.json"
|
import matchpoint from "../../assets/layers/matchpoint/matchpoint.json"
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import { ElementStorage } from "../../Logic/ElementStorage"
|
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
|
||||||
import { RelationId, WayId } from "../../Models/OsmFeature"
|
import { RelationId, WayId } from "../../Models/OsmFeature"
|
||||||
import { Feature, LineString, Polygon } from "geojson"
|
import { Feature, LineString, Polygon } from "geojson"
|
||||||
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
||||||
|
@ -313,10 +306,6 @@ export default class LocationInput
|
||||||
[this.map.leafletMap]
|
[this.map.leafletMap]
|
||||||
)
|
)
|
||||||
|
|
||||||
const animatedHand = Svg.hand_ui()
|
|
||||||
.SetStyle("width: 2rem; height: unset;")
|
|
||||||
.SetClass("hand-drag-animation block pointer-events-none")
|
|
||||||
|
|
||||||
return new Combine([
|
return new Combine([
|
||||||
new Combine([
|
new Combine([
|
||||||
Svg.move_arrows_ui()
|
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"
|
"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"),
|
this.map.SetClass("z-0 relative block w-full h-full bg-gray-100"),
|
||||||
]).ConstructElement()
|
]).ConstructElement()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -341,11 +326,4 @@ export default class LocationInput
|
||||||
.ConstructElement()
|
.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"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
|
||||||
export default class SimpleDatePicker extends InputElement<string> {
|
export default class SimpleDatePicker extends InputElement<string> {
|
||||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
|
||||||
private readonly value: UIEventSource<string>
|
private readonly value: UIEventSource<string>
|
||||||
private readonly _element: HTMLElement
|
private readonly _element: HTMLElement
|
||||||
|
|
||||||
|
|
|
@ -50,10 +50,6 @@ export class TextField extends InputElement<string> {
|
||||||
return this.value
|
return this.value
|
||||||
}
|
}
|
||||||
|
|
||||||
GetRawValue(): UIEventSource<string> {
|
|
||||||
return this._rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
IsValid(t: string): boolean {
|
IsValid(t: string): boolean {
|
||||||
if (t === undefined || t === null) {
|
if (t === undefined || t === null) {
|
||||||
return false
|
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
|
private _currentRasterLayer: string
|
||||||
|
|
||||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapProperties, "bounds">>) {
|
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>) {
|
||||||
this._maplibreMap = maplibreMap
|
this._maplibreMap = maplibreMap
|
||||||
|
|
||||||
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })
|
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
export let map: Writable<MaplibreMap>
|
export let map: Writable<MaplibreMap>
|
||||||
|
|
||||||
|
export let attribution = true
|
||||||
let center = {};
|
let center = {};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -28,6 +29,9 @@
|
||||||
<main>
|
<main>
|
||||||
<Map bind:center={center}
|
<Map bind:center={center}
|
||||||
bind:map={$map}
|
bind:map={$map}
|
||||||
|
{attribution}
|
||||||
|
css="./maplibre-gl.css"
|
||||||
|
|
||||||
id="map" location={{lng: 0, lat: 0, zoom: 0}} maxzoom=24 style={styleUrl} />
|
id="map" location={{lng: 0, lat: 0, zoom: 0}} maxzoom=24 style={styleUrl} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
@ -106,7 +106,7 @@ class PointRenderingLayer {
|
||||||
store = new ImmutableStore(<OsmTags>feature.properties)
|
store = new ImmutableStore(<OsmTags>feature.properties)
|
||||||
}
|
}
|
||||||
const { html, iconAnchor } = this._config.RenderIcon(store, true)
|
const { html, iconAnchor } = this._config.RenderIcon(store, true)
|
||||||
html.SetClass("marker")
|
html.SetClass("marker cursor-pointer")
|
||||||
const el = html.ConstructElement()
|
const el = html.ConstructElement()
|
||||||
|
|
||||||
if (this._onClick) {
|
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(linelayer, "visibility", visible ? "visible" : "none")
|
||||||
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none")
|
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none")
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { OsmTags } from "../../Models/OsmFeature"
|
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import Histogram from "../BigComponents/Histogram"
|
import Histogram from "../BigComponents/Histogram"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
export class HistogramViz implements SpecialVisualization {
|
export class HistogramViz implements SpecialVisualization {
|
||||||
funcName = "histogram"
|
funcName = "histogram"
|
||||||
docs = "Create a histogram for a list of given values, read from the properties."
|
docs = "Create a histogram for a list of given values, read from the properties."
|
||||||
example =
|
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 = [
|
args = [
|
||||||
{
|
{
|
||||||
name: "key",
|
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(
|
constr(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
|
|
|
@ -5,10 +5,7 @@ import { Feature } from "geojson"
|
||||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
|
||||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||||
import { stat } from "fs"
|
|
||||||
|
|
||||||
export class MinimapViz implements SpecialVisualization {
|
export class MinimapViz implements SpecialVisualization {
|
||||||
funcName = "minimap"
|
funcName = "minimap"
|
||||||
|
|
|
@ -54,11 +54,6 @@ export default class MoveWizard extends Toggle {
|
||||||
options: MoveConfig
|
options: MoveConfig
|
||||||
) {
|
) {
|
||||||
const t = Translations.t.move
|
const t = Translations.t.move
|
||||||
const loginButton = new Toggle(
|
|
||||||
t.loginToMove.SetClass("btn").onClick(() => state.osmConnection.AttemptLogin()),
|
|
||||||
undefined,
|
|
||||||
state.featureSwitchUserbadge
|
|
||||||
)
|
|
||||||
|
|
||||||
const reasons: MoveReason[] = []
|
const reasons: MoveReason[] = []
|
||||||
if (options.enableRelocation) {
|
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 TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import Img from "../Base/Img"
|
import Img from "../Base/Img"
|
||||||
import { SpecialVisualisationState } from "../SpecialVisualization"
|
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Displays the correct value for a known tagrendering
|
* Displays the correct value for a known tagrendering
|
||||||
|
@ -15,7 +15,7 @@ export default class TagRenderingAnswer extends VariableUiElement {
|
||||||
constructor(
|
constructor(
|
||||||
tagsSource: UIEventSource<any>,
|
tagsSource: UIEventSource<any>,
|
||||||
configuration: TagRenderingConfig,
|
configuration: TagRenderingConfig,
|
||||||
state: SpecialVisualisationState,
|
state: SpecialVisualizationState,
|
||||||
contentClasses: string = "",
|
contentClasses: string = "",
|
||||||
contentStyle: string = "",
|
contentStyle: string = "",
|
||||||
options?: {
|
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 { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import { InputElement, ReadonlyInputElement } from "../Input/InputElement"
|
import { InputElement, ReadonlyInputElement } from "../Input/InputElement"
|
||||||
import ValidatedTextField from "../Input/ValidatedTextField"
|
|
||||||
import { FixedInputElement } from "../Input/FixedInputElement"
|
import { FixedInputElement } from "../Input/FixedInputElement"
|
||||||
import { RadioButton } from "../Input/RadioButton"
|
import { RadioButton } from "../Input/RadioButton"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
|
|
@ -6,7 +6,6 @@ import BaseUIElement from "../BaseUIElement"
|
||||||
import Img from "../Base/Img"
|
import Img from "../Base/Img"
|
||||||
import { Review } from "mangrove-reviews-typescript"
|
import { Review } from "mangrove-reviews-typescript"
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"
|
|
||||||
|
|
||||||
export default class SingleReview extends Combine {
|
export default class SingleReview extends Combine {
|
||||||
constructor(review: Review & { madeByLoggedInUser: Store<boolean> }) {
|
constructor(review: Review & { madeByLoggedInUser: Store<boolean> }) {
|
||||||
|
|
|
@ -2,17 +2,13 @@ import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||||
import BaseUIElement from "./BaseUIElement"
|
import BaseUIElement from "./BaseUIElement"
|
||||||
import { DefaultGuiState } from "./DefaultGuiState"
|
import { DefaultGuiState } from "./DefaultGuiState"
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||||
import FeatureSource, {
|
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
||||||
IndexedFeatureSource,
|
|
||||||
WritableFeatureSource,
|
|
||||||
} from "../Logic/FeatureSource/FeatureSource"
|
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
import { Changes } from "../Logic/Osm/Changes"
|
import { Changes } from "../Logic/Osm/Changes"
|
||||||
import { MapProperties } from "../Models/MapProperties"
|
import { MapProperties } from "../Models/MapProperties"
|
||||||
import LayerState from "../Logic/State/LayerState"
|
import LayerState from "../Logic/State/LayerState"
|
||||||
import { Feature } from "geojson"
|
import { Feature, Geometry } from "geojson"
|
||||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
|
||||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
||||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||||
|
|
||||||
|
@ -58,6 +54,8 @@ export interface SpecialVisualization {
|
||||||
funcName: string
|
funcName: string
|
||||||
docs: string | BaseUIElement
|
docs: string | BaseUIElement
|
||||||
example?: string
|
example?: string
|
||||||
|
|
||||||
|
structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[]
|
||||||
args: { name: string; defaultValue?: string; doc: string; required?: false | boolean }[]
|
args: { name: string; defaultValue?: string; doc: string; required?: false | boolean }[]
|
||||||
getLayerDependencies?: (argument: string[]) => string[]
|
getLayerDependencies?: (argument: string[]) => string[]
|
||||||
|
|
||||||
|
@ -68,3 +66,11 @@ export interface SpecialVisualization {
|
||||||
feature: Feature
|
feature: Feature
|
||||||
): BaseUIElement
|
): 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 BaseUIElement from "./BaseUIElement"
|
||||||
import Title from "./Base/Title"
|
import Title from "./Base/Title"
|
||||||
import Table from "./Base/Table"
|
import Table from "./Base/Table"
|
||||||
import { SpecialVisualization } from "./SpecialVisualization"
|
import {
|
||||||
|
RenderingSpecification,
|
||||||
|
SpecialVisualization,
|
||||||
|
SpecialVisualizationState,
|
||||||
|
} from "./SpecialVisualization"
|
||||||
import { HistogramViz } from "./Popup/HistogramViz"
|
import { HistogramViz } from "./Popup/HistogramViz"
|
||||||
import { StealViz } from "./Popup/StealViz"
|
import { StealViz } from "./Popup/StealViz"
|
||||||
import { MinimapViz } from "./Popup/MinimapViz"
|
import { MinimapViz } from "./Popup/MinimapViz"
|
||||||
|
@ -51,10 +55,97 @@ import FeatureReviews from "../Logic/Web/MangroveReviews"
|
||||||
import Maproulette from "../Logic/Maproulette"
|
import Maproulette from "../Logic/Maproulette"
|
||||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||||
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
export default class SpecialVisualizations {
|
export default class SpecialVisualizations {
|
||||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
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 {
|
public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
|
||||||
if (typeof viz === "string") {
|
if (typeof viz === "string") {
|
||||||
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
|
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
|
||||||
|
@ -649,7 +740,7 @@ export default class SpecialVisualizations {
|
||||||
defaultValue: "mr_taskId",
|
defaultValue: "mr_taskId",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
constr: (state, tagsSource, args, guistate) => {
|
constr: (state, tagsSource, args) => {
|
||||||
let [message, image, message_closed, status, maproulette_id_key] = args
|
let [message, image, message_closed, status, maproulette_id_key] = args
|
||||||
if (image === "") {
|
if (image === "") {
|
||||||
image = "confirm"
|
image = "confirm"
|
||||||
|
@ -720,7 +811,7 @@ export default class SpecialVisualizations {
|
||||||
funcName: "statistics",
|
funcName: "statistics",
|
||||||
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
|
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
|
||||||
args: [],
|
args: [],
|
||||||
constr: (state, tagsSource, args, guiState) => {
|
constr: (state) => {
|
||||||
return new Combine(
|
return new Combine(
|
||||||
state.layout.layers
|
state.layout.layers
|
||||||
.filter((l) => l.name !== null)
|
.filter((l) => l.name !== null)
|
||||||
|
@ -852,4 +943,23 @@ export default class SpecialVisualizations {
|
||||||
|
|
||||||
return 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 { VariableUiElement } from "./Base/VariableUIElement"
|
||||||
import Combine from "./Base/Combine"
|
import Combine from "./Base/Combine"
|
||||||
import BaseUIElement from "./BaseUIElement"
|
import BaseUIElement from "./BaseUIElement"
|
||||||
import { DefaultGuiState } from "./DefaultGuiState"
|
|
||||||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
|
|
||||||
import LinkToWeblate from "./Base/LinkToWeblate"
|
import LinkToWeblate from "./Base/LinkToWeblate"
|
||||||
import { SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
|
import { SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
|
||||||
|
import SpecialVisualizations from "./SpecialVisualizations"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
export class SubstitutedTranslation extends VariableUiElement {
|
export class SubstitutedTranslation extends VariableUiElement {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
@ -21,10 +21,10 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
string,
|
string,
|
||||||
| BaseUIElement
|
| BaseUIElement
|
||||||
| ((
|
| ((
|
||||||
state: FeaturePipelineState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
guistate: DefaultGuiState
|
feature: Feature
|
||||||
) => BaseUIElement)
|
) => BaseUIElement)
|
||||||
> = undefined
|
> = undefined
|
||||||
) {
|
) {
|
||||||
|
@ -55,19 +55,23 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`)
|
txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
const allElements = SubstitutedTranslation.ExtractSpecialComponents(
|
const allElements = SpecialVisualizations.constructSpecification(
|
||||||
txt,
|
txt,
|
||||||
extraMappings
|
extraMappings
|
||||||
).map((proto) => {
|
).map((proto) => {
|
||||||
if (proto.fixed !== undefined) {
|
if (typeof proto === "string") {
|
||||||
if (tagsSource === undefined) {
|
if (tagsSource === undefined) {
|
||||||
return Utils.SubstituteKeys(proto.fixed, undefined)
|
return Utils.SubstituteKeys(proto, undefined)
|
||||||
}
|
}
|
||||||
return new VariableUiElement(
|
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) {
|
if (viz === undefined) {
|
||||||
console.error(
|
console.error(
|
||||||
"SPECIALRENDERING UNDEFINED for",
|
"SPECIALRENDERING UNDEFINED for",
|
||||||
|
@ -77,9 +81,12 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const feature = state.indexedFeatures.featuresById.data.get(
|
||||||
|
tagsSource.data.id
|
||||||
|
)
|
||||||
return viz.func
|
return viz.func
|
||||||
.constr(state, tagsSource, proto.special.args)
|
.constr(state, tagsSource, proto.args, feature)
|
||||||
?.SetStyle(proto.special.style)
|
?.SetStyle(proto.style)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e)
|
console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e)
|
||||||
return new FixedUiElement(
|
return new FixedUiElement(
|
||||||
|
@ -97,98 +104,4 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
|
|
||||||
this.SetClass("w-full")
|
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 { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
||||||
import Translations from "./i18n/Translations";
|
import Translations from "./i18n/Translations";
|
||||||
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid";
|
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||||
|
import Tr from "./Base/Tr.svelte";
|
||||||
|
|
||||||
export let layout: LayoutConfig;
|
export let layout: LayoutConfig;
|
||||||
const state = new ThemeViewState(layout);
|
const state = new ThemeViewState(layout);
|
||||||
|
@ -48,7 +49,7 @@
|
||||||
<div class="flex mr-2 items-center">
|
<div class="flex mr-2 items-center">
|
||||||
<img class="w-8 h-8 block mr-2" src={layout.icon}>
|
<img class="w-8 h-8 block mr-2" src={layout.icon}>
|
||||||
<b>
|
<b>
|
||||||
{layout.title}
|
<Tr t={layout.title}></Tr>
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
|
@ -58,9 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 mb-4 ml-4">
|
<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>
|
||||||
|
|
||||||
<div class="absolute bottom-0 right-0 mb-4 mr-4">
|
<div class="absolute bottom-0 right-0 mb-4 mr-4">
|
||||||
|
@ -86,17 +85,6 @@
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</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}>
|
<If condition={state.guistate.welcomeMessageIsOpened}>
|
||||||
<!-- Theme page -->
|
<!-- Theme page -->
|
||||||
|
@ -105,31 +93,47 @@
|
||||||
<div on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>Close</div>
|
<div on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>Close</div>
|
||||||
<TabGroup>
|
<TabGroup>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>About</Tab>
|
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 2</Tab>
|
<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>
|
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 3</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel class="flex flex-col">
|
<TabPanel class="flex flex-col">
|
||||||
<ToSvelte construct={() => layout.description}></ToSvelte>
|
<Tr t={layout.description}></Tr>
|
||||||
{Translations.t.general.welcomeExplanation.general}
|
<Tr t={Translations.t.general.welcomeExplanation.general}/>
|
||||||
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
||||||
<If condition={state.featureSwitches.featureSwitchAddNew}>
|
<If condition={state.featureSwitches.featureSwitchAddNew}>
|
||||||
{Translations.t.general.welcomeExplanation.addNew}
|
<Tr t={Translations.t.general.welcomeExplanation.addNew}/>
|
||||||
</If>
|
</If>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!--toTheMap,
|
<!--toTheMap,
|
||||||
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
|
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">
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</TabPanel>
|
</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>
|
<TabPanel>Content 3</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
|
@ -163,15 +167,14 @@
|
||||||
</div>
|
</div>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={selectedElement}>
|
{#if $selectedElement !== undefined && $selectedLayer !== undefined}
|
||||||
<div class="absolute top-0 right-0 normal-background">
|
<div class="absolute top-0 right-0 normal-background">
|
||||||
|
|
||||||
<SelectedElementView layer={selectedLayer} {selectedElement}
|
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
||||||
tags={selectedElementTags}></SelectedElementView>
|
tags={$selectedElementTags} state={state}></SelectedElementView>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</If>
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* WARNING: This is just for demonstration.
|
/* WARNING: This is just for demonstration.
|
||||||
Using :global() in this way can be risky. */
|
Using :global() in this way can be risky. */
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata"
|
import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata"
|
||||||
import { Translation, TypedTranslation } from "../i18n/Translation"
|
import { Translation, TypedTranslation } from "../i18n/Translation"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||||
|
|
|
@ -51,10 +51,6 @@ export class Translation extends BaseUIElement {
|
||||||
return this.textFor(Translation.forcedLanguage ?? Locale.language.data)
|
return this.textFor(Translation.forcedLanguage ?? Locale.language.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString() {
|
|
||||||
return this.txt
|
|
||||||
}
|
|
||||||
|
|
||||||
static ExtractAllTranslationsFrom(
|
static ExtractAllTranslationsFrom(
|
||||||
object: any,
|
object: any,
|
||||||
context = ""
|
context = ""
|
||||||
|
@ -91,6 +87,10 @@ export class Translation extends BaseUIElement {
|
||||||
return new Translation(translations)
|
return new Translation(translations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public toString() {
|
||||||
|
return this.txt
|
||||||
|
}
|
||||||
|
|
||||||
Destroy() {
|
Destroy() {
|
||||||
super.Destroy()
|
super.Destroy()
|
||||||
this.isDestroyed = true
|
this.isDestroyed = true
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { Utils } from "./Utils"
|
import { Utils } from "./Utils"
|
||||||
import AllThemesGui from "./UI/AllThemesGui"
|
import AllThemesGui from "./UI/AllThemesGui"
|
||||||
import { QueryParameters } from "./Logic/Web/QueryParameters"
|
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 layout = QueryParameters.GetQueryParameter("layout", undefined).data ?? ""
|
||||||
const customLayout = QueryParameters.GetQueryParameter("userlayout", undefined).data ?? ""
|
const customLayout = QueryParameters.GetQueryParameter("userlayout", undefined).data ?? ""
|
||||||
|
@ -32,23 +29,4 @@ if (layout !== "") {
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils.DisableLongPresses()
|
Utils.DisableLongPresses()
|
||||||
document.getElementById("decoration-desktop").remove()
|
new AllThemesGui().setup()
|
||||||
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
|
"hideInAnswer": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "max_bolts",
|
"id": "max_bolts",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"description": {
|
"description": {
|
||||||
"en": "A layer acting as library for icon-tagrenderings, especially to show as badge next to a POI"
|
"en": "A layer acting as library for icon-tagrenderings, especially to show as badge next to a POI"
|
||||||
},
|
},
|
||||||
"source":"special:library",
|
"source": "special:library",
|
||||||
"title": null,
|
"title": null,
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "import_candidate",
|
"id": "import_candidate",
|
||||||
"description": "Layer used as template in the importHelper",
|
"description": "Layer used as template in the importHelper",
|
||||||
"source":"special",
|
"source": "special",
|
||||||
"mapRendering": [
|
"mapRendering": [
|
||||||
{
|
{
|
||||||
"location": [
|
"location": [
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "matchpoint",
|
"id": "matchpoint",
|
||||||
"description": "The default rendering for a locationInput which snaps onto another object",
|
"description": "The default rendering for a locationInput which snaps onto another object",
|
||||||
"source":"special",
|
"source": "special",
|
||||||
"mapRendering": [
|
"mapRendering": [
|
||||||
{
|
{
|
||||||
"location": [
|
"location": [
|
||||||
|
|
|
@ -1183,6 +1183,10 @@ video {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-48 {
|
||||||
|
width: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
.min-w-min {
|
.min-w-min {
|
||||||
min-width: -webkit-min-content;
|
min-width: -webkit-min-content;
|
||||||
min-width: min-content;
|
min-width: min-content;
|
||||||
|
@ -1509,6 +1513,11 @@ video {
|
||||||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
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 {
|
.border-opacity-50 {
|
||||||
--tw-border-opacity: 0.5;
|
--tw-border-opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
@ -1580,10 +1589,6 @@ video {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-8 {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-3 {
|
.p-3 {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
@ -1596,6 +1601,10 @@ video {
|
||||||
padding: 0.125rem;
|
padding: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-8 {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.py-4 {
|
.py-4 {
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
|
@ -1921,6 +1930,10 @@ video {
|
||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ease-in-out {
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.z-above-map {
|
.z-above-map {
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
}
|
}
|
||||||
|
@ -1971,8 +1984,6 @@ video {
|
||||||
--shadow-color: #00000066;
|
--shadow-color: #00000066;
|
||||||
--return-to-the-map-height: 2em;
|
--return-to-the-map-height: 2em;
|
||||||
--image-carousel-height: 350px;
|
--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. */
|
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
|
||||||
--variable-title-height: 0px;
|
--variable-title-height: 0px;
|
||||||
}
|
}
|
||||||
|
@ -1989,31 +2000,6 @@ body {
|
||||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
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,
|
svg,
|
||||||
img {
|
img {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
@ -2255,28 +2241,6 @@ li::marker {
|
||||||
fill: var(--catch-detail-color) !important;
|
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 {
|
.single-layer-selection-toggle {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 2em;
|
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 ******************/
|
/***************** Info box (box containing features and questions ******************/
|
||||||
|
|
||||||
input {
|
input {
|
||||||
color: var(--foreground-color);
|
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 {
|
.floating-element-width {
|
||||||
max-width: calc(100vw - 5em);
|
max-width: calc(100vw - 5em);
|
||||||
width: 40em;
|
width: 40em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-div-icon svg {
|
|
||||||
width: calc(100%);
|
|
||||||
height: calc(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/****** ShareScreen *****/
|
/****** ShareScreen *****/
|
||||||
|
|
||||||
.literal-code {
|
.literal-code {
|
||||||
|
|
103
index.css
103
index.css
|
@ -87,9 +87,6 @@
|
||||||
--return-to-the-map-height: 2em;
|
--return-to-the-map-height: 2em;
|
||||||
--image-carousel-height: 350px;
|
--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. */
|
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
|
||||||
--variable-title-height: 0px;
|
--variable-title-height: 0px;
|
||||||
}
|
}
|
||||||
|
@ -106,29 +103,6 @@ body {
|
||||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
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,
|
svg,
|
||||||
img {
|
img {
|
||||||
|
@ -365,27 +339,6 @@ li::marker {
|
||||||
fill: var(--catch-detail-color) !important;
|
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 {
|
.single-layer-selection-toggle {
|
||||||
position: relative;
|
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 ******************/
|
/***************** Info box (box containing features and questions ******************/
|
||||||
|
@ -549,25 +460,11 @@ input {
|
||||||
color: var(--foreground-color);
|
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 {
|
.floating-element-width {
|
||||||
max-width: calc(100vw - 5em);
|
max-width: calc(100vw - 5em);
|
||||||
width: 40em;
|
width: 40em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-div-icon svg {
|
|
||||||
width: calc(100%);
|
|
||||||
height: calc(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/****** ShareScreen *****/
|
/****** ShareScreen *****/
|
||||||
|
|
||||||
|
|
16
index.html
16
index.html
|
@ -51,22 +51,8 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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 type="module" src="./all_themes_index.ts"></script>
|
||||||
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js"></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 AllThemesGui from "./UI/AllThemesGui"
|
||||||
import DetermineLayout from "./Logic/DetermineLayout"
|
import DetermineLayout from "./Logic/DetermineLayout"
|
||||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
|
||||||
import DefaultGUI from "./UI/DefaultGUI"
|
|
||||||
import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"
|
import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"
|
||||||
import { DefaultGuiState } from "./UI/DefaultGuiState"
|
import { DefaultGuiState } from "./UI/DefaultGuiState"
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"about": "Easily edit and add OpenStreetMap for a certain theme",
|
"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": {
|
"add": {
|
||||||
"addNew": "Add {category}",
|
"addNew": "Add {category}",
|
||||||
"addNewMapLabel": "Click here to add a new item",
|
"addNewMapLabel": "Click here to add a new item",
|
||||||
|
@ -203,6 +203,10 @@
|
||||||
"loginToStart": "Log in to answer this question",
|
"loginToStart": "Log in to answer this question",
|
||||||
"loginWithOpenStreetMap": "Login with OpenStreetMap",
|
"loginWithOpenStreetMap": "Login with OpenStreetMap",
|
||||||
"logout": "Log out",
|
"logout": "Log out",
|
||||||
|
"menu": {
|
||||||
|
"aboutMapComplete": "About MapComplete",
|
||||||
|
"filter": "Filter data"
|
||||||
|
},
|
||||||
"morescreen": {
|
"morescreen": {
|
||||||
"createYourOwnTheme": "Create your own MapComplete theme from scratch",
|
"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.",
|
"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": {
|
"gps_track": {
|
||||||
"name": "La teva traça recorreguda"
|
"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": {
|
"hackerspace": {
|
||||||
"presets": {
|
"presets": {
|
||||||
"1": {
|
"1": {
|
||||||
|
|
|
@ -4750,9 +4750,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"grass_in_parks": {
|
|
||||||
"description": "Sucht nach allen zugänglichen Grasflächen in öffentlichen Parks - dies sind 'Grünzonen'"
|
|
||||||
},
|
|
||||||
"hackerspace": {
|
"hackerspace": {
|
||||||
"description": "Hackerspace",
|
"description": "Hackerspace",
|
||||||
"name": "Hackerspaces",
|
"name": "Hackerspaces",
|
||||||
|
|
|
@ -4750,9 +4750,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"grass_in_parks": {
|
|
||||||
"description": "Searches for all accessible grass patches within public parks - these are 'groenzones'"
|
|
||||||
},
|
|
||||||
"hackerspace": {
|
"hackerspace": {
|
||||||
"description": "Hackerspace",
|
"description": "Hackerspace",
|
||||||
"name": "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": {
|
"hackerspace": {
|
||||||
"description": "Hackerspace",
|
"description": "Hackerspace",
|
||||||
"name": "Hackerspace",
|
"name": "Hackerspace",
|
||||||
|
|
|
@ -661,7 +661,7 @@
|
||||||
"grb": {
|
"grb": {
|
||||||
"description": "Aquest tema és un intent d'automatitzar la importació GRB.",
|
"description": "Aquest tema és un intent d'automatitzar la importació GRB.",
|
||||||
"layers": {
|
"layers": {
|
||||||
"1": {
|
"0": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"building type": {
|
"building type": {
|
||||||
"question": "Quin tipus d'edifici és aquest?"
|
"question": "Quin tipus d'edifici és aquest?"
|
||||||
|
|
|
@ -614,14 +614,14 @@
|
||||||
},
|
},
|
||||||
"grb": {
|
"grb": {
|
||||||
"layers": {
|
"layers": {
|
||||||
"1": {
|
"0": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"building type": {
|
"building type": {
|
||||||
"question": "Jaký druh budovy je toto?"
|
"question": "Jaký druh budovy je toto?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"6": {
|
"5": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"Import-button": {
|
"Import-button": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
|
|
|
@ -552,14 +552,14 @@
|
||||||
"grb": {
|
"grb": {
|
||||||
"description": "Dette tema er et forsøg på at hjælpe med at automatisere GRB-importen.",
|
"description": "Dette tema er et forsøg på at hjælpe med at automatisere GRB-importen.",
|
||||||
"layers": {
|
"layers": {
|
||||||
"1": {
|
"0": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"building type": {
|
"building type": {
|
||||||
"question": "Hvad er det for en bygning?"
|
"question": "Hvad er det for en bygning?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"6": {
|
"5": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"Import-button": {
|
"Import-button": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
|
|
|
@ -689,14 +689,14 @@
|
||||||
"grb": {
|
"grb": {
|
||||||
"description": "Dieses Thema ist ein Versuch, die Automatisierung des GRB-Imports zu unterstützen.",
|
"description": "Dieses Thema ist ein Versuch, die Automatisierung des GRB-Imports zu unterstützen.",
|
||||||
"layers": {
|
"layers": {
|
||||||
"1": {
|
"0": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"building type": {
|
"building type": {
|
||||||
"question": "Was ist das für ein Gebäude?"
|
"question": "Was ist das für ein Gebäude?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"6": {
|
"5": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"Import-button": {
|
"Import-button": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
|
|
|
@ -689,14 +689,14 @@
|
||||||
"grb": {
|
"grb": {
|
||||||
"description": "This theme is an attempt to help automating the GRB import.",
|
"description": "This theme is an attempt to help automating the GRB import.",
|
||||||
"layers": {
|
"layers": {
|
||||||
"1": {
|
"0": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"building type": {
|
"building type": {
|
||||||
"question": "What kind of building is this?"
|
"question": "What kind of building is this?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"6": {
|
"5": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"Import-button": {
|
"Import-button": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
|
|
|
@ -689,14 +689,14 @@
|
||||||
"grb": {
|
"grb": {
|
||||||
"description": "Este tema es un intento de automatizar la importación GRB.",
|
"description": "Este tema es un intento de automatizar la importación GRB.",
|
||||||
"layers": {
|
"layers": {
|
||||||
"1": {
|
"0": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"building type": {
|
"building type": {
|
||||||
"question": "¿Qué tipo de edificio es este?"
|
"question": "¿Qué tipo de edificio es este?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"6": {
|
"5": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"Import-button": {
|
"Import-button": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue