From 4d48b1cf2ba7c0355d16b9f9a85f1a2fd4e0c75b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 24 Mar 2023 19:21:15 +0100 Subject: [PATCH] refactoring(maplibre): WIP --- Customizations/AllKnownLayouts.ts | 10 + Logic/Actors/GeoLocationHandler.ts | 164 +++- Logic/Actors/InitialMapPositioning.ts | 64 ++ Logic/BBox.ts | 8 - Logic/ExtraFunctions.ts | 4 +- .../RenderingMultiPlexerFeatureSource.ts | 168 ---- Logic/GeoOperations.ts | 73 +- Logic/Maproulette.ts | 3 +- .../CreateMultiPolygonWithPointReuseAction.ts | 9 - Logic/Osm/Changes.ts | 13 +- Logic/Osm/Geocoding.ts | 5 +- Logic/State/ElementsState.ts | 91 --- Logic/State/FeaturePipelineState.ts | 36 +- Logic/State/GeoLocationState.ts | 4 +- Logic/State/MapState.ts | 248 +----- Logic/State/UserRelatedState.ts | 153 ++-- Models/GlobalFilter.ts | 13 + Models/LeafletMap.ts | 3 - Models/MapProperties.ts | 14 + Models/ThemeConfig/Conversion/PrepareLayer.ts | 2 - Models/ThemeConfig/Conversion/PrepareTheme.ts | 4 +- Models/ThemeConfig/Conversion/Validation.ts | 47 ++ Models/ThemeConfig/FilterConfig.ts | 7 +- Models/ThemeConfig/Json/LayerConfigJson.ts | 5 +- .../Json/PointRenderingConfigJson.ts | 7 +- Models/ThemeConfig/LayerConfig.ts | 19 +- Models/ThemeConfig/LayoutConfig.ts | 1 - Models/ThemeConfig/PointRenderingConfig.ts | 4 +- Models/ThemeConfig/TagRenderingConfig.ts | 20 +- UI/AutomatonGui.ts | 476 ----------- UI/Base/If.svelte | 14 + UI/Base/MapControlButton.svelte | 13 + UI/Base/Minimap.ts | 47 -- UI/Base/MinimapImplementation.ts | 422 ---------- UI/Base/SvelteUIElement.ts | 1 + UI/BigComponents/AddNewMarker.ts | 6 +- UI/BigComponents/Attribution.ts | 92 --- UI/BigComponents/GeolocationControl.ts | 30 +- UI/BigComponents/LeftControls.ts | 3 +- UI/BigComponents/LevelSelector.ts | 3 +- UI/BigComponents/RightControls.ts | 25 +- UI/BigComponents/SearchAndGo.ts | 11 +- UI/BigComponents/SimpleAddUI.ts | 7 +- UI/DashboardGui.ts | 305 ------- UI/DefaultGUI.ts | 3 - UI/ImportFlow/ConflationChecker.ts | 6 - UI/ImportFlow/MapPreview.ts | 38 +- UI/Map/MapLibreAdaptor.ts | 142 +++- UI/Map/MaplibreMap.svelte | 3 - UI/Map/ShowDataLayer.ts | 268 ++++++- .../ShowDataLayerOptions.ts | 2 +- .../ShowDataMultiLayer.ts | 11 +- UI/NewPoint/ConfirmLocationOfPoint.ts | 12 +- UI/{ => Popup}/AllTagsPanel.svelte | 6 +- UI/Popup/ImportButton.ts | 3 +- UI/Popup/SidedMinimap.ts | 52 -- UI/ShowDataLayer/ShowDataLayer.ts | 27 - .../ShowDataLayerImplementation.ts | 407 ---------- UI/ShowDataLayer/ShowTileInfo.ts | 64 -- UI/ShowDataLayer/TileHierarchyAggregator.ts | 57 +- UI/SpecialVisualizations.ts | 6 +- UI/ThemeViewGUI.svelte | 114 +++ UI/i18n/Translation.ts | 3 +- all_themes_index.ts | 3 - .../layers/cluster_style/cluster_style.json | 49 -- assets/layers/conflation/conflation.json | 11 +- assets/layers/current_view/current_view.json | 7 +- assets/layers/filters/filters.json | 4 +- assets/layers/gps_location/gps_location.json | 7 +- .../gps_location_history.json | 8 +- assets/layers/gps_track/gps_track.json | 5 +- .../layers/home_location/home_location.json | 7 +- assets/layers/icons/icons.json | 6 +- assets/layers/id_presets/id_presets.json | 6 +- .../import_candidate/import_candidate.json | 8 +- .../left_right_style/left_right_style.json | 35 - assets/layers/matchpoint/matchpoint.json | 8 +- assets/layers/range/range.json | 14 + assets/layers/type_node/type_node.json | 7 +- assets/layers/usersettings/usersettings.json | 6 +- css/index-tailwind-output.css | 198 +++-- index.ts | 16 +- package.json | 6 - public/vendor/images/layers-2x.png | Bin 1259 -> 0 bytes public/vendor/images/layers.png | Bin 696 -> 0 bytes public/vendor/images/marker-icon.png | Bin 1466 -> 0 bytes public/vendor/leaflet.css | 754 ------------------ test.ts | 95 +-- theme.html | 1 - 89 files changed, 1166 insertions(+), 3973 deletions(-) create mode 100644 Logic/Actors/InitialMapPositioning.ts delete mode 100644 Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts delete mode 100644 Logic/State/ElementsState.ts create mode 100644 Models/GlobalFilter.ts delete mode 100644 Models/LeafletMap.ts create mode 100644 Models/MapProperties.ts delete mode 100644 UI/AutomatonGui.ts create mode 100644 UI/Base/If.svelte create mode 100644 UI/Base/MapControlButton.svelte delete mode 100644 UI/Base/Minimap.ts delete mode 100644 UI/Base/MinimapImplementation.ts delete mode 100644 UI/BigComponents/Attribution.ts delete mode 100644 UI/DashboardGui.ts rename UI/{ShowDataLayer => Map}/ShowDataLayerOptions.ts (95%) rename UI/{ShowDataLayer => Map}/ShowDataMultiLayer.ts (73%) rename UI/{ => Popup}/AllTagsPanel.svelte (88%) delete mode 100644 UI/Popup/SidedMinimap.ts delete mode 100644 UI/ShowDataLayer/ShowDataLayer.ts delete mode 100644 UI/ShowDataLayer/ShowDataLayerImplementation.ts delete mode 100644 UI/ShowDataLayer/ShowTileInfo.ts create mode 100644 UI/ThemeViewGUI.svelte delete mode 100644 assets/layers/cluster_style/cluster_style.json delete mode 100644 assets/layers/left_right_style/left_right_style.json create mode 100644 assets/layers/range/range.json delete mode 100644 public/vendor/images/layers-2x.png delete mode 100644 public/vendor/images/layers.png delete mode 100644 public/vendor/images/marker-icon.png delete mode 100644 public/vendor/leaflet.css diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 12c27223c..f01bfe24d 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -49,4 +49,14 @@ export class AllKnownLayoutsLazy { export class AllKnownLayouts { public static allKnownLayouts: AllKnownLayoutsLazy = new AllKnownLayoutsLazy() + + static AllPublicLayers() { + const layers = [].concat( + ...this.allKnownLayouts + .values() + .filter((layout) => !layout.hideFromOverview) + .map((layout) => layout.layers) + ) + return layers + } } diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index a5f51959e..fe709f3e1 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -3,9 +3,13 @@ import { BBox } from "../BBox" import Constants from "../../Models/Constants" import { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState" import { UIEventSource } from "../UIEventSource" -import Loc from "../../Models/Loc" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" +import { Feature, LineString, Point } from "geojson" +import FeatureSource from "../FeatureSource/FeatureSource" +import { LocalStorageSource } from "../Web/LocalStorageSource" +import { GeoOperations } from "../GeoOperations" +import { OsmTags } from "../../Models/OsmFeature" +import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" +import { MapProperties } from "../../Models/MapProperties" /** * The geolocation-handler takes a map-location and a geolocation state. @@ -14,28 +18,39 @@ import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" */ export default class GeoLocationHandler { public readonly geolocationState: GeoLocationState - private readonly _state: { - currentUserLocation: SimpleFeatureSource - layoutToUse: LayoutConfig - locationControl: UIEventSource - selectedElement: UIEventSource - leafletMap?: UIEventSource - } + + /** + * The location as delivered by the GPS, wrapped as FeatureSource + */ + public currentUserLocation: FeatureSource + + /** + * All previously visited points (as 'Point'-objects), with their metadata + */ + public historicalUserLocations: FeatureSource + + /** + * A featureSource containing a single linestring which has the GPS-history of the user. + * However, metadata (such as when every single point was visited) is lost here (but is kept in `historicalUserLocations`. + * Note that this featureSource is _derived_ from 'historicalUserLocations' + */ + public historicalUserLocationsTrack: FeatureSource public readonly mapHasMoved: UIEventSource = new UIEventSource(false) + private readonly selectedElement: UIEventSource + private readonly mapProperties?: MapProperties + private readonly gpsLocationHistoryRetentionTime?: UIEventSource constructor( geolocationState: GeoLocationState, - state: { - locationControl: UIEventSource - currentUserLocation: SimpleFeatureSource - layoutToUse: LayoutConfig - selectedElement: UIEventSource - leafletMap?: UIEventSource - } + selectedElement: UIEventSource, + mapProperties?: MapProperties, + gpsLocationHistoryRetentionTime?: UIEventSource ) { this.geolocationState = geolocationState - this._state = state - const mapLocation = state.locationControl + const mapLocation = mapProperties.location + this.selectedElement = selectedElement + this.mapProperties = mapProperties + this.gpsLocationHistoryRetentionTime = gpsLocationHistoryRetentionTime // Did an interaction move the map? let self = this let initTime = new Date() @@ -54,7 +69,7 @@ export default class GeoLocationHandler { this.mapHasMoved.setData(true) } - this.geolocationState.currentGPSLocation.addCallbackAndRunD((newLocation) => { + this.geolocationState.currentGPSLocation.addCallbackAndRunD((_) => { const timeSinceLastRequest = (new Date().getTime() - geolocationState.requestMoment.data?.getTime() ?? 0) / 1000 if (!this.mapHasMoved.data) { @@ -65,25 +80,17 @@ export default class GeoLocationHandler { self.MoveMapToCurrentLocation() } - if (this.geolocationState.isLocked.data) { + if (!this.geolocationState.allowMoving.data) { // Jup, the map is locked to the bound location: move automatically self.MoveMapToCurrentLocation() return } }) - geolocationState.isLocked.map( - (isLocked) => { - if (isLocked) { - state.leafletMap?.data?.dragging?.disable() - } else { - state.leafletMap?.data?.dragging?.enable() - } - }, - [state.leafletMap] - ) + geolocationState.allowMoving.syncWith(mapProperties.allowMoving, true) this.CopyGeolocationIntoMapstate() + this.initUserLocationTrail() } /** @@ -95,12 +102,11 @@ export default class GeoLocationHandler { */ public MoveMapToCurrentLocation() { const newLocation = this.geolocationState.currentGPSLocation.data - const mapLocation = this._state.locationControl - const state = this._state + const mapLocation = this.mapProperties.location // We got a new location. // Do we move the map to it? - if (state.selectedElement.data !== undefined) { + if (this.selectedElement.data !== undefined) { // Nope, there is something selected, so we don't move to the current GPS-location return } @@ -110,8 +116,8 @@ export default class GeoLocationHandler { } // We check that the GPS location is not out of bounds - const bounds = state.layoutToUse.lockLocation - if (bounds && bounds !== true) { + const bounds = this.mapProperties.maxbounds.data + if (bounds !== undefined) { // B is an array with our lock-location const inRange = new BBox(bounds).contains([newLocation.longitude, newLocation.latitude]) if (!inRange) { @@ -119,22 +125,25 @@ export default class GeoLocationHandler { } } + console.trace("Moving the map to the GPS-location") mapLocation.setData({ - zoom: Math.max(mapLocation.data.zoom, 16), lon: newLocation.longitude, lat: newLocation.latitude, }) + const zoom = this.mapProperties.zoom + zoom.setData(Math.max(zoom.data, 16)) this.mapHasMoved.setData(true) this.geolocationState.requestMoment.setData(undefined) } private CopyGeolocationIntoMapstate() { - const state = this._state + const features: UIEventSource = new UIEventSource([]) + this.currentUserLocation = new StaticFeatureSource(features) this.geolocationState.currentGPSLocation.addCallbackAndRun((location) => { if (location === undefined) { return } - const feature = { + const feature = { type: "Feature", properties: { id: "gps", @@ -148,7 +157,82 @@ export default class GeoLocationHandler { }, } - state.currentUserLocation?.features?.setData([{ feature, freshness: new Date() }]) + features.setData([feature]) }) } + + private initUserLocationTrail() { + const features = LocalStorageSource.GetParsed("gps_location_history", []) + const now = new Date().getTime() + features.data = features.data.filter((ff) => { + if (ff.properties === undefined) { + return false + } + const point_time = new Date(ff.properties["date"]) + return ( + now - point_time.getTime() < + 1000 * (this.gpsLocationHistoryRetentionTime?.data ?? 24 * 60 * 60 * 1000) + ) + }) + features.ping() + let i = 0 + this.currentUserLocation?.features?.addCallbackAndRunD(([location]: [Feature]) => { + if (location === undefined) { + return + } + + const previousLocation = >features.data[features.data.length - 1] + if (previousLocation !== undefined) { + const previousLocationFreshness = new Date(previousLocation.properties.date) + const d = GeoOperations.distanceBetween( + <[number, number]>previousLocation.geometry.coordinates, + <[number, number]>location.geometry.coordinates + ) + let timeDiff = Number.MAX_VALUE // in seconds + const olderLocation = features.data[features.data.length - 2] + + if (olderLocation !== undefined) { + const olderLocationFreshness = new Date(olderLocation.properties.date) + timeDiff = + (new Date(previousLocationFreshness).getTime() - + new Date(olderLocationFreshness).getTime()) / + 1000 + } + if (d < 20 && timeDiff < 60) { + // Do not append changes less then 20m - it's probably noise anyway + return + } + } + + const feature = JSON.parse(JSON.stringify(location)) + feature.properties.id = "gps/" + features.data.length + i++ + features.data.push(feature) + features.ping() + }) + + this.historicalUserLocations = new StaticFeatureSource(features) + + const asLine = features.map((allPoints) => { + if (allPoints === undefined || allPoints.length < 2) { + return [] + } + + const feature: Feature = { + type: "Feature", + properties: { + id: "location_track", + "_date:now": new Date().toISOString(), + }, + geometry: { + type: "LineString", + coordinates: allPoints.map( + (ff: Feature) => <[number, number]>ff.geometry.coordinates + ), + }, + } + return [feature] + }) + this.historicalUserLocationsTrack = new StaticFeatureSource(asLine) + } } diff --git a/Logic/Actors/InitialMapPositioning.ts b/Logic/Actors/InitialMapPositioning.ts new file mode 100644 index 000000000..a51aea6c2 --- /dev/null +++ b/Logic/Actors/InitialMapPositioning.ts @@ -0,0 +1,64 @@ +import { UIEventSource } from "../UIEventSource" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { LocalStorageSource } from "../Web/LocalStorageSource" +import { QueryParameters } from "../Web/QueryParameters" + +/** + * This actor is responsible to set the map location. + * It will attempt to + * - Set the map to the position as passed in by the query parameters (if available) + * - Set the map to the position remembered in LocalStorage (if available) + * - Set the map to the layout default + * + * Additionally, it will save the map location to local storage + */ +export default class InitialMapPositioning { + public zoom: UIEventSource + public location: UIEventSource<{ lon: number; lat: number }> + constructor(layoutToUse: LayoutConfig) { + function localStorageSynced( + key: string, + deflt: number, + docs: string + ): UIEventSource { + const localStorage = LocalStorageSource.Get(key) + const previousValue = localStorage.data + const src = UIEventSource.asFloat( + QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage) + ) + + if (src.data === deflt) { + const prev = Number(previousValue) + if (!isNaN(prev)) { + src.setData(prev) + } + } + + return src + } + + // -- Location control initialization + this.zoom = localStorageSynced( + "z", + layoutToUse?.startZoom ?? 1, + "The initial/current zoom level" + ) + const lat = localStorageSynced( + "lat", + layoutToUse?.startLat ?? 0, + "The initial/current latitude" + ) + const lon = localStorageSynced( + "lon", + layoutToUse?.startLon ?? 0, + "The initial/current longitude of the app" + ) + + this.location = new UIEventSource({ lon: lon.data, lat: lat.data }) + this.location.addCallbackD((loc) => { + lat.setData(loc.lat) + lon.setData(loc.lon) + }) + // Note: this syncs only in one direction + } +} diff --git a/Logic/BBox.ts b/Logic/BBox.ts index 8e8bc14c6..c23f2073c 100644 --- a/Logic/BBox.ts +++ b/Logic/BBox.ts @@ -179,13 +179,6 @@ export class BBox { ]) } - toLeaflet(): [[number, number], [number, number]] { - return [ - [this.minLat, this.minLon], - [this.maxLat, this.maxLon], - ] - } - toLngLat(): [[number, number], [number, number]] { return [ [this.minLon, this.minLat], @@ -193,7 +186,6 @@ export class BBox { ] } - public asGeoJson(properties: T): Feature { return { type: "Feature", diff --git a/Logic/ExtraFunctions.ts b/Logic/ExtraFunctions.ts index dd568cab4..3da6f34b4 100644 --- a/Logic/ExtraFunctions.ts +++ b/Logic/ExtraFunctions.ts @@ -5,7 +5,7 @@ import BaseUIElement from "../UI/BaseUIElement" import List from "../UI/Base/List" import Title from "../UI/Base/Title" import { BBox } from "./BBox" -import { Feature, Geometry, MultiPolygon, Polygon } from "@turf/turf" +import { Feature, Geometry, MultiPolygon, Polygon } from "geojson" export interface ExtraFuncParams { /** @@ -68,7 +68,7 @@ class EnclosingFunc implements ExtraFunction { } if ( GeoOperations.completelyWithin( - feat, + feat, >otherFeature ) ) { diff --git a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts deleted file mode 100644 index 0b929713e..000000000 --- a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Store } from "../../UIEventSource" -import { GeoOperations } from "../../GeoOperations" -import FeatureSource from "../FeatureSource" -import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig" -import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" -import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig" -/** - * This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered. - */ -export default class RenderingMultiPlexerFeatureSource { - public readonly features: Store< - (any & { - pointRenderingIndex: number | undefined - lineRenderingIndex: number | undefined - })[] - > - private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[] - private readonly centroidRenderings: { rendering: PointRenderingConfig; index: number }[] - private readonly projectedCentroidRenderings: { - rendering: PointRenderingConfig - index: number - }[] - private readonly startRenderings: { rendering: PointRenderingConfig; index: number }[] - private readonly endRenderings: { rendering: PointRenderingConfig; index: number }[] - private readonly hasCentroid: boolean - private lineRenderObjects: LineRenderingConfig[] - - constructor(upstream: FeatureSource, layer: LayerConfig) { - const pointRenderObjects: { rendering: PointRenderingConfig; index: number }[] = - layer.mapRendering.map((r, i) => ({ - rendering: r, - index: i, - })) - this.pointRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("point")) - this.centroidRenderings = pointRenderObjects.filter((r) => - r.rendering.location.has("centroid") - ) - this.projectedCentroidRenderings = pointRenderObjects.filter((r) => - r.rendering.location.has("projected_centerpoint") - ) - this.startRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("start")) - this.endRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("end")) - this.hasCentroid = - this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0 - this.lineRenderObjects = layer.lineRendering - - this.features = upstream.features.map((features) => { - if (features === undefined) { - return undefined - } - - const withIndex: any[] = [] - - function addAsPoint(feat, rendering, coordinate) { - const patched = { - ...feat, - pointRenderingIndex: rendering.index, - } - patched.geometry = { - type: "Point", - coordinates: coordinate, - } - withIndex.push(patched) - } - - for (const feat of features) { - if (feat === undefined) { - continue - } - this.inspectFeature(feat, addAsPoint, withIndex) - } - - return withIndex - }) - } - - /** - * For every source feature, adds the necessary rendering-features - */ - private inspectFeature( - feat, - addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, - withIndex: any[] - ) { - if (feat.geometry.type === "Point") { - for (const rendering of this.pointRenderings) { - withIndex.push({ - ...feat, - pointRenderingIndex: rendering.index, - }) - } - } else if (feat.geometry.type === "MultiPolygon") { - if (this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0) { - const centerpoints: [number, number][] = (<[number, number][][][]>( - feat.geometry.coordinates - )).map((rings) => - GeoOperations.centerpointCoordinates({ - type: "Feature", - properties: {}, - geometry: { type: "Polygon", coordinates: rings }, - }) - ) - for (const centroidRendering of this.centroidRenderings) { - for (const centerpoint of centerpoints) { - addAsPoint(feat, centroidRendering, centerpoint) - } - } - - for (const centroidRendering of this.projectedCentroidRenderings) { - for (const centerpoint of centerpoints) { - addAsPoint(feat, centroidRendering, centerpoint) - } - } - } - - // AT last, add it 'as is' to what we should render - for (let i = 0; i < this.lineRenderObjects.length; i++) { - withIndex.push({ - ...feat, - lineRenderingIndex: i, - }) - } - } else { - // This is a a line or polygon: add the centroids - let centerpoint: [number, number] = undefined - let projectedCenterPoint: [number, number] = undefined - if (this.hasCentroid) { - centerpoint = GeoOperations.centerpointCoordinates(feat) - if (this.projectedCentroidRenderings.length > 0) { - projectedCenterPoint = <[number, number]>( - GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates - ) - } - } - for (const rendering of this.centroidRenderings) { - addAsPoint(feat, rendering, centerpoint) - } - - if (feat.geometry.type === "LineString") { - for (const rendering of this.projectedCentroidRenderings) { - addAsPoint(feat, rendering, projectedCenterPoint) - } - - // Add start- and endpoints - const coordinates = feat.geometry.coordinates - for (const rendering of this.startRenderings) { - addAsPoint(feat, rendering, coordinates[0]) - } - for (const rendering of this.endRenderings) { - const coordinate = coordinates[coordinates.length - 1] - addAsPoint(feat, rendering, coordinate) - } - } else { - for (const rendering of this.projectedCentroidRenderings) { - addAsPoint(feat, rendering, centerpoint) - } - } - - // AT last, add it 'as is' to what we should render - for (let i = 0; i < this.lineRenderObjects.length; i++) { - withIndex.push({ - ...feat, - lineRenderingIndex: i, - }) - } - } - } -} diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 2ceb9b2d4..7957fb8cd 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -1,15 +1,8 @@ import { BBox } from "./BBox" import LayerConfig from "../Models/ThemeConfig/LayerConfig" import * as turf from "@turf/turf" -import { - AllGeoJSON, - booleanWithin, - Coord, - Feature, - Geometry, - MultiPolygon, - Polygon, -} from "@turf/turf" +import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf" +import { Feature, Geometry, MultiPolygon, Polygon } from "geojson" import { GeoJSON, LineString, Point, Position } from "geojson" import togpx from "togpx" import Constants from "../Models/Constants" @@ -263,7 +256,10 @@ export class GeoOperations { * @param way The road on which you want to find a point * @param point Point defined as [lon, lat] */ - public static nearestPoint(way: Feature, point: [number, number]) { + public static nearestPoint( + way: Feature, + point: [number, number] + ): Feature { if (way.geometry.type === "Polygon") { way = { ...way } way.geometry = { ...way.geometry } @@ -710,6 +706,63 @@ export class GeoOperations { return true } + /** + * + * + * const f = (type, feature: Feature) => GeoOperations.featureToCoordinateWithRenderingType(feature, type) + * const g = geometry => ( {type: "Feature", properties: {}, geometry}) + * f("point", g({type:"Point", coordinates:[1,2]})) // => [1,2] + * f("centroid", g({type:"Point", coordinates:[1,2]})) // => undefined + * f("start", g({type:"Point", coordinates:[1,2]})) // => undefined + * f("centroid", g({type:"LineString", coordinates:[[1,2], [3,4]]})) // => [2,3] + * f("centroid", g({type:"Polygon", coordinates:[[[1,2], [3,4], [1,2]]]})) // => [2,3] + * f("projected_centerpoint", g({type:"LineString", coordinates:[[1,2], [3,4]]})) // => [1.9993137596003214,2.999313759600321] + * f("start", g({type:"LineString", coordinates:[[1,2], [3,4]]})) // => [1,2] + * f("end", g({type:"LineString", coordinates:[[1,2], [3,4]]})) // => [3,4] + * + */ + public static featureToCoordinateWithRenderingType( + feature: Feature, + location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string + ): [number, number] | undefined { + switch (location) { + case "point": + if (feature.geometry.type === "Point") { + return <[number, number]>feature.geometry.coordinates + } + return undefined + case "centroid": + if (feature.geometry.type === "Point") { + return undefined + } + return GeoOperations.centerpointCoordinates(feature) + case "projected_centerpoint": + if ( + feature.geometry.type === "LineString" || + feature.geometry.type === "MultiLineString" + ) { + const centerpoint = GeoOperations.centerpointCoordinates(feature) + const projected = GeoOperations.nearestPoint( + >feature, + centerpoint + ) + return <[number, number]>projected.geometry.coordinates + } + return undefined + case "start": + if (feature.geometry.type === "LineString") { + return <[number, number]>feature.geometry.coordinates[0] + } + return undefined + case "end": + if (feature.geometry.type === "LineString") { + return <[number, number]>feature.geometry.coordinates.at(-1) + } + return undefined + default: + throw "Unkown location type: " + location + } + } private static pointWithinRing(x: number, y: number, ring: [number, number][]) { let inside = false for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { diff --git a/Logic/Maproulette.ts b/Logic/Maproulette.ts index 15e11ccba..03e377736 100644 --- a/Logic/Maproulette.ts +++ b/Logic/Maproulette.ts @@ -29,8 +29,9 @@ export default class Maproulette { /** * The API key to use for all requests */ - private apiKey: string + private readonly apiKey: string + public static singleton = new Maproulette() /** * Creates a new Maproulette instance * @param endpoint The API endpoint to use diff --git a/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts b/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts index e7e625fd2..9e46a3791 100644 --- a/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts +++ b/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts @@ -59,15 +59,6 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct } } - public async getPreview(): Promise { - const outerPreview = await this.createOuterWay.getPreview() - outerPreview.features.data.push({ - freshness: new Date(), - feature: this.geojsonPreview, - }) - return outerPreview - } - protected async CreateChangeDescriptions(changes: Changes): Promise { console.log("Running CMPWPRA") const descriptions: ChangeDescription[] = [] diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index c2568def4..67e44a4a8 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -22,24 +22,25 @@ export class Changes { /** * All the newly created features as featureSource + all the modified features */ - public features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) + public readonly features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) public readonly pendingChanges: UIEventSource = LocalStorageSource.GetParsed("pending-changes", []) public readonly allChanges = new UIEventSource(undefined) public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection } public readonly extraComment: UIEventSource = new UIEventSource(undefined) - private historicalUserLocations: FeatureSource + private readonly historicalUserLocations: FeatureSource private _nextId: number = -1 // Newly assigned ID's are negative private readonly isUploading = new UIEventSource(false) private readonly previouslyCreated: OsmObject[] = [] private readonly _leftRightSensitive: boolean - private _changesetHandler: ChangesetHandler + private readonly _changesetHandler: ChangesetHandler constructor( state?: { allElements: ElementStorage osmConnection: OsmConnection + historicalUserLocations: FeatureSource }, leftRightSensitive: boolean = false ) { @@ -53,6 +54,7 @@ export class Changes { state.allElements, this ) + this.historicalUserLocations = state.historicalUserLocations // Note: a changeset might be reused which was opened just before and might have already used some ids // This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset @@ -164,7 +166,6 @@ export class Changes { const now = new Date() const recentLocationPoints = locations - .map((ff) => ff.feature) .filter((feat) => feat.geometry.type === "Point") .filter((feat) => { const visitTime = new Date( @@ -582,8 +583,4 @@ export class Changes { ) return result } - - public setHistoricalUserLocations(locations: FeatureSource) { - this.historicalUserLocations = locations - } } diff --git a/Logic/Osm/Geocoding.ts b/Logic/Osm/Geocoding.ts index 4e349d8ba..d0241608c 100644 --- a/Logic/Osm/Geocoding.ts +++ b/Logic/Osm/Geocoding.ts @@ -1,4 +1,3 @@ -import State from "../../State" import { Utils } from "../../Utils" import { BBox } from "../BBox" @@ -14,8 +13,8 @@ export interface GeoCodeResult { export class Geocoding { private static readonly host = "https://nominatim.openstreetmap.org/search?" - static async Search(query: string): Promise { - const b = State?.state?.currentBounds?.data ?? BBox.global + static async Search(query: string, bbox: BBox): Promise { + const b = bbox ?? BBox.global const url = Geocoding.host + "format=json&limit=1&viewbox=" + diff --git a/Logic/State/ElementsState.ts b/Logic/State/ElementsState.ts deleted file mode 100644 index cecb04009..000000000 --- a/Logic/State/ElementsState.ts +++ /dev/null @@ -1,91 +0,0 @@ -import FeatureSwitchState from "./FeatureSwitchState" -import { ElementStorage } from "../ElementStorage" -import { Changes } from "../Osm/Changes" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import { UIEventSource } from "../UIEventSource" -import Loc from "../../Models/Loc" -import { BBox } from "../BBox" -import { QueryParameters } from "../Web/QueryParameters" -import { LocalStorageSource } from "../Web/LocalStorageSource" -import { Utils } from "../../Utils" -import ChangeToElementsActor from "../Actors/ChangeToElementsActor" -import PendingChangesUploader from "../Actors/PendingChangesUploader" - -/** - * The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc - */ -export default class ElementsState extends FeatureSwitchState { - /** - The mapping from id -> UIEventSource - */ - public allElements: ElementStorage = new ElementStorage() - - /** - The latest element that was selected - */ - public readonly selectedElement = new UIEventSource(undefined, "Selected element") - - /** - * The map location: currently centered lat, lon and zoom - */ - public readonly locationControl = new UIEventSource(undefined, "locationControl") - - /** - * The current visible extent of the screen - */ - public readonly currentBounds = new UIEventSource(undefined) - - constructor(layoutToUse: LayoutConfig) { - super(layoutToUse) - - function localStorageSynced( - key: string, - deflt: number, - docs: string - ): UIEventSource { - const localStorage = LocalStorageSource.Get(key) - const previousValue = localStorage.data - const src = UIEventSource.asFloat( - QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage) - ) - - if (src.data === deflt) { - const prev = Number(previousValue) - if (!isNaN(prev)) { - src.setData(prev) - } - } - - return src - } - - // -- Location control initialization - const zoom = localStorageSynced( - "z", - layoutToUse?.startZoom ?? 1, - "The initial/current zoom level" - ) - const lat = localStorageSynced( - "lat", - layoutToUse?.startLat ?? 0, - "The initial/current latitude" - ) - const lon = localStorageSynced( - "lon", - layoutToUse?.startLon ?? 0, - "The initial/current longitude of the app" - ) - - this.locationControl.setData({ - zoom: Utils.asFloat(zoom.data), - lat: Utils.asFloat(lat.data), - lon: Utils.asFloat(lon.data), - }) - this.locationControl.addCallback((latlonz) => { - // Sync the location controls - zoom.setData(latlonz.zoom) - lat.setData(latlonz.lat) - lon.setData(latlonz.lon) - }) - } -} diff --git a/Logic/State/FeaturePipelineState.ts b/Logic/State/FeaturePipelineState.ts index 60e9fa788..5f7fe819d 100644 --- a/Logic/State/FeaturePipelineState.ts +++ b/Logic/State/FeaturePipelineState.ts @@ -1,9 +1,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import FeaturePipeline from "../FeatureSource/FeaturePipeline" import { Tiles } from "../../Models/TileRange" -import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer" import { TileHierarchyAggregator } from "../../UI/ShowDataLayer/TileHierarchyAggregator" -import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo" import { UIEventSource } from "../UIEventSource" import MapState from "./MapState" import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler" @@ -14,6 +12,7 @@ import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator" import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import ShowDataLayer from "../../UI/Map/ShowDataLayer" export default class FeaturePipelineState extends MapState { /** @@ -116,14 +115,12 @@ export default class FeaturePipelineState extends MapState { [self.currentBounds, source.layer.isDisplayed, sourceBBox] ) - new ShowDataLayer({ + new ShowDataLayer(self.maplibreMap, { features: source, - leafletMap: self.leafletMap, - layerToShow: source.layer.layerDef, + layer: source.layer.layerDef, doShowLayer: doShowFeatures, selectedElement: self.selectedElement, - state: self, - popup: (tags, layer) => self.CreatePopup(tags, layer), + buildPopup: (tags, layer) => self.CreatePopup(tags, layer), }) } @@ -136,8 +133,6 @@ export default class FeaturePipelineState extends MapState { sourcesToRegister.forEach((source) => self.metatagRecalculator.registerSource(source)) new SelectedFeatureHandler(Hash.hash, this) - - this.AddClusteringToMap(this.leafletMap) } public CreatePopup(tags: UIEventSource, layer: LayerConfig): ScrollableFullScreen { @@ -148,27 +143,4 @@ export default class FeaturePipelineState extends MapState { this.popups.set(tags.data.id, popup) return popup } - - /** - * Adds the cluster-tiles to the given map - * @param leafletMap: a UIEventSource possible having a leaflet map - * @constructor - */ - public AddClusteringToMap(leafletMap: UIEventSource) { - const clustering = this.layoutToUse.clustering - const self = this - new ShowDataLayer({ - features: this.featureAggregator.getCountsForZoom( - clustering, - this.locationControl, - clustering.minNeededElements - ), - leafletMap: leafletMap, - layerToShow: ShowTileInfo.styling, - popup: this.featureSwitchIsDebugging.data - ? (tags, layer) => new FeatureInfoBox(tags, layer, self) - : undefined, - state: this, - }) - } } diff --git a/Logic/State/GeoLocationState.ts b/Logic/State/GeoLocationState.ts index b6bd13e24..f0a9566be 100644 --- a/Logic/State/GeoLocationState.ts +++ b/Logic/State/GeoLocationState.ts @@ -30,7 +30,7 @@ export class GeoLocationState { /** * If true: the map will center (and re-center) to this location */ - public readonly isLocked: UIEventSource = new UIEventSource(false) + public readonly allowMoving: UIEventSource = new UIEventSource(true) public readonly currentGPSLocation: UIEventSource = new UIEventSource(undefined) @@ -72,7 +72,6 @@ export class GeoLocationState { self._previousLocationGrant.setData("false") } }) - console.log("Previous location grant:", this._previousLocationGrant.data) if (this._previousLocationGrant.data === "true") { // A previous visit successfully granted permission. Chance is high that we are allowed to use it again! @@ -87,7 +86,6 @@ export class GeoLocationState { } this.requestPermission() } - window["geolocation_state"] = this } /** diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index 724797585..ad29cfaa6 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -1,51 +1,31 @@ -import UserRelatedState from "./UserRelatedState" -import { Store, Stores, UIEventSource } from "../UIEventSource" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { Store, UIEventSource } from "../UIEventSource" import Attribution from "../../UI/BigComponents/Attribution" -import Minimap, { MinimapObj } from "../../UI/Base/Minimap" -import { Tiles } from "../../Models/TileRange" import BaseUIElement from "../../UI/BaseUIElement" import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" import { QueryParameters } from "../Web/QueryParameters" import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer" import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" -import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" import { LocalStorageSource } from "../Web/LocalStorageSource" -import { GeoOperations } from "../GeoOperations" import TitleHandler from "../Actors/TitleHandler" import { BBox } from "../BBox" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import { TiledStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource" -import { Translation, TypedTranslation } from "../../UI/i18n/Translation" -import { Tag } from "../Tags/Tag" +import StaticFeatureSource, { + TiledStaticFeatureSource, +} from "../FeatureSource/Sources/StaticFeatureSource" import { OsmConnection } from "../Osm/OsmConnection" -import { Feature, LineString } from "geojson" -import { OsmTags } from "../../Models/OsmFeature" - -export interface GlobalFilter { - filter: FilterState - id: string - onNewPoint: { - safetyCheck: Translation - confirmAddNew: TypedTranslation<{ preset: Translation }> - tags: Tag[] - } -} +import { Feature } from "geojson" +import { Map as MlMap } from "maplibre-gl" +import { GlobalFilter } from "../../Models/GlobalFilter" +import { MapProperties } from "../../Models/MapProperties" +import ShowDataLayer from "../../UI/Map/ShowDataLayer" /** * Contains all the leaflet-map related state */ -export default class MapState extends UserRelatedState { - /** - The leaflet instance of the big basemap - */ - public leafletMap = new UIEventSource(undefined, "leafletmap") +export default class MapState { + - /** - * The current background layer - */ - public backgroundLayer: UIEventSource /** * Last location where a click was registered */ @@ -58,34 +38,6 @@ export default class MapState extends UserRelatedState { * The bounds of the current map view */ public currentView: FeatureSourceForLayer & Tiled - /** - * The location as delivered by the GPS - */ - public currentUserLocation: SimpleFeatureSource - - /** - * All previously visited points, with their metadata - */ - public historicalUserLocations: SimpleFeatureSource - /** - * The number of seconds that the GPS-locations are stored in memory. - * Time in seconds - */ - public gpsLocationHistoryRetentionTime = new UIEventSource( - 7 * 24 * 60 * 60, - "gps_location_retention" - ) - /** - * A featureSource containing a single linestring which has the GPS-history of the user. - * However, metadata (such as when every single point was visited) is lost here (but is kept in `historicalUserLocations`. - * Note that this featureSource is _derived_ from 'historicalUserLocations' - */ - public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled - - /** - * A feature source containing the current home location of the user - */ - public homeLocation: FeatureSourceForLayer & Tiled /** * A builtin layer which contains the selected element. @@ -94,7 +46,7 @@ export default class MapState extends UserRelatedState { */ public selectedElementsLayer: FeatureSourceForLayer & Tiled - public readonly mainMapObject: BaseUIElement & MinimapObj + public readonly mainMapObject: BaseUIElement /** * Which layers are enabled in the current theme and what filters are applied onto them @@ -114,9 +66,7 @@ export default class MapState extends UserRelatedState { */ public overlayToggles: { config: TilesourceConfig; isDisplayed: UIEventSource }[] - constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { - super(layoutToUse, options) - + constructor() { this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl) let defaultLayer = AvailableBaseLayers.osmCarto @@ -130,13 +80,6 @@ export default class MapState extends UserRelatedState { this.backgroundLayer = new UIEventSource(defaultLayer) this.backgroundLayer.addCallbackAndRunD((layer) => self.backgroundLayerId.setData(layer.id)) - const attr = new Attribution( - this.locationControl, - this.osmConnection.userDetails, - this.layoutToUse, - this.currentBounds - ) - // Will write into this.leafletMap this.mainMapObject = Minimap.createMiniMap({ background: this.backgroundLayer, @@ -162,12 +105,8 @@ export default class MapState extends UserRelatedState { MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection) ) - this.lockBounds() this.AddAllOverlaysToMap(this.leafletMap) - this.initHomeLocation() - this.initGpsLocation() - this.initUserLocationTrail() this.initCurrentView() this.initSelectedElement() @@ -189,17 +128,6 @@ export default class MapState extends UserRelatedState { } } - private lockBounds() { - const layout = this.layoutToUse - if (!layout?.lockLocation) { - return - } - console.warn("Locking the bounds to ", layout.lockLocation) - this.mainMapObject.installBounds( - new BBox(layout.lockLocation), - this.featureSwitchIsTesting.data - ) - } private initCurrentView() { let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter( @@ -244,17 +172,6 @@ export default class MapState extends UserRelatedState { this.currentView = new TiledStaticFeatureSource(features, currentViewLayer) } - private initGpsLocation() { - // Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler - const gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter( - (l) => l.layerDef.id === "gps_location" - )[0] - if (gpsLayerDef === undefined) { - return - } - this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0)) - } - private initSelectedElement() { const layerDef: FilteredLayer = this.filteredLayers.data.filter( (l) => l.layerDef.id === "selected_element" @@ -281,145 +198,6 @@ export default class MapState extends UserRelatedState { this.selectedElementsLayer = new TiledStaticFeatureSource(store, layerDef) } - private initUserLocationTrail() { - const features = LocalStorageSource.GetParsed<{ feature: any; freshness: Date }[]>( - "gps_location_history", - [] - ) - const now = new Date().getTime() - features.data = features.data - .map((ff) => ({ feature: ff.feature, freshness: new Date(ff.freshness) })) - .filter( - (ff) => - now - ff.freshness.getTime() < 1000 * this.gpsLocationHistoryRetentionTime.data - ) - features.ping() - const self = this - let i = 0 - this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => { - if (location === undefined) { - return - } - - const previousLocation = features.data[features.data.length - 1] - if (previousLocation !== undefined) { - const d = GeoOperations.distanceBetween( - previousLocation.feature.geometry.coordinates, - location.feature.geometry.coordinates - ) - let timeDiff = Number.MAX_VALUE // in seconds - const olderLocation = features.data[features.data.length - 2] - if (olderLocation !== undefined) { - timeDiff = - (new Date(previousLocation.freshness).getTime() - - new Date(olderLocation.freshness).getTime()) / - 1000 - } - if (d < 20 && timeDiff < 60) { - // Do not append changes less then 20m - it's probably noise anyway - return - } - } - - const feature = JSON.parse(JSON.stringify(location.feature)) - feature.properties.id = "gps/" + features.data.length - i++ - features.data.push({ feature, freshness: new Date() }) - features.ping() - }) - - let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter( - (l) => l.layerDef.id === "gps_location_history" - )[0] - if (gpsLayerDef !== undefined) { - this.historicalUserLocations = new SimpleFeatureSource( - gpsLayerDef, - Tiles.tile_index(0, 0, 0), - features - ) - this.changes.setHistoricalUserLocations(this.historicalUserLocations) - } - - const asLine = features.map((allPoints) => { - if (allPoints === undefined || allPoints.length < 2) { - return [] - } - - const feature: Feature = { - type: "Feature", - properties: { - id: "location_track", - "_date:now": new Date().toISOString(), - }, - geometry: { - type: "LineString", - coordinates: allPoints.map((ff) => ff.feature.geometry.coordinates), - }, - } - - self.allElements.ContainingFeatures.set(feature.properties.id, feature) - - return [ - { - feature, - freshness: new Date(), - }, - ] - }) - let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter( - (l) => l.layerDef.id === "gps_track" - )[0] - if (gpsLineLayerDef !== undefined) { - this.historicalUserLocationsTrack = new TiledStaticFeatureSource( - asLine, - gpsLineLayerDef - ) - } - } - - private initHomeLocation() { - const empty = [] - const feature = Stores.ListStabilized( - this.osmConnection.userDetails.map((userDetails) => { - if (userDetails === undefined) { - return undefined - } - const home = userDetails.home - if (home === undefined) { - return undefined - } - return [home.lon, home.lat] - }) - ).map((homeLonLat) => { - if (homeLonLat === undefined) { - return empty - } - return [ - { - feature: { - type: "Feature", - properties: { - id: "home", - "user:home": "yes", - _lon: homeLonLat[0], - _lat: homeLonLat[1], - }, - geometry: { - type: "Point", - coordinates: homeLonLat, - }, - }, - freshness: new Date(), - }, - ] - }) - - const flayer = this.filteredLayers.data.filter((l) => l.layerDef.id === "home_location")[0] - if (flayer !== undefined) { - this.homeLocation = new TiledStaticFeatureSource(feature, flayer) - } - } - private static getPref( osmConnection: OsmConnection, key: string, diff --git a/Logic/State/UserRelatedState.ts b/Logic/State/UserRelatedState.ts index cbd0d889a..c178aa984 100644 --- a/Logic/State/UserRelatedState.ts +++ b/Logic/State/UserRelatedState.ts @@ -1,21 +1,17 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import { OsmConnection } from "../Osm/OsmConnection" import { MangroveIdentity } from "../Web/MangroveReviews" -import { Store, UIEventSource } from "../UIEventSource" -import { QueryParameters } from "../Web/QueryParameters" +import { Store, Stores, UIEventSource } from "../UIEventSource" import Locale from "../../UI/i18n/Locale" -import ElementsState from "./ElementsState" -import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater" import { Changes } from "../Osm/Changes" -import ChangeToElementsActor from "../Actors/ChangeToElementsActor" -import PendingChangesUploader from "../Actors/PendingChangesUploader" -import Maproulette from "../Maproulette" +import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" +import FeatureSource from "../FeatureSource/FeatureSource" /** * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, * which layers they enabled, ... */ -export default class UserRelatedState extends ElementsState { +export default class UserRelatedState { /** The user credentials */ @@ -29,29 +25,22 @@ export default class UserRelatedState extends ElementsState { */ public mangroveIdentity: MangroveIdentity - /** - * Maproulette connection - */ - public maprouletteConnection: Maproulette - public readonly installedUserThemes: Store public readonly showAllQuestionsAtOnce: UIEventSource + public readonly homeLocation: FeatureSource - constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { - super(layoutToUse) + /** + * The number of seconds that the GPS-locations are stored in memory. + * Time in seconds + */ + public gpsLocationHistoryRetentionTime = new UIEventSource( + 7 * 24 * 60 * 60, + "gps_location_retention" + ) - this.osmConnection = new OsmConnection({ - dryRun: this.featureSwitchIsTesting, - fakeUser: this.featureSwitchFakeUser.data, - oauth_token: QueryParameters.GetQueryParameter( - "oauth_token", - undefined, - "Used to complete the login" - ), - osmConfiguration: <"osm" | "osm-test">this.featureSwitchApiURL.data, - attemptLogin: options?.attemptLogin, - }) + constructor(osmConnection: OsmConnection, availableLanguages?: string[]) { + this.osmConnection = osmConnection { const translationMode: UIEventSource = this.osmConnection.GetPreference("translation-mode") @@ -72,49 +61,22 @@ export default class UserRelatedState extends ElementsState { }) } - this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false) this.showAllQuestionsAtOnce = UIEventSource.asBoolean( this.osmConnection.GetPreference("show-all-questions", "false", { documentation: "Either 'true' or 'false'. If set, all questions will be shown all at once", }) ) - new ChangeToElementsActor(this.changes, this.allElements) - new PendingChangesUploader(this.changes, this.selectedElement) this.mangroveIdentity = new MangroveIdentity( this.osmConnection.GetLongPreference("identity", "mangrove") ) - this.maprouletteConnection = new Maproulette() + this.InitializeLanguage(availableLanguages) - if (layoutToUse?.hideFromOverview) { - this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => { - if (loggedIn) { - this.osmConnection - .GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled") - .setData("true") - return true - } - }) - } - - if (this.layoutToUse !== undefined && !this.layoutToUse.official) { - console.log("Marking unofficial theme as visited") - this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id).setData( - JSON.stringify({ - id: this.layoutToUse.id, - icon: this.layoutToUse.icon, - title: this.layoutToUse.title.translations, - shortDescription: this.layoutToUse.shortDescription.translations, - definition: this.layoutToUse["definition"], - }) - ) - } - - this.InitializeLanguage() - new SelectedElementTagsUpdater(this) this.installedUserThemes = this.InitInstalledUserThemes() + + this.homeLocation = this.initHomeLocation() } public GetUnofficialTheme(id: string): @@ -159,26 +121,50 @@ export default class UserRelatedState extends ElementsState { } } - private InitializeLanguage() { - const layoutToUse = this.layoutToUse + public markLayoutAsVisited(layout: LayoutConfig) { + if (!layout) { + console.error("Trying to mark a layout as visited, but ", layout, " got passed") + return + } + if (layout.hideFromOverview) { + this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => { + if (loggedIn) { + this.osmConnection + .GetPreference("hidden-theme-" + layout?.id + "-enabled") + .setData("true") + return true + } + }) + } + if (!layout.official) { + this.osmConnection.GetLongPreference("unofficial-theme-" + layout.id).setData( + JSON.stringify({ + id: layout.id, + icon: layout.icon, + title: layout.title.translations, + shortDescription: layout.shortDescription.translations, + definition: layout["definition"], + }) + ) + } + } + + private InitializeLanguage(availableLanguages?: string[]) { Locale.language.syncWith(this.osmConnection.GetPreference("language")) Locale.language.addCallback((currentLanguage) => { - if (layoutToUse === undefined) { - return - } if (Locale.showLinkToWeblate.data) { return true // Disable auto switching as we are in translators mode } - if (this.layoutToUse.language.indexOf(currentLanguage) < 0) { + if (availableLanguages?.indexOf(currentLanguage) < 0) { console.log( "Resetting language to", - layoutToUse.language[0], + availableLanguages[0], "as", currentLanguage, " is unsupported" ) // The current language is not supported -> switch to a supported one - Locale.language.setData(layoutToUse.language[0]) + Locale.language.setData(availableLanguages[0]) } }) Locale.language.ping() @@ -193,4 +179,43 @@ export default class UserRelatedState extends ElementsState { .map((k) => k.substring(prefix.length, k.length - postfix.length)) ) } + + private initHomeLocation(): FeatureSource { + const empty = [] + const feature = Stores.ListStabilized( + this.osmConnection.userDetails.map((userDetails) => { + if (userDetails === undefined) { + return undefined + } + const home = userDetails.home + if (home === undefined) { + return undefined + } + return [home.lon, home.lat] + }) + ).map((homeLonLat) => { + if (homeLonLat === undefined) { + return empty + } + return [ + { + feature: { + type: "Feature", + properties: { + id: "home", + "user:home": "yes", + _lon: homeLonLat[0], + _lat: homeLonLat[1], + }, + geometry: { + type: "Point", + coordinates: homeLonLat, + }, + }, + freshness: new Date(), + }, + ] + }) + return new StaticFeatureSource(feature) + } } diff --git a/Models/GlobalFilter.ts b/Models/GlobalFilter.ts new file mode 100644 index 000000000..c57a818a7 --- /dev/null +++ b/Models/GlobalFilter.ts @@ -0,0 +1,13 @@ +import { Translation, TypedTranslation } from "../UI/i18n/Translation" +import { FilterState } from "./FilteredLayer" +import { Tag } from "../Logic/Tags/Tag" + +export interface GlobalFilter { + filter: FilterState + id: string + onNewPoint: { + safetyCheck: Translation + confirmAddNew: TypedTranslation<{ preset: Translation }> + tags: Tag[] + } +} diff --git a/Models/LeafletMap.ts b/Models/LeafletMap.ts deleted file mode 100644 index e3f0ae850..000000000 --- a/Models/LeafletMap.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default interface LeafletMap { - getBounds(): [[number, number], [number, number]] -} diff --git a/Models/MapProperties.ts b/Models/MapProperties.ts new file mode 100644 index 000000000..9ac228f57 --- /dev/null +++ b/Models/MapProperties.ts @@ -0,0 +1,14 @@ +import { Store, UIEventSource } from "../Logic/UIEventSource" +import { BBox } from "../Logic/BBox" +import { RasterLayerPolygon } from "./RasterLayers" + +export interface MapProperties { + readonly location: UIEventSource<{ lon: number; lat: number }> + readonly zoom: UIEventSource + readonly bounds: Store + readonly rasterLayer: UIEventSource + + readonly maxbounds: UIEventSource + + readonly allowMoving: UIEventSource +} diff --git a/Models/ThemeConfig/Conversion/PrepareLayer.ts b/Models/ThemeConfig/Conversion/PrepareLayer.ts index aaf84bc3a..0bb635ef6 100644 --- a/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -23,8 +23,6 @@ import predifined_filters from "../../../assets/layers/filters/filters.json" import { TagConfigJson } from "../Json/TagConfigJson" import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" import LineRenderingConfigJson from "../Json/LineRenderingConfigJson" -import { type } from "os" -import exp from "constants" class ExpandFilter extends DesugaringStep { private static readonly predefinedFilters = ExpandFilter.load_filters() diff --git a/Models/ThemeConfig/Conversion/PrepareTheme.ts b/Models/ThemeConfig/Conversion/PrepareTheme.ts index f65a768da..15e41503f 100644 --- a/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -175,7 +175,7 @@ class SubstituteLayer extends Conversion { - private _state: DesugaringContext + private readonly _state: DesugaringContext constructor(state: DesugaringContext) { super( @@ -430,7 +430,7 @@ class AddDependencyLayersToTheme extends DesugaringStep { constructor(state: DesugaringContext) { super( `If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically) - + Note that these layers are added _at the start_ of the layer list, meaning that they will see _every_ feature. Furthermore, \`passAllFeatures\` will be set, so that they won't steal away features from further layers. Some layers (e.g. \`all_buildings_and_walls\' or \'streets_with_a_name\') are invisible, so by default, \'force_load\' is set too. diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 5e5a92d34..765b095d6 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -15,6 +15,7 @@ import Svg from "../../../Svg" import FilterConfigJson from "../Json/FilterConfigJson" import DeleteConfig from "../DeleteConfig" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" +import ValidatedTextField from "../../../UI/Input/ValidatedTextField" class ValidateLanguageCompleteness extends DesugaringStep { private readonly _languages: string[] @@ -594,6 +595,7 @@ export class DetectMappingsWithImages extends DesugaringStep { private _options: { noQuestionHintCheck: boolean } + constructor(options: { noQuestionHintCheck: boolean }) { super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks") this._options = options @@ -637,6 +639,19 @@ class MiscTagRenderingChecks extends DesugaringStep { } } } + const freeformType = json["freeform"]?.["type"] + if (freeformType) { + if (ValidatedTextField.AvailableTypes().indexOf(freeformType) < 0) { + throw ( + "At " + + context + + ".freeform.type is an unknown type: " + + freeformType + + "; try one of " + + ValidatedTextField.AvailableTypes().join(", ") + ) + } + } return { result: json, errors, @@ -905,6 +920,38 @@ export class ValidateLayer extends DesugaringStep { } } +export class ValidateFilter extends DesugaringStep { + constructor() { + super("Detect common errors in the filters", [], "ValidateFilter") + } + + convert( + filter: FilterConfigJson, + context: string + ): { + result: FilterConfigJson + errors?: string[] + warnings?: string[] + information?: string[] + } { + const errors = [] + for (const option of filter.options) { + for (let i = 0; i < option.fields.length; i++) { + const field = option.fields[i] + const type = field.type ?? "string" + if (!ValidatedTextField.ForType(type) !== undefined) { + continue + } + const err = `Invalid filter: ${type} is not a valid validated textfield type (at ${context}.fields[${i}])\n\tTry one of ${Array.from( + ValidatedTextField.AvailableTypes() + ).join(",")}` + errors.push(err) + } + } + return { result: filter, errors } + } +} + export class DetectDuplicateFilters extends DesugaringStep<{ layers: LayerConfigJson[] themes: LayoutConfigJson[] diff --git a/Models/ThemeConfig/FilterConfig.ts b/Models/ThemeConfig/FilterConfig.ts index 989d49f5b..4be92468d 100644 --- a/Models/ThemeConfig/FilterConfig.ts +++ b/Models/ThemeConfig/FilterConfig.ts @@ -3,7 +3,6 @@ import { TagsFilter } from "../../Logic/Tags/TagsFilter" import FilterConfigJson from "./Json/FilterConfigJson" import Translations from "../../UI/i18n/Translations" import { TagUtils } from "../../Logic/Tags/TagUtils" -import ValidatedTextField from "../../UI/Input/ValidatedTextField" import { TagConfigJson } from "./Json/TagConfigJson" import { UIEventSource } from "../../Logic/UIEventSource" import { FilterState } from "../FilteredLayer" @@ -54,11 +53,7 @@ export default class FilterConfig { const fields: { name: string; type: string }[] = (option.fields ?? []).map((f, i) => { const type = f.type ?? "string" - if (!ValidatedTextField.ForType(type) === undefined) { - throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from( - ValidatedTextField.AvailableTypes() - ).join(",")}` - } + // Type is validated against 'ValidatedTextField' in Validation.ts, in ValidateFilterConfig if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) { throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]` } diff --git a/Models/ThemeConfig/Json/LayerConfigJson.ts b/Models/ThemeConfig/Json/LayerConfigJson.ts index 69e132e8a..67a6f5571 100644 --- a/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -40,8 +40,9 @@ export interface LayerConfigJson { * * Every source _must_ define which tags _must_ be present in order to be picked up. * + * Note: a source must always be defined. 'special' is only allowed if this is a builtin-layer */ - source: { + source: "special" | "special:library" | ({ /** * Every source must set which tags have to be present in order to load the given layer. */ @@ -102,7 +103,7 @@ export interface LayerConfigJson { * Setting this key will rename this field into 'id' */ idKey?: string - } + }) ) /** diff --git a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts index 7fe5e62e8..17e243924 100644 --- a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts @@ -12,8 +12,11 @@ import { TagConfigJson } from "./TagConfigJson" export default interface PointRenderingConfigJson { /** * All the locations that this point should be rendered at. - * Using `location: ["point", "centroid"] will always render centerpoint. - * 'projected_centerpoint' will show an item on the line itself, near the middle of the line. (LineStrings only) + * Possible values are: + * - `point`: only renders points at their location + * - `centroid`: show a symbol at the centerpoint of a (multi)Linestring and (multi)polygon. Points will _not_ be rendered with this + * - `projected_centerpoint`: Only on (multi)linestrings: calculate the centerpoint and snap it to the way + * - `start` and `end`: only on linestrings: add a point to the first/last coordinate of the LineString */ location: ("point" | "centroid" | "start" | "end" | "projected_centerpoint" | string)[] diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index 24fc7bb14..03be4abee 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -29,7 +29,7 @@ import { Overpass } from "../../Logic/Osm/Overpass" import Constants from "../Constants" import { FixedUiElement } from "../../UI/Base/FixedUiElement" import Svg from "../../Svg" -import { UIEventSource } from "../../Logic/UIEventSource" +import { ImmutableStore } from "../../Logic/UIEventSource" import { OsmTags } from "../OsmFeature" export default class LayerConfig extends WithContextLoader { @@ -37,7 +37,10 @@ export default class LayerConfig extends WithContextLoader { public readonly id: string public readonly name: Translation public readonly description: Translation - public readonly source: SourceConfig + /** + * Only 'null' for special, privileged layers + */ + public readonly source: SourceConfig | null public readonly calculatedTags: [string, string, boolean][] public readonly doNotDownload: boolean public readonly passAllFeatures: boolean @@ -83,7 +86,9 @@ export default class LayerConfig extends WithContextLoader { throw "Layer " + this.id + " does not define a source section (" + context + ")" } - if (json.source.osmTags === undefined) { + if(json.source === "special" || json.source === "special:library"){ + this.source = null + }else if (json.source.osmTags === undefined) { throw ( "Layer " + this.id + @@ -584,11 +589,9 @@ export default class LayerConfig extends WithContextLoader { .filter((mr) => mr.location.has("point")) .map( (mr) => - mr.GenerateLeafletStyle( - new UIEventSource({ id: "node/-1" }), - false, - { includeBadges: false } - ).html + mr.RenderIcon(new ImmutableStore({ id: "node/-1" }), false, { + includeBadges: false, + }).html ) .find((i) => i !== undefined) } diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index 4763169de..4068b4fe8 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -8,7 +8,6 @@ import { ExtractImages } from "./Conversion/FixImages" import ExtraLinkConfig from "./ExtraLinkConfig" import { Utils } from "../../Utils" import LanguageUtils from "../../Utils/LanguageUtils" - /** * Minimal information about a theme **/ diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts index d6a3317df..9e0fdd559 100644 --- a/Models/ThemeConfig/PointRenderingConfig.ts +++ b/Models/ThemeConfig/PointRenderingConfig.ts @@ -175,7 +175,7 @@ export default class PointRenderingConfig extends WithContextLoader { ) } - public GenerateLeafletStyle( + public RenderIcon( tags: Store, clickable: boolean, options?: { @@ -210,7 +210,7 @@ export default class PointRenderingConfig extends WithContextLoader { // in MapLibre, the offset is relative to the _center_ of the object, with left = [-x, 0] and up = [0,-y] let anchorW = 0 - let anchorH = iconH / 2 + let anchorH = 0 if (mode === "left") { anchorW = -iconW / 2 } diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index a20011d74..69b75609a 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -3,7 +3,6 @@ import { TagsFilter } from "../../Logic/Tags/TagsFilter" import Translations from "../../UI/i18n/Translations" import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils" import { And } from "../../Logic/Tags/And" -import ValidatedTextField from "../../UI/Input/ValidatedTextField" import { Utils } from "../../Utils" import { Tag } from "../../Logic/Tags/Tag" import BaseUIElement from "../../UI/BaseUIElement" @@ -132,17 +131,6 @@ export default class TagRenderingConfig { } const type = json.freeform.type ?? "string" - if (ValidatedTextField.AvailableTypes().indexOf(type) < 0) { - throw ( - "At " + - context + - ".freeform.type is an unknown type: " + - type + - "; try one of " + - ValidatedTextField.AvailableTypes().join(", ") - ) - } - let placeholder: Translation = Translations.T(json.freeform.placeholder) if (placeholder === undefined) { const typeDescription = Translations.t.validation[type]?.description @@ -182,13 +170,7 @@ export default class TagRenderingConfig { } } - if ( - this.freeform.type !== undefined && - ValidatedTextField.AvailableTypes().indexOf(this.freeform.type) < 0 - ) { - const knownKeys = ValidatedTextField.AvailableTypes().join(", ") - throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}` - } + // freeform.type is validated in Validation.ts so that we don't need ValidatedTextFields here if (this.freeform.addExtraTags) { const usedKeys = new And(this.freeform.addExtraTags).usedKeys() if (usedKeys.indexOf(this.freeform.key) >= 0) { diff --git a/UI/AutomatonGui.ts b/UI/AutomatonGui.ts deleted file mode 100644 index 1a8b446a8..000000000 --- a/UI/AutomatonGui.ts +++ /dev/null @@ -1,476 +0,0 @@ -import BaseUIElement from "./BaseUIElement" -import Combine from "./Base/Combine" -import Svg from "../Svg" -import Title from "./Base/Title" -import Toggle from "./Input/Toggle" -import { SubtleButton } from "./Base/SubtleButton" -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" -import ValidatedTextField from "./Input/ValidatedTextField" -import { Utils } from "../Utils" -import { UIEventSource } from "../Logic/UIEventSource" -import { VariableUiElement } from "./Base/VariableUIElement" -import { FixedUiElement } from "./Base/FixedUiElement" -import { Tiles } from "../Models/TileRange" -import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" -import { DropDown } from "./Input/DropDown" -import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" -import MinimapImplementation from "./Base/MinimapImplementation" -import { OsmConnection } from "../Logic/Osm/OsmConnection" -import { BBox } from "../Logic/BBox" -import MapState from "../Logic/State/MapState" -import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline" -import LayerConfig from "../Models/ThemeConfig/LayerConfig" -import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" -import FeatureSource from "../Logic/FeatureSource/FeatureSource" -import List from "./Base/List" -import { QueryParameters } from "../Logic/Web/QueryParameters" -import { SubstitutedTranslation } from "./SubstitutedTranslation" -import { AutoAction } from "./Popup/AutoApplyButton" -import DynamicGeoJsonTileSource from "../Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource" -import themeOverview from "../assets/generated/theme_overview.json" - -class AutomationPanel extends Combine { - private static readonly openChangeset = new UIEventSource(undefined) - - constructor( - layoutToUse: LayoutConfig, - indices: number[], - extraCommentText: UIEventSource, - tagRenderingToAutomate: { layer: LayerConfig; tagRendering: TagRenderingConfig } - ) { - const layerId = tagRenderingToAutomate.layer.id - const trId = tagRenderingToAutomate.tagRendering.id - const tileState = LocalStorageSource.GetParsed( - "automation-tile_state-" + layerId + "-" + trId, - {} - ) - const logMessages = new UIEventSource([]) - if (indices === undefined) { - throw "No tiles loaded - can not automate" - } - const openChangeset = AutomationPanel.openChangeset - - openChangeset.addCallbackAndRun((cs) => - console.trace("Sync current open changeset to:", cs) - ) - - const nextTileToHandle = tileState.map((handledTiles) => { - for (const index of indices) { - if (handledTiles[index] !== undefined) { - // Already handled - continue - } - return index - } - return undefined - }) - nextTileToHandle.addCallback((t) => console.warn("Next tile to handle is", t)) - - const neededTimes = new UIEventSource([]) - const automaton = new VariableUiElement( - nextTileToHandle.map((tileIndex) => { - if (tileIndex === undefined) { - return new FixedUiElement("All done!").SetClass("thanks") - } - console.warn("Triggered map on nextTileToHandle", tileIndex) - const start = new Date() - return AutomationPanel.TileHandler( - layoutToUse, - tileIndex, - layerId, - tagRenderingToAutomate.tagRendering, - extraCommentText, - (result, logMessage) => { - const end = new Date() - const timeNeeded = (end.getTime() - start.getTime()) / 1000 - neededTimes.data.push(timeNeeded) - neededTimes.ping() - tileState.data[tileIndex] = result - tileState.ping() - if (logMessage !== undefined) { - logMessages.data.push(logMessage) - logMessages.ping() - } - } - ) - }) - ) - - const statistics = new VariableUiElement( - tileState.map((states) => { - let total = 0 - const perResult = new Map() - for (const key in states) { - total++ - const result = states[key] - perResult.set(result, (perResult.get(result) ?? 0) + 1) - } - - let sum = 0 - neededTimes.data.forEach((v) => { - sum = sum + v - }) - let timePerTile = sum / neededTimes.data.length - - return new Combine([ - "Handled " + total + "/" + indices.length + " tiles: ", - new List( - Array.from(perResult.keys()).map((key) => key + ": " + perResult.get(key)) - ), - "Handling one tile needs " + - Math.floor(timePerTile * 100) / 100 + - "s on average. Estimated time left: " + - Utils.toHumanTime((indices.length - total) * timePerTile), - ]).SetClass("flex flex-col") - }) - ) - - super([ - statistics, - automaton, - new SubtleButton(undefined, "Clear fixed").onClick(() => { - const st = tileState.data - for (const tileIndex in st) { - if (st[tileIndex] === "fixed") { - delete st[tileIndex] - } - } - - tileState.ping() - }), - new VariableUiElement(logMessages.map((logMessages) => new List(logMessages))), - ]) - this.SetClass("flex flex-col") - } - - private static TileHandler( - layoutToUse: LayoutConfig, - tileIndex: number, - targetLayer: string, - targetAction: TagRenderingConfig, - extraCommentText: UIEventSource, - whenDone: (result: string, logMessage?: string) => void - ): BaseUIElement { - const state = new MapState(layoutToUse, { attemptLogin: false }) - extraCommentText.syncWith(state.changes.extraComment) - const [z, x, y] = Tiles.tile_from_index(tileIndex) - state.locationControl.setData({ - zoom: z, - lon: x, - lat: y, - }) - state.currentBounds.setData(BBox.fromTileIndex(tileIndex)) - - let targetTiles: UIEventSource = new UIEventSource([]) - const pipeline = new FeaturePipeline((tile) => { - const layerId = tile.layer.layerDef.id - if (layerId === targetLayer) { - targetTiles.data.push(tile) - targetTiles.ping() - } - }, state) - - state.locationControl.ping() - state.currentBounds.ping() - const stateToShow = new UIEventSource("") - - pipeline.runningQuery.map( - async (isRunning) => { - if (targetTiles.data.length === 0) { - stateToShow.setData("No data loaded yet...") - return - } - if (isRunning) { - stateToShow.setData( - "Waiting for all layers to be loaded... Has " + - targetTiles.data.length + - " tiles already" - ) - return - } - if (targetTiles.data.length === 0) { - stateToShow.setData("No features found to apply the action") - whenDone("empty") - return true - } - stateToShow.setData("Gathering applicable elements") - - let handled = 0 - let inspected = 0 - let log = [] - for (const targetTile of targetTiles.data) { - for (const ffs of targetTile.features.data) { - inspected++ - if (inspected % 10 === 0) { - stateToShow.setData( - "Inspected " + - inspected + - " features, updated " + - handled + - " features" - ) - } - const feature = ffs.feature - const renderingTr = targetAction.GetRenderValue(feature.properties) - const rendering = renderingTr.txt - log.push( - "" + - feature.properties.id + - ": " + - new SubstitutedTranslation( - renderingTr, - new UIEventSource(feature.properties), - undefined - ).ConstructElement().textContent - ) - const actions = Utils.NoNull( - SubstitutedTranslation.ExtractSpecialComponents(rendering).map( - (obj) => obj.special - ) - ) - for (const action of actions) { - const auto = action.func - if (auto.supportsAutoAction !== true) { - continue - } - - await auto.applyActionOn( - { - layoutToUse: state.layoutToUse, - changes: state.changes, - }, - state.allElements.getEventSourceById(feature.properties.id), - action.args - ) - handled++ - } - } - } - stateToShow.setData( - "Done! Inspected " + inspected + " features, updated " + handled + " features" - ) - - if (inspected === 0) { - whenDone("empty") - return true - } - - if (handled === 0) { - whenDone("no-action", "Inspected " + inspected + " elements: " + log.join("; ")) - } else { - state.osmConnection.AttemptLogin() - state.changes.flushChanges("handled tile automatically, time to flush!") - whenDone( - "fixed", - "Updated " + - handled + - " elements, inspected " + - inspected + - ": " + - log.join("; ") - ) - } - return true - }, - [targetTiles] - ) - - return new Combine([ - new Title("Performing action for tile " + tileIndex, 1), - new VariableUiElement(stateToShow), - ]).SetClass("flex flex-col") - } -} - -class AutomatonGui { - constructor() { - const osmConnection = new OsmConnection({ - singlePage: false, - oauth_token: QueryParameters.GetQueryParameter("oauth_token", "OAuth token"), - }) - - new Combine([ - new Combine([ - Svg.robot_svg().SetClass("w-24 h-24 p-4 rounded-full subtle-background"), - new Combine([ - new Title("MapComplete Automaton", 1), - "This page helps to automate certain tasks for a theme. Expert use only.", - ]).SetClass("flex flex-col m-4"), - ]).SetClass("flex"), - new Toggle( - AutomatonGui.GenerateMainPanel(), - new SubtleButton(Svg.osm_logo_svg(), "Login to get started").onClick(() => - osmConnection.AttemptLogin() - ), - osmConnection.isLoggedIn - ), - ]) - .SetClass("block p-4") - .AttachTo("main") - } - - private static GenerateMainPanel(): BaseUIElement { - const themeSelect = new DropDown( - "Select a theme", - Array.from(themeOverview).map((l) => ({ value: l.id, shown: l.id })) - ) - - LocalStorageSource.Get("automation-theme-id", "missing_streets").syncWith( - themeSelect.GetValue() - ) - - const tilepath = ValidatedTextField.ForType("url").ConstructInputElement({ - placeholder: "Specifiy the path of the overview", - inputStyle: "width: 100%", - }) - tilepath.SetClass("w-full") - LocalStorageSource.Get("automation-tile_path").syncWith(tilepath.GetValue(), true) - - let tilesToRunOver = tilepath.GetValue().bind((path) => { - if (path === undefined) { - return undefined - } - return UIEventSource.FromPromiseWithErr(Utils.downloadJsonCached(path, 1000 * 60 * 60)) - }) - - const targetZoom = 14 - - const tilesPerIndex = tilesToRunOver.map((tiles) => { - if (tiles === undefined || tiles["error"] !== undefined) { - return undefined - } - let indexes: number[] = [] - const tilesS = tiles["success"] - DynamicGeoJsonTileSource.RegisterWhitelist(tilepath.GetValue().data, tilesS) - const z = Number(tilesS["zoom"]) - for (const key in tilesS) { - if (key === "zoom") { - continue - } - const x = Number(key) - const ys = tilesS[key] - indexes.push(...ys.map((y) => Tiles.tile_index(z, x, y))) - } - - console.log("Got ", indexes.length, "indexes") - let rezoomed = new Set() - for (const index of indexes) { - let [z, x, y] = Tiles.tile_from_index(index) - while (z > targetZoom) { - z-- - x = Math.floor(x / 2) - y = Math.floor(y / 2) - } - rezoomed.add(Tiles.tile_index(z, x, y)) - } - - return Array.from(rezoomed) - }) - - const extraComment = ValidatedTextField.ForType("text").ConstructInputElement() - LocalStorageSource.Get("automaton-extra-comment").syncWith(extraComment.GetValue()) - - return new Combine([ - themeSelect, - "Specify the path to a tile overview. This is a hosted .json of the format {x : [y0, y1, y2], x1: [y0, ...]} where x is a string and y are numbers", - tilepath, - "Add an extra comment:", - extraComment, - new VariableUiElement( - extraComment - .GetValue() - .map((c) => "Your comment is " + (c?.length ?? 0) + "/200 characters long") - ).SetClass("subtle"), - new VariableUiElement( - tilesToRunOver.map((t) => { - if (t === undefined) { - return "No path given or still loading..." - } - if (t["error"] !== undefined) { - return new FixedUiElement("Invalid URL or data: " + t["error"]).SetClass( - "alert" - ) - } - - return new FixedUiElement( - "Loaded " + tilesPerIndex.data.length + " tiles to automated over" - ).SetClass("thanks") - }) - ), - new VariableUiElement( - themeSelect - .GetValue() - .map((id) => AllKnownLayouts.allKnownLayouts.get(id)) - .map( - (layoutToUse) => { - if (layoutToUse === undefined) { - return new FixedUiElement("Select a valid layout") - } - if ( - tilesPerIndex.data === undefined || - tilesPerIndex.data.length === 0 - ) { - return "No tiles given" - } - - const automatableTagRenderings: { - layer: LayerConfig - tagRendering: TagRenderingConfig - }[] = [] - for (const layer of layoutToUse.layers) { - for (const tagRendering of layer.tagRenderings) { - if (tagRendering.group === "auto") { - automatableTagRenderings.push({ - layer, - tagRendering: tagRendering, - }) - } - } - } - console.log("Automatable tag renderings:", automatableTagRenderings) - if (automatableTagRenderings.length === 0) { - return new FixedUiElement( - 'This theme does not have any tagRendering with "group": "auto" set' - ).SetClass("alert") - } - const pickAuto = new DropDown("Pick the action to automate", [ - { - value: undefined, - shown: "Pick an option", - }, - ...automatableTagRenderings.map((config) => ({ - shown: config.layer.id + " - " + config.tagRendering.id, - value: config, - })), - ]) - - return new Combine([ - pickAuto, - new VariableUiElement( - pickAuto - .GetValue() - .map((auto) => - auto === undefined - ? undefined - : new AutomationPanel( - layoutToUse, - tilesPerIndex.data, - extraComment.GetValue(), - auto - ) - ) - ), - ]) - }, - [tilesPerIndex] - ) - ).SetClass("flex flex-col"), - ]).SetClass("flex flex-col") - } -} - -MinimapImplementation.initialize() - -new AutomatonGui() diff --git a/UI/Base/If.svelte b/UI/Base/If.svelte new file mode 100644 index 000000000..124ff330b --- /dev/null +++ b/UI/Base/If.svelte @@ -0,0 +1,14 @@ + + +{#if _c} + +{/if} diff --git a/UI/Base/MapControlButton.svelte b/UI/Base/MapControlButton.svelte new file mode 100644 index 000000000..fd87d3d24 --- /dev/null +++ b/UI/Base/MapControlButton.svelte @@ -0,0 +1,13 @@ + + + +
dispatch("click", e)} class="subtle-background block rounded-full min-w-10 h-10 pointer-events-auto m-0.5 md:m-1 p-1"> + +
diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts deleted file mode 100644 index 13ac4b5f7..000000000 --- a/UI/Base/Minimap.ts +++ /dev/null @@ -1,47 +0,0 @@ -import BaseUIElement from "../BaseUIElement" -import Loc from "../../Models/Loc" -import BaseLayer from "../../Models/BaseLayer" -import { UIEventSource } from "../../Logic/UIEventSource" -import { BBox } from "../../Logic/BBox" - -export interface MinimapOptions { - background?: UIEventSource - location?: UIEventSource - bounds?: UIEventSource - allowMoving?: boolean - leafletOptions?: any - attribution?: BaseUIElement | boolean - onFullyLoaded?: (leaflet: L.Map) => void - leafletMap?: UIEventSource - lastClickLocation?: UIEventSource<{ lat: number; lon: number }> - addLayerControl?: boolean | false -} - -export interface MinimapObj { - readonly leafletMap: UIEventSource - readonly location: UIEventSource - readonly bounds: UIEventSource - - installBounds(factor: number | BBox, showRange?: boolean): void - - TakeScreenshot(format): Promise - TakeScreenshot(format: "image"): Promise - TakeScreenshot(format: "blob"): Promise - TakeScreenshot(format?: "image" | "blob"): Promise -} - -export default class Minimap { - /** - * A stub implementation. The actual implementation is injected later on, but only in the browser. - * importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it - */ - - private constructor() {} - - /** - * Construct a minimap - */ - public static createMiniMap: (options?: MinimapOptions) => BaseUIElement & MinimapObj = (_) => { - throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()" - } -} diff --git a/UI/Base/MinimapImplementation.ts b/UI/Base/MinimapImplementation.ts deleted file mode 100644 index a830f6b1d..000000000 --- a/UI/Base/MinimapImplementation.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { Utils } from "../../Utils" -import BaseUIElement from "../BaseUIElement" -import { UIEventSource } from "../../Logic/UIEventSource" -import Loc from "../../Models/Loc" -import BaseLayer from "../../Models/BaseLayer" -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" -import * as L from "leaflet" -import { LeafletMouseEvent, Map } from "leaflet" -import Minimap, { MinimapObj, MinimapOptions } from "./Minimap" -import { BBox } from "../../Logic/BBox" -import "leaflet-polylineoffset" -import { SimpleMapScreenshoter } from "leaflet-simple-map-screenshoter" -import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" -import ShowDataLayerImplementation from "../ShowDataLayer/ShowDataLayerImplementation" -import FilteredLayer from "../../Models/FilteredLayer" -import ScrollableFullScreen from "./ScrollableFullScreen" -import Constants from "../../Models/Constants" -import StrayClickHandler from "../../Logic/Actors/StrayClickHandler" - -/** - * The stray-click-hanlders adds a marker to the map if no feature was clicked. - * Shows the given uiToShow-element in the messagebox - */ -class StrayClickHandlerImplementation { - private _lastMarker - - constructor( - state: { - LastClickLocation: UIEventSource<{ lat: number; lon: number }> - selectedElement: UIEventSource - filteredLayers: UIEventSource - leafletMap: UIEventSource - }, - uiToShow: ScrollableFullScreen, - iconToShow: BaseUIElement - ) { - const self = this - const leafletMap = state.leafletMap - state.filteredLayers.data.forEach((filteredLayer) => { - filteredLayer.isDisplayed.addCallback((isEnabled) => { - if (isEnabled && self._lastMarker && leafletMap.data !== undefined) { - // When a layer is activated, we remove the 'last click location' in order to force the user to reclick - // This reclick might be at a location where a feature now appeared... - state.leafletMap.data.removeLayer(self._lastMarker) - } - }) - }) - - state.LastClickLocation.addCallback(function (lastClick) { - if (self._lastMarker !== undefined) { - state.leafletMap.data?.removeLayer(self._lastMarker) - } - - if (lastClick === undefined) { - return - } - - state.selectedElement.setData(undefined) - const clickCoor: [number, number] = [lastClick.lat, lastClick.lon] - self._lastMarker = L.marker(clickCoor, { - icon: L.divIcon({ - html: iconToShow.ConstructElement(), - iconSize: [50, 50], - iconAnchor: [25, 50], - popupAnchor: [0, -45], - }), - }) - - self._lastMarker.addTo(leafletMap.data) - - self._lastMarker.on("click", () => { - if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) { - leafletMap.data.flyTo( - clickCoor, - Constants.userJourney.minZoomLevelToAddNewPoints - ) - return - } - - uiToShow.Activate() - }) - }) - - state.selectedElement.addCallback(() => { - if (self._lastMarker !== undefined) { - leafletMap.data.removeLayer(self._lastMarker) - this._lastMarker = undefined - } - }) - } -} - -export default class MinimapImplementation extends BaseUIElement implements MinimapObj { - private static _nextId = 0 - public readonly leafletMap: UIEventSource - public readonly location: UIEventSource - public readonly bounds: UIEventSource | undefined - private readonly _id: string - private readonly _background: UIEventSource - private _isInited = false - private _allowMoving: boolean - private readonly _leafletoptions: any - private readonly _onFullyLoaded: (leaflet: L.Map) => void - private readonly _attribution: BaseUIElement | boolean - private readonly _addLayerControl: boolean - private readonly _options: MinimapOptions - - private constructor(options?: MinimapOptions) { - super() - options = options ?? {} - this._id = "minimap" + MinimapImplementation._nextId - MinimapImplementation._nextId++ - this.leafletMap = options.leafletMap ?? new UIEventSource(undefined) - this._background = - options?.background ?? new UIEventSource(AvailableBaseLayers.osmCarto) - this.location = options?.location ?? new UIEventSource({ lat: 0, lon: 0, zoom: 1 }) - this.bounds = options?.bounds - this._allowMoving = options.allowMoving ?? true - this._leafletoptions = options.leafletOptions ?? {} - this._onFullyLoaded = options.onFullyLoaded - this._attribution = options.attribution - this._addLayerControl = options.addLayerControl ?? false - this._options = options - this.SetClass("relative") - } - - public static initialize() { - Minimap.createMiniMap = (options) => new MinimapImplementation(options) - ShowDataLayer.actualContstructor = (options) => new ShowDataLayerImplementation(options) - StrayClickHandler.construct = ( - state: { - LastClickLocation: UIEventSource<{ lat: number; lon: number }> - selectedElement: UIEventSource - filteredLayers: UIEventSource - leafletMap: UIEventSource - }, - uiToShow: ScrollableFullScreen, - iconToShow: BaseUIElement - ) => { - return new StrayClickHandlerImplementation(state, uiToShow, iconToShow) - } - } - - public installBounds(factor: number | BBox, showRange?: boolean) { - this.leafletMap.addCallbackD((leaflet) => { - let bounds: { getEast(); getNorth(); getWest(); getSouth() } - if (typeof factor === "number") { - const lbounds = leaflet.getBounds().pad(factor) - leaflet.setMaxBounds(lbounds) - bounds = lbounds - } else { - // @ts-ignore - leaflet.setMaxBounds(factor.toLeaflet()) - bounds = factor - } - - if (showRange) { - const data = { - type: "FeatureCollection", - features: [ - { - type: "Feature", - geometry: { - type: "LineString", - coordinates: [ - [bounds.getEast(), bounds.getNorth()], - [bounds.getWest(), bounds.getNorth()], - [bounds.getWest(), bounds.getSouth()], - - [bounds.getEast(), bounds.getSouth()], - [bounds.getEast(), bounds.getNorth()], - ], - }, - }, - ], - } - // @ts-ignore - L.geoJSON(data, { - style: { - color: "#f44", - weight: 4, - opacity: 0.7, - }, - }).addTo(leaflet) - } - }) - } - - Destroy() { - super.Destroy() - console.warn("Decomissioning minimap", this._id) - const mp = this.leafletMap.data - this.leafletMap.setData(null) - mp.off() - mp.remove() - } - - /** - * Takes a screenshot of the current map - * @param format: image: give a base64 encoded png image; - * @constructor - */ - public async TakeScreenshot(): Promise - public async TakeScreenshot(format: "image"): Promise - public async TakeScreenshot(format: "blob"): Promise - public async TakeScreenshot(format: "image" | "blob"): Promise - public async TakeScreenshot(format: "image" | "blob" = "image"): Promise { - console.log("Taking a screenshot...") - const screenshotter = new SimpleMapScreenshoter() - screenshotter.addTo(this.leafletMap.data) - const result = await screenshotter.takeScreen(format ?? "image") - if (format === "image" && typeof result === "string") { - return result - } - if (format === "blob" && result instanceof Blob) { - return result - } - throw "Something went wrong while creating the screenshot: " + result - } - - protected InnerConstructElement(): HTMLElement { - const div = document.createElement("div") - div.id = this._id - div.style.height = "100%" - div.style.width = "100%" - div.style.minWidth = "40px" - div.style.minHeight = "40px" - div.style.position = "relative" - const wrapper = document.createElement("div") - wrapper.appendChild(div) - const self = this - // @ts-ignore - const resizeObserver = new ResizeObserver((_) => { - if (wrapper.clientHeight === 0 || wrapper.clientWidth === 0) { - return - } - if ( - wrapper.offsetParent === null || - window.getComputedStyle(wrapper).display === "none" - ) { - // Not visible - return - } - try { - self.InitMap() - } catch (e) { - console.debug("Could not construct a minimap:", e) - } - - try { - self.leafletMap?.data?.invalidateSize() - } catch (e) { - console.debug("Could not invalidate size of a minimap:", e) - } - }) - - resizeObserver.observe(div) - - if (this._addLayerControl) { - const switcher = new BackgroundMapSwitch( - { - locationControl: this.location, - backgroundLayer: this._background, - }, - this._background - ).SetClass("top-0 right-0 z-above-map absolute") - wrapper.appendChild(switcher.ConstructElement()) - } - - return wrapper - } - - private InitMap() { - if (this._constructedHtmlElement === undefined) { - // This element isn't initialized yet - return - } - - if (document.getElementById(this._id) === null) { - // not yet attached, we probably got some other event - return - } - - if (this._isInited) { - return - } - this._isInited = true - const location = this.location - const self = this - let currentLayer = this._background.data.layer() - let latLon = <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0] - if (isNaN(latLon[0]) || isNaN(latLon[1])) { - latLon = [0, 0] - } - const options = { - center: latLon, - zoom: location.data?.zoom ?? 2, - layers: [currentLayer], - zoomControl: false, - attributionControl: this._attribution !== undefined, - dragging: this._allowMoving, - scrollWheelZoom: this._allowMoving, - doubleClickZoom: this._allowMoving, - keyboard: this._allowMoving, - touchZoom: this._allowMoving, - // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving, - fadeAnimation: this._allowMoving, - maxZoom: 21, - } - - Utils.Merge(this._leafletoptions, options) - /* - * Somehow, the element gets '_leaflet_id' set on chrome. - * When attempting to init this leaflet map, it'll throw an exception and the map won't show up. - * Simply removing '_leaflet_id' fixes the issue. - * See https://github.com/pietervdvn/MapComplete/issues/726 - * */ - delete document.getElementById(this._id)["_leaflet_id"] - - const map = L.map(this._id, options) - if (self._onFullyLoaded !== undefined) { - currentLayer.on("load", () => { - console.log("Fully loaded all tiles!") - self._onFullyLoaded(map) - }) - } - - // Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then - // We give a bit of leeway for people on the edges - // Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/ - - map.setMaxBounds([ - [-100, -200], - [100, 200], - ]) - - if (this._attribution !== undefined) { - if (this._attribution === true) { - map.attributionControl.setPrefix(false) - } else { - map.attributionControl.setPrefix("") - } - } - - this._background.addCallbackAndRun((layer) => { - const newLayer = layer.layer() - if (currentLayer !== undefined) { - map.removeLayer(currentLayer) - } - currentLayer = newLayer - if (self._onFullyLoaded !== undefined) { - currentLayer.on("load", () => { - console.log("Fully loaded all tiles!") - self._onFullyLoaded(map) - }) - } - map.addLayer(newLayer) - if (self._attribution !== true && self._attribution !== false) { - self._attribution?.AttachTo("leaflet-attribution") - } - }) - - let isRecursing = false - map.on("moveend", function () { - if (isRecursing) { - return - } - if ( - map.getZoom() === location.data.zoom && - map.getCenter().lat === location.data.lat && - map.getCenter().lng === location.data.lon - ) { - return - } - location.data.zoom = map.getZoom() - location.data.lat = map.getCenter().lat - location.data.lon = map.getCenter().lng - isRecursing = true - location.ping() - - if (self.bounds !== undefined) { - self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) - } - - isRecursing = false // This is ugly, I know - }) - - location.addCallback((loc) => { - const mapLoc = map.getCenter() - const dlat = Math.abs(loc.lat - mapLoc[0]) - const dlon = Math.abs(loc.lon - mapLoc[1]) - - if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) { - return - } - map.setView([loc.lat, loc.lon], loc.zoom) - }) - - if (self.bounds !== undefined) { - self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) - } - - if (this._options.lastClickLocation) { - const lastClickLocation = this._options.lastClickLocation - map.addEventListener("click", function (e: LeafletMouseEvent) { - if (e.originalEvent["dismissed"]) { - return - } - lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng }) - }) - - map.on("contextmenu", function (e) { - // @ts-ignore - lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng }) - map.setZoom(map.getZoom() + 1) - }) - } - - this.leafletMap.setData(map) - } -} diff --git a/UI/Base/SvelteUIElement.ts b/UI/Base/SvelteUIElement.ts index 67115f3c4..68a51304a 100644 --- a/UI/Base/SvelteUIElement.ts +++ b/UI/Base/SvelteUIElement.ts @@ -4,6 +4,7 @@ import { SvelteComponentTyped } from "svelte" /** * The SvelteUIComponent serves as a translating class which which wraps a SvelteElement into the BaseUIElement framework. + * Also see ToSvelte.svelte for the opposite conversion */ export default class SvelteUIElement< Props extends Record = any, diff --git a/UI/BigComponents/AddNewMarker.ts b/UI/BigComponents/AddNewMarker.ts index 6c3fe93d6..c260846fd 100644 --- a/UI/BigComponents/AddNewMarker.ts +++ b/UI/BigComponents/AddNewMarker.ts @@ -1,4 +1,4 @@ -import { UIEventSource } from "../../Logic/UIEventSource" +import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"; import Combine from "../Base/Combine" import Translations from "../i18n/Translations" import { VariableUiElement } from "../Base/VariableUIElement" @@ -24,13 +24,13 @@ export default class AddNewMarker extends Combine { for (const preset of filteredLayer.layerDef.presets) { const tags = TagUtils.KVtoProperties(preset.tags) const icon = layer.mapRendering[0] - .GenerateLeafletStyle(new UIEventSource(tags), false) + .RenderIcon(new ImmutableStore(tags), false) .html.SetClass("block relative") .SetStyle("width: 42px; height: 42px;") icons.push(icon) if (last === undefined) { last = layer.mapRendering[0] - .GenerateLeafletStyle(new UIEventSource(tags), false) + .RenderIcon(new ImmutableStore(tags), false) .html.SetClass("block relative") .SetStyle("width: 42px; height: 42px;") } diff --git a/UI/BigComponents/Attribution.ts b/UI/BigComponents/Attribution.ts deleted file mode 100644 index d1f7bb497..000000000 --- a/UI/BigComponents/Attribution.ts +++ /dev/null @@ -1,92 +0,0 @@ -import Link from "../Base/Link" -import Svg from "../../Svg" -import Combine from "../Base/Combine" -import { UIEventSource } from "../../Logic/UIEventSource" -import UserDetails from "../../Logic/Osm/OsmConnection" -import Constants from "../../Models/Constants" -import Loc from "../../Models/Loc" -import { VariableUiElement } from "../Base/VariableUIElement" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import { BBox } from "../../Logic/BBox" -import { Utils } from "../../Utils" -import Translations from "../i18n/Translations" - -/** - * The bottom right attribution panel in the leaflet map - */ -export default class Attribution extends Combine { - constructor( - location: UIEventSource, - userDetails: UIEventSource, - layoutToUse: LayoutConfig, - currentBounds: UIEventSource - ) { - const mapComplete = new Link( - `Mapcomplete ${Constants.vNumber}`, - "https://github.com/pietervdvn/MapComplete", - true - ) - const reportBug = new Link( - Svg.bug_ui().SetClass("small-image"), - "https://github.com/pietervdvn/MapComplete/issues", - true - ) - - const layoutId = layoutToUse?.id - const stats = new Link( - Svg.statistics_ui().SetClass("small-image"), - Utils.OsmChaLinkFor(31, layoutId), - true - ) - - const idLink = location.map( - (location) => - `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${ - location?.lat ?? 0 - }/${location?.lon ?? 0}` - ) - const editHere = new Link(Svg.pencil_ui().SetClass("small-image"), idLink, true) - - const mapillaryLink = location.map( - (location) => - `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${ - location?.lon ?? 0 - }&z=${Math.max((location?.zoom ?? 2) - 1, 1)}` - ) - const mapillary = new Link( - Svg.mapillary_black_ui().SetClass("small-image"), - mapillaryLink, - true - ) - - const mapDataByOsm = new Link( - Translations.t.general.attribution.mapDataByOsm, - "https://openstreetmap.org/copyright", - true - ) - - const editWithJosm = new VariableUiElement( - userDetails.map( - (userDetails) => { - if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) { - return undefined - } - const bounds: any = currentBounds.data - if (bounds === undefined) { - return undefined - } - const top = bounds.getNorth() - const bottom = bounds.getSouth() - const right = bounds.getEast() - const left = bounds.getWest() - - const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` - return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true) - }, - [location, currentBounds] - ) - ) - super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary, mapDataByOsm]) - this.SetClass("flex") - } -} diff --git a/UI/BigComponents/GeolocationControl.ts b/UI/BigComponents/GeolocationControl.ts index 9350d252a..ee5465b12 100644 --- a/UI/BigComponents/GeolocationControl.ts +++ b/UI/BigComponents/GeolocationControl.ts @@ -1,25 +1,19 @@ import { VariableUiElement } from "../Base/VariableUIElement" import Svg from "../../Svg" -import { UIEventSource } from "../../Logic/UIEventSource" +import { Store, UIEventSource } from "../../Logic/UIEventSource" import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler" import { BBox } from "../../Logic/BBox" -import Loc from "../../Models/Loc" import Hotkeys from "../Base/Hotkeys" import Translations from "../i18n/Translations" import Constants from "../../Models/Constants" +import { MapProperties } from "../../Models/MapProperties" /** * Displays an icon depending on the state of the geolocation. * Will set the 'lock' if clicked twice */ export class GeolocationControl extends VariableUiElement { - constructor( - geolocationHandler: GeoLocationHandler, - state: { - locationControl: UIEventSource - currentBounds: UIEventSource - } - ) { + constructor(geolocationHandler: GeoLocationHandler, state: MapProperties) { const lastClick = new UIEventSource(undefined) lastClick.addCallbackD((date) => { geolocationHandler.geolocationState.requestMoment.setData(date) @@ -48,7 +42,7 @@ export class GeolocationControl extends VariableUiElement { if (permission === "denied") { return Svg.location_refused_svg() } - if (geolocationState.isLocked.data) { + if (!geolocationState.allowMoving.data) { return Svg.location_locked_svg() } @@ -77,7 +71,7 @@ export class GeolocationControl extends VariableUiElement { }, [ geolocationState.currentGPSLocation, - geolocationState.isLocked, + geolocationState.allowMoving, geolocationHandler.mapHasMoved, lastClickWithinThreeSecs, lastRequestWithinTimeout, @@ -95,9 +89,9 @@ export class GeolocationControl extends VariableUiElement { await geolocationState.requestPermission() } - if (geolocationState.isLocked.data === true) { + if (geolocationState.allowMoving.data === false) { // Unlock - geolocationState.isLocked.setData(false) + geolocationState.allowMoving.setData(true) return } @@ -109,21 +103,17 @@ export class GeolocationControl extends VariableUiElement { // A location _is_ known! Let's move to this location const currentLocation = geolocationState.currentGPSLocation.data - const inBounds = state.currentBounds.data.contains([ + const inBounds = state.bounds.data.contains([ currentLocation.longitude, currentLocation.latitude, ]) geolocationHandler.MoveMapToCurrentLocation() if (inBounds) { - const lc = state.locationControl.data - state.locationControl.setData({ - ...lc, - zoom: lc.zoom + 3, - }) + state.zoom.update((z) => z + 3) } if (lastClickWithinThreeSecs.data) { - geolocationState.isLocked.setData(true) + geolocationState.allowMoving.setData(false) lastClick.setData(undefined) return } diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index 9a342c0ba..7d37489e0 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -11,7 +11,6 @@ import BackgroundMapSwitch from "./BackgroundMapSwitch" import Lazy from "../Base/Lazy" import { VariableUiElement } from "../Base/VariableUIElement" import FeatureInfoBox from "../Popup/FeatureInfoBox" -import CopyrightPanel from "./CopyrightPanel" import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" import Hotkeys from "../Base/Hotkeys" import { DefaultGuiState } from "../DefaultGuiState" @@ -21,7 +20,7 @@ export default class LeftControls extends Combine { const currentViewFL = state.currentView?.layer const currentViewAction = new Toggle( new Lazy(() => { - const feature: Store = state.currentView.features.map((ffs) => ffs[0]?.feature) + const feature: Store = state.currentView.features.map((ffs) => ffs[0]) const icon = new VariableUiElement( feature.map((feature) => { const defaultIcon = Svg.checkbox_empty_svg() diff --git a/UI/BigComponents/LevelSelector.ts b/UI/BigComponents/LevelSelector.ts index 789380db9..d40020f25 100644 --- a/UI/BigComponents/LevelSelector.ts +++ b/UI/BigComponents/LevelSelector.ts @@ -1,5 +1,5 @@ import FloorLevelInputElement from "../Input/FloorLevelInputElement" -import MapState, { GlobalFilter } from "../../Logic/State/MapState" +import MapState from "../../Logic/State/MapState" import { TagsFilter } from "../../Logic/Tags/TagsFilter" import { RegexTag } from "../../Logic/Tags/RegexTag" import { Or } from "../../Logic/Tags/Or" @@ -11,6 +11,7 @@ import { BBox } from "../../Logic/BBox" import { TagUtils } from "../../Logic/Tags/TagUtils" import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" import { Store } from "../../Logic/UIEventSource" +import { GlobalFilter } from "../../Logic/State/GlobalFilter" /*** * The element responsible for the level input element and picking the right level, showing and hiding at the right time, ... diff --git a/UI/BigComponents/RightControls.ts b/UI/BigComponents/RightControls.ts index 189559ba5..c34daa479 100644 --- a/UI/BigComponents/RightControls.ts +++ b/UI/BigComponents/RightControls.ts @@ -9,30 +9,9 @@ import LevelSelector from "./LevelSelector" import { GeolocationControl } from "./GeolocationControl" export default class RightControls extends Combine { - constructor( - state: MapState & { featurePipeline: FeaturePipeline }, - geolocationHandler: GeoLocationHandler - ) { - const geolocationButton = Toggle.If(state.featureSwitchGeolocation, () => - new MapControlButton(new GeolocationControl(geolocationHandler, state), { - dontStyle: true, - }).SetClass("p-1") - ) - - const plus = new MapControlButton(Svg.plus_svg()).onClick(() => { - state.locationControl.data.zoom++ - state.locationControl.ping() - }) - - const min = new MapControlButton(Svg.min_svg()).onClick(() => { - state.locationControl.data.zoom-- - state.locationControl.ping() - }) - + constructor(state: MapState & { featurePipeline: FeaturePipeline }) { const levelSelector = new LevelSelector(state) - super( - [levelSelector, plus, min, geolocationButton].map((el) => el.SetClass("m-0.5 md:m-1")) - ) + super([levelSelector].map((el) => el.SetClass("m-0.5 md:m-1"))) this.SetClass("flex flex-col items-center") } } diff --git a/UI/BigComponents/SearchAndGo.ts b/UI/BigComponents/SearchAndGo.ts index 56290896d..f6063c860 100644 --- a/UI/BigComponents/SearchAndGo.ts +++ b/UI/BigComponents/SearchAndGo.ts @@ -1,4 +1,4 @@ -import { UIEventSource } from "../../Logic/UIEventSource" +import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Translation } from "../i18n/Translation" import Svg from "../../Svg" import { TextField } from "../Input/TextField" @@ -7,10 +7,15 @@ import Translations from "../i18n/Translations" import Hash from "../../Logic/Web/Hash" import Combine from "../Base/Combine" import Locale from "../i18n/Locale" +import { BBox } from "../../Logic/BBox" export default class SearchAndGo extends Combine { private readonly _searchField: TextField - constructor(state: { leafletMap: UIEventSource; selectedElement?: UIEventSource }) { + constructor(state: { + leafletMap: UIEventSource + selectedElement?: UIEventSource + bounds?: Store + }) { const goButton = Svg.search_ui().SetClass("w-8 h-8 full-rounded border-black float-right") const placeholder = new UIEventSource(Translations.t.general.search.search) @@ -49,7 +54,7 @@ export default class SearchAndGo extends Combine { searchField.GetValue().setData("") placeholder.setData(Translations.t.general.search.searching) try { - const result = await Geocoding.Search(searchString) + const result = await Geocoding.Search(searchString, state.bounds.data) console.log("Search result", result) if (result.length == 0) { diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 2cebb2512..3582848ae 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -1,7 +1,7 @@ /** * Asks to add a feature at the last clicked location, at least if zoom is sufficient */ -import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import Svg from "../../Svg" import { SubtleButton } from "../Base/SubtleButton" import Combine from "../Base/Combine" @@ -22,13 +22,12 @@ import { Changes } from "../../Logic/Osm/Changes" import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" import { ElementStorage } from "../../Logic/ElementStorage" import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint" -import BaseLayer from "../../Models/BaseLayer" import Loading from "../Base/Loading" import Hash from "../../Logic/Web/Hash" -import { GlobalFilter } from "../../Logic/State/MapState" import { WayId } from "../../Models/OsmFeature" import { Tag } from "../../Logic/Tags/Tag" import { LoginToggle } from "../Popup/LoginButton" +import { GlobalFilter } from "../../Models/GlobalFilter" /* * The SimpleAddUI is a single panel, which can have multiple states: @@ -288,7 +287,7 @@ export default class SimpleAddUI extends LoginToggle { const tags = TagUtils.KVtoProperties(preset.tags ?? []) let icon: () => BaseUIElement = () => layer.layerDef.mapRendering[0] - .GenerateLeafletStyle(new UIEventSource(tags), false) + .RenderIcon(new ImmutableStore(tags), false) .html.SetClass("w-12 h-12 block relative") const presetInfo: PresetInfo = { layerToAddTo: layer, diff --git a/UI/DashboardGui.ts b/UI/DashboardGui.ts deleted file mode 100644 index 213e692bf..000000000 --- a/UI/DashboardGui.ts +++ /dev/null @@ -1,305 +0,0 @@ -import FeaturePipelineState from "../Logic/State/FeaturePipelineState" -import { DefaultGuiState } from "./DefaultGuiState" -import { FixedUiElement } from "./Base/FixedUiElement" -import { Utils } from "../Utils" -import Combine from "./Base/Combine" -import ShowDataLayer from "./ShowDataLayer/ShowDataLayer" -import LayerConfig from "../Models/ThemeConfig/LayerConfig" -import home_location_json from "../assets/layers/home_location/home_location.json" -import State from "../State" -import Title from "./Base/Title" -import { MinimapObj } from "./Base/Minimap" -import BaseUIElement from "./BaseUIElement" -import { VariableUiElement } from "./Base/VariableUIElement" -import { GeoOperations } from "../Logic/GeoOperations" -import { OsmFeature } from "../Models/OsmFeature" -import SearchAndGo from "./BigComponents/SearchAndGo" -import FeatureInfoBox from "./Popup/FeatureInfoBox" -import { UIEventSource } from "../Logic/UIEventSource" -import LanguagePicker from "./LanguagePicker" -import Lazy from "./Base/Lazy" -import TagRenderingAnswer from "./Popup/TagRenderingAnswer" -import Hash from "../Logic/Web/Hash" -import FilterView from "./BigComponents/FilterView" -import Translations from "./i18n/Translations" -import Constants from "../Models/Constants" -import SimpleAddUI from "./BigComponents/SimpleAddUI" -import BackToIndex from "./BigComponents/BackToIndex" -import StatisticsPanel from "./BigComponents/StatisticsPanel" - -export default class DashboardGui { - private readonly state: FeaturePipelineState - private readonly currentView: UIEventSource<{ - title: string | BaseUIElement - contents: string | BaseUIElement - }> = new UIEventSource(undefined) - - constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { - this.state = state - } - - private viewSelector( - shown: BaseUIElement, - title: string | BaseUIElement, - contents: string | BaseUIElement, - hash?: string - ): BaseUIElement { - const currentView = this.currentView - const v = { title, contents } - shown.SetClass("pl-1 pr-1 rounded-md") - shown.onClick(() => { - currentView.setData(v) - }) - Hash.hash.addCallbackAndRunD((h) => { - if (h === hash) { - currentView.setData(v) - } - }) - currentView.addCallbackAndRunD((cv) => { - if (cv == v) { - shown.SetClass("bg-unsubtle") - Hash.hash.setData(hash) - } else { - shown.RemoveClass("bg-unsubtle") - } - }) - return shown - } - - private singleElementCache: Record = {} - - private singleElementView( - element: OsmFeature, - layer: LayerConfig, - distance: number - ): BaseUIElement { - if (this.singleElementCache[element.properties.id] !== undefined) { - return this.singleElementCache[element.properties.id] - } - const tags = this.state.allElements.getEventSourceById(element.properties.id) - const title = new Combine([ - new Title(new TagRenderingAnswer(tags, layer.title, this.state), 4), - distance < 900 - ? Math.floor(distance) + "m away" - : Utils.Round(distance / 1000) + "km away", - ]).SetClass("flex justify-between") - - return (this.singleElementCache[element.properties.id] = this.viewSelector( - title, - new Lazy(() => FeatureInfoBox.GenerateTitleBar(tags, layer, this.state)), - new Lazy(() => FeatureInfoBox.GenerateContent(tags, layer, this.state)) - // element.properties.id - )) - } - - private mainElementsView( - elements: { element: OsmFeature; layer: LayerConfig; distance: number }[] - ): BaseUIElement { - const self = this - if (elements === undefined) { - return new FixedUiElement("Initializing") - } - if (elements.length == 0) { - return new FixedUiElement("No elements in view") - } - return new Combine( - elements.map((e) => self.singleElementView(e.element, e.layer, e.distance)) - ) - } - - private documentationButtonFor(layerConfig: LayerConfig): BaseUIElement { - return this.viewSelector( - Translations.W(layerConfig.name?.Clone() ?? layerConfig.id), - new Combine(["Documentation about ", layerConfig.name?.Clone() ?? layerConfig.id]), - layerConfig.GenerateDocumentation([]), - "documentation-" + layerConfig.id - ) - } - - private allDocumentationButtons(): BaseUIElement { - const layers = this.state.layoutToUse.layers - .filter((l) => Constants.priviliged_layers.indexOf(l.id) < 0) - .filter((l) => !l.id.startsWith("note_import_")) - - if (layers.length === 1) { - return this.documentationButtonFor(layers[0]) - } - return this.viewSelector( - new FixedUiElement("Documentation"), - "Documentation", - new Combine(layers.map((l) => this.documentationButtonFor(l).SetClass("flex flex-col"))) - ) - } - - public setup(): void { - const state = this.state - - if (this.state.layoutToUse.customCss !== undefined) { - if (window.location.pathname.indexOf("index") >= 0) { - Utils.LoadCustomCss(this.state.layoutToUse.customCss) - } - } - const map = this.SetupMap() - - Utils.downloadJson("./service-worker-version") - .then((data) => console.log("Service worker", data)) - .catch((_) => console.log("Service worker not active")) - - document.getElementById("centermessage").classList.add("hidden") - - const layers: Record = {} - for (const layer of state.layoutToUse.layers) { - layers[layer.id] = layer - } - - const self = this - const elementsInview = new UIEventSource< - { - distance: number - center: [number, number] - element: OsmFeature - layer: LayerConfig - }[] - >([]) - - function update() { - const mapCenter = <[number, number]>[ - self.state.locationControl.data.lon, - self.state.locationControl.data.lon, - ] - const elements = self.state.featurePipeline - .getAllVisibleElementsWithmeta(self.state.currentBounds.data) - .map((el) => { - const distance = GeoOperations.distanceBetween(el.center, mapCenter) - return { ...el, distance } - }) - elements.sort((e0, e1) => e0.distance - e1.distance) - elementsInview.setData(elements) - } - - map.bounds.addCallbackAndRun(update) - state.featurePipeline.newDataLoadedSignal.addCallback(update) - state.filteredLayers.addCallbackAndRun((fls) => { - for (const fl of fls) { - fl.isDisplayed.addCallback(update) - fl.appliedFilters.addCallback(update) - } - }) - - const filterView = new Lazy(() => { - return new FilterView(state.filteredLayers, state.overlayToggles, state) - }) - const welcome = new Combine([ - state.layoutToUse.description, - state.layoutToUse.descriptionTail, - ]) - self.currentView.setData({ title: state.layoutToUse.title, contents: welcome }) - const filterViewIsOpened = new UIEventSource(false) - filterViewIsOpened.addCallback((_) => - self.currentView.setData({ title: "filters", contents: filterView }) - ) - - const newPointIsShown = new UIEventSource(false) - const addNewPoint = new SimpleAddUI( - new UIEventSource(true), - new UIEventSource(undefined), - filterViewIsOpened, - state, - state.locationControl - ) - const addNewPointTitle = "Add a missing point" - this.currentView.addCallbackAndRunD((cv) => { - newPointIsShown.setData(cv.contents === addNewPoint) - }) - newPointIsShown.addCallbackAndRun((isShown) => { - if (isShown) { - if (self.currentView.data.contents !== addNewPoint) { - self.currentView.setData({ title: addNewPointTitle, contents: addNewPoint }) - } - } else { - if (self.currentView.data.contents === addNewPoint) { - self.currentView.setData(undefined) - } - } - }) - - new Combine([ - new Combine([ - this.viewSelector( - new Title(state.layoutToUse.title.Clone(), 2), - state.layoutToUse.title.Clone(), - welcome, - "welcome" - ), - map.SetClass("w-full h-64 shrink-0 rounded-lg"), - new SearchAndGo(state), - this.viewSelector( - new Title( - new VariableUiElement( - elementsInview.map( - (elements) => "There are " + elements?.length + " elements in view" - ) - ) - ), - "Statistics", - new StatisticsPanel(elementsInview, this.state), - "statistics" - ), - - this.viewSelector(new FixedUiElement("Filter"), "Filters", filterView, "filters"), - this.viewSelector( - new Combine(["Add a missing point"]), - addNewPointTitle, - addNewPoint - ), - - new VariableUiElement( - elementsInview.map((elements) => - this.mainElementsView(elements).SetClass("block m-2") - ) - ).SetClass( - "block shrink-2 overflow-x-auto h-full border-2 border-subtle rounded-lg" - ), - this.allDocumentationButtons(), - new LanguagePicker(Object.keys(state.layoutToUse.title.translations)).SetClass( - "mt-2" - ), - new BackToIndex(), - ]).SetClass("w-1/2 lg:w-1/4 m-4 flex flex-col shrink-0 grow-0"), - new VariableUiElement( - this.currentView.map(({ title, contents }) => { - return new Combine([ - new Title(Translations.W(title), 2).SetClass( - "shrink-0 border-b-4 border-subtle" - ), - Translations.W(contents).SetClass("shrink-2 overflow-y-auto block"), - ]).SetClass("flex flex-col h-full") - }) - ).SetClass( - "w-1/2 lg:w-3/4 m-4 p-2 border-2 border-subtle rounded-xl m-4 ml-0 mr-8 shrink-0 grow-0" - ), - ]) - .SetClass("flex h-full") - .AttachTo("leafletDiv") - } - - private SetupMap(): MinimapObj & BaseUIElement { - const state = this.state - - new ShowDataLayer({ - leafletMap: state.leafletMap, - layerToShow: new LayerConfig(home_location_json, "home_location", true), - features: state.homeLocation, - state, - }) - - state.leafletMap.addCallbackAndRunD((_) => { - // Lets assume that all showDataLayers are initialized at this point - state.selectedElement.ping() - State.state.locationControl.ping() - return true - }) - - return state.mainMapObject - } -} diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index f2e3c8a4c..c793c7a42 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -171,9 +171,6 @@ export default class DefaultGUI { const state = this.state const guiState = this.guiState - // Attach the map - state.mainMapObject.SetClass("w-full h-full").AttachTo("leafletDiv") - this.setupClickDialogOnMap(guiState.filterViewIsOpened, state) new ShowDataLayer({ diff --git a/UI/ImportFlow/ConflationChecker.ts b/UI/ImportFlow/ConflationChecker.ts index 5ec1de443..70ba96c4d 100644 --- a/UI/ImportFlow/ConflationChecker.ts +++ b/UI/ImportFlow/ConflationChecker.ts @@ -138,12 +138,6 @@ export default class ConflationChecker location, background, bounds: currentBounds, - attribution: new Attribution( - location, - state.osmConnection.userDetails, - undefined, - currentBounds - ), }) osmLiveData.SetClass("w-full").SetStyle("height: 500px") diff --git a/UI/ImportFlow/MapPreview.ts b/UI/ImportFlow/MapPreview.ts index 9fb71489d..c000c7a68 100644 --- a/UI/ImportFlow/MapPreview.ts +++ b/UI/ImportFlow/MapPreview.ts @@ -9,10 +9,6 @@ import { DropDown } from "../Input/DropDown" import { Utils } from "../../Utils" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import Loc from "../../Models/Loc" -import Minimap from "../Base/Minimap" -import Attribution from "../BigComponents/Attribution" -import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" -import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import Toggle from "../Input/Toggle" import { VariableUiElement } from "../Base/VariableUIElement" @@ -21,12 +17,14 @@ import { FlowStep } from "./FlowStep" import ScrollableFullScreen from "../Base/ScrollableFullScreen" import Title from "../Base/Title" import CheckBoxes from "../Input/Checkboxes" -import AllTagsPanel from "../AllTagsPanel.svelte" +import AllTagsPanel from "../Popup/AllTagsPanel.svelte" import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" import { Feature, Point } from "geojson" import DivContainer from "../Base/DivContainer" -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import SvelteUIElement from "../Base/SvelteUIElement" +import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" +import { MapLibreAdaptor } from "../Map/MapLibreAdaptor" +import ShowDataLayer from "../Map/ShowDataLayer" class PreviewPanel extends ScrollableFullScreen { constructor(tags: UIEventSource) { @@ -110,21 +108,11 @@ export class MapPreview return matching }) - const background = new UIEventSource(AvailableBaseLayers.osmCarto) + const background = new UIEventSource(AvailableRasterLayers.osmCarto) const location = new UIEventSource({ lat: 0, lon: 0, zoom: 1 }) const currentBounds = new UIEventSource(undefined) - const map = Minimap.createMiniMap({ - allowMoving: true, - location, - background, - bounds: currentBounds, - attribution: new Attribution( - location, - state.osmConnection.userDetails, - undefined, - currentBounds - ), - }) + const { ui, mapproperties, map } = MapLibreAdaptor.construct() + const layerControl = new BackgroundMapSwitch( { backgroundLayer: background, @@ -132,15 +120,14 @@ export class MapPreview }, background ) - map.SetClass("w-full").SetStyle("height: 500px") + ui.SetClass("w-full").SetStyle("height: 500px") layerPicker.GetValue().addCallbackAndRunD((layerToShow) => { - new ShowDataLayer({ - layerToShow, + new ShowDataLayer(map, { + layer: layerToShow, zoomToFeatures: true, features: new StaticFeatureSource(matching), - leafletMap: map.leafletMap, - popup: (tag) => new PreviewPanel(tag), + buildPopup: (tag) => new PreviewPanel(tag), }) }) @@ -171,9 +158,8 @@ export class MapPreview new Title(t.title, 1), layerPicker, new Toggle(t.autodetected.SetClass("thanks"), undefined, autodetected), - mismatchIndicator, - map, + ui, new DivContainer("fullscreen"), layerControl, confirm, diff --git a/UI/Map/MapLibreAdaptor.ts b/UI/Map/MapLibreAdaptor.ts index 682ab9f70..3b9aba3ac 100644 --- a/UI/Map/MapLibreAdaptor.ts +++ b/UI/Map/MapLibreAdaptor.ts @@ -1,34 +1,56 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" import type { Map as MLMap } from "maplibre-gl" +import { Map as MlMap } from "maplibre-gl" import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers" import { Utils } from "../../Utils" import { BBox } from "../../Logic/BBox" +import { MapProperties } from "../../Models/MapProperties" +import SvelteUIElement from "../Base/SvelteUIElement" +import MaplibreMap from "./MaplibreMap.svelte" +import Constants from "../../Models/Constants" -export interface MapState { +/** + * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` + */ +export class MapLibreAdaptor implements MapProperties { + private static maplibre_control_handlers = [ + "scrollZoom", + "boxZoom", + "dragRotate", + "dragPan", + "keyboard", + "doubleClickZoom", + "touchZoomRotate", + ] readonly location: UIEventSource<{ lon: number; lat: number }> readonly zoom: UIEventSource readonly bounds: Store readonly rasterLayer: UIEventSource -} -export class MapLibreAdaptor implements MapState { + readonly maxbounds: UIEventSource + readonly allowMoving: UIEventSource private readonly _maplibreMap: Store - - readonly location: UIEventSource<{ lon: number; lat: number }> - readonly zoom: UIEventSource - readonly bounds: Store - readonly rasterLayer: UIEventSource private readonly _bounds: UIEventSource - /** * Used for internal bookkeeping (to remove a rasterLayer when done loading) * @private */ private _currentRasterLayer: string - constructor(maplibreMap: Store, state?: Partial>) { + + constructor(maplibreMap: Store, state?: Partial>) { this._maplibreMap = maplibreMap this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 }) this.zoom = state?.zoom ?? new UIEventSource(1) + this.zoom.addCallbackAndRunD((z) => { + if (z < 0) { + this.zoom.setData(0) + } + if (z > 24) { + this.zoom.setData(24) + } + }) + this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined) + this.allowMoving = state?.allowMoving ?? new UIEventSource(true) this._bounds = new UIEventSource(BBox.global) this.bounds = this._bounds this.rasterLayer = @@ -38,20 +60,26 @@ export class MapLibreAdaptor implements MapState { maplibreMap.addCallbackAndRunD((map) => { map.on("load", () => { self.setBackground() + self.MoveMapToCurrentLoc(self.location.data) + self.SetZoom(self.zoom.data) + self.setMaxBounds(self.maxbounds.data) + self.setAllowMoving(self.allowMoving.data) }) - self.MoveMapToCurrentLoc(this.location.data) - self.SetZoom(this.zoom.data) + self.MoveMapToCurrentLoc(self.location.data) + self.SetZoom(self.zoom.data) + self.setMaxBounds(self.maxbounds.data) + self.setAllowMoving(self.allowMoving.data) map.on("moveend", () => { const dt = this.location.data dt.lon = map.getCenter().lng dt.lat = map.getCenter().lat this.location.ping() - this.zoom.setData(map.getZoom()) + this.zoom.setData(Math.round(map.getZoom() * 10) / 10) }) }) this.rasterLayer.addCallback((_) => - self.setBackground().catch((e) => { + self.setBackground().catch((_) => { console.error("Could not set background") }) ) @@ -60,25 +88,25 @@ export class MapLibreAdaptor implements MapState { self.MoveMapToCurrentLoc(loc) }) this.zoom.addCallbackAndRunD((z) => self.SetZoom(z)) + this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox)) + this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving)) } - private SetZoom(z: number) { - const map = this._maplibreMap.data - if (map === undefined || z === undefined) { - return - } - if (map.getZoom() !== z) { - map.setZoom(z) - } - } - private MoveMapToCurrentLoc(loc: { lat: number; lon: number }) { - const map = this._maplibreMap.data - if (map === undefined || loc === undefined) { - return - } - const center = map.getCenter() - if (center.lng !== loc.lon || center.lat !== loc.lat) { - map.setCenter({ lng: loc.lon, lat: loc.lat }) + /** + * Convenience constructor + */ + public static construct(): { + map: Store + ui: SvelteUIElement + mapproperties: MapProperties + } { + const mlmap = new UIEventSource(undefined) + return { + map: mlmap, + ui: new SvelteUIElement(MaplibreMap, { + map: mlmap, + }), + mapproperties: new MapLibreAdaptor(mlmap), } } @@ -103,7 +131,6 @@ export class MapLibreAdaptor implements MapState { const subdomains = url.match(/\{switch:([a-zA-Z0-9,]*)}/) if (subdomains !== null) { - console.log("Found a switch:", subdomains) const options = subdomains[1].split(",") const option = options[Math.floor(Math.random() * options.length)] url = url.replace(subdomains[0], option) @@ -112,6 +139,28 @@ export class MapLibreAdaptor implements MapState { return url } + private SetZoom(z: number) { + const map = this._maplibreMap.data + if (!map || z === undefined) { + return + } + if (Math.abs(map.getZoom() - z) > 0.01) { + map.setZoom(z) + } + } + + private MoveMapToCurrentLoc(loc: { lat: number; lon: number }) { + const map = this._maplibreMap.data + if (!map || loc === undefined) { + return + } + + const center = map.getCenter() + if (center.lng !== loc.lon || center.lat !== loc.lat) { + map.setCenter({ lng: loc.lon, lat: loc.lat }) + } + } + private async awaitStyleIsLoaded(): Promise { const map = this._maplibreMap.data if (map === undefined) { @@ -125,7 +174,6 @@ export class MapLibreAdaptor implements MapState { private removeCurrentLayer(map: MLMap) { if (this._currentRasterLayer) { // hide the previous layer - console.log("Removing previous layer", this._currentRasterLayer) map.removeLayer(this._currentRasterLayer) map.removeSource(this._currentRasterLayer) } @@ -185,4 +233,32 @@ export class MapLibreAdaptor implements MapState { this.removeCurrentLayer(map) this._currentRasterLayer = background?.id } + + private setMaxBounds(bbox: undefined | BBox) { + const map = this._maplibreMap.data + if (map === undefined) { + return + } + if (bbox) { + map.setMaxBounds(bbox.toLngLat()) + } else { + map.setMaxBounds(null) + } + } + + private setAllowMoving(allow: true | boolean | undefined) { + const map = this._maplibreMap.data + if (map === undefined) { + return + } + if (allow === false) { + for (const id of MapLibreAdaptor.maplibre_control_handlers) { + map[id].disable() + } + } else { + for (const id of MapLibreAdaptor.maplibre_control_handlers) { + map[id].enable() + } + } + } } diff --git a/UI/Map/MaplibreMap.svelte b/UI/Map/MaplibreMap.svelte index 25d9462d2..dcd1a1cd6 100644 --- a/UI/Map/MaplibreMap.svelte +++ b/UI/Map/MaplibreMap.svelte @@ -8,8 +8,6 @@ import { Map } from "@onsvisual/svelte-maps"; import type { Map as MaplibreMap } from "maplibre-gl"; import type { Writable } from "svelte/store"; - import type Loc from "../../Models/Loc"; - import { UIEventSource } from "../../Logic/UIEventSource"; /** @@ -30,7 +28,6 @@
diff --git a/UI/Map/ShowDataLayer.ts b/UI/Map/ShowDataLayer.ts index 904fa59d0..c5e1ee15a 100644 --- a/UI/Map/ShowDataLayer.ts +++ b/UI/Map/ShowDataLayer.ts @@ -1,108 +1,294 @@ import { ImmutableStore, Store } from "../../Logic/UIEventSource" import type { Map as MlMap } from "maplibre-gl" import { Marker } from "maplibre-gl" -import { ShowDataLayerOptions } from "../ShowDataLayer/ShowDataLayerOptions" +import { ShowDataLayerOptions } from "./ShowDataLayerOptions" import { GeoOperations } from "../../Logic/GeoOperations" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig" -import { OsmFeature, OsmTags } from "../../Models/OsmFeature" +import { OsmTags } from "../../Models/OsmFeature" import FeatureSource from "../../Logic/FeatureSource/FeatureSource" import { BBox } from "../../Logic/BBox" - +import { Feature, LineString } from "geojson" +import ScrollableFullScreen from "../Base/ScrollableFullScreen" +import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig" +import { Utils } from "../../Utils" +import * as range_layer from "../../assets/layers/range/range.json" +import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" class PointRenderingLayer { private readonly _config: PointRenderingConfig private readonly _fetchStore?: (id: string) => Store private readonly _map: MlMap + private readonly _onClick: (id: string) => void + private readonly _allMarkers: Map = new Map() constructor( map: MlMap, features: FeatureSource, config: PointRenderingConfig, - fetchStore?: (id: string) => Store + visibility?: Store, + fetchStore?: (id: string) => Store, + onClick?: (id: string) => void ) { this._config = config this._map = map this._fetchStore = fetchStore - const cache: Map = new Map() + this._onClick = onClick const self = this - features.features.addCallbackAndRunD((features) => { - const unseenKeys = new Set(cache.keys()) - for (const { feature } of features) { - const id = feature.properties.id + + features.features.addCallbackAndRunD((features) => self.updateFeatures(features)) + visibility?.addCallbackAndRunD((visible) => self.setVisibility(visible)) + } + + private updateFeatures(features: Feature[]) { + const cache = this._allMarkers + const unseenKeys = new Set(cache.keys()) + for (const location of this._config.location) { + for (const feature of features) { + const loc = GeoOperations.featureToCoordinateWithRenderingType( + feature, + location + ) + if (loc === undefined) { + continue + } + const id = feature.properties.id + "-" + location unseenKeys.delete(id) - const loc = GeoOperations.centerpointCoordinates(feature) + if (cache.has(id)) { - console.log("Not creating a marker for ", id) const cached = cache.get(id) const oldLoc = cached.getLngLat() - console.log("OldLoc vs newLoc", oldLoc, loc) if (loc[0] !== oldLoc.lng && loc[1] !== oldLoc.lat) { cached.setLngLat(loc) - console.log("MOVED") } continue } - console.log("Creating a marker for ", id) - const marker = self.addPoint(feature) + const marker = this.addPoint(feature, loc) cache.set(id, marker) } + } - for (const unseenKey of unseenKeys) { - cache.get(unseenKey).remove() - cache.delete(unseenKey) - } - }) + for (const unseenKey of unseenKeys) { + cache.get(unseenKey).remove() + cache.delete(unseenKey) + } } - private addPoint(feature: OsmFeature): Marker { + private setVisibility(visible: boolean) { + for (const marker of this._allMarkers.values()) { + if (visible) { + marker.getElement().classList.remove("hidden") + } else { + marker.getElement().classList.add("hidden") + } + } + } + + private addPoint(feature: Feature, loc: [number, number]): Marker { let store: Store if (this._fetchStore) { store = this._fetchStore(feature.properties.id) } else { - store = new ImmutableStore(feature.properties) + store = new ImmutableStore(feature.properties) } - const { html, iconAnchor } = this._config.GenerateLeafletStyle(store, true) + const { html, iconAnchor } = this._config.RenderIcon(store, true) html.SetClass("marker") const el = html.ConstructElement() - el.addEventListener("click", function () { - window.alert("Hello world!") - }) + if (this._onClick) { + const self = this + el.addEventListener("click", function () { + self._onClick(feature.properties.id) + }) + } - return new Marker(el) - .setLngLat(GeoOperations.centerpointCoordinates(feature)) - .setOffset(iconAnchor) - .addTo(this._map) + return new Marker(el).setLngLat(loc).setOffset(iconAnchor).addTo(this._map) } } -export class ShowDataLayer { +class LineRenderingLayer { + /** + * These are dynamic properties + * @private + */ + private static readonly lineConfigKeys = [ + "color", + "width", + "lineCap", + "offset", + "fill", + "fillColor", + ] + private readonly _map: MlMap + private readonly _config: LineRenderingConfig + private readonly _visibility?: Store + private readonly _fetchStore?: (id: string) => Store + private readonly _onClick?: (id: string) => void + private readonly _layername: string + + constructor( + map: MlMap, + features: FeatureSource, + layername: string, + config: LineRenderingConfig, + visibility?: Store, + fetchStore?: (id: string) => Store, + onClick?: (id: string) => void + ) { + this._layername = layername + this._map = map + this._config = config + this._visibility = visibility + this._fetchStore = fetchStore + this._onClick = onClick + const self = this + features.features.addCallbackAndRunD((features) => self.update(features)) + } + + private async update(features: Feature[]) { + const map = this._map + while (!map.isStyleLoaded()) { + await Utils.waitFor(100) + } + map.addSource(this._layername, { + type: "geojson", + data: { + type: "FeatureCollection", + features, + }, + promoteId: "id", + }) + for (let i = 0; i < features.length; i++) { + const feature = features[i] + const id = feature.properties.id ?? "" + i + const tags = this._fetchStore(id) + tags.addCallbackAndRunD((properties) => { + const config = this._config + + const calculatedProps = {} + for (const key of LineRenderingLayer.lineConfigKeys) { + const v = config[key]?.GetRenderValue(properties)?.Subs(properties).txt + calculatedProps[key] = v + } + + map.setFeatureState({ source: this._layername, id }, calculatedProps) + }) + } + + map.addLayer({ + source: this._layername, + id: this._layername + "_line", + type: "line", + filter: ["in", ["geometry-type"], ["literal", ["LineString", "MultiLineString"]]], + layout: {}, + paint: { + "line-color": ["feature-state", "color"], + "line-width": ["feature-state", "width"], + "line-offset": ["feature-state", "offset"], + }, + }) + + /*[ + "color", + "width", + "dashArray", + "lineCap", + "offset", + "fill", + "fillColor", + ]*/ + map.addLayer({ + source: this._layername, + id: this._layername + "_polygon", + type: "fill", + filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]], + layout: {}, + paint: { + "fill-color": ["feature-state", "fillColor"], + }, + }) + } +} + +export default class ShowDataLayer { private readonly _map: Store - private _options: ShowDataLayerOptions & { layer: LayerConfig } + private readonly _options: ShowDataLayerOptions & { layer: LayerConfig } + private readonly _popupCache: Map constructor(map: Store, options: ShowDataLayerOptions & { layer: LayerConfig }) { this._map = map this._options = options + this._popupCache = new Map() const self = this map.addCallbackAndRunD((map) => self.initDrawFeatures(map)) } - private initDrawFeatures(map: MlMap) { - for (const pointRenderingConfig of this._options.layer.mapRendering) { - new PointRenderingLayer( - map, - this._options.features, - pointRenderingConfig, - this._options.fetchStore - ) + private static rangeLayer = new LayerConfig( + range_layer, + "ShowDataLayer.ts:range.json" + ) + + public static showRange( + map: Store, + features: FeatureSource, + doShowLayer?: Store + ): ShowDataLayer { + return new ShowDataLayer(map, { + layer: ShowDataLayer.rangeLayer, + features, + doShowLayer, + }) + } + + private openOrReusePopup(id: string): void { + if (this._popupCache.has(id)) { + this._popupCache.get(id).Activate() + return } + const tags = this._options.fetchStore(id) + if (!tags) { + return + } + const popup = this._options.buildPopup(tags, this._options.layer) + this._popupCache.set(id, popup) + popup.Activate() + } + + private zoomToCurrentFeatures(map: MlMap) { if (this._options.zoomToFeatures) { const features = this._options.features.features.data - const bbox = BBox.bboxAroundAll(features.map((f) => BBox.get(f.feature))) + const bbox = BBox.bboxAroundAll(features.map(BBox.get)) map.fitBounds(bbox.toLngLat(), { padding: { top: 10, bottom: 10, left: 10, right: 10 }, }) } } + + private initDrawFeatures(map: MlMap) { + const { features, doShowLayer, fetchStore, buildPopup } = this._options + const onClick = buildPopup === undefined ? undefined : (id) => this.openOrReusePopup(id) + for (const lineRenderingConfig of this._options.layer.lineRendering) { + new LineRenderingLayer( + map, + features, + "test", + lineRenderingConfig, + doShowLayer, + fetchStore, + onClick + ) + } + + for (const pointRenderingConfig of this._options.layer.mapRendering) { + new PointRenderingLayer( + map, + features, + pointRenderingConfig, + doShowLayer, + fetchStore, + onClick + ) + } + features.features.addCallbackAndRunD((_) => this.zoomToCurrentFeatures(map)) + } } diff --git a/UI/ShowDataLayer/ShowDataLayerOptions.ts b/UI/Map/ShowDataLayerOptions.ts similarity index 95% rename from UI/ShowDataLayer/ShowDataLayerOptions.ts rename to UI/Map/ShowDataLayerOptions.ts index 9a66d0d49..dde88b663 100644 --- a/UI/ShowDataLayer/ShowDataLayerOptions.ts +++ b/UI/Map/ShowDataLayerOptions.ts @@ -33,5 +33,5 @@ export interface ShowDataLayerOptions { /** * Function which fetches the relevant store */ - fetchStore?: (id: string) => Store + fetchStore?: (id: string) => UIEventSource } diff --git a/UI/ShowDataLayer/ShowDataMultiLayer.ts b/UI/Map/ShowDataMultiLayer.ts similarity index 73% rename from UI/ShowDataLayer/ShowDataMultiLayer.ts rename to UI/Map/ShowDataMultiLayer.ts index ef2eabab4..84710b6fc 100644 --- a/UI/ShowDataLayer/ShowDataMultiLayer.ts +++ b/UI/Map/ShowDataMultiLayer.ts @@ -6,18 +6,21 @@ import ShowDataLayer from "./ShowDataLayer" import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" import FilteredLayer from "../../Models/FilteredLayer" import { ShowDataLayerOptions } from "./ShowDataLayerOptions" - +import { Map as MlMap } from "maplibre-gl" export default class ShowDataMultiLayer { - constructor(options: ShowDataLayerOptions & { layers: Store }) { + constructor( + map: Store, + options: ShowDataLayerOptions & { layers: Store } + ) { new PerLayerFeatureSourceSplitter( options.layers, (perLayer) => { const newOptions = { ...options, - layerToShow: perLayer.layer.layerDef, + layer: perLayer.layer.layerDef, features: perLayer, } - new ShowDataLayer(newOptions) + new ShowDataLayer(map, newOptions) }, options.features ) diff --git a/UI/NewPoint/ConfirmLocationOfPoint.ts b/UI/NewPoint/ConfirmLocationOfPoint.ts index 8aed2d01d..47a203ecb 100644 --- a/UI/NewPoint/ConfirmLocationOfPoint.ts +++ b/UI/NewPoint/ConfirmLocationOfPoint.ts @@ -13,13 +13,13 @@ import Toggle from "../Input/Toggle" import SimpleAddUI, { PresetInfo } from "../BigComponents/SimpleAddUI" import Img from "../Base/Img" import Title from "../Base/Title" -import { GlobalFilter } from "../../Logic/State/MapState" import { VariableUiElement } from "../Base/VariableUIElement" import { Tag } from "../../Logic/Tags/Tag" import { WayId } from "../../Models/OsmFeature" import { Translation } from "../i18n/Translation" -import { Feature } from "geojson"; -import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"; +import { Feature } from "geojson" +import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" +import { GlobalFilter } from "../../Logic/State/GlobalFilter" export default class ConfirmLocationOfPoint extends Combine { constructor( @@ -69,7 +69,7 @@ export default class ConfirmLocationOfPoint extends Combine { let snapToFeatures: UIEventSource = undefined let mapBounds: UIEventSource = undefined if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) { - snapToFeatures = new UIEventSource< Feature[]>([]) + snapToFeatures = new UIEventSource([]) mapBounds = new UIEventSource(undefined) } @@ -110,9 +110,7 @@ export default class ConfirmLocationOfPoint extends Combine { console.log("Snapping to", layerId) state.featurePipeline .GetFeaturesWithin(layerId, bbox) - ?.forEach((feats) => - allFeatures.push(...feats) - ) + ?.forEach((feats) => allFeatures.push(...(feats))) }) console.log("Snapping to", allFeatures) snapToFeatures.setData(allFeatures) diff --git a/UI/AllTagsPanel.svelte b/UI/Popup/AllTagsPanel.svelte similarity index 88% rename from UI/AllTagsPanel.svelte rename to UI/Popup/AllTagsPanel.svelte index 4eab95c50..b2beb2cef 100644 --- a/UI/AllTagsPanel.svelte +++ b/UI/Popup/AllTagsPanel.svelte @@ -1,7 +1,7 @@ + + +
+ +
+ +
+ +
+ +
+
+ +
+ + + mapproperties.zoom.update(z => z+1)}> + + + mapproperties.zoom.update(z => z-1)}> + + + + + + new GeolocationControl(geolocation, mapproperties).SetClass("block w-8 h-8")}> + + +
+ +
+
+ diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index bed4c6c6a..c18583295 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -2,7 +2,6 @@ import Locale from "./Locale" import { Utils } from "../../Utils" import BaseUIElement from "../BaseUIElement" import LinkToWeblate from "../Base/LinkToWeblate" -import { SvelteComponent } from "svelte" export class Translation extends BaseUIElement { public static forcedLanguage = undefined @@ -299,7 +298,7 @@ export class Translation extends BaseUIElement { } } -export class TypedTranslation extends Translation { +export class TypedTranslation> extends Translation { constructor(translations: Record, context?: string) { super(translations, context) } diff --git a/all_themes_index.ts b/all_themes_index.ts index fb15e2671..d9f8d534f 100644 --- a/all_themes_index.ts +++ b/all_themes_index.ts @@ -1,5 +1,3 @@ -import MinimapImplementation from "./UI/Base/MinimapImplementation" - import { Utils } from "./Utils" import AllThemesGui from "./UI/AllThemesGui" import { QueryParameters } from "./Logic/Web/QueryParameters" @@ -46,7 +44,6 @@ if (mode.data === "statistics") { new FixedUiElement("").AttachTo("centermessage") new StatisticsGUI().SetClass("w-full h-full pointer-events-auto").AttachTo("topleft-tools") } else if (mode.data === "pdf") { - MinimapImplementation.initialize() new FixedUiElement("").AttachTo("centermessage") const div = document.createElement("div") div.id = "extra_div_for_maps" diff --git a/assets/layers/cluster_style/cluster_style.json b/assets/layers/cluster_style/cluster_style.json deleted file mode 100644 index 081cbb5d3..000000000 --- a/assets/layers/cluster_style/cluster_style.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "id": "cluster_style", - "description": "The style for the clustering in all themes. Enable `debug=true` to peak into clustered tiles", - "source": { - "osmTags": "tileId~*" - }, - "title": "Clustered data", - "tagRenderings": [ - "all_tags" - ], - "mapRendering": [ - { - "label": { - "render": "
{showCount}
", - "mappings": [ - { - "if": "showCount>1000", - "then": "
{kilocount}K
" - } - ] - }, - "location": [ - "point" - ] - }, - { - "color": { - "render": "#3c3", - "mappings": [ - { - "if": "showCount>200", - "then": "#f33" - }, - { - "if": "showCount>100", - "then": "#c93" - }, - { - "if": "showCount>50", - "then": "#cc3" - } - ] - }, - "width": { - "render": "1" - } - } - ] -} \ No newline at end of file diff --git a/assets/layers/conflation/conflation.json b/assets/layers/conflation/conflation.json index ffafc9bca..5cc23d054 100644 --- a/assets/layers/conflation/conflation.json +++ b/assets/layers/conflation/conflation.json @@ -2,14 +2,7 @@ "id": "conflation", "description": "If the import-button moves OSM points, the imported way points or conflates, a preview is shown. This layer defines how this preview is rendered. This layer cannot be included in a theme.", "minzoom": 1, - "source": { - "osmTags": { - "or": [ - "move=yes", - "newpoint=yes" - ] - } - }, + "source": "special", "name": "Conflation", "title": "Conflation", "mapRendering": [ @@ -86,4 +79,4 @@ } } ] -} \ No newline at end of file +} diff --git a/assets/layers/current_view/current_view.json b/assets/layers/current_view/current_view.json index b3649f4bd..da1e0f47f 100644 --- a/assets/layers/current_view/current_view.json +++ b/assets/layers/current_view/current_view.json @@ -1,10 +1,7 @@ { "id": "current_view", "description": "A meta-layer which contains one single feature, namely the BBOX of the current map view. This can be used to trigger special actions. If a popup is defined for this layer, this popup will be accessible via an extra button on screen.\n\nThe icon on the button is the default icon of the layer, but can be customized by detecting 'button=yes'.", - "source": { - "osmTags": "current_view=yes", - "maxCacheAge": 0 - }, + "source": "special", "shownByDefault": false, "title": "Current View", "tagRenderings": [], @@ -13,4 +10,4 @@ "color": "#cccc0088" } ] -} \ No newline at end of file +} diff --git a/assets/layers/filters/filters.json b/assets/layers/filters/filters.json index 579ceec66..d00f29601 100644 --- a/assets/layers/filters/filters.json +++ b/assets/layers/filters/filters.json @@ -2,9 +2,7 @@ "id": "filters", "description": "This layer acts as library for common filters", "mapRendering": null, - "source": { - "osmTags": "id~*" - }, + "source": "special:library", "filter": [ { "id": "open_now", diff --git a/assets/layers/gps_location/gps_location.json b/assets/layers/gps_location/gps_location.json index 9d11abeab..a5beb0913 100644 --- a/assets/layers/gps_location/gps_location.json +++ b/assets/layers/gps_location/gps_location.json @@ -2,10 +2,7 @@ "id": "gps_location", "description": "Meta layer showing the current location of the user. Add this to your theme and override the icon to change the appearance of the current location. The object will always have `id=gps` and will have _all_ the properties included in the [`Coordinates`-object](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates) returned by the browser.", "minzoom": 0, - "source": { - "osmTags": "id=gps", - "maxCacheAge": 0 - }, + "source": "special", "mapRendering": [ { "icon": { @@ -38,4 +35,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/assets/layers/gps_location_history/gps_location_history.json b/assets/layers/gps_location_history/gps_location_history.json index 3e4f0f582..34167beb5 100644 --- a/assets/layers/gps_location_history/gps_location_history.json +++ b/assets/layers/gps_location_history/gps_location_history.json @@ -3,11 +3,7 @@ "description": "Meta layer which contains the previous locations of the user as single points. This is mainly for technical reasons, e.g. to keep match the distance to the modified object", "minzoom": 1, "name": null, - "source": { - "osmTags": "user:location=yes", - "#": "Cache is disabled here as these points are kept seperately", - "maxCacheAge": 0 - }, + "source": "special", "shownByDefault": false, "mapRendering": [ { @@ -19,4 +15,4 @@ "iconSize": "5,5,center" } ] -} \ No newline at end of file +} diff --git a/assets/layers/gps_track/gps_track.json b/assets/layers/gps_track/gps_track.json index 8d4243403..94bb2a40b 100644 --- a/assets/layers/gps_track/gps_track.json +++ b/assets/layers/gps_track/gps_track.json @@ -2,10 +2,7 @@ "id": "gps_track", "description": "Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track.", "minzoom": 0, - "source": { - "osmTags": "id=location_track", - "maxCacheAge": 0 - }, + "source": "special", "title": { "render": "Your travelled path" }, diff --git a/assets/layers/home_location/home_location.json b/assets/layers/home_location/home_location.json index 74276dc09..b9fc29509 100644 --- a/assets/layers/home_location/home_location.json +++ b/assets/layers/home_location/home_location.json @@ -2,10 +2,7 @@ "id": "home_location", "description": "Meta layer showing the home location of the user. The home location can be set in the [profile settings](https://www.openstreetmap.org/profile/edit) of OpenStreetMap.", "minzoom": 0, - "source": { - "osmTags": "user:home=yes", - "maxCacheAge": 0 - }, + "source":"special", "mapRendering": [ { "icon": { @@ -20,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/assets/layers/icons/icons.json b/assets/layers/icons/icons.json index d128dfb9b..0aa21f6a3 100644 --- a/assets/layers/icons/icons.json +++ b/assets/layers/icons/icons.json @@ -3,9 +3,7 @@ "description": { "en": "A layer acting as library for icon-tagrenderings, especially to show as badge next to a POI" }, - "source": { - "osmTags": "id~*" - }, + "source":"special:library", "title": null, "tagRenderings": [ { @@ -127,4 +125,4 @@ } ], "mapRendering": null -} \ No newline at end of file +} diff --git a/assets/layers/id_presets/id_presets.json b/assets/layers/id_presets/id_presets.json index caf39d97a..152dbc7be 100644 --- a/assets/layers/id_presets/id_presets.json +++ b/assets/layers/id_presets/id_presets.json @@ -4,9 +4,7 @@ "en": "Layer containing various presets and questions generated by ID. These are meant to be reused in other layers by importing the tagRenderings with `id_preset." }, "#dont-translate": "*", - "source": { - "osmTags": "id~*" - }, + "source": "special:library", "title": null, "mapRendering": null, "tagRenderings": [ @@ -20217,4 +20215,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/assets/layers/import_candidate/import_candidate.json b/assets/layers/import_candidate/import_candidate.json index 70720181e..52ed5ba3b 100644 --- a/assets/layers/import_candidate/import_candidate.json +++ b/assets/layers/import_candidate/import_candidate.json @@ -1,11 +1,7 @@ { "id": "import_candidate", "description": "Layer used in the importHelper", - "source": { - "osmTags": { - "and": [] - } - }, + "source":"special", "mapRendering": [ { "location": [ @@ -23,4 +19,4 @@ "render": "{all_tags()}" } ] -} \ No newline at end of file +} diff --git a/assets/layers/left_right_style/left_right_style.json b/assets/layers/left_right_style/left_right_style.json deleted file mode 100644 index 9b8f82d13..000000000 --- a/assets/layers/left_right_style/left_right_style.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "id": "left_right_style", - "description": "Special meta-style which will show one single line, either on the left or on the right depending on the id. This is used in the small popups with left_right roads. Cannot be included in a theme", - "source": { - "osmTags": { - "or": [ - "id=left", - "id=right" - ] - } - }, - "mapRendering": [ - { - "width": 15, - "color": { - "render": "#ff000088", - "mappings": [ - { - "if": "id=left", - "then": "#0000ff88" - } - ] - }, - "offset": { - "render": "-15", - "mappings": [ - { - "if": "id=right", - "then": "15" - } - ] - } - } - ] -} \ No newline at end of file diff --git a/assets/layers/matchpoint/matchpoint.json b/assets/layers/matchpoint/matchpoint.json index 0a3d3080f..5814561b8 100644 --- a/assets/layers/matchpoint/matchpoint.json +++ b/assets/layers/matchpoint/matchpoint.json @@ -1,11 +1,7 @@ { "id": "matchpoint", "description": "The default rendering for a locationInput which snaps onto another object", - "source": { - "osmTags": { - "and": [] - } - }, + "source":"special", "mapRendering": [ { "location": [ @@ -15,4 +11,4 @@ "icon": "./assets/svg/crosshair-empty.svg" } ] -} \ No newline at end of file +} diff --git a/assets/layers/range/range.json b/assets/layers/range/range.json new file mode 100644 index 000000000..1ea7aa459 --- /dev/null +++ b/assets/layers/range/range.json @@ -0,0 +1,14 @@ +{ + "id": "range", + "description": "Meta-layer, simply showing a bbox in red", + "title": null, + "source": "special", + "name": null, + "mapRendering": [ + { + "width": 4, + "fill": "no", + "color": "#ff000088" + } + ] +} diff --git a/assets/layers/type_node/type_node.json b/assets/layers/type_node/type_node.json index 7f9228a51..20679e2d7 100644 --- a/assets/layers/type_node/type_node.json +++ b/assets/layers/type_node/type_node.json @@ -2,12 +2,9 @@ "id": "type_node", "description": "This is a priviliged meta_layer which exports _every_ point in OSM. This only works if zoomed below the point that the full tile is loaded (and not loaded via Overpass). Note that this point will also contain a property `parent_ways` which contains all the ways this node is part of as a list. This is mainly used for extremely specialized themes, which do advanced conflations. Expert use only.", "minzoom": 18, - "source": { - "osmTags": "id~node/.*", - "maxCacheAge": 0 - }, + "source": "special", "mapRendering": null, "name": "All OSM Nodes", "title": "OSM node {id}", "tagRendering": [] -} \ No newline at end of file +} diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index bf45f4f69..c99060648 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -6,9 +6,7 @@ "nl": "Een speciale lag die niet getoond wordt op de kaart, maar die de instellingen van de gebruiker weergeeft" }, "title": null, - "source": { - "osmTags": "id~*" - }, + "source": "special", "calculatedTags": [ "_mastodon_candidate_md=feat.properties._description.match(/\\[[^\\]]*\\]\\((.*(mastodon|en.osm.town).*)\\).*/)?.at(1)", "_d=feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? ''", @@ -320,4 +318,4 @@ } ], "mapRendering": null -} \ No newline at end of file +} diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 9ab2f3115..a518e8e70 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -706,24 +706,24 @@ video { bottom: 0px; } -.right-1\/3 { - right: 33.333333%; -} - -.top-4 { - top: 1rem; -} - .top-0 { top: 0px; } +.left-0 { + left: 0px; +} + .right-0 { right: 0px; } -.left-0 { - left: 0px; +.right-1\/3 { + right: 33.333333%; +} + +.top-4 { + top: 1rem; } .bottom-2 { @@ -766,10 +766,6 @@ video { margin: 1.25rem; } -.m-4 { - margin: 1rem; -} - .m-2 { margin: 0.5rem; } @@ -786,6 +782,10 @@ video { margin: 0.75rem; } +.m-4 { + margin: 1rem; +} + .m-1 { margin: 0.25rem; } @@ -827,18 +827,6 @@ video { margin-bottom: 1rem; } -.mt-2 { - margin-top: 0.5rem; -} - -.ml-0 { - margin-left: 0px; -} - -.mr-8 { - margin-right: 2rem; -} - .mt-1 { margin-top: 0.25rem; } @@ -871,6 +859,10 @@ video { margin-left: 0.25rem; } +.mt-2 { + margin-top: 0.5rem; +} + .mb-2 { margin-bottom: 0.5rem; } @@ -995,10 +987,6 @@ video { height: 100%; } -.h-64 { - height: 16rem; -} - .h-min { height: -webkit-min-content; height: min-content; @@ -1024,6 +1012,14 @@ video { height: 3rem; } +.h-screen { + height: 100vh; +} + +.h-7 { + height: 1.75rem; +} + .h-4 { height: 1rem; } @@ -1036,10 +1032,6 @@ video { height: 0.75rem; } -.h-screen { - height: 100vh; -} - .h-11 { height: 2.75rem; } @@ -1052,6 +1044,10 @@ video { height: 24rem; } +.h-64 { + height: 16rem; +} + .h-0 { height: 0px; } @@ -1084,14 +1080,6 @@ video { width: 100%; } -.w-24 { - width: 6rem; -} - -.w-1\/2 { - width: 50%; -} - .w-6 { width: 1.5rem; } @@ -1116,6 +1104,14 @@ video { width: 3rem; } +.w-screen { + width: 100vw; +} + +.w-7 { + width: 1.75rem; +} + .w-4 { width: 1rem; } @@ -1128,10 +1124,6 @@ video { width: 0.75rem; } -.w-screen { - width: 100vw; -} - .w-11 { width: 2.75rem; } @@ -1142,6 +1134,10 @@ video { width: fit-content; } +.w-1\/2 { + width: 50%; +} + .w-max { width: -webkit-max-content; width: max-content; @@ -1156,6 +1152,10 @@ video { width: min-content; } +.w-24 { + width: 6rem; +} + .w-auto { width: auto; } @@ -1189,10 +1189,6 @@ video { flex-grow: 1; } -.grow-0 { - flex-grow: 0; -} - .grow { flex-grow: 1; } @@ -1337,10 +1333,6 @@ video { overflow: scroll; } -.overflow-x-auto { - overflow-x: auto; -} - .overflow-y-auto { overflow-y: auto; } @@ -1376,6 +1368,14 @@ video { border-radius: 1.5rem; } +.rounded { + border-radius: 0.25rem; +} + +.rounded-full { + border-radius: 9999px; +} + .rounded-xl { border-radius: 0.75rem; } @@ -1384,22 +1384,14 @@ video { border-radius: 0.5rem; } -.rounded { - border-radius: 0.25rem; +.rounded-md { + border-radius: 0.375rem; } .rounded-2xl { border-radius: 1rem; } -.rounded-full { - border-radius: 9999px; -} - -.rounded-md { - border-radius: 0.375rem; -} - .rounded-sm { border-radius: 0.125rem; } @@ -1409,20 +1401,16 @@ video { border-bottom-left-radius: 0.25rem; } -.border { - border-width: 1px; -} - .border-2 { border-width: 2px; } -.border-4 { - border-width: 4px; +.border { + border-width: 1px; } -.border-b-4 { - border-bottom-width: 4px; +.border-4 { + border-width: 4px; } .border-l-4 { @@ -1455,6 +1443,11 @@ video { border-color: rgb(219 234 254 / var(--tw-border-opacity)); } +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity)); +} + .border-gray-300 { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); @@ -1499,11 +1492,6 @@ video { background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } -.bg-unsubtle { - --tw-bg-opacity: 1; - background-color: rgb(191 219 254 / var(--tw-bg-opacity)); -} - .bg-red-400 { --tw-bg-opacity: 1; background-color: rgb(248 113 113 / var(--tw-bg-opacity)); @@ -1519,11 +1507,6 @@ video { background-color: rgb(156 163 175 / var(--tw-bg-opacity)); } -.bg-indigo-100 { - --tw-bg-opacity: 1; - background-color: rgb(224 231 255 / var(--tw-bg-opacity)); -} - .bg-black { --tw-bg-opacity: 1; background-color: rgb(0 0 0 / var(--tw-bg-opacity)); @@ -1534,6 +1517,11 @@ video { background-color: rgb(229 231 235 / var(--tw-bg-opacity)); } +.bg-indigo-100 { + --tw-bg-opacity: 1; + background-color: rgb(224 231 255 / var(--tw-bg-opacity)); +} + .bg-gray-100 { --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity)); @@ -1558,14 +1546,14 @@ video { padding: 1rem; } -.p-2 { - padding: 0.5rem; -} - .p-1 { padding: 0.25rem; } +.p-2 { + padding: 0.5rem; +} + .p-3 { padding: 0.75rem; } @@ -1602,14 +1590,6 @@ video { padding-left: 0.75rem; } -.pl-1 { - padding-left: 0.25rem; -} - -.pr-1 { - padding-right: 0.25rem; -} - .pb-12 { padding-bottom: 3rem; } @@ -1634,6 +1614,14 @@ video { padding-bottom: 0.25rem; } +.pl-1 { + padding-left: 0.25rem; +} + +.pr-1 { + padding-right: 0.25rem; +} + .pt-2 { padding-top: 0.5rem; } @@ -1686,6 +1674,10 @@ video { text-align: center; } +.text-justify { + text-align: justify; +} + .align-baseline { vertical-align: baseline; } @@ -1808,6 +1800,11 @@ video { text-decoration-line: line-through; } +.antialiased { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + .opacity-50 { opacity: 0.5; } @@ -1907,15 +1904,14 @@ video { color: var(--subtle-detail-color-contrast); } -.bg-unsubtle { - background-color: var(--unsubtle-detail-color); - color: var(--unsubtle-detail-color-contrast); -} - .\[key\:string\] { key: string; } +.\[_\:string\] { + _: string; +} + :root { /* The main colour scheme of mapcomplete is configured here. * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. @@ -2915,10 +2911,6 @@ input { width: 75%; } - .lg\:w-1\/4 { - width: 25%; - } - .lg\:w-1\/6 { width: 16.666667%; } diff --git a/index.ts b/index.ts index 43642982c..3a8fec515 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,5 @@ import { FixedUiElement } from "./UI/Base/FixedUiElement" import Combine from "./UI/Base/Combine" -import MinimapImplementation from "./UI/Base/MinimapImplementation" import { Utils } from "./Utils" import AllThemesGui from "./UI/AllThemesGui" import DetermineLayout from "./Logic/DetermineLayout" @@ -9,11 +8,7 @@ import DefaultGUI from "./UI/DefaultGUI" import State from "./State" import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation" import { DefaultGuiState } from "./UI/DefaultGuiState" -import { QueryParameters } from "./Logic/Web/QueryParameters" -import DashboardGui from "./UI/DashboardGui" -// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts running from console -MinimapImplementation.initialize() ShowOverlayLayerImplementation.Implement() // Miscelleanous Utils.DisableLongPresses() @@ -38,16 +33,7 @@ class Init { // @ts-ignore window.mapcomplete_state = State.state - const mode = QueryParameters.GetQueryParameter( - "mode", - "map", - "The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'" - ) - if (mode.data === "dashboard") { - new DashboardGui(State.state, guiState).setup() - } else { - new DefaultGUI(State.state, guiState).setup() - } + new DefaultGUI(State.state, guiState).setup() } } diff --git a/package.json b/package.json index a224b3ac7..b1b3564c4 100644 --- a/package.json +++ b/package.json @@ -83,10 +83,6 @@ "jest-mock": "^29.4.1", "jspdf": "^2.5.1", "latlon2country": "^1.2.6", - "leaflet": "^1.9.2", - "leaflet-polylineoffset": "^1.1.1", - "leaflet-providers": "^1.13.0", - "leaflet-simple-map-screenshoter": "^0.4.5", "libphonenumber-js": "^1.10.8", "lz-string": "^1.4.4", "mangrove-reviews-typescript": "^1.1.0", @@ -116,8 +112,6 @@ "@tsconfig/svelte": "^3.0.0", "@types/chai": "^4.3.0", "@types/geojson": "^7946.0.10", - "@types/leaflet-markercluster": "^1.0.3", - "@types/leaflet-providers": "^1.2.0", "@types/lz-string": "^1.3.34", "@types/node": "^18.11.18", "@types/papaparse": "^5.3.1", diff --git a/public/vendor/images/layers-2x.png b/public/vendor/images/layers-2x.png deleted file mode 100644 index 200c333dca9652ac4cba004d609e5af4eee168c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1259 zcmVFhCYNy;#0irRPomHqW|G1C*;4?@4#E?jH>?v@U%cy?3dQAc-DchXVErpOh~ z-jbon+tNbnl6hoEb;)TVk+%hTDDi_G%i3*RZ&15!$Fjr^f;Ke&A@|?=`2&+{zr+3a z{D*=t(`AXyS%X7N z%a#RZw6vD^t_rnM`L4E>m=U&R!A-&}nZIi$BOPvkhrCuUe@BN~-lRD)f44;J%TwgE zcze8u!PQ_NR7?o(NylLXVTfDO zxs5=@|GsYEsNo4M#nT%N!UE(?dnS)t2+{ELYAFp*3=iF=|EQnTp`#vlSXuGVraYo? z+RCzXo6h3qA8{KG?S4nE(lM+;Eb4nT3XV;7gcAxUi5m)`k5tv}cPy()8ZR3TLW3I- zAS^}cq-IJvL7a4RgR!yk@~RT%$lA7{L5ES*hyx)M4(yxI$Ub(4f)K|^v1>zvwQY!_ zIrWw8q9GS^!Dp~}+?mbnB6jDF8mVlbQ!jFKDY;w=7;XO{9bq7>LXGK24WA`;rL)_Z z)&j}pbV(;6gY;VMhbxgvn`X;6x}VUEE-7 z%)7j-%t8S=ZL3yc)HbXDAqJZvBTPoiW_A-+a8m3_Z?v{DN7Tnr#O_VUMT0UBt$;p` zDh6JbGHN8JJ*JN%y2%msb97@_S>9!%Egwk;?PEkU9ntz&3uR}%Fj5d$JHQbQb3}a{ zSzFT^#n=VInPpcAS}CNxj?_ zVscANk5Cfz(51EI1pz};AWWb|kgbYNb4wCEGUn3+eMUMV?1-{=I4TlmLJMot@rd07 zZuo2hk1ccu{YmGkcYdWAVdk{Z4Nm?^cTD&}jGm+Q1SYIXMwmG*oO*83&#>l%nbR`G zhh=lZ%xIb7kU3#;TBbfECrnC9P=-XpL|TG2BoZdj61*XiFbW8?1Z_wp%#;>${SUIy V$8qr;L*)Pf002ovPDHLkV1hYLS~36t diff --git a/public/vendor/images/layers.png b/public/vendor/images/layers.png deleted file mode 100644 index 1a72e5784b2b456eac5d7670738db80697af3377..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmV;p0!RIcP)*@&l2<6p=!C&s@#ZL+%BQvF&b?w6S%wp=I>1QHj7AP5C)IWy#b znXXB;g;j=$a-tW89K%FbDceHVq&unY*Wx3L#=EGWH=rjqnp|4c_Ulec!ql3#G-5ZF zVlbBA@XP=)C8U&+Lrc)S4O5%1$&{(;7R^K(CSnvSr$v;+B$8q&7Bf|h$#PARo1^%M zf1H^nG-EiXVXr07OH(*8R)xa|FD;lXUlg_-%)~ZGsL2cX0NXaAzN2q%jqLRR6ruVk8`Jb7n#{`T;o@`F= z#3YcynIR^s83UNF3D!f5m#Mg)NJ24&Qfrqb&_z=yF;=B)#9Iq7u-@^O!(mW{D;qvr zPc)gVb%aowtS8m@ElL4A9G>w#ffQ~q{i&_i)*6f^)Sz|C?C>zb4Uo?H<-&Hz@a?J; z$ml@zGygWofb9$ZBj6aLjpLhsT2AzjOu=-*u_gSCUP001cn1^@s6z>|W`000GnNklGNuHDcIX17Zdjl&3`L?0sTjIws<{((Dh&g-s0<@jYQyl?D*X^?%13;ml^gy> ziMrY_^1WI=(g@LMizu=zCoA>C`6|QEq1eV92k*7m>G65*&@&6)aC&e}G zI)pf-Za|N`DT&Cn1J|o`19mumxW~hiKiKyc-P`S@q)rdTo84@QI@;0yXrG%9uhI>A zG5QHb6s4=<6xy{1 z@NMxEkryp{LS44%z$3lP^cX!9+2-;CTt3wM4(k*#C{aiIiLuB>jJj;KPhPzIC00bL zU3a#;aJld94lCW=`4&aAy8M7PY=HQ>O%$YEP4c4UY#CRxfgbE~(|uiI=YS8q;O9y6 zmIkXzR`}p7ti|PrM3a}WMnR=3NVnWdAAR>b9X@)DKL6=YsvmH%?I24wdq?Gh54_;# z$?_LvgjEdspdQlft#4CQ z`2Zyvy?*)N1Ftw|{_hakhG9WjS?Az@I@+IZ8JbWewR!XUK4&6346+d#~gsE0SY(LX8&JfY>Aj)RxGy96nwhs2rv zzW6pTnMpFkDSkT*a*6Dx|u@ds6ISVn0@^RmIsKZ5Y;bazbc;tTSq(kg(=481ODrPyNB6n z-$+U}(w$m6U6H$w17Bw+wDaFIe~GvNMYvnw31MpY0eQKT9l>SU``8k7w4)z!GZKMI z#_cEKq7k~i%nlK@6c-K?+R;B#5$?T#YpKD`t_4bAs^#E+@5QW$@OX3*`;(#{U^d-vY)&xEE>n5lYl&T?Am svg, -.leaflet-pane > canvas, -.leaflet-zoom-box, -.leaflet-image-layer, -.leaflet-layer { - position: absolute; - left: 0; - top: 0; -} - -.leaflet-container { - overflow: hidden; -} - -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-user-drag: none; -} - -/* Prevents IE11 from highlighting tiles in blue */ -.leaflet-tile::selection { - background: transparent; -} - -/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ -.leaflet-safari .leaflet-tile { - image-rendering: -webkit-optimize-contrast; -} - -/* hack that prevents hw layers "stretching" when loading new tiles */ -.leaflet-safari .leaflet-tile-container { - width: 1600px; - height: 1600px; - -webkit-transform-origin: 0 0; -} - -.leaflet-marker-icon, -.leaflet-marker-shadow { - display: block; -} - -/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ -/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ -.leaflet-container .leaflet-overlay-pane svg, -.leaflet-container .leaflet-marker-pane img, -.leaflet-container .leaflet-shadow-pane img, -.leaflet-container .leaflet-tile-pane img, -.leaflet-container img.leaflet-image-layer, -.leaflet-container .leaflet-tile { - max-width: none !important; - max-height: none !important; -} - -.leaflet-container.leaflet-touch-zoom { - -ms-touch-action: pan-x pan-y; - touch-action: pan-x pan-y; -} - -.leaflet-container.leaflet-touch-drag { - -ms-touch-action: pinch-zoom; - /* Fallback for FF which doesn't support pinch-zoom */ - touch-action: none; - touch-action: pinch-zoom; -} - -.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { - -ms-touch-action: none; - touch-action: none; -} - -.leaflet-container { - -webkit-tap-highlight-color: transparent; -} - -.leaflet-container a { - -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); -} - -.leaflet-tile { - filter: inherit; - visibility: hidden; -} - -.leaflet-tile-loaded { - visibility: inherit; -} - -.leaflet-zoom-box { - width: 0; - height: 0; - -moz-box-sizing: border-box; - box-sizing: border-box; - z-index: 800; -} - -/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ -.leaflet-overlay-pane svg { - -moz-user-select: none; -} - -.leaflet-pane { - z-index: 400; -} - -.leaflet-tile-pane { - z-index: 200; -} - -.leaflet-overlay-pane { - z-index: 400; -} - -.leaflet-shadow-pane { - z-index: 500; -} - -.leaflet-marker-pane { - z-index: 600; -} - -.leaflet-tooltip-pane { - z-index: 650; -} - -.leaflet-popup-pane { - z-index: 700; -} - -.leaflet-map-pane canvas { - z-index: 100; -} - -.leaflet-map-pane svg { - z-index: 200; -} - -.leaflet-vml-shape { - width: 1px; - height: 1px; -} - -.lvml { - behavior: url(#default#VML); - display: inline-block; - position: absolute; -} - - -/* control positioning */ - -.leaflet-control { - position: relative; - z-index: 800; - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; -} - -.leaflet-top, -.leaflet-bottom { - position: absolute; - z-index: 1000; - pointer-events: none; -} - -.leaflet-top { - top: 0; -} - -.leaflet-right { - right: 0; -} - -.leaflet-bottom { - bottom: 0; -} - -.leaflet-left { - left: 0; -} - -.leaflet-control { - float: left; - clear: both; -} - -.leaflet-right .leaflet-control { - float: right; -} - -.leaflet-top .leaflet-control { - margin-top: 10px; -} - -.leaflet-bottom .leaflet-control { - margin-bottom: 10px; -} - -.leaflet-left .leaflet-control { - margin-left: 10px; -} - -.leaflet-right .leaflet-control { - margin-right: 10px; -} - - -/* zoom and fade animations */ - -.leaflet-fade-anim .leaflet-tile { - will-change: opacity; -} - -.leaflet-fade-anim .leaflet-popup { - opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; -} - -.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { - opacity: 1; -} - -.leaflet-zoom-animated { - -webkit-transform-origin: 0 0; - -ms-transform-origin: 0 0; - transform-origin: 0 0; -} - -.leaflet-zoom-anim .leaflet-zoom-animated { - will-change: transform; -} - -.leaflet-zoom-anim .leaflet-zoom-animated { - -webkit-transition: -webkit-transform 0.25s cubic-bezier(0, 0, 0.25, 1); - -moz-transition: -moz-transform 0.25s cubic-bezier(0, 0, 0.25, 1); - transition: transform 0.25s cubic-bezier(0, 0, 0.25, 1); -} - -.leaflet-zoom-anim .leaflet-tile, -.leaflet-pan-anim .leaflet-tile { - -webkit-transition: none; - -moz-transition: none; - transition: none; -} - -.leaflet-zoom-anim .leaflet-zoom-hide { - visibility: hidden; -} - - -/* cursors */ - -.leaflet-interactive { - cursor: pointer; -} - -.leaflet-grab { - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: grab; -} - -.leaflet-crosshair, -.leaflet-crosshair .leaflet-interactive { - cursor: crosshair; -} - -.leaflet-popup-pane, -.leaflet-control { - cursor: auto; -} - -.leaflet-dragging .leaflet-grab, -.leaflet-dragging .leaflet-grab .leaflet-interactive, -.leaflet-dragging .leaflet-marker-draggable { - cursor: move; - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - cursor: grabbing; -} - -/* marker & overlays interactivity */ -.leaflet-marker-icon, -.leaflet-marker-shadow, -.leaflet-image-layer, -.leaflet-pane > svg path, -.leaflet-tile-container { - pointer-events: none; -} - -.leaflet-marker-icon.leaflet-interactive, -.leaflet-image-layer.leaflet-interactive, -.leaflet-pane > svg path.leaflet-interactive, -svg.leaflet-image-layer.leaflet-interactive path { - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; -} - -/* visual tweaks */ - -.leaflet-container { - background: #ddd; - outline: 0; -} - -.leaflet-container a { - color: #0078A8; -} - -.leaflet-container a.leaflet-active { - outline: 2px solid orange; -} - -.leaflet-zoom-box { - border: 2px dotted #38f; - background: rgba(255, 255, 255, 0.5); -} - - -/* general typography */ -.leaflet-container { - font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; -} - - -/* general toolbar styles */ - -.leaflet-bar { - box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); - border-radius: 4px; -} - -.leaflet-bar a, -.leaflet-bar a:hover { - background-color: #fff; - border-bottom: 1px solid #ccc; - width: 26px; - height: 26px; - line-height: 26px; - display: block; - text-align: center; - text-decoration: none; - color: black; -} - -.leaflet-bar a, -.leaflet-control-layers-toggle { - background-position: 50% 50%; - background-repeat: no-repeat; - display: block; -} - -.leaflet-bar a:hover { - background-color: #f4f4f4; -} - -.leaflet-bar a:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; -} - -.leaflet-bar a:last-child { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom: none; -} - -.leaflet-bar a.leaflet-disabled { - cursor: default; - background-color: #f4f4f4; - color: #bbb; -} - -.leaflet-touch .leaflet-bar a { - width: 30px; - height: 30px; - line-height: 30px; -} - -.leaflet-touch .leaflet-bar a:first-child { - border-top-left-radius: 2px; - border-top-right-radius: 2px; -} - -.leaflet-touch .leaflet-bar a:last-child { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; -} - -/* zoom control */ - -.leaflet-control-zoom-in, -.leaflet-control-zoom-out { - font: bold 18px 'Lucida Console', Monaco, monospace; - text-indent: 1px; -} - -.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { - font-size: 22px; -} - - -/* layers control */ - -.leaflet-control-layers { - box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4); - background: #fff; - border-radius: 5px; -} - -.leaflet-control-layers-toggle { - background-image: url(images/layers.png); - width: 36px; - height: 36px; -} - -.leaflet-retina .leaflet-control-layers-toggle { - background-image: url(images/layers-2x.png); - background-size: 26px 26px; -} - -.leaflet-touch .leaflet-control-layers-toggle { - width: 44px; - height: 44px; -} - -.leaflet-control-layers .leaflet-control-layers-list, -.leaflet-control-layers-expanded .leaflet-control-layers-toggle { - display: none; -} - -.leaflet-control-layers-expanded .leaflet-control-layers-list { - display: block; - position: relative; -} - -.leaflet-control-layers-expanded { - padding: 6px 10px 6px 6px; - color: #333; - background: #fff; -} - -.leaflet-control-layers-scrollbar { - overflow-y: scroll; - overflow-x: hidden; - padding-right: 5px; -} - -.leaflet-control-layers-selector { - margin-top: 2px; - position: relative; - top: 1px; -} - -.leaflet-control-layers label { - display: block; -} - -.leaflet-control-layers-separator { - height: 0; - border-top: 1px solid #ddd; - margin: 5px -10px 5px -6px; -} - -/* Default icon URLs */ -.leaflet-default-icon-path { - background-image: url(images/marker-icon.png); -} - - -/* attribution and scale controls */ - -.leaflet-container .leaflet-control-attribution { - background: #fff; - background: rgba(255, 255, 255, 0.7); - margin: 0; -} - -.leaflet-control-attribution, -.leaflet-control-scale-line { - padding: 0 5px; - color: #333; -} - -.leaflet-control-attribution a { - text-decoration: none; -} - -.leaflet-control-attribution a:hover { - text-decoration: underline; -} - -.leaflet-container .leaflet-control-attribution, -.leaflet-container .leaflet-control-scale { - font-size: 11px; -} - -.leaflet-left .leaflet-control-scale { - margin-left: 5px; -} - -.leaflet-bottom .leaflet-control-scale { - margin-bottom: 5px; -} - -.leaflet-control-scale-line { - border: 2px solid #777; - border-top: none; - line-height: 1.1; - padding: 2px 5px 1px; - font-size: 11px; - white-space: nowrap; - overflow: hidden; - -moz-box-sizing: border-box; - box-sizing: border-box; - - background: #fff; - background: rgba(255, 255, 255, 0.5); -} - -.leaflet-control-scale-line:not(:first-child) { - border-top: 2px solid #777; - border-bottom: none; - margin-top: -2px; -} - -.leaflet-control-scale-line:not(:first-child):not(:last-child) { - border-bottom: 2px solid #777; -} - -.leaflet-touch .leaflet-control-attribution, -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - box-shadow: none; -} - -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - border: 2px solid rgba(0, 0, 0, 0.2); - background-clip: padding-box; -} - - -/* popup */ - -.leaflet-popup { - position: absolute; - text-align: center; - margin-bottom: 20px; -} - -.leaflet-popup-content-wrapper { - padding: 1px; - text-align: left; - border-radius: 12px; -} - -.leaflet-popup-content { - margin: 13px 19px; - line-height: 1.4; -} - -.leaflet-popup-content p { - margin: 18px 0; -} - -.leaflet-popup-tip-container { - width: 40px; - height: 20px; - position: absolute; - left: 50%; - margin-left: -20px; - overflow: hidden; - pointer-events: none; -} - -.leaflet-popup-tip { - width: 17px; - height: 17px; - padding: 1px; - - margin: -10px auto 0; - - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); -} - -.leaflet-popup-content-wrapper, -.leaflet-popup-tip { - background: white; - color: #333; - box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4); -} - -.leaflet-container a.leaflet-popup-close-button { - position: absolute; - top: 0; - right: 0; - padding: 4px 4px 0 0; - border: none; - text-align: center; - width: 18px; - height: 14px; - font: 16px/14px Tahoma, Verdana, sans-serif; - color: #c3c3c3; - text-decoration: none; - font-weight: bold; - background: transparent; -} - -.leaflet-container a.leaflet-popup-close-button:hover { - color: #999; -} - -.leaflet-popup-scrolled { - overflow: auto; - border-bottom: 1px solid #ddd; - border-top: 1px solid #ddd; -} - -.leaflet-oldie .leaflet-popup-content-wrapper { - zoom: 1; -} - -.leaflet-oldie .leaflet-popup-tip { - width: 24px; - margin: 0 auto; - - -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; - filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); -} - -.leaflet-oldie .leaflet-popup-tip-container { - margin-top: -1px; -} - -.leaflet-oldie .leaflet-control-zoom, -.leaflet-oldie .leaflet-control-layers, -.leaflet-oldie .leaflet-popup-content-wrapper, -.leaflet-oldie .leaflet-popup-tip { - border: 1px solid #999; -} - - -/* div icon */ - -.leaflet-div-icon { - background: #fff; - border: 1px solid #666; -} - - -/* Tooltip */ -/* Base styles for the element that has a tooltip */ -.leaflet-tooltip { - position: absolute; - padding: 6px; - background-color: #fff; - border: 1px solid #fff; - border-radius: 3px; - color: #222; - white-space: nowrap; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - pointer-events: none; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); -} - -.leaflet-tooltip.leaflet-clickable { - cursor: pointer; - pointer-events: auto; -} - -.leaflet-tooltip-top:before, -.leaflet-tooltip-bottom:before, -.leaflet-tooltip-left:before, -.leaflet-tooltip-right:before { - position: absolute; - pointer-events: none; - border: 6px solid transparent; - background: transparent; - content: ""; -} - -/* Directions */ - -.leaflet-tooltip-bottom { - margin-top: 6px; -} - -.leaflet-tooltip-top { - margin-top: -6px; -} - -.leaflet-tooltip-bottom:before, -.leaflet-tooltip-top:before { - left: 50%; - margin-left: -6px; -} - -.leaflet-tooltip-top:before { - bottom: 0; - margin-bottom: -12px; - border-top-color: #fff; -} - -.leaflet-tooltip-bottom:before { - top: 0; - margin-top: -12px; - margin-left: -6px; - border-bottom-color: #fff; -} - -.leaflet-tooltip-left { - margin-left: -6px; -} - -.leaflet-tooltip-right { - margin-left: 6px; -} - -.leaflet-tooltip-left:before, -.leaflet-tooltip-right:before { - top: 50%; - margin-top: -6px; -} - -.leaflet-tooltip-left:before { - right: 0; - margin-right: -12px; - border-left-color: #fff; -} - -.leaflet-tooltip-right:before { - left: 0; - margin-left: -12px; - border-right-color: #fff; -} diff --git a/test.ts b/test.ts index 167ce125a..c2d791548 100644 --- a/test.ts +++ b/test.ts @@ -1,94 +1,15 @@ import SvelteUIElement from "./UI/Base/SvelteUIElement" -import MaplibreMap from "./UI/Map/MaplibreMap.svelte" -import { UIEventSource } from "./Logic/UIEventSource" -import { MapLibreAdaptor } from "./UI/Map/MapLibreAdaptor" -import { AvailableRasterLayers, RasterLayerPolygon } from "./Models/RasterLayers" -import type { Map as MlMap } from "maplibre-gl" -import { ShowDataLayer } from "./UI/Map/ShowDataLayer" -import LayerConfig from "./Models/ThemeConfig/LayerConfig" -import * as bench from "./assets/generated/layers/bench.json" -import { Utils } from "./Utils" -import SimpleFeatureSource from "./Logic/FeatureSource/Sources/SimpleFeatureSource" -import { FilterState } from "./Models/FilteredLayer" +import ThemeViewGUI from "./UI/ThemeViewGUI.svelte" import { FixedUiElement } from "./UI/Base/FixedUiElement" +import { QueryParameters } from "./Logic/Web/QueryParameters" +import { AllKnownLayoutsLazy } from "./Customizations/AllKnownLayouts" async function main() { - const mlmap = new UIEventSource(undefined) - const location = new UIEventSource<{ lon: number; lat: number }>({ - lat: 51.1, - lon: 3.1, - }) - new SvelteUIElement(MaplibreMap, { - map: mlmap, - }) - .SetClass("border border-black") - .SetStyle("height: 50vh; width: 90%; margin: 1%") - .AttachTo("maindiv") - const bg = new UIEventSource(undefined) - const mla = new MapLibreAdaptor(mlmap, { - rasterLayer: bg, - location, - }) - - const features = new UIEventSource([ - { - feature: { - type: "Feature", - properties: { - hello: "world", - id: "" + 1, - }, - geometry: { - type: "Point", - coordinates: [3.1, 51.2], - }, - }, - freshness: new Date(), - }, - ]) - const layer = new LayerConfig(bench) - const options = { - zoomToFeatures: false, - features: new SimpleFeatureSource( - { - layerDef: layer, - isDisplayed: new UIEventSource(true), - appliedFilters: new UIEventSource>(undefined), - }, - 0, - features - ), - layer, - } - new ShowDataLayer(mlmap, options) - mla.zoom.set(9) - mla.location.set({ lon: 3.1, lat: 51.1 }) - const availableLayers = AvailableRasterLayers.layersAvailableAt(location) - // new BackgroundLayerResetter(bg, availableLayers) - // new SvelteUIElement(RasterLayerPicker, { availableLayers, value: bg }).AttachTo("extradiv") - for (let i = 0; i <= 10; i++) { - await Utils.waitFor(1000) - features.ping() - new FixedUiElement("> " + (5 - i)).AttachTo("extradiv") - } - options.zoomToFeatures = false - features.setData([ - { - feature: { - type: "Feature", - properties: { - hello: "world", - id: "" + 1, - }, - geometry: { - type: "Point", - coordinates: [3.103, 51.10003], - }, - }, - freshness: new Date(), - }, - ]) - new FixedUiElement("> OK").AttachTo("extradiv") + new FixedUiElement("Determining layout...").AttachTo("maindiv") + const qp = QueryParameters.GetQueryParameter("layout", "benches") + const layout = new AllKnownLayoutsLazy().get(qp.data) + console.log("Using layout", layout.id) + new SvelteUIElement(ThemeViewGUI, { layout }).AttachTo("maindiv") } main().then((_) => {}) diff --git a/theme.html b/theme.html index 5828c58e9..50d3fd9cb 100644 --- a/theme.html +++ b/theme.html @@ -4,7 +4,6 @@ -