More refactoring
This commit is contained in:
parent
5d0fe31c41
commit
41e6a2c760
147 changed files with 1540 additions and 1797 deletions
|
@ -2,7 +2,6 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
|||
import { Utils } from "../Utils"
|
||||
import known_themes from "../assets/generated/known_layers.json"
|
||||
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import { ALL } from "dns"
|
||||
import { AllKnownLayouts } from "./AllKnownLayouts"
|
||||
export class AllSharedLayers {
|
||||
public static sharedLayers: Map<string, LayerConfig> = AllSharedLayers.getSharedLayers()
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"
|
||||
import Combine from "../../UI/Base/Combine"
|
||||
import { Utils } from "../../Utils"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Feature } from "geojson"
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
|
||||
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer.svelte"
|
||||
|
||||
export default class TitleHandler {
|
||||
constructor(
|
||||
|
@ -32,7 +33,7 @@ export default class TitleHandler {
|
|||
const tagsSource =
|
||||
allElements.getStore(tags.id) ??
|
||||
new UIEventSource<Record<string, string>>(tags)
|
||||
const title = new TagRenderingAnswer(tagsSource, layer.title, {})
|
||||
const title = new SvelteUIElement(TagRenderingAnswer, { tags: tagsSource })
|
||||
return (
|
||||
new Combine([defaultTitle, " | ", title]).ConstructElement()
|
||||
?.textContent ?? defaultTitle
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import FeatureSource, { Tiled } from "../FeatureSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { BBox } from "../../BBox"
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import Loc from "../../../Models/Loc"
|
||||
import FeatureSource from "../FeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import TileLocalStorage from "./TileLocalStorage"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { BBox } from "../../BBox"
|
||||
import { FeatureSourceForLayer } from "../FeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
export default class SimpleFeatureSource implements FeatureSourceForLayer {
|
||||
|
|
52
Logic/FeatureSource/Sources/SnappingFeatureSource.ts
Normal file
52
Logic/FeatureSource/Sources/SnappingFeatureSource.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import FeatureSource from "../FeatureSource"
|
||||
import { Store } from "../../UIEventSource"
|
||||
import { Feature, Point } from "geojson"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
|
||||
export interface SnappingOptions {
|
||||
/**
|
||||
* If the distance is bigger then this amount, don't snap.
|
||||
* In meter
|
||||
*/
|
||||
maxDistance?: number
|
||||
}
|
||||
|
||||
export default class SnappingFeatureSource implements FeatureSource {
|
||||
public readonly features: Store<Feature<Point>[]>
|
||||
|
||||
constructor(
|
||||
snapTo: FeatureSource,
|
||||
location: Store<{ lon: number; lat: number }>,
|
||||
options?: SnappingOptions
|
||||
) {
|
||||
const simplifiedFeatures = snapTo.features.mapD((features) =>
|
||||
features
|
||||
.filter((feature) => feature.geometry.type !== "Point")
|
||||
.map((f) => GeoOperations.forceLineString(<any>f))
|
||||
)
|
||||
|
||||
location.mapD(
|
||||
({ lon, lat }) => {
|
||||
const features = snapTo.features.data
|
||||
const loc: [number, number] = [lon, lat]
|
||||
const maxDistance = (options?.maxDistance ?? 1000) * 1000
|
||||
let bestSnap: Feature<Point, { "snapped-to": string; dist: number }> = undefined
|
||||
for (const feature of features) {
|
||||
const snapped = GeoOperations.nearestPoint(<any>feature, loc)
|
||||
if (snapped.properties.dist > maxDistance) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
bestSnap === undefined ||
|
||||
bestSnap.properties.dist > snapped.properties.dist
|
||||
) {
|
||||
snapped.properties["snapped-to"] = feature.properties.id
|
||||
bestSnap = <any>snapped
|
||||
}
|
||||
}
|
||||
return bestSnap
|
||||
},
|
||||
[snapTo.features]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource"
|
|||
import StaticFeatureSource from "./StaticFeatureSource"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import { BBox } from "../../BBox"
|
||||
import exp from "constants"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
GeoJSON,
|
||||
Geometry,
|
||||
LineString,
|
||||
MultiLineString,
|
||||
MultiPolygon,
|
||||
Point,
|
||||
Polygon,
|
||||
|
@ -272,17 +273,42 @@ export class GeoOperations {
|
|||
* @param point Point defined as [lon, lat]
|
||||
*/
|
||||
public static nearestPoint(
|
||||
way: Feature<LineString | Polygon>,
|
||||
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
||||
point: [number, number]
|
||||
): Feature<Point> {
|
||||
): Feature<
|
||||
Point,
|
||||
{
|
||||
index: number
|
||||
dist: number
|
||||
location: number
|
||||
}
|
||||
> {
|
||||
return <any>(
|
||||
turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" })
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to reuse the coordinates of the way as LineString.
|
||||
* Mostly used as helper for 'nearestPoint'
|
||||
* @param way
|
||||
*/
|
||||
public static forceLineString(
|
||||
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
|
||||
): Feature<LineString | MultiLineString> {
|
||||
if (way.geometry.type === "Polygon") {
|
||||
way = { ...way }
|
||||
way.geometry = { ...way.geometry }
|
||||
way.geometry.type = "LineString"
|
||||
way.geometry.coordinates = (<Polygon>way.geometry).coordinates[0]
|
||||
} else if (way.geometry.type === "MultiPolygon") {
|
||||
way = { ...way }
|
||||
way.geometry = { ...way.geometry }
|
||||
way.geometry.type = "MultiLineString"
|
||||
way.geometry.coordinates = (<MultiPolygon>way.geometry).coordinates[0]
|
||||
}
|
||||
|
||||
return turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" })
|
||||
return <any>way
|
||||
}
|
||||
|
||||
public static toCSV(features: any[]): string {
|
||||
|
|
|
@ -5,6 +5,7 @@ import GenericImageProvider from "./GenericImageProvider"
|
|||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
import { WikidataImageProvider } from "./WikidataImageProvider"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
|
||||
/**
|
||||
* A generic 'from the interwebz' image picker, without attribution
|
||||
|
@ -44,7 +45,7 @@ export default class AllImageProviders {
|
|||
UIEventSource<ProvidedImage[]>
|
||||
>()
|
||||
|
||||
public static LoadImagesFor(tags: Store<any>, tagKey?: string[]): Store<ProvidedImage[]> {
|
||||
public static LoadImagesFor(tags: Store<OsmTags>, tagKey?: string[]): Store<ProvidedImage[]> {
|
||||
if (tags.data.id === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export default class ChangeLocationAction extends OsmChangeAction {
|
|||
this._meta = meta
|
||||
}
|
||||
|
||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
protected async CreateChangeDescriptions(): Promise<ChangeDescription[]> {
|
||||
const d: ChangeDescription = {
|
||||
changes: {
|
||||
lat: this._newLonLat[1],
|
||||
|
|
|
@ -71,7 +71,7 @@ export default class ChangeTagAction extends OsmChangeAction {
|
|||
return { k: key.trim(), v: value.trim() }
|
||||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
async CreateChangeDescriptions(): Promise<ChangeDescription[]> {
|
||||
const changedTags: { k: string; v: string }[] = this._tagsFilter
|
||||
.asChange(this._currentTags)
|
||||
.map(ChangeTagAction.checkChange)
|
||||
|
|
|
@ -3,7 +3,6 @@ import { OsmConnection } from "../Osm/OsmConnection"
|
|||
import { MangroveIdentity } from "../Web/MangroveReviews"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import { Changes } from "../Osm/Changes"
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import FeatureSource from "../FeatureSource/FeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
|
|
|
@ -122,7 +122,7 @@ export class Tag extends TagsFilter {
|
|||
return [this]
|
||||
}
|
||||
|
||||
asChange(properties: any): { k: string; v: string }[] {
|
||||
asChange(): { k: string; v: string }[] {
|
||||
return [{ k: this.key, v: this.value }]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import { RasterLayerPolygon } from "./RasterLayers"
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import Table from "../../UI/Base/Table"
|
|||
import FilterConfigJson from "./Json/FilterConfigJson"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import { Overpass } from "../../Logic/Osm/Overpass"
|
||||
import Constants from "../Constants"
|
||||
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
|
||||
import Svg from "../../Svg"
|
||||
import { ImmutableStore } from "../../Logic/UIEventSource"
|
||||
|
|
|
@ -108,7 +108,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
|
||||
|
||||
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
|
||||
const indexedElements = new LayoutSource(
|
||||
this.indexedFeatures = new LayoutSource(
|
||||
layout.layers,
|
||||
this.featureSwitches,
|
||||
new StaticFeatureSource([]),
|
||||
|
@ -116,6 +116,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.osmConnection.Backend(),
|
||||
(id) => this.layerState.filteredLayers.get(id).isDisplayed
|
||||
)
|
||||
const indexedElements = this.indexedFeatures
|
||||
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||
Array.from(this.layerState.filteredLayers.values()),
|
||||
|
|
|
@ -15,7 +15,6 @@ import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
|||
export default class AllThemesGui {
|
||||
setup() {
|
||||
try {
|
||||
new FixedUiElement("").AttachTo("centermessage")
|
||||
const osmConnection = new OsmConnection()
|
||||
const state = new UserRelatedState(osmConnection)
|
||||
const intro = new Combine([
|
||||
|
@ -38,15 +37,14 @@ export default class AllThemesGui {
|
|||
new FixedUiElement("v" + Constants.vNumber),
|
||||
])
|
||||
.SetClass("block m-5 lg:w-3/4 lg:ml-40")
|
||||
.SetStyle("pointer-events: all;")
|
||||
.AttachTo("top-left")
|
||||
.AttachTo("main")
|
||||
} catch (e) {
|
||||
console.error(">>>> CRITICAL", e)
|
||||
new FixedUiElement(
|
||||
"Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!"
|
||||
)
|
||||
.SetClass("alert")
|
||||
.AttachTo("centermessage")
|
||||
.AttachTo("main")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "./VariableUIElement"
|
||||
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Stores } from "../../Logic/UIEventSource"
|
||||
import Loading from "./Loading"
|
||||
|
||||
export default class AsyncLazy extends BaseUIElement {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { UIElement } from "../UIElement"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
|
||||
/**
|
||||
|
|
89
UI/Base/DragInvitation.svelte
Normal file
89
UI/Base/DragInvitation.svelte
Normal file
|
@ -0,0 +1,89 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* This overlay element will regularly show a hand that swipes over the underlying element.
|
||||
* This element will hide as soon as the Store 'hideSignal' receives a change (which is not undefined)
|
||||
*/
|
||||
import ToSvelte from "./ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
let mainElem: HTMLElement;
|
||||
export let hideSignal: Store<any>;
|
||||
function hide(){
|
||||
console.trace("Hiding...")
|
||||
mainElem.style.visibility = "hidden";
|
||||
}
|
||||
if (hideSignal) {
|
||||
onDestroy(hideSignal.addCallbackD(() => {
|
||||
console.trace("Hiding invitation")
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
$: {
|
||||
console.log("Binding listeners on", mainElem)
|
||||
mainElem?.addEventListener("click",_ => hide())
|
||||
mainElem?.addEventListener("touchstart",_ => hide())
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full">
|
||||
<div id="hand-container">
|
||||
<ToSvelte construct={Svg.hand_ui}></ToSvelte>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@keyframes hand-drag-animation {
|
||||
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
6% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
12% {
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
24% {
|
||||
opacity: 1;
|
||||
transform: rotate(-00deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
36% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#hand-container {
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
left: calc(50% + 4rem);
|
||||
top: calc(50%);
|
||||
opacity: 0.7;
|
||||
animation: hand-drag-animation 4s ease-in-out infinite;
|
||||
transform-origin: 50% 125%;
|
||||
}
|
||||
|
||||
</style>
|
19
UI/Base/FromHtml.svelte
Normal file
19
UI/Base/FromHtml.svelte
Normal file
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Given an HTML string, properly shows this
|
||||
*/
|
||||
|
||||
export let src: string;
|
||||
let htmlElem: HTMLElement;
|
||||
$: {
|
||||
if(htmlElem !== undefined){
|
||||
htmlElem.innerHTML = src
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if src !== undefined}
|
||||
<span bind:this={htmlElem}></span>
|
||||
{/if}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
|
||||
export default class Link extends BaseUIElement {
|
||||
private readonly _href: string | Store<string>
|
||||
|
|
30
UI/Base/Tr.svelte
Normal file
30
UI/Base/Tr.svelte
Normal file
|
@ -0,0 +1,30 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Properly renders a translation
|
||||
*/
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import { onDestroy } from "svelte";
|
||||
import Locale from "../i18n/Locale";
|
||||
import { Utils } from "../../Utils";
|
||||
import FromHtml from "./FromHtml.svelte";
|
||||
|
||||
export let t: Translation;
|
||||
export let tags: Record<string, string> | undefined;
|
||||
// Text for the current language
|
||||
let txt: string | undefined;
|
||||
|
||||
onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||
const translation = t?.textFor(l)
|
||||
if(translation === undefined){
|
||||
return
|
||||
}
|
||||
if(tags){
|
||||
txt = Utils.SubstituteKeys(txt, tags)
|
||||
}else{
|
||||
txt = translation
|
||||
}
|
||||
}));
|
||||
|
||||
</script>
|
||||
|
||||
<FromHtml src={txt}></FromHtml>
|
|
@ -13,7 +13,6 @@ import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
|
|||
import Toggle from "../Input/Toggle"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
import DefaultGUI from "../DefaultGUI"
|
||||
|
||||
export class BackToThemeOverview extends Toggle {
|
||||
constructor(
|
||||
|
|
|
@ -14,7 +14,6 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
|||
import { BBox } from "../../Logic/BBox"
|
||||
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
|
||||
import geojson2svg from "geojson2svg"
|
||||
import Constants from "../../Models/Constants"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
|
||||
export class DownloadPanel extends Toggle {
|
||||
|
|
|
@ -17,7 +17,6 @@ import UserRelatedState from "../../Logic/State/UserRelatedState"
|
|||
import Loc from "../../Models/Loc"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import CopyrightPanel from "./CopyrightPanel"
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import PrivacyPolicy from "./PrivacyPolicy"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
|
||||
Translations.t;
|
||||
export let bounds: UIEventSource<BBox>
|
||||
export let layout: LayoutConfig;
|
||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
export let selectedElement: UIEventSource<Feature>;
|
||||
export let selectedLayer: UIEventSource<LayerConfig>;
|
||||
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
<script lang="ts">
|
||||
import type { Feature } from "geojson";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import TagRenderingAnswer from "../Popup/TagRenderingAnswer";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import { VariableUiElement } from "../Base/VariableUIElement.js";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { onDestroy } from "svelte";
|
||||
import TagRenderingAnswer from "../Popup/TagRenderingAnswer.svelte";
|
||||
|
||||
export let selectedElement: UIEventSource<Feature>;
|
||||
export let layer: UIEventSource<LayerConfig>;
|
||||
export let tags: Store<UIEventSource<Record<string, string>>>;
|
||||
let _tags: UIEventSource<Record<string, string>>;
|
||||
onDestroy(tags.subscribe(tags => {
|
||||
_tags = tags;
|
||||
return false
|
||||
}));
|
||||
export let selectedElement: Feature;
|
||||
export let layer: LayerConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
|
||||
export let specialVisState: SpecialVisualizationState;
|
||||
export let state: SpecialVisualizationState;
|
||||
|
||||
/**
|
||||
* const title = new TagRenderingAnswer(
|
||||
|
@ -46,30 +38,27 @@
|
|||
</script>
|
||||
|
||||
<div>
|
||||
<div on:click={() =>selectedElement.setData(undefined)}>close</div>
|
||||
<div class="flex flex-col sm:flex-row flex-grow justify-between">
|
||||
<!-- Title element-->
|
||||
<ToSvelte
|
||||
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, layer.data.title, specialVisState), [layer]))}></ToSvelte>
|
||||
<h3>
|
||||
<TagRenderingAnswer config={layer.title} {tags} {selectedElement}></TagRenderingAnswer>
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2">
|
||||
|
||||
{#each $layer.titleIcons as titleIconConfig (titleIconConfig.id)}
|
||||
{#each layer.titleIcons as titleIconConfig (titleIconConfig.id)}
|
||||
<div class="w-8 h-8">
|
||||
<ToSvelte
|
||||
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, titleIconConfig, specialVisState)))}></ToSvelte>
|
||||
<TagRenderingAnswer config={titleIconConfig} {tags} {selectedElement}></TagRenderingAnswer>
|
||||
</div>
|
||||
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
|
||||
{#each Object.keys($_tags) as key}
|
||||
<li><b>{key}</b>=<b>{$_tags[key]}</b></li>
|
||||
<div class="flex flex-col">
|
||||
{#each layer.tagRenderings as config (config.id)}
|
||||
<TagRenderingAnswer {tags} {config} {state}></TagRenderingAnswer>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import Constants from "../../Models/Constants"
|
||||
import type Loc from "../../Models/Loc"
|
||||
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
|
||||
export let theme: LayoutInformation
|
||||
export let isCustom: boolean = false
|
||||
|
@ -16,8 +17,8 @@
|
|||
$: title = new Translation(
|
||||
theme.title,
|
||||
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
|
||||
).toString()
|
||||
$: description = new Translation(theme.shortDescription).toString()
|
||||
)
|
||||
$: description = new Translation(theme.shortDescription)
|
||||
|
||||
// TODO: Improve this function
|
||||
function createUrl(
|
||||
|
@ -83,8 +84,10 @@
|
|||
<img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt="" />
|
||||
<span slot="message" class="message">
|
||||
<span>
|
||||
<span>{title}</span>
|
||||
<span>{description}</span>
|
||||
<Tr t={title}></Tr>
|
||||
<span class="subtle">
|
||||
<Tr t={description}></Tr>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</SubtleButton>
|
||||
|
|
|
@ -5,7 +5,6 @@ import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs"
|
|||
import MapControlButton from "./MapControlButton"
|
||||
import Svg from "../Svg"
|
||||
import Toggle from "./Input/Toggle"
|
||||
import SearchAndGo from "./BigComponents/SearchAndGo"
|
||||
import BaseUIElement from "./BaseUIElement"
|
||||
import LeftControls from "./BigComponents/LeftControls"
|
||||
import RightControls from "./BigComponents/RightControls"
|
||||
|
@ -26,7 +25,6 @@ import UserInformationPanel from "./BigComponents/UserInformation"
|
|||
import { LoginToggle } from "./Popup/LoginButton"
|
||||
import { FixedUiElement } from "./Base/FixedUiElement"
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import { GeoLocationState } from "../Logic/State/GeoLocationState"
|
||||
import Hotkeys from "./Base/Hotkeys"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Utils } from "../../Utils"
|
||||
import Combine from "../Base/Combine"
|
||||
|
|
|
@ -17,7 +17,6 @@ import Minimap from "../Base/Minimap"
|
|||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
||||
import Loc from "../../Models/Loc"
|
||||
import Attribution from "../BigComponents/Attribution"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import ValidatedTextField from "../Input/ValidatedTextField"
|
||||
|
|
|
@ -7,7 +7,6 @@ import Translations from "../i18n/Translations"
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { UIElement } from "../UIElement"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
|
||||
export interface FlowStep<T> extends BaseUIElement {
|
||||
readonly IsValid: Store<boolean>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { BBox } from "../../Logic/BBox"
|
|||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { DropDown } from "../Input/DropDown"
|
||||
import { Utils } from "../../Utils"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
import { InputElement } from "./InputElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { Utils } from "../../Utils"
|
||||
import Loc from "../../Models/Loc"
|
||||
import Minimap from "../Base/Minimap"
|
||||
|
||||
/**
|
||||
* Selects a direction in degrees
|
||||
*/
|
||||
export default class DirectionInput extends InputElement<string> {
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
private readonly _location: UIEventSource<Loc>
|
||||
private readonly value: UIEventSource<string>
|
||||
private background
|
||||
|
||||
constructor(
|
||||
mapBackground: UIEventSource<any>,
|
||||
location: UIEventSource<Loc>,
|
||||
value?: UIEventSource<string>
|
||||
) {
|
||||
super()
|
||||
this._location = location
|
||||
this.value = value ?? new UIEventSource<string>(undefined)
|
||||
this.background = mapBackground
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.value
|
||||
}
|
||||
|
||||
IsValid(str: string): boolean {
|
||||
const t = Number(str)
|
||||
return !isNaN(t) && t >= 0 && t <= 360
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
let map: BaseUIElement = new FixedUiElement("")
|
||||
if (!Utils.runningFromConsole) {
|
||||
map = Minimap.createMiniMap({
|
||||
background: this.background,
|
||||
allowMoving: false,
|
||||
location: this._location,
|
||||
})
|
||||
}
|
||||
|
||||
const element = new Combine([
|
||||
Svg.direction_stroke_svg()
|
||||
.SetStyle(
|
||||
`position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${
|
||||
this.value.data ?? 0
|
||||
}deg);`
|
||||
)
|
||||
.SetClass("direction-svg relative")
|
||||
.SetStyle("z-index: 1000"),
|
||||
map.SetStyle(`position: absolute;top: 0;left: 0;width: 100%;height: 100%;`),
|
||||
])
|
||||
.SetStyle("width: min(100%, 25em); height: 0; padding-bottom: 100%") // A bit a weird CSS , see https://stackoverflow.com/questions/13851940/pure-css-solution-square-elements#19448481
|
||||
.SetClass("relative block bg-white border border-black overflow-hidden rounded-full")
|
||||
.ConstructElement()
|
||||
|
||||
this.value.addCallbackAndRunD((rotation) => {
|
||||
const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement
|
||||
cone.style.transform = `rotate(${rotation}deg)`
|
||||
})
|
||||
|
||||
this.RegisterTriggers(element)
|
||||
element.style.overflow = "hidden"
|
||||
element.style.display = "block"
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
private RegisterTriggers(htmlElement: HTMLElement) {
|
||||
const self = this
|
||||
|
||||
function onPosChange(x: number, y: number) {
|
||||
const rect = htmlElement.getBoundingClientRect()
|
||||
const dx = -(rect.left + rect.right) / 2 + x
|
||||
const dy = (rect.top + rect.bottom) / 2 - y
|
||||
const angle = (180 * Math.atan2(dy, dx)) / Math.PI
|
||||
const angleGeo = Math.floor((450 - angle) % 360)
|
||||
self.value.setData("" + angleGeo)
|
||||
}
|
||||
|
||||
htmlElement.ontouchmove = (ev: TouchEvent) => {
|
||||
onPosChange(ev.touches[0].clientX, ev.touches[0].clientY)
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
htmlElement.ontouchstart = (ev: TouchEvent) => {
|
||||
onPosChange(ev.touches[0].clientX, ev.touches[0].clientY)
|
||||
}
|
||||
|
||||
let isDown = false
|
||||
|
||||
htmlElement.onmousedown = (ev: MouseEvent) => {
|
||||
isDown = true
|
||||
onPosChange(ev.clientX, ev.clientY)
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
htmlElement.onmouseup = (ev) => {
|
||||
isDown = false
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
htmlElement.onmousemove = (ev: MouseEvent) => {
|
||||
if (isDown) {
|
||||
onPosChange(ev.clientX, ev.clientY)
|
||||
}
|
||||
ev.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { InputElement } from "./InputElement"
|
||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import Slider from "./Slider"
|
||||
import { ClickableToggle } from "./Toggle"
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import { ReadonlyInputElement } from "./InputElement"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Minimap, { MinimapObj } from "../Base/Minimap"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "./Toggle"
|
||||
import matchpoint from "../../assets/layers/matchpoint/matchpoint.json"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { ElementStorage } from "../../Logic/ElementStorage"
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
||||
import { RelationId, WayId } from "../../Models/OsmFeature"
|
||||
import { Feature, LineString, Polygon } from "geojson"
|
||||
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
||||
|
@ -313,10 +306,6 @@ export default class LocationInput
|
|||
[this.map.leafletMap]
|
||||
)
|
||||
|
||||
const animatedHand = Svg.hand_ui()
|
||||
.SetStyle("width: 2rem; height: unset;")
|
||||
.SetClass("hand-drag-animation block pointer-events-none")
|
||||
|
||||
return new Combine([
|
||||
new Combine([
|
||||
Svg.move_arrows_ui()
|
||||
|
@ -328,10 +317,6 @@ export default class LocationInput
|
|||
"background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%; opacity: 0.5"
|
||||
),
|
||||
|
||||
new Toggle(undefined, animatedHand, hasMoved)
|
||||
.SetClass("block w-0 h-0 z-10 relative")
|
||||
.SetStyle("left: calc(50% + 3rem); top: calc(50% + 2rem); opacity: 0.7"),
|
||||
|
||||
this.map.SetClass("z-0 relative block w-full h-full bg-gray-100"),
|
||||
]).ConstructElement()
|
||||
} catch (e) {
|
||||
|
@ -341,11 +326,4 @@ export default class LocationInput
|
|||
.ConstructElement()
|
||||
}
|
||||
}
|
||||
|
||||
TakeScreenshot(format: "image"): Promise<string>
|
||||
TakeScreenshot(format: "blob"): Promise<Blob>
|
||||
TakeScreenshot(format: "image" | "blob"): Promise<string | Blob>
|
||||
TakeScreenshot(format: "image" | "blob"): Promise<string | Blob> {
|
||||
return this.map.TakeScreenshot(format)
|
||||
}
|
||||
}
|
||||
|
|
1
UI/Input/README.md
Normal file
1
UI/Input/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
This is the old, deprecated directory. New, SVelte-based items go into `InputElement`
|
|
@ -2,7 +2,6 @@ import { InputElement } from "./InputElement"
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
export default class SimpleDatePicker extends InputElement<string> {
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
private readonly value: UIEventSource<string>
|
||||
private readonly _element: HTMLElement
|
||||
|
||||
|
|
|
@ -50,10 +50,6 @@ export class TextField extends InputElement<string> {
|
|||
return this.value
|
||||
}
|
||||
|
||||
GetRawValue(): UIEventSource<string> {
|
||||
return this._rawValue
|
||||
}
|
||||
|
||||
IsValid(t: string): boolean {
|
||||
if (t === undefined || t === null) {
|
||||
return false
|
||||
|
|
File diff suppressed because it is too large
Load diff
70
UI/InputElement/Helpers/DirectionInput.svelte
Normal file
70
UI/InputElement/Helpers/DirectionInput.svelte
Normal file
|
@ -0,0 +1,70 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { MapProperties } from "../../../Models/MapProperties";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor";
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import Svg from "../../../Svg.js";
|
||||
|
||||
/**
|
||||
* A visualisation to pick a direction on a map background
|
||||
*/
|
||||
export let value: UIEventSource<undefined | number>;
|
||||
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> };
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let mla = new MapLibreAdaptor(map, mapProperties);
|
||||
mla.allowMoving.setData(false)
|
||||
mla.allowZooming.setData(false)
|
||||
let directionElem: HTMLElement | undefined;
|
||||
$: value.addCallbackAndRunD(degrees => {
|
||||
console.log("Degrees are", degrees, directionElem);
|
||||
if (directionElem === undefined) {
|
||||
return;
|
||||
}
|
||||
directionElem.style.rotate = degrees + "deg";
|
||||
});
|
||||
|
||||
let mainElem : HTMLElement
|
||||
function onPosChange(x: number, y: number) {
|
||||
const rect = mainElem.getBoundingClientRect();
|
||||
const dx = -(rect.left + rect.right) / 2 + x;
|
||||
const dy = (rect.top + rect.bottom) / 2 - y;
|
||||
const angle = (180 * Math.atan2(dy, dx)) / Math.PI;
|
||||
const angleGeo = Math.floor((450 - angle) % 360);
|
||||
value.setData(angleGeo);
|
||||
}
|
||||
|
||||
let isDown = false;
|
||||
</script>
|
||||
|
||||
<div bind:this={mainElem} class="relative w-48 h-48 cursor-pointer overflow-hidden"
|
||||
on:click={e => onPosChange(e.x, e.y)}
|
||||
on:mousedown={e => {
|
||||
isDown = true
|
||||
onPosChange(e.clientX, e.clientY)
|
||||
} }
|
||||
on:mousemove={e => {
|
||||
if(isDown){
|
||||
onPosChange(e.clientX, e.clientY)
|
||||
|
||||
}}}
|
||||
|
||||
on:mouseup={() => {
|
||||
isDown = false
|
||||
} }
|
||||
on:touchmove={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}
|
||||
|
||||
|
||||
on:touchstart={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}>
|
||||
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
|
||||
<MaplibreMap {map} attribution={false}></MaplibreMap>
|
||||
</div>
|
||||
|
||||
<div bind:this={directionElem} class="absolute w-full h-full top-0 left-0 border border-red-500">
|
||||
|
||||
<ToSvelte construct={ Svg.direction_stroke_svg}>
|
||||
|
||||
</ToSvelte>
|
||||
</div>
|
||||
</div>
|
42
UI/InputElement/Helpers/LocationInput.svelte
Normal file
42
UI/InputElement/Helpers/LocationInput.svelte
Normal file
|
@ -0,0 +1,42 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { MapProperties } from "../../../Models/MapProperties";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor";
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte";
|
||||
import Svg from "../../../Svg";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import DragInvitation from "../../Base/DragInvitation.svelte";
|
||||
|
||||
/**
|
||||
* A visualisation to pick a direction on a map background
|
||||
*/
|
||||
export let value: UIEventSource<{lon: number, lat: number}>;
|
||||
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> };
|
||||
/**
|
||||
* Called when setup is done, cna be used to add layrs to the map
|
||||
*/
|
||||
export let onCreated : (value: Store<{lon: number, lat: number}> , map: Store<MlMap>, mapProperties: MapProperties ) => void
|
||||
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let mla = new MapLibreAdaptor(map, mapProperties);
|
||||
mla.allowMoving.setData(true)
|
||||
mla.allowZooming.setData(true)
|
||||
|
||||
if(onCreated){
|
||||
onCreated(value, map, mla)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative h-32 cursor-pointer overflow-hidden">
|
||||
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
|
||||
<MaplibreMap {map} attribution={false}></MaplibreMap>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50">
|
||||
<ToSvelte construct={() => Svg.move_arrows_svg().SetClass("h-full")}></ToSvelte>
|
||||
</div>
|
||||
|
||||
<DragInvitation></DragInvitation>
|
||||
|
||||
</div>
|
13
UI/InputElement/InputHelper.svelte
Normal file
13
UI/InputElement/InputHelper.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Constructs an input helper element for the given type.
|
||||
* Note that all values are stringified
|
||||
*/
|
||||
|
||||
import { AvailableInputHelperType } from "./InputHelpers";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
|
||||
export let type : AvailableInputHelperType
|
||||
export let value : UIEventSource<string>
|
||||
|
||||
</script>
|
16
UI/InputElement/InputHelpers.ts
Normal file
16
UI/InputElement/InputHelpers.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
|
||||
export type AvailableInputHelperType = typeof InputHelpers.AvailableInputHelpers[number]
|
||||
|
||||
export default class InputHelpers {
|
||||
public static readonly AvailableInputHelpers = [] as const
|
||||
/**
|
||||
* To port
|
||||
* direction
|
||||
* opening_hours
|
||||
* color
|
||||
* length
|
||||
* date
|
||||
* wikidata
|
||||
*/
|
||||
}
|
119
UI/InputElement/ValidatedTextField.ts
Normal file
119
UI/InputElement/ValidatedTextField.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import Combine from "../Base/Combine"
|
||||
import Title from "../Base/Title"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import WikidataValidator from "./Validators/WikidataValidator"
|
||||
import StringValidator from "./Validators/StringValidator"
|
||||
import TextValidator from "./Validators/TextValidator"
|
||||
import DateValidator from "./Validators/DateValidator"
|
||||
import LengthValidator from "./Validators/LengthValidator"
|
||||
import IntValidator from "./Validators/IntValidator"
|
||||
import EmailValidator from "./Validators/EmailValidator"
|
||||
import DirectionValidator from "./Validators/DirectionValidator"
|
||||
import NatValidator from "./Validators/NatValidator"
|
||||
import OpeningHoursValidator from "./Validators/OpeningHoursValidator"
|
||||
import PFloatValidator from "./Validators/PFloatValidator"
|
||||
import ColorValidator from "./Validators/ColorValidator"
|
||||
import PhoneValidator from "./Validators/PhoneValidator"
|
||||
import UrlValidator from "./Validators/UrlValidator"
|
||||
import FloatValidator from "./Validators/FloatValidator"
|
||||
import PNatValidator from "./Validators/PNatValidator"
|
||||
|
||||
/**
|
||||
* A 'TextFieldValidator' contains various methods to check and cleanup an entered value or to give feedback.
|
||||
* They also double as an index of supported types for textfields in MapComplete
|
||||
*/
|
||||
export abstract class Validator {
|
||||
public readonly name: string
|
||||
/*
|
||||
* An explanation for the theme builder.
|
||||
* This can indicate which special input element is used, ...
|
||||
* */
|
||||
public readonly explanation: string
|
||||
/**
|
||||
* What HTML-inputmode to use
|
||||
*/
|
||||
public readonly inputmode?: string
|
||||
|
||||
constructor(name: string, explanation: string | BaseUIElement, inputmode?: string) {
|
||||
this.name = name
|
||||
this.inputmode = inputmode
|
||||
if (this.name.endsWith("textfield")) {
|
||||
this.name = this.name.substr(0, this.name.length - "TextField".length)
|
||||
}
|
||||
if (this.name.endsWith("textfielddef")) {
|
||||
this.name = this.name.substr(0, this.name.length - "TextFieldDef".length)
|
||||
}
|
||||
if (typeof explanation === "string") {
|
||||
this.explanation = explanation
|
||||
} else {
|
||||
this.explanation = explanation.AsMarkdown()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a piece of feedback. By default, validation.<type> will be used, resulting in a generic 'not a valid <type>'.
|
||||
* However, inheritors might overwrite this to give more specific feedback
|
||||
* @param s
|
||||
*/
|
||||
public getFeedback(s: string): Translation {
|
||||
const tr = Translations.t.validation[this.name]
|
||||
if (tr !== undefined) {
|
||||
return tr["feedback"]
|
||||
}
|
||||
}
|
||||
|
||||
public isValid(string: string, requestCountry: () => string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
public reformat(s: string, country?: () => string): string {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
export default class Validators {
|
||||
private static readonly AllValidators: ReadonlyArray<Validator> = [
|
||||
new StringValidator(),
|
||||
new TextValidator(),
|
||||
new DateValidator(),
|
||||
new NatValidator(),
|
||||
new IntValidator(),
|
||||
new LengthValidator(),
|
||||
new DirectionValidator(),
|
||||
new WikidataValidator(),
|
||||
new PNatValidator(),
|
||||
new FloatValidator(),
|
||||
new PFloatValidator(),
|
||||
new EmailValidator(),
|
||||
new UrlValidator(),
|
||||
new PhoneValidator(),
|
||||
new OpeningHoursValidator(),
|
||||
new ColorValidator(),
|
||||
]
|
||||
public static allTypes: Map<string, Validator> = Validators.allTypesDict()
|
||||
|
||||
public static HelpText(): BaseUIElement {
|
||||
const explanations: BaseUIElement[] = Validators.AllValidators.map((type) =>
|
||||
new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")
|
||||
)
|
||||
return new Combine([
|
||||
new Title("Available types for text fields", 1),
|
||||
"The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them",
|
||||
...explanations,
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
public static AvailableTypes(): string[] {
|
||||
return Validators.AllValidators.map((tp) => tp.name)
|
||||
}
|
||||
|
||||
private static allTypesDict(): Map<string, Validator> {
|
||||
const types = new Map<string, Validator>()
|
||||
for (const tp of Validators.AllValidators) {
|
||||
types.set(tp.name, tp)
|
||||
}
|
||||
return types
|
||||
}
|
||||
}
|
7
UI/InputElement/Validators/ColorValidator.ts
Normal file
7
UI/InputElement/Validators/ColorValidator.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class ColorValidator extends Validator {
|
||||
constructor() {
|
||||
super("color", "Shows a color picker")
|
||||
}
|
||||
}
|
23
UI/InputElement/Validators/DateValidator.ts
Normal file
23
UI/InputElement/Validators/DateValidator.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class DateValidator extends Validator {
|
||||
constructor() {
|
||||
super("date", "A date with date picker")
|
||||
}
|
||||
|
||||
isValid(str: string): boolean {
|
||||
return !isNaN(new Date(str).getTime())
|
||||
}
|
||||
|
||||
reformat(str: string) {
|
||||
const d = new Date(str)
|
||||
let month = "" + (d.getMonth() + 1)
|
||||
let day = "" + d.getDate()
|
||||
const year = d.getFullYear()
|
||||
|
||||
if (month.length < 2) month = "0" + month
|
||||
if (day.length < 2) day = "0" + day
|
||||
|
||||
return [year, month, day].join("-")
|
||||
}
|
||||
}
|
17
UI/InputElement/Validators/DirectionValidator.ts
Normal file
17
UI/InputElement/Validators/DirectionValidator.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
import IntValidator from "./IntValidator";
|
||||
|
||||
export default class DirectionValidator extends IntValidator {
|
||||
constructor() {
|
||||
super(
|
||||
"direction",
|
||||
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)"
|
||||
)
|
||||
}
|
||||
|
||||
reformat(str): string {
|
||||
const n = Number(str) % 360
|
||||
return "" + n
|
||||
}
|
||||
|
||||
}
|
39
UI/InputElement/Validators/EmailValidator.ts
Normal file
39
UI/InputElement/Validators/EmailValidator.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Validator } from "../ValidatedTextField.js"
|
||||
import { Translation } from "../../i18n/Translation.js"
|
||||
import Translations from "../../i18n/Translations.js"
|
||||
import * as emailValidatorLibrary from "email-validator"
|
||||
export default class EmailValidator extends Validator {
|
||||
constructor() {
|
||||
super("email", "An email adress", "email")
|
||||
}
|
||||
|
||||
isValid = (str) => {
|
||||
if (str === undefined) {
|
||||
return false
|
||||
}
|
||||
str = str.trim()
|
||||
if (str.startsWith("mailto:")) {
|
||||
str = str.substring("mailto:".length)
|
||||
}
|
||||
return emailValidatorLibrary.validate(str)
|
||||
}
|
||||
|
||||
reformat = (str) => {
|
||||
if (str === undefined) {
|
||||
return undefined
|
||||
}
|
||||
str = str.trim()
|
||||
if (str.startsWith("mailto:")) {
|
||||
str = str.substring("mailto:".length)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
getFeedback(s: string): Translation {
|
||||
if (s.indexOf("@") < 0) {
|
||||
return Translations.t.validation.email.noAt
|
||||
}
|
||||
|
||||
return super.getFeedback(s)
|
||||
}
|
||||
}
|
27
UI/InputElement/Validators/FloatValidator.ts
Normal file
27
UI/InputElement/Validators/FloatValidator.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class FloatValidator extends Validator {
|
||||
inputmode = "decimal"
|
||||
|
||||
constructor(name?: string, explanation?: string) {
|
||||
super(name ?? "float", explanation ?? "A decimal number", "decimal")
|
||||
}
|
||||
|
||||
isValid(str) {
|
||||
return !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(",")
|
||||
}
|
||||
|
||||
reformat(str): string {
|
||||
return "" + Number(str)
|
||||
}
|
||||
|
||||
getFeedback(s: string): Translation {
|
||||
if (isNaN(Number(s))) {
|
||||
return Translations.t.validation.nat.notANumber
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
29
UI/InputElement/Validators/IntValidator.ts
Normal file
29
UI/InputElement/Validators/IntValidator.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class IntValidator extends Validator {
|
||||
constructor(name?: string, explanation?: string) {
|
||||
super(
|
||||
name ?? "int",
|
||||
explanation ?? "A whole number, either positive, negative or zero",
|
||||
"numeric"
|
||||
)
|
||||
}
|
||||
|
||||
isValid(str): boolean {
|
||||
str = "" + str
|
||||
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))
|
||||
}
|
||||
|
||||
getFeedback(s: string): Translation {
|
||||
const n = Number(s)
|
||||
if (isNaN(n)) {
|
||||
return Translations.t.validation.nat.notANumber
|
||||
}
|
||||
if (Math.floor(n) !== n) {
|
||||
return Translations.t.validation.nat.mustBeWhole
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
16
UI/InputElement/Validators/LengthValidator.ts
Normal file
16
UI/InputElement/Validators/LengthValidator.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class LengthValidator extends Validator {
|
||||
constructor() {
|
||||
super(
|
||||
"distance",
|
||||
'A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]',
|
||||
"decimal"
|
||||
)
|
||||
}
|
||||
|
||||
isValid = (str) => {
|
||||
const t = Number(str)
|
||||
return !isNaN(t)
|
||||
}
|
||||
}
|
30
UI/InputElement/Validators/NatValidator.ts
Normal file
30
UI/InputElement/Validators/NatValidator.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import IntValidator from "./IntValidator"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
|
||||
export default class NatValidator extends IntValidator {
|
||||
constructor(name?: string, explanation?: string) {
|
||||
super(name ?? "nat", explanation ?? "A whole, positive number or zero")
|
||||
}
|
||||
|
||||
isValid(str): boolean {
|
||||
if (str === undefined) {
|
||||
return false
|
||||
}
|
||||
str = "" + str
|
||||
|
||||
return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
|
||||
}
|
||||
|
||||
getFeedback(s: string): Translation {
|
||||
const spr = super.getFeedback(s)
|
||||
if (spr !== undefined) {
|
||||
return spr
|
||||
}
|
||||
const n = Number(s)
|
||||
if (n < 0) {
|
||||
return Translations.t.validation.nat.mustBePositive
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
54
UI/InputElement/Validators/OpeningHoursValidator.ts
Normal file
54
UI/InputElement/Validators/OpeningHoursValidator.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
import Combine from "../../Base/Combine"
|
||||
import Title from "../../Base/Title"
|
||||
import Table from "../../Base/Table"
|
||||
|
||||
export default class OpeningHoursValidator extends Validator {
|
||||
constructor() {
|
||||
super(
|
||||
"opening_hours",
|
||||
new Combine([
|
||||
"Has extra elements to easily input when a POI is opened.",
|
||||
new Title("Helper arguments"),
|
||||
new Table(
|
||||
["name", "doc"],
|
||||
[
|
||||
[
|
||||
"options",
|
||||
new Combine([
|
||||
"A JSON-object of type `{ prefix: string, postfix: string }`. ",
|
||||
new Table(
|
||||
["subarg", "doc"],
|
||||
[
|
||||
[
|
||||
"prefix",
|
||||
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse.",
|
||||
],
|
||||
[
|
||||
"postfix",
|
||||
"Piece of text that will always be added to the end of the generated opening hours",
|
||||
],
|
||||
]
|
||||
),
|
||||
]),
|
||||
],
|
||||
]
|
||||
),
|
||||
new Title("Example usage"),
|
||||
"To add a conditional (based on time) access restriction:\n\n```\n" +
|
||||
`
|
||||
"freeform": {
|
||||
"key": "access:conditional",
|
||||
"type": "opening_hours",
|
||||
"helperArgs": [
|
||||
{
|
||||
"prefix":"no @ (",
|
||||
"postfix":")"
|
||||
}
|
||||
]
|
||||
}` +
|
||||
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`",
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
23
UI/InputElement/Validators/PFloatValidator.ts
Normal file
23
UI/InputElement/Validators/PFloatValidator.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class PFloatValidator extends Validator {
|
||||
constructor() {
|
||||
super("pfloat", "A positive decimal number or zero")
|
||||
}
|
||||
|
||||
isValid = (str) =>
|
||||
!isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(",")
|
||||
|
||||
getFeedback(s: string): Translation {
|
||||
const spr = super.getFeedback(s)
|
||||
if (spr !== undefined) {
|
||||
return spr
|
||||
}
|
||||
if (Number(s) < 0) {
|
||||
return Translations.t.validation.nat.mustBePositive
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
27
UI/InputElement/Validators/PNatValidator.ts
Normal file
27
UI/InputElement/Validators/PNatValidator.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import NatValidator from "./NatValidator"
|
||||
|
||||
export default class PNatValidator extends NatValidator {
|
||||
constructor() {
|
||||
super("pnat", "A strict positive number")
|
||||
}
|
||||
|
||||
getFeedback(s: string): Translation {
|
||||
const spr = super.getFeedback(s)
|
||||
if (spr !== undefined) {
|
||||
return spr
|
||||
}
|
||||
if (Number(s) === 0) {
|
||||
return Translations.t.validation.pnat.noZero
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
isValid = (str) => {
|
||||
if (!super.isValid(str)) {
|
||||
return false
|
||||
}
|
||||
return Number(str) > 0
|
||||
}
|
||||
}
|
32
UI/InputElement/Validators/PhoneValidator.ts
Normal file
32
UI/InputElement/Validators/PhoneValidator.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
import { parsePhoneNumberFromString } from "libphonenumber-js"
|
||||
|
||||
export default class PhoneValidator extends Validator {
|
||||
constructor() {
|
||||
super("phone", "A phone number", "tel")
|
||||
}
|
||||
|
||||
isValid(str, country: () => string): boolean {
|
||||
if (str === undefined) {
|
||||
return false
|
||||
}
|
||||
if (str.startsWith("tel:")) {
|
||||
str = str.substring("tel:".length)
|
||||
}
|
||||
let countryCode = undefined
|
||||
if (country !== undefined) {
|
||||
countryCode = country()?.toUpperCase()
|
||||
}
|
||||
return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false
|
||||
}
|
||||
|
||||
reformat = (str, country: () => string) => {
|
||||
if (str.startsWith("tel:")) {
|
||||
str = str.substring("tel:".length)
|
||||
}
|
||||
return parsePhoneNumberFromString(
|
||||
str,
|
||||
country()?.toUpperCase() as any
|
||||
)?.formatInternational()
|
||||
}
|
||||
}
|
8
UI/InputElement/Validators/StringValidator.ts
Normal file
8
UI/InputElement/Validators/StringValidator.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class StringValidator extends Validator {
|
||||
constructor() {
|
||||
super("string", "A simple piece of text")
|
||||
}
|
||||
|
||||
}
|
7
UI/InputElement/Validators/TextValidator.ts
Normal file
7
UI/InputElement/Validators/TextValidator.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class TextValidator extends Validator {
|
||||
constructor() {
|
||||
super("text", "A longer piece of text. Uses an textArea instead of a textField", "text")
|
||||
}
|
||||
}
|
75
UI/InputElement/Validators/UrlValidator.ts
Normal file
75
UI/InputElement/Validators/UrlValidator.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class UrlValidator extends Validator {
|
||||
constructor() {
|
||||
super(
|
||||
"url",
|
||||
"The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user. Furthermore, some tracking parameters will be removed",
|
||||
"url"
|
||||
)
|
||||
}
|
||||
reformat(str: string): string {
|
||||
try {
|
||||
let url: URL
|
||||
// str = str.toLowerCase() // URLS are case sensitive. Lowercasing them might break some URLS. See #763
|
||||
if (
|
||||
!str.startsWith("http://") &&
|
||||
!str.startsWith("https://") &&
|
||||
!str.startsWith("http:")
|
||||
) {
|
||||
url = new URL("https://" + str)
|
||||
} else {
|
||||
url = new URL(str)
|
||||
}
|
||||
const blacklistedTrackingParams = [
|
||||
"fbclid", // Oh god, how I hate the fbclid. Let it burn, burn in hell!
|
||||
"gclid",
|
||||
"cmpid",
|
||||
"agid",
|
||||
"utm",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"campaignid",
|
||||
"campaign",
|
||||
"AdGroupId",
|
||||
"AdGroup",
|
||||
"TargetId",
|
||||
"msclkid",
|
||||
]
|
||||
for (const dontLike of blacklistedTrackingParams) {
|
||||
url.searchParams.delete(dontLike.toLowerCase())
|
||||
}
|
||||
let cleaned = url.toString()
|
||||
if (cleaned.endsWith("/") && !str.endsWith("/")) {
|
||||
// Do not add a trailing '/' if it wasn't typed originally
|
||||
cleaned = cleaned.substr(0, cleaned.length - 1)
|
||||
}
|
||||
|
||||
if (!str.startsWith("http") && cleaned.startsWith("https://")) {
|
||||
cleaned = cleaned.substr("https://".length)
|
||||
}
|
||||
|
||||
return cleaned
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
isValid(str: string): boolean {
|
||||
try {
|
||||
if (
|
||||
!str.startsWith("http://") &&
|
||||
!str.startsWith("https://") &&
|
||||
!str.startsWith("http:")
|
||||
) {
|
||||
str = "https://" + str
|
||||
}
|
||||
const url = new URL(str)
|
||||
const dotIndex = url.host.indexOf(".")
|
||||
return dotIndex > 0 && url.host[url.host.length - 1] !== "."
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
179
UI/InputElement/Validators/WikidataValidator.ts
Normal file
179
UI/InputElement/Validators/WikidataValidator.ts
Normal file
|
@ -0,0 +1,179 @@
|
|||
import Combine from "../../Base/Combine"
|
||||
import Title from "../../Base/Title"
|
||||
import Table from "../../Base/Table"
|
||||
import Wikidata from "../../../Logic/Web/Wikidata"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import Locale from "../../i18n/Locale"
|
||||
import { Utils } from "../../../Utils"
|
||||
import WikidataSearchBox from "../../Wikipedia/WikidataSearchBox"
|
||||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class WikidataValidator extends Validator {
|
||||
constructor() {
|
||||
super(
|
||||
"wikidata",
|
||||
new Combine([
|
||||
"A wikidata identifier, e.g. Q42.",
|
||||
new Title("Helper arguments"),
|
||||
new Table(
|
||||
["name", "doc"],
|
||||
[
|
||||
["key", "the value of this tag will initialize search (default: name)"],
|
||||
[
|
||||
"options",
|
||||
new Combine([
|
||||
"A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.",
|
||||
new Table(
|
||||
["subarg", "doc"],
|
||||
[
|
||||
[
|
||||
"removePrefixes",
|
||||
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
|
||||
],
|
||||
[
|
||||
"removePostfixes",
|
||||
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
|
||||
],
|
||||
[
|
||||
"instanceOf",
|
||||
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans",
|
||||
],
|
||||
[
|
||||
"notInstanceof",
|
||||
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results",
|
||||
],
|
||||
]
|
||||
),
|
||||
]),
|
||||
],
|
||||
]
|
||||
),
|
||||
new Title("Example usage"),
|
||||
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
|
||||
|
||||
\`\`\`json
|
||||
"freeform": {
|
||||
"key": "name:etymology:wikidata",
|
||||
"type": "wikidata",
|
||||
"helperArgs": [
|
||||
"name",
|
||||
{
|
||||
"removePostfixes": {"en": [
|
||||
"street",
|
||||
"boulevard",
|
||||
"path",
|
||||
"square",
|
||||
"plaza",
|
||||
],
|
||||
"nl": ["straat","plein","pad","weg",laan"],
|
||||
"fr":["route (de|de la|de l'| de le)"]
|
||||
},
|
||||
|
||||
"#": "Remove streets and parks from the search results:"
|
||||
"notInstanceOf": ["Q79007","Q22698"]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Another example is to search for species and trees:
|
||||
|
||||
\`\`\`json
|
||||
"freeform": {
|
||||
"key": "species:wikidata",
|
||||
"type": "wikidata",
|
||||
"helperArgs": [
|
||||
"species",
|
||||
{
|
||||
"instanceOf": [10884, 16521]
|
||||
}]
|
||||
}
|
||||
\`\`\`
|
||||
`,
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
public isValid(str): boolean {
|
||||
if (str === undefined) {
|
||||
return false
|
||||
}
|
||||
if (str.length <= 2) {
|
||||
return false
|
||||
}
|
||||
return !str.split(";").some((str) => Wikidata.ExtractKey(str) === undefined)
|
||||
}
|
||||
|
||||
public reformat(str) {
|
||||
if (str === undefined) {
|
||||
return undefined
|
||||
}
|
||||
let out = str
|
||||
.split(";")
|
||||
.map((str) => Wikidata.ExtractKey(str))
|
||||
.join("; ")
|
||||
if (str.endsWith(";")) {
|
||||
out = out + ";"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
public inputHelper(currentValue, inputHelperOptions) {
|
||||
const args = inputHelperOptions.args ?? []
|
||||
const searchKey = args[0] ?? "name"
|
||||
|
||||
const searchFor = <string>(
|
||||
(inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "")
|
||||
)
|
||||
|
||||
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
|
||||
const options: any = args[1]
|
||||
if (searchFor !== undefined && options !== undefined) {
|
||||
const prefixes = <string[] | Record<string, string[]>>options["removePrefixes"] ?? []
|
||||
const postfixes = <string[] | Record<string, string[]>>options["removePostfixes"] ?? []
|
||||
const defaultValueCandidate = Locale.language.map((lg) => {
|
||||
const prefixesUnrwapped: RegExp[] = (
|
||||
Array.isArray(prefixes) ? prefixes : prefixes[lg] ?? []
|
||||
).map((s) => new RegExp("^" + s, "i"))
|
||||
const postfixesUnwrapped: RegExp[] = (
|
||||
Array.isArray(postfixes) ? postfixes : postfixes[lg] ?? []
|
||||
).map((s) => new RegExp(s + "$", "i"))
|
||||
let clipped = searchFor
|
||||
|
||||
for (const postfix of postfixesUnwrapped) {
|
||||
const match = searchFor.match(postfix)
|
||||
if (match !== null) {
|
||||
clipped = searchFor.substring(0, searchFor.length - match[0].length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of prefixesUnrwapped) {
|
||||
const match = searchFor.match(prefix)
|
||||
if (match !== null) {
|
||||
clipped = searchFor.substring(match[0].length)
|
||||
break
|
||||
}
|
||||
}
|
||||
return clipped
|
||||
})
|
||||
|
||||
defaultValueCandidate.addCallbackAndRun((clipped) => searchForValue.setData(clipped))
|
||||
}
|
||||
|
||||
let instanceOf: number[] = Utils.NoNull(
|
||||
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
|
||||
)
|
||||
let notInstanceOf: number[] = Utils.NoNull(
|
||||
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
|
||||
)
|
||||
|
||||
return new WikidataSearchBox({
|
||||
value: currentValue,
|
||||
searchText: searchForValue,
|
||||
instanceOf,
|
||||
notInstanceOf,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ export class MapLibreAdaptor implements MapProperties {
|
|||
*/
|
||||
private _currentRasterLayer: string
|
||||
|
||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapProperties, "bounds">>) {
|
||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>) {
|
||||
this._maplibreMap = maplibreMap
|
||||
|
||||
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
export let map: Writable<MaplibreMap>
|
||||
|
||||
export let attribution = true
|
||||
let center = {};
|
||||
|
||||
onMount(() => {
|
||||
|
@ -28,6 +29,9 @@
|
|||
<main>
|
||||
<Map bind:center={center}
|
||||
bind:map={$map}
|
||||
{attribution}
|
||||
css="./maplibre-gl.css"
|
||||
|
||||
id="map" location={{lng: 0, lat: 0, zoom: 0}} maxzoom=24 style={styleUrl} />
|
||||
</main>
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ class PointRenderingLayer {
|
|||
store = new ImmutableStore(<OsmTags>feature.properties)
|
||||
}
|
||||
const { html, iconAnchor } = this._config.RenderIcon(store, true)
|
||||
html.SetClass("marker")
|
||||
html.SetClass("marker cursor-pointer")
|
||||
const el = html.ConstructElement()
|
||||
|
||||
if (this._onClick) {
|
||||
|
@ -244,7 +244,7 @@ class LineRenderingLayer {
|
|||
},
|
||||
})
|
||||
|
||||
this._visibility.addCallbackAndRunD((visible) => {
|
||||
this._visibility?.addCallbackAndRunD((visible) => {
|
||||
map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none")
|
||||
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none")
|
||||
})
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Histogram from "../BigComponents/Histogram"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
export class HistogramViz implements SpecialVisualization {
|
||||
funcName = "histogram"
|
||||
docs = "Create a histogram for a list of given values, read from the properties."
|
||||
example =
|
||||
"`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram"
|
||||
'`{histogram(\'some_key\')}` with properties being `{some_key: ["a","b","a","c"]} to create a histogram'
|
||||
args = [
|
||||
{
|
||||
name: "key",
|
||||
|
@ -29,6 +30,22 @@ export class HistogramViz implements SpecialVisualization {
|
|||
},
|
||||
]
|
||||
|
||||
structuredExamples(): { feature: Feature; args: string[] }[] {
|
||||
return [
|
||||
{
|
||||
feature: <Feature>{
|
||||
type: "Feature",
|
||||
properties: { values: `["a","b","a","b","b","b","c","c","c","d","d"]` },
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [0, 0],
|
||||
},
|
||||
},
|
||||
args: ["values"],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
|
|
|
@ -5,10 +5,7 @@ import { Feature } from "geojson"
|
|||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import { stat } from "fs"
|
||||
|
||||
export class MinimapViz implements SpecialVisualization {
|
||||
funcName = "minimap"
|
||||
|
|
|
@ -54,11 +54,6 @@ export default class MoveWizard extends Toggle {
|
|||
options: MoveConfig
|
||||
) {
|
||||
const t = Translations.t.move
|
||||
const loginButton = new Toggle(
|
||||
t.loginToMove.SetClass("btn").onClick(() => state.osmConnection.AttemptLogin()),
|
||||
undefined,
|
||||
state.featureSwitchUserbadge
|
||||
)
|
||||
|
||||
const reasons: MoveReason[] = []
|
||||
if (options.enableRelocation) {
|
||||
|
|
34
UI/Popup/SpecialTranslation.svelte
Normal file
34
UI/Popup/SpecialTranslation.svelte
Normal file
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import SpecialVisualizations from "../SpecialVisualizations";
|
||||
import { onDestroy } from "svelte";
|
||||
import Locale from "../i18n/Locale";
|
||||
import type { RenderingSpecification, SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { Utils } from "../../Utils.js";
|
||||
import type { Feature } from "geojson";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource.js";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import FromHtml from "../Base/FromHtml.svelte";
|
||||
|
||||
/**
|
||||
* The 'specialTranslation' renders a `Translation`-object, but interprets the special values as well
|
||||
*/
|
||||
export let t: Translation;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let feature: Feature;
|
||||
let txt: string;
|
||||
onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||
txt = t.textFor(l);
|
||||
}));
|
||||
let specs: RenderingSpecification[];
|
||||
specs = SpecialVisualizations.constructSpecification(txt);
|
||||
</script>
|
||||
|
||||
{#each specs as specpart}
|
||||
{#if typeof specpart === "string"}
|
||||
<FromHtml src= {Utils.SubstituteKeys(specpart, $tags)}></FromHtml>
|
||||
{:else if $tags !== undefined }
|
||||
<ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature)}></ToSvelte>
|
||||
{/if}
|
||||
{/each}
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { Utils } from "../../Utils";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import TagRenderingMapping from "./TagRenderingMapping.svelte";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { Feature } from "geojson";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let tags: UIEventSource<Record<string, string> | undefined>;
|
||||
let _tags : Record<string, string>
|
||||
onDestroy(tags.addCallbackAndRun(tags => {
|
||||
_tags = tags
|
||||
}))
|
||||
export let state: SpecialVisualizationState
|
||||
export let selectedElement: Feature
|
||||
export let config: TagRenderingConfig;
|
||||
let trs: { then: Translation; icon?: string; iconClass?: string }[];
|
||||
$: trs = Utils.NoNull(config?.GetRenderValues(_tags));
|
||||
</script>
|
||||
|
||||
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(tags))}
|
||||
<div>
|
||||
{#if trs.length === 1}
|
||||
<TagRenderingMapping mapping={trs[0]} {tags} {state} feature={selectedElement}></TagRenderingMapping>
|
||||
{/if}
|
||||
{#if trs.length > 1}
|
||||
{#each trs as mapping}
|
||||
<TagRenderingMapping mapping={trs} {tags} {state} feature=""{selectedElement}></TagRenderingMapping>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
|
@ -6,7 +6,7 @@ import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
|||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import Combine from "../Base/Combine"
|
||||
import Img from "../Base/Img"
|
||||
import { SpecialVisualisationState } from "../SpecialVisualization"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
/***
|
||||
* Displays the correct value for a known tagrendering
|
||||
|
@ -15,7 +15,7 @@ export default class TagRenderingAnswer extends VariableUiElement {
|
|||
constructor(
|
||||
tagsSource: UIEventSource<any>,
|
||||
configuration: TagRenderingConfig,
|
||||
state: SpecialVisualisationState,
|
||||
state: SpecialVisualizationState,
|
||||
contentClasses: string = "",
|
||||
contentStyle: string = "",
|
||||
options?: {
|
||||
|
|
32
UI/Popup/TagRenderingMapping.svelte
Normal file
32
UI/Popup/TagRenderingMapping.svelte
Normal file
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import SpecialTranslation from "./SpecialTranslation.svelte";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { Feature } from "geojson";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
|
||||
export let selectedElement: Feature
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let state: SpecialVisualizationState
|
||||
export let mapping: {
|
||||
then: Translation; icon?: string; iconClass?: | "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "small-height"
|
||||
| "medium-height"
|
||||
| "large-height"
|
||||
};
|
||||
let iconclass = "mapping-icon-" + mapping.iconClass;
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if mapping.icon !== undefined}
|
||||
<div class="flex">
|
||||
<img class={iconclass+" mr-1"} src={mapping.icon}>
|
||||
<SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation>
|
||||
</div>
|
||||
{:else if mapping.then !== undefined}
|
||||
<SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation>
|
||||
{/if}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import { InputElement, ReadonlyInputElement } from "../Input/InputElement"
|
||||
import ValidatedTextField from "../Input/ValidatedTextField"
|
||||
import { FixedInputElement } from "../Input/FixedInputElement"
|
||||
import { RadioButton } from "../Input/RadioButton"
|
||||
import { Utils } from "../../Utils"
|
||||
|
|
|
@ -6,7 +6,6 @@ import BaseUIElement from "../BaseUIElement"
|
|||
import Img from "../Base/Img"
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"
|
||||
|
||||
export default class SingleReview extends Combine {
|
||||
constructor(review: Review & { madeByLoggedInUser: Store<boolean> }) {
|
||||
|
|
|
@ -2,17 +2,13 @@ import { Store, UIEventSource } from "../Logic/UIEventSource"
|
|||
import BaseUIElement from "./BaseUIElement"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import FeatureSource, {
|
||||
IndexedFeatureSource,
|
||||
WritableFeatureSource,
|
||||
} from "../Logic/FeatureSource/FeatureSource"
|
||||
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../Logic/Osm/Changes"
|
||||
import { MapProperties } from "../Models/MapProperties"
|
||||
import LayerState from "../Logic/State/LayerState"
|
||||
import { Feature } from "geojson"
|
||||
import { Feature, Geometry } from "geojson"
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
|
||||
|
@ -58,6 +54,8 @@ export interface SpecialVisualization {
|
|||
funcName: string
|
||||
docs: string | BaseUIElement
|
||||
example?: string
|
||||
|
||||
structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[]
|
||||
args: { name: string; defaultValue?: string; doc: string; required?: false | boolean }[]
|
||||
getLayerDependencies?: (argument: string[]) => string[]
|
||||
|
||||
|
@ -68,3 +66,11 @@ export interface SpecialVisualization {
|
|||
feature: Feature
|
||||
): BaseUIElement
|
||||
}
|
||||
|
||||
export type RenderingSpecification =
|
||||
| string
|
||||
| {
|
||||
func: SpecialVisualization
|
||||
args: string[]
|
||||
style: string
|
||||
}
|
||||
|
|
|
@ -3,7 +3,11 @@ import { FixedUiElement } from "./Base/FixedUiElement"
|
|||
import BaseUIElement from "./BaseUIElement"
|
||||
import Title from "./Base/Title"
|
||||
import Table from "./Base/Table"
|
||||
import { SpecialVisualization } from "./SpecialVisualization"
|
||||
import {
|
||||
RenderingSpecification,
|
||||
SpecialVisualization,
|
||||
SpecialVisualizationState,
|
||||
} from "./SpecialVisualization"
|
||||
import { HistogramViz } from "./Popup/HistogramViz"
|
||||
import { StealViz } from "./Popup/StealViz"
|
||||
import { MinimapViz } from "./Popup/MinimapViz"
|
||||
|
@ -51,10 +55,97 @@ import FeatureReviews from "../Logic/Web/MangroveReviews"
|
|||
import Maproulette from "../Logic/Maproulette"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
export default class SpecialVisualizations {
|
||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
||||
|
||||
/**
|
||||
*
|
||||
* For a given string, returns a specification what parts are fixed and what parts are special renderings.
|
||||
* Note that _normal_ substitutions are ignored.
|
||||
*
|
||||
* // Return empty list on empty input
|
||||
* SubstitutedTranslation.ExtractSpecialComponents("") // => []
|
||||
*
|
||||
* // Advanced cases with commas, braces and newlines should be handled without problem
|
||||
* const templates = SubstitutedTranslation.ExtractSpecialComponents("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.osm.be/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}")
|
||||
* const templ = templates[0]
|
||||
* templ.special.func.funcName // => "send_email"
|
||||
* templ.special.args[0] = "{email}"
|
||||
*/
|
||||
public static constructSpecification(
|
||||
template: string,
|
||||
extraMappings: SpecialVisualization[] = []
|
||||
): RenderingSpecification[] {
|
||||
if (template === "") {
|
||||
return []
|
||||
}
|
||||
|
||||
const allKnownSpecials = extraMappings.concat(SpecialVisualizations.specialVisualizations)
|
||||
for (const knownSpecial of allKnownSpecials) {
|
||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||
const matched = template.match(
|
||||
new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s")
|
||||
)
|
||||
if (matched != null) {
|
||||
// We found a special component that should be brought to live
|
||||
const partBefore = SpecialVisualizations.constructSpecification(
|
||||
matched[1],
|
||||
extraMappings
|
||||
)
|
||||
const argument = matched[2].trim()
|
||||
const style = matched[3]?.substring(1) ?? ""
|
||||
const partAfter = SpecialVisualizations.constructSpecification(
|
||||
matched[4],
|
||||
extraMappings
|
||||
)
|
||||
const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
|
||||
if (argument.length > 0) {
|
||||
const realArgs = argument.split(",").map((str) =>
|
||||
str
|
||||
.trim()
|
||||
.replace(/&LPARENS/g, "(")
|
||||
.replace(/&RPARENS/g, ")")
|
||||
.replace(/&LBRACE/g, "{")
|
||||
.replace(/&RBRACE/g, "}")
|
||||
.replace(/&COMMA/g, ",")
|
||||
)
|
||||
for (let i = 0; i < realArgs.length; i++) {
|
||||
if (args.length <= i) {
|
||||
args.push(realArgs[i])
|
||||
} else {
|
||||
args[i] = realArgs[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const element: RenderingSpecification = {
|
||||
args: args,
|
||||
style: style,
|
||||
func: knownSpecial,
|
||||
}
|
||||
return [...partBefore, element, ...partAfter]
|
||||
}
|
||||
}
|
||||
|
||||
// Let's to a small sanity check to help the theme designers:
|
||||
if (template.search(/{[^}]+\([^}]*\)}/) >= 0) {
|
||||
// Hmm, we might have found an invalid rendering name
|
||||
console.warn(
|
||||
"Found a suspicious special rendering value in: ",
|
||||
template,
|
||||
" did you mean one of: "
|
||||
/*SpecialVisualizations.specialVisualizations
|
||||
.map((sp) => sp.funcName + "()")
|
||||
.join(", ")*/
|
||||
)
|
||||
}
|
||||
|
||||
// IF we end up here, no changes have to be made - except to remove any resting {}
|
||||
return [template]
|
||||
}
|
||||
|
||||
public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
|
||||
if (typeof viz === "string") {
|
||||
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
|
||||
|
@ -649,7 +740,7 @@ export default class SpecialVisualizations {
|
|||
defaultValue: "mr_taskId",
|
||||
},
|
||||
],
|
||||
constr: (state, tagsSource, args, guistate) => {
|
||||
constr: (state, tagsSource, args) => {
|
||||
let [message, image, message_closed, status, maproulette_id_key] = args
|
||||
if (image === "") {
|
||||
image = "confirm"
|
||||
|
@ -720,7 +811,7 @@ export default class SpecialVisualizations {
|
|||
funcName: "statistics",
|
||||
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
|
||||
args: [],
|
||||
constr: (state, tagsSource, args, guiState) => {
|
||||
constr: (state) => {
|
||||
return new Combine(
|
||||
state.layout.layers
|
||||
.filter((l) => l.name !== null)
|
||||
|
@ -852,4 +943,23 @@ export default class SpecialVisualizations {
|
|||
|
||||
return specialVisualizations
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public static renderExampleOfSpecial(
|
||||
state: SpecialVisualizationState,
|
||||
s: SpecialVisualization
|
||||
): BaseUIElement {
|
||||
const examples =
|
||||
s.structuredExamples === undefined
|
||||
? []
|
||||
: s.structuredExamples().map((e) => {
|
||||
return s.constr(
|
||||
state,
|
||||
new UIEventSource<Record<string, string>>(e.feature.properties),
|
||||
e.args,
|
||||
e.feature
|
||||
)
|
||||
})
|
||||
return new Combine([new Title(s.funcName), s.docs, ...examples])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@ import { Utils } from "../Utils"
|
|||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import Combine from "./Base/Combine"
|
||||
import BaseUIElement from "./BaseUIElement"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
|
||||
import LinkToWeblate from "./Base/LinkToWeblate"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
|
||||
import SpecialVisualizations from "./SpecialVisualizations"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
export class SubstitutedTranslation extends VariableUiElement {
|
||||
public constructor(
|
||||
|
@ -21,10 +21,10 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
string,
|
||||
| BaseUIElement
|
||||
| ((
|
||||
state: FeaturePipelineState,
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
guistate: DefaultGuiState
|
||||
feature: Feature
|
||||
) => BaseUIElement)
|
||||
> = undefined
|
||||
) {
|
||||
|
@ -55,19 +55,23 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`)
|
||||
})
|
||||
|
||||
const allElements = SubstitutedTranslation.ExtractSpecialComponents(
|
||||
const allElements = SpecialVisualizations.constructSpecification(
|
||||
txt,
|
||||
extraMappings
|
||||
).map((proto) => {
|
||||
if (proto.fixed !== undefined) {
|
||||
if (typeof proto === "string") {
|
||||
if (tagsSource === undefined) {
|
||||
return Utils.SubstituteKeys(proto.fixed, undefined)
|
||||
return Utils.SubstituteKeys(proto, undefined)
|
||||
}
|
||||
return new VariableUiElement(
|
||||
tagsSource.map((tags) => Utils.SubstituteKeys(proto.fixed, tags))
|
||||
tagsSource.map((tags) => Utils.SubstituteKeys(proto, tags))
|
||||
)
|
||||
}
|
||||
const viz = proto.special
|
||||
const viz: {
|
||||
func: SpecialVisualization
|
||||
args: string[]
|
||||
style: string
|
||||
} = proto
|
||||
if (viz === undefined) {
|
||||
console.error(
|
||||
"SPECIALRENDERING UNDEFINED for",
|
||||
|
@ -77,9 +81,12 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
return undefined
|
||||
}
|
||||
try {
|
||||
const feature = state.indexedFeatures.featuresById.data.get(
|
||||
tagsSource.data.id
|
||||
)
|
||||
return viz.func
|
||||
.constr(state, tagsSource, proto.special.args)
|
||||
?.SetStyle(proto.special.style)
|
||||
.constr(state, tagsSource, proto.args, feature)
|
||||
?.SetStyle(proto.style)
|
||||
} catch (e) {
|
||||
console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e)
|
||||
return new FixedUiElement(
|
||||
|
@ -97,98 +104,4 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
|
||||
this.SetClass("w-full")
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* // Return empty list on empty input
|
||||
* SubstitutedTranslation.ExtractSpecialComponents("") // => []
|
||||
*
|
||||
* // Advanced cases with commas, braces and newlines should be handled without problem
|
||||
* const templates = SubstitutedTranslation.ExtractSpecialComponents("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.osm.be/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}")
|
||||
* const templ = templates[0]
|
||||
* templ.special.func.funcName // => "send_email"
|
||||
* templ.special.args[0] = "{email}"
|
||||
*/
|
||||
public static ExtractSpecialComponents(
|
||||
template: string,
|
||||
extraMappings: SpecialVisualization[] = []
|
||||
): {
|
||||
fixed?: string
|
||||
special?: {
|
||||
func: SpecialVisualization
|
||||
args: string[]
|
||||
style: string
|
||||
}
|
||||
}[] {
|
||||
if (template === "") {
|
||||
return []
|
||||
}
|
||||
|
||||
for (const knownSpecial of extraMappings.concat(
|
||||
[] // TODO enable SpecialVisualizations.specialVisualizations
|
||||
)) {
|
||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||
const matched = template.match(
|
||||
new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s")
|
||||
)
|
||||
if (matched != null) {
|
||||
// We found a special component that should be brought to live
|
||||
const partBefore = SubstitutedTranslation.ExtractSpecialComponents(
|
||||
matched[1],
|
||||
extraMappings
|
||||
)
|
||||
const argument = matched[2].trim()
|
||||
const style = matched[3]?.substring(1) ?? ""
|
||||
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(
|
||||
matched[4],
|
||||
extraMappings
|
||||
)
|
||||
const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
|
||||
if (argument.length > 0) {
|
||||
const realArgs = argument.split(",").map((str) =>
|
||||
str
|
||||
.trim()
|
||||
.replace(/&LPARENS/g, "(")
|
||||
.replace(/&RPARENS/g, ")")
|
||||
.replace(/&LBRACE/g, "{")
|
||||
.replace(/&RBRACE/g, "}")
|
||||
.replace(/&COMMA/g, ",")
|
||||
)
|
||||
for (let i = 0; i < realArgs.length; i++) {
|
||||
if (args.length <= i) {
|
||||
args.push(realArgs[i])
|
||||
} else {
|
||||
args[i] = realArgs[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let element
|
||||
element = {
|
||||
special: {
|
||||
args: args,
|
||||
style: style,
|
||||
func: knownSpecial,
|
||||
},
|
||||
}
|
||||
return [...partBefore, element, ...partAfter]
|
||||
}
|
||||
}
|
||||
|
||||
// Let's to a small sanity check to help the theme designers:
|
||||
if (template.search(/{[^}]+\([^}]*\)}/) >= 0) {
|
||||
// Hmm, we might have found an invalid rendering name
|
||||
console.warn(
|
||||
"Found a suspicious special rendering value in: ",
|
||||
template,
|
||||
" did you mean one of: "
|
||||
/*SpecialVisualizations.specialVisualizations
|
||||
.map((sp) => sp.funcName + "()")
|
||||
.join(", ")*/
|
||||
)
|
||||
}
|
||||
|
||||
// IF we end up here, no changes have to be made - except to remove any resting {}
|
||||
return [{ fixed: template }]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
||||
import Translations from "./i18n/Translations";
|
||||
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import Tr from "./Base/Tr.svelte";
|
||||
|
||||
export let layout: LayoutConfig;
|
||||
const state = new ThemeViewState(layout);
|
||||
|
@ -48,7 +49,7 @@
|
|||
<div class="flex mr-2 items-center">
|
||||
<img class="w-8 h-8 block mr-2" src={layout.icon}>
|
||||
<b>
|
||||
{layout.title}
|
||||
<Tr t={layout.title}></Tr>
|
||||
</b>
|
||||
</div>
|
||||
</MapControlButton>
|
||||
|
@ -58,9 +59,7 @@
|
|||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 mb-4 ml-4">
|
||||
<MapControlButton on:click={() => state.guistate.filterViewIsOpened.setData(true)}>
|
||||
<ToSvelte class="w-7 h-7 block" construct={Svg.layers_ui}></ToSvelte>
|
||||
</MapControlButton>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 right-0 mb-4 mr-4">
|
||||
|
@ -86,17 +85,6 @@
|
|||
</If>
|
||||
</div>
|
||||
|
||||
<If condition={state.guistate.filterViewIsOpened}>
|
||||
<div class="normal-background absolute bottom-0 left-0 flex flex-col">
|
||||
<div on:click={() => state.guistate.filterViewIsOpened.setData(false)}>Close</div>
|
||||
<!-- Filter panel -- TODO move to actual location-->
|
||||
{#each layout.layers as layer}
|
||||
<Filterview filteredLayer={state.layerState.filteredLayers.get(layer.id)}></Filterview>
|
||||
{/each}
|
||||
|
||||
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={state.guistate.welcomeMessageIsOpened}>
|
||||
<!-- Theme page -->
|
||||
|
@ -105,31 +93,47 @@
|
|||
<div on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>Close</div>
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>About</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 2</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<Tr t={layout.title}/>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<Tr t={Translations.t.general.menu.filter}/>
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel class="flex flex-col">
|
||||
<ToSvelte construct={() => layout.description}></ToSvelte>
|
||||
{Translations.t.general.welcomeExplanation.general}
|
||||
<Tr t={layout.description}></Tr>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.general}/>
|
||||
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
||||
<If condition={state.featureSwitches.featureSwitchAddNew}>
|
||||
{Translations.t.general.welcomeExplanation.addNew}
|
||||
<Tr t={Translations.t.general.welcomeExplanation.addNew}/>
|
||||
</If>
|
||||
{/if}
|
||||
|
||||
<!--toTheMap,
|
||||
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
|
||||
-->
|
||||
<ToSvelte construct= {() => layout.descriptionTail}></ToSvelte>
|
||||
<Tr t={layout.descriptionTail}></Tr>
|
||||
<div class="m-x-8">
|
||||
<button class="subtle-background rounded w-full p-4">Explore the map</button>
|
||||
<button class="subtle-background rounded w-full p-4"
|
||||
on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>
|
||||
<Tr t={Translations.t.general.openTheMap} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel>Content 2</TabPanel>
|
||||
<TabPanel>
|
||||
<div class="flex flex-col">
|
||||
<!-- Filter panel -- TODO move to actual location-->
|
||||
{#each layout.layers as layer}
|
||||
<Filterview filteredLayer={state.layerState.filteredLayers.get(layer.id)}></Filterview>
|
||||
{/each}
|
||||
|
||||
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>Content 3</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
@ -163,15 +167,14 @@
|
|||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={selectedElement}>
|
||||
{#if $selectedElement !== undefined && $selectedLayer !== undefined}
|
||||
<div class="absolute top-0 right-0 normal-background">
|
||||
|
||||
<SelectedElementView layer={selectedLayer} {selectedElement}
|
||||
tags={selectedElementTags}></SelectedElementView>
|
||||
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
||||
tags={$selectedElementTags} state={state}></SelectedElementView>
|
||||
|
||||
</div>
|
||||
</If>
|
||||
|
||||
{/if}
|
||||
<style>
|
||||
/* WARNING: This is just for demonstration.
|
||||
Using :global() in this way can be risky. */
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata"
|
||||
import { Translation, TypedTranslation } from "../i18n/Translation"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
|
|
|
@ -51,10 +51,6 @@ export class Translation extends BaseUIElement {
|
|||
return this.textFor(Translation.forcedLanguage ?? Locale.language.data)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.txt
|
||||
}
|
||||
|
||||
static ExtractAllTranslationsFrom(
|
||||
object: any,
|
||||
context = ""
|
||||
|
@ -91,6 +87,10 @@ export class Translation extends BaseUIElement {
|
|||
return new Translation(translations)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.txt
|
||||
}
|
||||
|
||||
Destroy() {
|
||||
super.Destroy()
|
||||
this.isDestroyed = true
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { Utils } from "./Utils"
|
||||
import AllThemesGui from "./UI/AllThemesGui"
|
||||
import { QueryParameters } from "./Logic/Web/QueryParameters"
|
||||
import StatisticsGUI from "./UI/StatisticsGUI"
|
||||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||
import { PdfExportGui } from "./UI/BigComponents/PdfExportGui"
|
||||
|
||||
const layout = QueryParameters.GetQueryParameter("layout", undefined).data ?? ""
|
||||
const customLayout = QueryParameters.GetQueryParameter("userlayout", undefined).data ?? ""
|
||||
|
@ -32,23 +29,4 @@ if (layout !== "") {
|
|||
}
|
||||
|
||||
Utils.DisableLongPresses()
|
||||
document.getElementById("decoration-desktop").remove()
|
||||
const mode = QueryParameters.GetQueryParameter(
|
||||
"mode",
|
||||
"map",
|
||||
"The mode the application starts in, e.g. 'statistics'"
|
||||
)
|
||||
|
||||
if (mode.data === "statistics") {
|
||||
console.log("Statistics mode!")
|
||||
new FixedUiElement("").AttachTo("centermessage")
|
||||
new StatisticsGUI().SetClass("w-full h-full pointer-events-auto").AttachTo("topleft-tools")
|
||||
} else if (mode.data === "pdf") {
|
||||
new FixedUiElement("").AttachTo("centermessage")
|
||||
const div = document.createElement("div")
|
||||
div.id = "extra_div_for_maps"
|
||||
new PdfExportGui(div.id).SetClass("pointer-events-auto").AttachTo("topleft-tools")
|
||||
document.getElementById("topleft-tools").appendChild(div)
|
||||
} else {
|
||||
new AllThemesGui().setup()
|
||||
}
|
||||
new AllThemesGui().setup()
|
||||
|
|
|
@ -324,7 +324,6 @@
|
|||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
"id": "max_bolts",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"description": {
|
||||
"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,
|
||||
"tagRenderings": [
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "import_candidate",
|
||||
"description": "Layer used as template in the importHelper",
|
||||
"source":"special",
|
||||
"source": "special",
|
||||
"mapRendering": [
|
||||
{
|
||||
"location": [
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "matchpoint",
|
||||
"description": "The default rendering for a locationInput which snaps onto another object",
|
||||
"source":"special",
|
||||
"source": "special",
|
||||
"mapRendering": [
|
||||
{
|
||||
"location": [
|
||||
|
|
|
@ -1183,6 +1183,10 @@ video {
|
|||
width: auto;
|
||||
}
|
||||
|
||||
.w-48 {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.min-w-min {
|
||||
min-width: -webkit-min-content;
|
||||
min-width: min-content;
|
||||
|
@ -1509,6 +1513,11 @@ video {
|
|||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-red-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-opacity-50 {
|
||||
--tw-border-opacity: 0.5;
|
||||
}
|
||||
|
@ -1580,10 +1589,6 @@ video {
|
|||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.p-3 {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
@ -1596,6 +1601,10 @@ video {
|
|||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
|
@ -1921,6 +1930,10 @@ video {
|
|||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.ease-in-out {
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.z-above-map {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
@ -1971,8 +1984,6 @@ video {
|
|||
--shadow-color: #00000066;
|
||||
--return-to-the-map-height: 2em;
|
||||
--image-carousel-height: 350px;
|
||||
/* The border colour of the leaflet popup */
|
||||
--popup-border: white;
|
||||
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
|
||||
--variable-title-height: 0px;
|
||||
}
|
||||
|
@ -1989,31 +2000,6 @@ body {
|
|||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.leaflet-overlay-pane .leaflet-zoom-animated {
|
||||
/* Another workaround to keep leaflet working */
|
||||
width: initial !important;
|
||||
height: initial !important;
|
||||
box-sizing: initial !important;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon img {
|
||||
-webkit-touch-callout: none;
|
||||
/* prevent callout to copy image, etc when tap to hold */
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.badge {
|
||||
}
|
||||
|
||||
.badge svg {
|
||||
/*Workaround for leaflet*/
|
||||
width: unset !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
svg,
|
||||
img {
|
||||
box-sizing: content-box;
|
||||
|
@ -2255,28 +2241,6 @@ li::marker {
|
|||
fill: var(--catch-detail-color) !important;
|
||||
}
|
||||
|
||||
#leafletDiv {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
background-color: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
border: 2px solid var(--popup-border);
|
||||
box-shadow: 0 3px 14px var(--shadow-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
font: unset !important;
|
||||
background-color: var(--background-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background-color: var(--popup-border) !important;
|
||||
color: var(--popup-border) !important;
|
||||
box-shadow: 0 3px 14px var(--shadow-color) !important;
|
||||
}
|
||||
|
||||
.single-layer-selection-toggle {
|
||||
position: relative;
|
||||
width: 2em;
|
||||
|
@ -2408,131 +2372,17 @@ li::marker {
|
|||
}
|
||||
}
|
||||
|
||||
.hand-drag-animation {
|
||||
-webkit-animation: hand-drag-animation 6s ease-in-out infinite;
|
||||
animation: hand-drag-animation 6s ease-in-out infinite;
|
||||
-webkit-transform-origin: 50% 125%;
|
||||
transform-origin: 50% 125%;
|
||||
}
|
||||
|
||||
@-webkit-keyframes hand-drag-animation {
|
||||
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
|
||||
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
6% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
12% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
24% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(-00deg);
|
||||
transform: rotate(-00deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
36% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hand-drag-animation {
|
||||
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
|
||||
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
6% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
12% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
24% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(-00deg);
|
||||
transform: rotate(-00deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
36% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(-30deg);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
}
|
||||
|
||||
/***************** Info box (box containing features and questions ******************/
|
||||
|
||||
input {
|
||||
color: var(--foreground-color);
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
width: 45em !important;
|
||||
margin: 0.25rem !important;
|
||||
}
|
||||
|
||||
.leaflet-div-icon {
|
||||
background-color: unset !important;
|
||||
border: unset !important;
|
||||
}
|
||||
|
||||
.floating-element-width {
|
||||
max-width: calc(100vw - 5em);
|
||||
width: 40em;
|
||||
}
|
||||
|
||||
.leaflet-div-icon svg {
|
||||
width: calc(100%);
|
||||
height: calc(100%);
|
||||
}
|
||||
|
||||
/****** ShareScreen *****/
|
||||
|
||||
.literal-code {
|
||||
|
|
103
index.css
103
index.css
|
@ -87,9 +87,6 @@
|
|||
--return-to-the-map-height: 2em;
|
||||
--image-carousel-height: 350px;
|
||||
|
||||
/* The border colour of the leaflet popup */
|
||||
--popup-border: white;
|
||||
|
||||
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
|
||||
--variable-title-height: 0px;
|
||||
}
|
||||
|
@ -106,29 +103,6 @@ body {
|
|||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.leaflet-overlay-pane .leaflet-zoom-animated {
|
||||
/* Another workaround to keep leaflet working */
|
||||
width: initial !important;
|
||||
height: initial !important;
|
||||
box-sizing: initial !important;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon img {
|
||||
-webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.badge {
|
||||
}
|
||||
|
||||
.badge svg {
|
||||
/*Workaround for leaflet*/
|
||||
width: unset !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
svg,
|
||||
img {
|
||||
|
@ -365,27 +339,6 @@ li::marker {
|
|||
fill: var(--catch-detail-color) !important;
|
||||
}
|
||||
|
||||
#leafletDiv {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
background-color: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
border: 2px solid var(--popup-border);
|
||||
box-shadow: 0 3px 14px var(--shadow-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
font: unset !important;
|
||||
background-color: var(--background-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background-color: var(--popup-border) !important;
|
||||
color: var(--popup-border) !important;
|
||||
box-shadow: 0 3px 14px var(--shadow-color) !important;
|
||||
}
|
||||
|
||||
.single-layer-selection-toggle {
|
||||
position: relative;
|
||||
|
@ -499,48 +452,6 @@ li::marker {
|
|||
}
|
||||
}
|
||||
|
||||
.hand-drag-animation {
|
||||
animation: hand-drag-animation 6s ease-in-out infinite;
|
||||
transform-origin: 50% 125%;
|
||||
}
|
||||
|
||||
@keyframes hand-drag-animation {
|
||||
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
6% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
12% {
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
24% {
|
||||
opacity: 1;
|
||||
transform: rotate(-00deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
36% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************** Info box (box containing features and questions ******************/
|
||||
|
@ -549,25 +460,11 @@ input {
|
|||
color: var(--foreground-color);
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
width: 45em !important;
|
||||
margin: 0.25rem !important;
|
||||
}
|
||||
|
||||
.leaflet-div-icon {
|
||||
background-color: unset !important;
|
||||
border: unset !important;
|
||||
}
|
||||
|
||||
.floating-element-width {
|
||||
max-width: calc(100vw - 5em);
|
||||
width: 40em;
|
||||
}
|
||||
|
||||
.leaflet-div-icon svg {
|
||||
width: calc(100%);
|
||||
height: calc(100%);
|
||||
}
|
||||
|
||||
/****** ShareScreen *****/
|
||||
|
||||
|
|
16
index.html
16
index.html
|
@ -51,22 +51,8 @@
|
|||
</head>
|
||||
<body>
|
||||
|
||||
<div id="decoration-desktop" style="position: fixed; left: 1em; bottom: 1em; width:35vh; height:35vh;">
|
||||
<!-- A nice decoration while loading or on errors -->
|
||||
<!-- DECORATION 0 START -->
|
||||
<img src="./assets/svg/add.svg"/>
|
||||
<!-- DECORATION 0 END -->
|
||||
</div>
|
||||
|
||||
<div id="top-left">
|
||||
</div>
|
||||
|
||||
<div class="clutter absolute h-24 left-24 right-24 top-56 text-xl text-center"
|
||||
id="centermessage" style="z-index: 4000">
|
||||
Loading MapComplete, hang on...
|
||||
</div>
|
||||
|
||||
|
||||
<div id="main"></div>
|
||||
<script type="module" src="./all_themes_index.ts"></script>
|
||||
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js"></script>
|
||||
|
||||
|
|
1
index.ts
1
index.ts
|
@ -4,7 +4,6 @@ import { Utils } from "./Utils"
|
|||
import AllThemesGui from "./UI/AllThemesGui"
|
||||
import DetermineLayout from "./Logic/DetermineLayout"
|
||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
|
||||
import DefaultGUI from "./UI/DefaultGUI"
|
||||
import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"
|
||||
import { DefaultGuiState } from "./UI/DefaultGuiState"
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
},
|
||||
"general": {
|
||||
"about": "Easily edit and add OpenStreetMap for a certain theme",
|
||||
"aboutMapcomplete": "<h3>About</h3><p>Use MapComplete to add OpenStreetMap info on a <b>single theme.</b> Answer questions, and within minutes your contributions are available everywhere. In most themes you can add pictures or even leave a review. The <b>theme maintainer</b> defines elements, questions and languages for it.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete.</li><li>The fullscreen version offers info about OpenStreetMap.</li><li>Viewing works without login, but editing requires an OSM account.</li><li>If you are not logged in, you are asked to do so</li><li>Once you answered a single question, you can add new features to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='{osmcha_link}' target='_blank' >OsmCha</a>.</p>",
|
||||
"aboutMapcomplete": "<p>Use MapComplete to add OpenStreetMap info on a <b>single theme.</b> Answer questions, and within minutes your contributions are available everywhere. In most themes you can add pictures or even leave a review. The <b>theme maintainer</b> defines elements, questions and languages for it.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete.</li><li>The fullscreen version offers info about OpenStreetMap.</li><li>Viewing works without login, but editing requires an OSM account.</li><li>If you are not logged in, you are asked to do so</li><li>Once you answered a single question, you can add new features to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='{osmcha_link}' target='_blank' >OsmCha</a>.</p>",
|
||||
"add": {
|
||||
"addNew": "Add {category}",
|
||||
"addNewMapLabel": "Click here to add a new item",
|
||||
|
@ -203,6 +203,10 @@
|
|||
"loginToStart": "Log in to answer this question",
|
||||
"loginWithOpenStreetMap": "Login with OpenStreetMap",
|
||||
"logout": "Log out",
|
||||
"menu": {
|
||||
"aboutMapComplete": "About MapComplete",
|
||||
"filter": "Filter data"
|
||||
},
|
||||
"morescreen": {
|
||||
"createYourOwnTheme": "Create your own MapComplete theme from scratch",
|
||||
"hiddenExplanation": "These themes are only accessible to those with the link. You have discovered {hidden_discovered} of {total_hidden} hidden themes.",
|
||||
|
|
|
@ -1809,9 +1809,6 @@
|
|||
"gps_track": {
|
||||
"name": "La teva traça recorreguda"
|
||||
},
|
||||
"grass_in_parks": {
|
||||
"description": "Cerques per a tots els camins d'herba accessibles dins dels parcs públics - aquests són «groenzones»"
|
||||
},
|
||||
"hackerspace": {
|
||||
"presets": {
|
||||
"1": {
|
||||
|
|
|
@ -4750,9 +4750,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"grass_in_parks": {
|
||||
"description": "Sucht nach allen zugänglichen Grasflächen in öffentlichen Parks - dies sind 'Grünzonen'"
|
||||
},
|
||||
"hackerspace": {
|
||||
"description": "Hackerspace",
|
||||
"name": "Hackerspaces",
|
||||
|
|
|
@ -4750,9 +4750,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"grass_in_parks": {
|
||||
"description": "Searches for all accessible grass patches within public parks - these are 'groenzones'"
|
||||
},
|
||||
"hackerspace": {
|
||||
"description": "Hackerspace",
|
||||
"name": "Hackerspace",
|
||||
|
|
|
@ -4657,18 +4657,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"grass_in_parks": {
|
||||
"description": "Dit zoekt naar alle toegankelijke grasvelden binnen publieke parken - dit zijn 'groenzones'",
|
||||
"name": "Toegankelijke grasvelden in parken",
|
||||
"title": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "{name}"
|
||||
}
|
||||
},
|
||||
"render": "Speelweide in een park"
|
||||
}
|
||||
},
|
||||
"hackerspace": {
|
||||
"description": "Hackerspace",
|
||||
"name": "Hackerspace",
|
||||
|
|
|
@ -661,7 +661,7 @@
|
|||
"grb": {
|
||||
"description": "Aquest tema és un intent d'automatitzar la importació GRB.",
|
||||
"layers": {
|
||||
"1": {
|
||||
"0": {
|
||||
"tagRenderings": {
|
||||
"building type": {
|
||||
"question": "Quin tipus d'edifici és aquest?"
|
||||
|
|
|
@ -614,14 +614,14 @@
|
|||
},
|
||||
"grb": {
|
||||
"layers": {
|
||||
"1": {
|
||||
"0": {
|
||||
"tagRenderings": {
|
||||
"building type": {
|
||||
"question": "Jaký druh budovy je toto?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"5": {
|
||||
"tagRenderings": {
|
||||
"Import-button": {
|
||||
"mappings": {
|
||||
|
|
|
@ -552,14 +552,14 @@
|
|||
"grb": {
|
||||
"description": "Dette tema er et forsøg på at hjælpe med at automatisere GRB-importen.",
|
||||
"layers": {
|
||||
"1": {
|
||||
"0": {
|
||||
"tagRenderings": {
|
||||
"building type": {
|
||||
"question": "Hvad er det for en bygning?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"5": {
|
||||
"tagRenderings": {
|
||||
"Import-button": {
|
||||
"mappings": {
|
||||
|
|
|
@ -689,14 +689,14 @@
|
|||
"grb": {
|
||||
"description": "Dieses Thema ist ein Versuch, die Automatisierung des GRB-Imports zu unterstützen.",
|
||||
"layers": {
|
||||
"1": {
|
||||
"0": {
|
||||
"tagRenderings": {
|
||||
"building type": {
|
||||
"question": "Was ist das für ein Gebäude?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"5": {
|
||||
"tagRenderings": {
|
||||
"Import-button": {
|
||||
"mappings": {
|
||||
|
|
|
@ -689,14 +689,14 @@
|
|||
"grb": {
|
||||
"description": "This theme is an attempt to help automating the GRB import.",
|
||||
"layers": {
|
||||
"1": {
|
||||
"0": {
|
||||
"tagRenderings": {
|
||||
"building type": {
|
||||
"question": "What kind of building is this?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"5": {
|
||||
"tagRenderings": {
|
||||
"Import-button": {
|
||||
"mappings": {
|
||||
|
|
|
@ -689,14 +689,14 @@
|
|||
"grb": {
|
||||
"description": "Este tema es un intento de automatizar la importación GRB.",
|
||||
"layers": {
|
||||
"1": {
|
||||
"0": {
|
||||
"tagRenderings": {
|
||||
"building type": {
|
||||
"question": "¿Qué tipo de edificio es este?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"5": {
|
||||
"tagRenderings": {
|
||||
"Import-button": {
|
||||
"mappings": {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue