refactoring(maplibre): WIP

This commit is contained in:
Pieter Vander Vennet 2023-03-24 19:21:15 +01:00
parent 231d67361e
commit 4d48b1cf2b
89 changed files with 1166 additions and 3973 deletions

View file

@ -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
}
}

View file

@ -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<Loc>
selectedElement: UIEventSource<any>
leafletMap?: UIEventSource<any>
}
/**
* 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<boolean> = new UIEventSource<boolean>(false)
private readonly selectedElement: UIEventSource<any>
private readonly mapProperties?: MapProperties
private readonly gpsLocationHistoryRetentionTime?: UIEventSource<number>
constructor(
geolocationState: GeoLocationState,
state: {
locationControl: UIEventSource<Loc>
currentUserLocation: SimpleFeatureSource
layoutToUse: LayoutConfig
selectedElement: UIEventSource<any>
leafletMap?: UIEventSource<any>
}
selectedElement: UIEventSource<any>,
mapProperties?: MapProperties,
gpsLocationHistoryRetentionTime?: UIEventSource<number>
) {
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<Feature[]> = new UIEventSource<Feature[]>([])
this.currentUserLocation = new StaticFeatureSource(features)
this.geolocationState.currentGPSLocation.addCallbackAndRun((location) => {
if (location === undefined) {
return
}
const feature = {
const feature = <Feature>{
type: "Feature",
properties: <GeoLocationPointProperties>{
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<Feature[]>("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<Point>]) => {
if (location === undefined) {
return
}
const previousLocation = <Feature<Point>>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<LineString, OsmTags> = {
type: "Feature",
properties: {
id: "location_track",
"_date:now": new Date().toISOString(),
},
geometry: {
type: "LineString",
coordinates: allPoints.map(
(ff: Feature<Point>) => <[number, number]>ff.geometry.coordinates
),
},
}
return [feature]
})
this.historicalUserLocationsTrack = new StaticFeatureSource(asLine)
}
}

View file

@ -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<number>
public location: UIEventSource<{ lon: number; lat: number }>
constructor(layoutToUse: LayoutConfig) {
function localStorageSynced(
key: string,
deflt: number,
docs: string
): UIEventSource<number> {
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
}
}

View file

@ -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<T>(properties: T): Feature<Polygon, T> {
return {
type: "Feature",

View file

@ -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,
<Feature>feat,
<Feature<Polygon | MultiPolygon, any>>otherFeature
)
) {

View file

@ -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,
})
}
}
}
}

View file

@ -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<LineString | Polygon>, point: [number, number]) {
public static nearestPoint(
way: Feature<LineString | Polygon>,
point: [number, number]
): Feature<Point> {
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 => (<Feature> {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<LineString>>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++) {

View file

@ -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

View file

@ -59,15 +59,6 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
}
}
public async getPreview(): Promise<FeatureSource> {
const outerPreview = await this.createOuterWay.getPreview()
outerPreview.features.data.push({
freshness: new Date(),
feature: this.geojsonPreview,
})
return outerPreview
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
console.log("Running CMPWPRA")
const descriptions: ChangeDescription[] = []

View file

@ -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<ChangeDescription[]> =
LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection }
public readonly extraComment: UIEventSource<string> = 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
}
}

View file

@ -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<GeoCodeResult[]> {
const b = State?.state?.currentBounds?.data ?? BBox.global
static async Search(query: string, bbox: BBox): Promise<GeoCodeResult[]> {
const b = bbox ?? BBox.global
const url =
Geocoding.host +
"format=json&limit=1&viewbox=" +

View file

@ -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<properties>
*/
public allElements: ElementStorage = new ElementStorage()
/**
The latest element that was selected
*/
public readonly selectedElement = new UIEventSource<any>(undefined, "Selected element")
/**
* The map location: currently centered lat, lon and zoom
*/
public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl")
/**
* The current visible extent of the screen
*/
public readonly currentBounds = new UIEventSource<BBox>(undefined)
constructor(layoutToUse: LayoutConfig) {
super(layoutToUse)
function localStorageSynced(
key: string,
deflt: number,
docs: string
): UIEventSource<number> {
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)
})
}
}

View file

@ -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<any>, 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<any>) {
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,
})
}
}

View file

@ -30,7 +30,7 @@ export class GeoLocationState {
/**
* If true: the map will center (and re-center) to this location
*/
public readonly isLocked: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true)
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
new UIEventSource<GeolocationCoordinates | undefined>(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
}
/**

View file

@ -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<any /*L.Map*/>(undefined, "leafletmap")
export default class MapState {
/**
* The current background layer
*/
public backgroundLayer: UIEventSource<BaseLayer>
/**
* 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<boolean> }[]
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<BaseLayer>(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<LineString, OsmTags> = {
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,

View file

@ -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<string[]>
public readonly showAllQuestionsAtOnce: UIEventSource<boolean>
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<undefined | "true" | "false" | "mobile" | string> =
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
Locale.language.syncWith(this.osmConnection.GetPreference("language"))
Locale.language.addCallback((currentLanguage) => {
if (layoutToUse === undefined) {
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 (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)
}
}

13
Models/GlobalFilter.ts Normal file
View file

@ -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[]
}
}

View file

@ -1,3 +0,0 @@
export default interface LeafletMap {
getBounds(): [[number, number], [number, number]]
}

14
Models/MapProperties.ts Normal file
View file

@ -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<number>
readonly bounds: Store<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
readonly maxbounds: UIEventSource<undefined | BBox>
readonly allowMoving: UIEventSource<true | boolean>
}

View file

@ -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<LayerConfigJson> {
private static readonly predefinedFilters = ExpandFilter.load_filters()

View file

@ -175,7 +175,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
}
class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
private _state: DesugaringContext
private readonly _state: DesugaringContext
constructor(state: DesugaringContext) {
super(

View file

@ -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<any> {
private readonly _languages: string[]
@ -594,6 +595,7 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
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<TagRenderingConfigJson> {
}
}
}
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<LayerConfigJson> {
}
}
export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
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[]

View file

@ -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}]`
}

View file

@ -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
}
})
)
/**

View file

@ -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)[]

View file

@ -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<OsmTags>({ id: "node/-1" }),
false,
{ includeBadges: false }
).html
mr.RenderIcon(new ImmutableStore<OsmTags>({ id: "node/-1" }), false, {
includeBadges: false,
}).html
)
.find((i) => i !== undefined)
}

View file

@ -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
**/

View file

@ -175,7 +175,7 @@ export default class PointRenderingConfig extends WithContextLoader {
)
}
public GenerateLeafletStyle(
public RenderIcon(
tags: Store<OsmTags>,
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
}

View file

@ -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 = <Translation>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) {

View file

@ -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<number>(undefined)
constructor(
layoutToUse: LayoutConfig,
indices: number[],
extraCommentText: UIEventSource<string>,
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<string[]>([])
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<number[]>([])
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<string, number>()
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<string>,
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<FeatureSource[]> = new UIEventSource<FeatureSource[]>([])
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(
"<a href='https://openstreetmap.org/" +
feature.properties.id +
"' target='_blank'>" +
feature.properties.id +
"</a>: " +
new SubstitutedTranslation(
renderingTr,
new UIEventSource<any>(feature.properties),
undefined
).ConstructElement().textContent
)
const actions = Utils.NoNull(
SubstitutedTranslation.ExtractSpecialComponents(rendering).map(
(obj) => obj.special
)
)
for (const action of actions) {
const auto = <AutoAction>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<string>(
"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<number>()
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()

14
UI/Base/If.svelte Normal file
View file

@ -0,0 +1,14 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
/**
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
*/
export let condition: UIEventSource<boolean>;
let _c = condition.data;
condition.addCallback(c => _c = c)
</script>
{#if _c}
<slot></slot>
{/if}

View file

@ -0,0 +1,13 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
/**
* A round button with an icon and possible a small text, which hovers above the map
*/
const dispatch = createEventDispatcher()
</script>
<div on:click={e => 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">
<slot class="m-4"></slot>
</div>

View file

@ -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<BaseLayer>
location?: UIEventSource<Loc>
bounds?: UIEventSource<BBox>
allowMoving?: boolean
leafletOptions?: any
attribution?: BaseUIElement | boolean
onFullyLoaded?: (leaflet: L.Map) => void
leafletMap?: UIEventSource<any>
lastClickLocation?: UIEventSource<{ lat: number; lon: number }>
addLayerControl?: boolean | false
}
export interface MinimapObj {
readonly leafletMap: UIEventSource<any>
readonly location: UIEventSource<Loc>
readonly bounds: UIEventSource<BBox>
installBounds(factor: number | BBox, showRange?: boolean): void
TakeScreenshot(format): Promise<string>
TakeScreenshot(format: "image"): Promise<string>
TakeScreenshot(format: "blob"): Promise<Blob>
TakeScreenshot(format?: "image" | "blob"): Promise<string | Blob>
}
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()"
}
}

View file

@ -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<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
},
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<Map>
public readonly location: UIEventSource<Loc>
public readonly bounds: UIEventSource<BBox> | undefined
private readonly _id: string
private readonly _background: UIEventSource<BaseLayer>
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<Map>(undefined)
this._background =
options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
this.location = options?.location ?? new UIEventSource<Loc>({ 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<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
},
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<string>
public async TakeScreenshot(format: "image"): Promise<string>
public async TakeScreenshot(format: "blob"): Promise<Blob>
public async TakeScreenshot(format: "image" | "blob"): Promise<string | Blob>
public async TakeScreenshot(format: "image" | "blob" = "image"): Promise<string | Blob> {
console.log("Taking a screenshot...")
const screenshotter = new SimpleMapScreenshoter()
screenshotter.addTo(this.leafletMap.data)
const result = <any>await screenshotter.takeScreen(<any>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("<span id='leaflet-attribution'></span>")
}
}
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)
}
}

View file

@ -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<string, any> = any,

View file

@ -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<any>(tags), false)
.RenderIcon(new ImmutableStore<any>(tags), false)
.html.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;")
icons.push(icon)
if (last === undefined) {
last = layer.mapRendering[0]
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
.RenderIcon(new ImmutableStore<any>(tags), false)
.html.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;")
}

View file

@ -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<Loc>,
userDetails: UIEventSource<UserDetails>,
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>
) {
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")
}
}

View file

@ -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<Loc>
currentBounds: UIEventSource<BBox>
}
) {
constructor(geolocationHandler: GeoLocationHandler, state: MapProperties) {
const lastClick = new UIEventSource<Date>(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
}

View file

@ -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<any> = state.currentView.features.map((ffs) => ffs[0]?.feature)
const feature: Store<any> = state.currentView.features.map((ffs) => ffs[0])
const icon = new VariableUiElement(
feature.map((feature) => {
const defaultIcon = Svg.checkbox_empty_svg()

View file

@ -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, ...

View file

@ -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")
}
}

View file

@ -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<any>; selectedElement?: UIEventSource<any> }) {
constructor(state: {
leafletMap: UIEventSource<any>
selectedElement?: UIEventSource<any>
bounds?: Store<BBox>
}) {
const goButton = Svg.search_ui().SetClass("w-8 h-8 full-rounded border-black float-right")
const placeholder = new UIEventSource<Translation>(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) {

View file

@ -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<any>(tags), false)
.RenderIcon(new ImmutableStore<any>(tags), false)
.html.SetClass("w-12 h-12 block relative")
const presetInfo: PresetInfo = {
layerToAddTo: layer,

View file

@ -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<string, BaseUIElement> = {}
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<string, LayerConfig> = {}
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
}
}

View file

@ -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({

View file

@ -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")

View file

@ -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<any>) {
@ -110,21 +108,11 @@ export class MapPreview
return matching
})
const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
const background = new UIEventSource<RasterLayerPolygon>(AvailableRasterLayers.osmCarto)
const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
const currentBounds = new UIEventSource<BBox>(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,

View file

@ -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<number>
readonly bounds: Store<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
}
export class MapLibreAdaptor implements MapState {
readonly maxbounds: UIEventSource<BBox | undefined>
readonly allowMoving: UIEventSource<true | boolean | undefined>
private readonly _maplibreMap: Store<MLMap>
readonly location: UIEventSource<{ lon: number; lat: number }>
readonly zoom: UIEventSource<number>
readonly bounds: Store<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
private readonly _bounds: UIEventSource<BBox>
/**
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
* @private
*/
private _currentRasterLayer: string
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapState, "bounds">>) {
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapProperties, "bounds">>) {
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))
}
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
this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox))
this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving))
}
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<MLMap>
ui: SvelteUIElement
mapproperties: MapProperties
} {
const mlmap = new UIEventSource<MlMap>(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<void> {
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()
}
}
}
}

View file

@ -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 @@
<main>
<Map bind:center={center}
bind:map={$map}
controls="true"
id="map" location={{lng: 0, lat: 0, zoom: 0}} maxzoom=24 style={styleUrl} />
</main>

View file

@ -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<OsmTags>
private readonly _map: MlMap
private readonly _onClick: (id: string) => void
private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>()
constructor(
map: MlMap,
features: FeatureSource,
config: PointRenderingConfig,
fetchStore?: (id: string) => Store<OsmTags>
visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<OsmTags>,
onClick?: (id: string) => void
) {
this._config = config
this._map = map
this._fetchStore = fetchStore
const cache: Map<string, Marker> = new Map<string, Marker>()
this._onClick = onClick
const self = this
features.features.addCallbackAndRunD((features) => {
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 { feature } of features) {
const id = feature.properties.id
for (const location of this._config.location) {
for (const feature of features) {
const loc = GeoOperations.featureToCoordinateWithRenderingType(
<any>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)
}
})
}
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<OsmTags>
if (this._fetchStore) {
store = this._fetchStore(feature.properties.id)
} else {
store = new ImmutableStore(feature.properties)
store = new ImmutableStore(<OsmTags>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()
if (this._onClick) {
const self = this
el.addEventListener("click", function () {
window.alert("Hello world!")
self._onClick(feature.properties.id)
})
}
return new Marker(el).setLngLat(loc).setOffset(iconAnchor).addTo(this._map)
}
}
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<boolean>
private readonly _fetchStore?: (id: string) => Store<OsmTags>
private readonly _onClick?: (id: string) => void
private readonly _layername: string
constructor(
map: MlMap,
features: FeatureSource,
layername: string,
config: LineRenderingConfig,
visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<OsmTags>,
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"],
},
})
return new Marker(el)
.setLngLat(GeoOperations.centerpointCoordinates(feature))
.setOffset(iconAnchor)
.addTo(this._map)
/*[
"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 class ShowDataLayer {
export default class ShowDataLayer {
private readonly _map: Store<MlMap>
private _options: ShowDataLayerOptions & { layer: LayerConfig }
private readonly _options: ShowDataLayerOptions & { layer: LayerConfig }
private readonly _popupCache: Map<string, ScrollableFullScreen>
constructor(map: Store<MlMap>, 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(
<LayerConfigJson>range_layer,
"ShowDataLayer.ts:range.json"
)
public static showRange(
map: Store<MlMap>,
features: FeatureSource,
doShowLayer?: Store<boolean>
): 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))
}
}

View file

@ -33,5 +33,5 @@ export interface ShowDataLayerOptions {
/**
* Function which fetches the relevant store
*/
fetchStore?: (id: string) => Store<OsmTags>
fetchStore?: (id: string) => UIEventSource<OsmTags>
}

View file

@ -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<FilteredLayer[]> }) {
constructor(
map: Store<MlMap>,
options: ShowDataLayerOptions & { layers: Store<FilteredLayer[]> }
) {
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
)

View file

@ -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(
@ -110,9 +110,7 @@ export default class ConfirmLocationOfPoint extends Combine {
console.log("Snapping to", layerId)
state.featurePipeline
.GetFeaturesWithin(layerId, bbox)
?.forEach((feats) =>
allFeatures.push(...<any[]>feats)
)
?.forEach((feats) => allFeatures.push(...(<any[]>feats)))
})
console.log("Snapping to", allFeatures)
snapToFeatures.setData(allFeatures)

View file

@ -1,7 +1,7 @@
<script lang="ts">
import ToSvelte from "./Base/ToSvelte.svelte"
import Table from "./Base/Table"
import { UIEventSource } from "../Logic/UIEventSource"
import ToSvelte from "../Base/ToSvelte.svelte"
import Table from "../Base/Table"
import { UIEventSource } from "../../Logic/UIEventSource"
//Svelte props
export let tags: UIEventSource<any>

View file

@ -45,6 +45,7 @@ import { ElementStorage } from "../../Logic/ElementStorage"
import Hash from "../../Logic/Web/Hash"
import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig"
import { SpecialVisualization } from "../SpecialVisualization"
import Maproulette from "../../Logic/Maproulette";
/**
* A helper class for the various import-flows.
@ -720,7 +721,7 @@ export class ImportPointButton extends AbstractImportButton {
)
} else {
console.log("Marking maproulette task as fixed")
await state.maprouletteConnection.closeTask(Number(maproulette_id))
await Maproulette.singleton.closeTask(Number(maproulette_id))
originalFeatureTags.data["mr_taskStatus"] = "Fixed"
originalFeatureTags.ping()
}

View file

@ -1,52 +0,0 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import Loc from "../../Models/Loc"
import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import left_right_style_json from "../../assets/layers/left_right_style/left_right_style.json"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { SpecialVisualization } from "../SpecialVisualization"
export class SidedMinimap implements SpecialVisualization {
funcName = "sided_minimap"
docs =
"A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced"
args = [
{
doc: "The side to show, either `left` or `right`",
name: "side",
required: true,
},
]
example = "`{sided_minimap(left)}`"
public constr(state, tagSource, args) {
const properties = tagSource.data
const locationSource = new UIEventSource<Loc>({
lat: Number(properties._lat),
lon: Number(properties._lon),
zoom: 18,
})
const minimap = Minimap.createMiniMap({
background: state.backgroundLayer,
location: locationSource,
allowMoving: false,
})
const side = args[0]
const feature = state.allElements.ContainingFeatures.get(tagSource.data.id)
const copy = { ...feature }
copy.properties = {
id: side,
}
new ShowDataLayer({
leafletMap: minimap["leafletMap"],
zoomToFeatures: true,
layerToShow: new LayerConfig(left_right_style_json, "all_known_layers", true),
features: StaticFeatureSource.fromGeojson([copy]),
state,
})
minimap.SetStyle("overflow: hidden; pointer-events: none;")
return minimap
}
}

View file

@ -1,27 +0,0 @@
/**
* The data layer shows all the given geojson elements with the appropriate icon etc
*/
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
export default class ShowDataLayer {
public static actualContstructor: (
options: ShowDataLayerOptions & { layerToShow: LayerConfig }
) => void = undefined
/**
* Creates a datalayer.
*
* If 'createPopup' is set, this function is called every time that 'popupOpen' is called
* @param options
*/
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
if (ShowDataLayer.actualContstructor === undefined) {
console.error(
"Show data layer is called, but it isn't initialized yet. Call ` ShowDataLayer.actualContstructor = (options => new ShowDataLayerImplementation(options)) ` somewhere, e.g. in your init"
)
return
}
ShowDataLayer.actualContstructor(options)
}
}

View file

@ -1,407 +0,0 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
import { ElementStorage } from "../../Logic/ElementStorage"
import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { LeafletMouseEvent, PathOptions } from "leaflet"
import Hash from "../../Logic/Web/Hash"
import { BBox } from "../../Logic/BBox"
import { Utils } from "../../Utils"
/*
// import 'leaflet-polylineoffset';
We don't actually import it here. It is imported in the 'MinimapImplementation'-class, which'll result in a patched 'L' object.
Even though actually importing this here would seem cleaner, we don't do this as this breaks some scripts:
- Scripts are ran in ts-node
- ts-node doesn't define the 'window'-object
- Importing this will execute some code which needs the window object
*/
/**
* The data layer shows all the given geojson elements with the appropriate icon etc
*/
export default class ShowDataLayerImplementation {
private static dataLayerIds = 0
private readonly _leafletMap: Store<L.Map>
private readonly _enablePopups: boolean
private readonly _features: RenderingMultiPlexerFeatureSource
private readonly _layerToShow: LayerConfig
private readonly _selectedElement: UIEventSource<any>
private readonly allElements: ElementStorage
// Used to generate a fresh ID when needed
private _cleanCount = 0
private geoLayer = undefined
/**
* A collection of functions to call when the current geolayer is unregistered
*/
private unregister: (() => void)[] = []
private isDirty = false
/**
* If the selected element triggers, this is used to lookup the correct layer and to open the popup
* Used to avoid a lot of callbacks on the selected element
*
* Note: the key of this dictionary is 'feature.properties.id+features.geometry.type' as one feature might have multiple presentations
* @private
*/
private readonly leafletLayersPerId = new Map<
string,
{ feature: any; activateFunc: (event: LeafletMouseEvent) => void }
>()
private readonly showDataLayerid: number
private readonly createPopup: (
tags: UIEventSource<any>,
layer: LayerConfig
) => ScrollableFullScreen
/**
* Creates a datalayer.
*
* If 'createPopup' is set, this function is called every time that 'popupOpen' is called
* @param options
*/
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
this._leafletMap = options.leafletMap
this.showDataLayerid = ShowDataLayerImplementation.dataLayerIds
ShowDataLayerImplementation.dataLayerIds++
if (options.features === undefined) {
console.error("Invalid ShowDataLayer invocation: options.features is undefed")
throw "Invalid ShowDataLayer invocation: options.features is undefed"
}
this._features = new RenderingMultiPlexerFeatureSource(
options.features,
options.layerToShow
)
this._layerToShow = options.layerToShow
this._selectedElement = options.selectedElement
this.allElements = options.state?.allElements
this.createPopup = undefined
this._enablePopups = options.popup !== undefined
if (options.popup !== undefined) {
this.createPopup = options.popup
}
const self = this
options.leafletMap.addCallback(() => {
return self.update(options)
})
this._features.features.addCallback((_) => self.update(options))
options.doShowLayer?.addCallback((doShow) => {
const mp = options.leafletMap.data
if (mp === null) {
self.Destroy()
return true
}
if (mp == undefined) {
return
}
if (doShow) {
if (self.isDirty) {
return self.update(options)
} else {
mp.addLayer(this.geoLayer)
}
} else {
if (this.geoLayer !== undefined) {
mp.removeLayer(this.geoLayer)
this.unregister.forEach((f) => f())
this.unregister = []
}
}
})
this._selectedElement?.addCallbackAndRunD((selected) => {
if (selected === undefined) {
ScrollableFullScreen.collapse()
return
}
self.openPopupOfSelectedElement(selected)
})
this.update(options)
}
private Destroy() {
this.unregister.forEach((f) => f())
}
private openPopupOfSelectedElement(selected) {
if (selected === undefined) {
return
}
if (this._leafletMap.data === undefined) {
return
}
const v = this.leafletLayersPerId.get(selected.properties.id)
if (v === undefined) {
return
}
const feature = v.feature
if (selected.properties.id !== feature.properties.id) {
return
}
if (feature.id !== feature.properties.id) {
// Probably a feature which has renamed
// the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too
console.log("Not opening the popup for", feature, "as probably renamed")
return
}
v.activateFunc(null)
}
private update(options: ShowDataLayerOptions): boolean {
if (this._features.features.data === undefined) {
return
}
this.isDirty = true
if (options?.doShowLayer?.data === false) {
return
}
const mp = options.leafletMap.data
if (mp === null) {
return true // Unregister as the map has been destroyed
}
if (mp === undefined) {
return
}
this._cleanCount++
// clean all the old stuff away, if any
if (this.geoLayer !== undefined) {
mp.removeLayer(this.geoLayer)
}
const self = this
this.geoLayer = new L.LayerGroup()
const selfLayer = this.geoLayer
const allFeats = this._features.features.data
for (const feat of allFeats) {
if (feat === undefined) {
continue
}
// Why not one geojson layer with _all_ features, and attaching a right-click onto every feature individually?
// Because that somehow doesn't work :(
const feature = feat
const geojsonLayer = L.geoJSON(feature, {
style: (feature) => <PathOptions>self.createStyleFor(feature),
pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng),
onEachFeature: (feature, leafletLayer) =>
self.postProcessFeature(feature, leafletLayer),
})
if (feature.geometry.type === "Point") {
geojsonLayer.on({
contextmenu: (e) => {
const o = self.leafletLayersPerId.get(feature?.properties?.id)
o?.activateFunc(<LeafletMouseEvent>e)
Utils.preventDefaultOnMouseEvent(e.originalEvent)
},
dblclick: (e) => {
const o = self.leafletLayersPerId.get(feature?.properties?.id)
o?.activateFunc(<LeafletMouseEvent>e)
Utils.preventDefaultOnMouseEvent(e.originalEvent)
},
})
}
this.geoLayer.addLayer(geojsonLayer)
try {
if (feat.geometry.type === "LineString") {
const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates)
const tagsSource =
this.allElements?.addOrGetElement(feat) ??
new UIEventSource<any>(feat.properties)
let offsettedLine
tagsSource
.map((tags) =>
this._layerToShow.lineRendering[
feat.lineRenderingIndex
].GenerateLeafletStyle(tags)
)
.withEqualityStabilized((a, b) => {
if (a === b) {
return true
}
if (a === undefined || b === undefined) {
return false
}
return (
a.offset === b.offset &&
a.color === b.color &&
a.weight === b.weight &&
a.dashArray === b.dashArray
)
})
.addCallbackAndRunD((lineStyle) => {
if (offsettedLine !== undefined) {
self.geoLayer.removeLayer(offsettedLine)
}
// @ts-ignore
offsettedLine = L.polyline(coords, lineStyle)
this.postProcessFeature(feat, offsettedLine)
offsettedLine.addTo(this.geoLayer)
// If 'self.geoLayer' is not the same as the layer the feature is added to, we can safely remove this callback
return self.geoLayer !== selfLayer
})
} else {
geojsonLayer.addData(feat)
}
} catch (e) {
console.error(
"Could not add ",
feat,
"to the geojson layer in leaflet due to",
e,
e.stack
)
}
}
if ((options.zoomToFeatures ?? false) && allFeats.length > 0) {
let bound = undefined
for (const feat of allFeats) {
const fbound = BBox.get(feat)
bound = bound?.unionWith(fbound) ?? fbound
}
if (bound !== undefined) {
mp.fitBounds(bound?.toLeaflet(), { animate: false })
}
}
if (options.doShowLayer?.data ?? true) {
mp.addLayer(this.geoLayer)
}
this.isDirty = false
this.openPopupOfSelectedElement(this._selectedElement?.data)
}
private createStyleFor(feature) {
const tagsSource =
this.allElements?.addOrGetElement(feature) ?? new UIEventSource<any>(feature.properties)
// Every object is tied to exactly one layer
const layer = this._layerToShow
const pointRenderingIndex = feature.pointRenderingIndex
const lineRenderingIndex = feature.lineRenderingIndex
if (pointRenderingIndex !== undefined) {
const style = layer.mapRendering[pointRenderingIndex].GenerateLeafletStyle(
tagsSource,
this._enablePopups
)
return {
icon: style,
}
}
if (lineRenderingIndex !== undefined) {
return layer.lineRendering[lineRenderingIndex].GenerateLeafletStyle(tagsSource.data)
}
throw "Neither lineRendering nor mapRendering defined for " + feature
}
private pointToLayer(feature, latLng): L.Layer {
// Leaflet cannot handle geojson points natively
// We have to convert them to the appropriate icon
// Click handling is done in the next step
const layer: LayerConfig = this._layerToShow
if (layer === undefined) {
return
}
let tagSource =
this.allElements?.getEventSourceById(feature.properties.id) ??
new UIEventSource<any>(feature.properties)
const clickable =
!(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) &&
this._enablePopups
let style: any = layer.mapRendering[feature.pointRenderingIndex].GenerateLeafletStyle(
tagSource,
clickable
)
const baseElement = style.html
if (!this._enablePopups) {
baseElement.SetStyle("cursor: initial !important")
}
style.html = style.html.ConstructElement()
return L.marker(latLng, {
icon: L.divIcon(style),
})
}
/**
* Creates a function which, for the given feature, will open the featureInfoBox (and lazyly create it)
* This function is cached
* @param feature
* @param key
* @param layer
* @private
*/
private createActivateFunction(feature, key: string, layer: LayerConfig): (event) => void {
if (this.leafletLayersPerId.has(key)) {
return this.leafletLayersPerId.get(key).activateFunc
}
let infobox: ScrollableFullScreen = undefined
const self = this
function activate(event: LeafletMouseEvent) {
Utils.preventDefaultOnMouseEvent(event)
if (infobox === undefined) {
const tags =
self.allElements?.getEventSourceById(key) ??
new UIEventSource<any>(feature.properties)
infobox = self.createPopup(tags, layer)
self.unregister.push(() => {
console.log("Destroying infobox")
infobox.Destroy()
})
}
infobox.Activate()
self._selectedElement.setData(
self.allElements.ContainingFeatures.get(feature.id) ?? feature
)
}
return activate
}
/**
* Post processing - basically adding the popup
* @param feature
* @param leafletLayer
* @private
*/
private postProcessFeature(feature, leafletLayer: L.Evented) {
const layer: LayerConfig = this._layerToShow
if (layer.title === undefined || !this._enablePopups) {
// No popup action defined -> Don't do anything
// or probably a map in the popup - no popups needed!
return
}
const key = feature.properties.id
const activate = this.createActivateFunction(feature, key, layer)
// We also have to open on rightclick, doubleclick, ... as users sometimes do this. See #1219
leafletLayer.on({
dblclick: activate,
contextmenu: activate,
// click: activate,
})
leafletLayer.addEventListener("click", activate)
// Add the feature to the index to open the popup when needed
this.leafletLayersPerId.set(key, {
feature: feature,
activateFunc: activate,
})
if (Hash.hash.data === key) {
activate(null)
}
}
}

View file

@ -1,64 +0,0 @@
import FeatureSource, { Tiled } from "../../Logic/FeatureSource/FeatureSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import ShowDataLayer from "./ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { GeoOperations } from "../../Logic/GeoOperations"
import { Tiles } from "../../Models/TileRange"
import clusterstyle from "../../assets/layers/cluster_style/cluster_style.json"
export default class ShowTileInfo {
public static readonly styling = new LayerConfig(clusterstyle, "ShowTileInfo", true)
constructor(
options: {
source: FeatureSource & Tiled
leafletMap: UIEventSource<any>
layer?: LayerConfig
doShowLayer?: UIEventSource<boolean>
},
state
) {
const source = options.source
const metaFeature: Store<{ feature; freshness: Date }[]> = source.features.map(
(features) => {
const bbox = source.bbox
const [z, x, y] = Tiles.tile_from_index(source.tileIndex)
const box = {
type: "Feature",
properties: {
z: z,
x: x,
y: y,
tileIndex: source.tileIndex,
source: source.name,
count: features.length,
tileId: source.name + "/" + source.tileIndex,
},
geometry: {
type: "Polygon",
coordinates: [
[
[bbox.minLon, bbox.minLat],
[bbox.minLon, bbox.maxLat],
[bbox.maxLon, bbox.maxLat],
[bbox.maxLon, bbox.minLat],
[bbox.minLon, bbox.minLat],
],
],
},
}
const center = GeoOperations.centerpoint(box)
return [box, center].map((feature) => ({ feature, freshness: new Date() }))
}
)
new ShowDataLayer({
layerToShow: ShowTileInfo.styling,
features: new StaticFeatureSource(metaFeature),
leafletMap: options.leafletMap,
doShowLayer: options.doShowLayer,
state,
})
}
}

View file

@ -5,9 +5,9 @@ import FeatureSource, {
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Tiles } from "../../Models/TileRange"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { BBox } from "../../Logic/BBox"
import FilteredLayer from "../../Models/FilteredLayer"
import { Feature } from "geojson"
/**
* A feature source containing but a single feature, which keeps stats about a tile
@ -17,16 +17,14 @@ export class TileHierarchyAggregator implements FeatureSource {
public totalValue: number = 0
public showCount: number = 0
public hiddenCount: number = 0
public readonly features = new UIEventSource<{ feature: any; freshness: Date }[]>(
TileHierarchyAggregator.empty
)
public readonly features = new UIEventSource<Feature[]>(TileHierarchyAggregator.empty)
public readonly name
private _parent: TileHierarchyAggregator
private _root: TileHierarchyAggregator
private _z: number
private _x: number
private _y: number
private _tileIndex: number
private readonly _z: number
private readonly _x: number
private readonly _y: number
private readonly _tileIndex: number
private _counter: SingleTileCounter
private _subtiles: [
TileHierarchyAggregator,
@ -158,42 +156,6 @@ export class TileHierarchyAggregator implements FeatureSource {
}
this.updateSignal.setData(source)
}
getCountsForZoom(
clusteringConfig: { maxZoom: number },
locationControl: UIEventSource<{ zoom: number }>,
cutoff: number = 0
): FeatureSource {
const self = this
const empty = []
const features = locationControl
.map((loc) => loc.zoom)
.map(
(targetZoom) => {
if (targetZoom - 1 > clusteringConfig.maxZoom) {
return empty
}
const features: { feature: any; freshness: Date }[] = []
self.visitSubTiles((aggr) => {
if (aggr.showCount < cutoff) {
return false
}
if (aggr._z === targetZoom) {
features.push(...aggr.features.data)
return false
}
return aggr._z <= targetZoom
})
return features
},
[this.updateSignal.stabilized(500)]
)
return new StaticFeatureSource(features)
}
private update() {
const newMap = new Map<string, number>()
let total = 0
@ -254,13 +216,6 @@ export class TileHierarchyAggregator implements FeatureSource {
this.features.ping()
}
}
private visitSubTiles(f: (aggr: TileHierarchyAggregator) => boolean) {
const visitFurther = f(this)
if (visitFurther) {
this._subtiles.forEach((tile) => tile?.visitSubTiles(f))
}
}
}
/**

View file

@ -7,7 +7,6 @@ import { SpecialVisualization } from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { StealViz } from "./Popup/StealViz"
import { MinimapViz } from "./Popup/MinimapViz"
import { SidedMinimap } from "./Popup/SidedMinimap"
import { ShareLinkViz } from "./Popup/ShareLinkViz"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
@ -20,7 +19,7 @@ import { CloseNoteButton } from "./Popup/CloseNoteButton"
import { NearbyImageVis } from "./Popup/NearbyImageVis"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
import { Stores, UIEventSource } from "../Logic/UIEventSource"
import AllTagsPanel from "./AllTagsPanel.svelte"
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
import { ImageCarousel } from "./Image/ImageCarousel"
import { ImageUploadFlow } from "./Image/ImageUploadFlow"
@ -142,7 +141,6 @@ export default class SpecialVisualizations {
new HistogramViz(),
new StealViz(),
new MinimapViz(),
new SidedMinimap(),
new ShareLinkViz(),
new UploadToOsmViz(),
new MultiApplyViz(),
@ -664,7 +662,7 @@ export default class SpecialVisualizations {
const maproulette_id =
tagsSource.data[maproulette_id_key] ?? tagsSource.data.id
try {
await state.maprouletteConnection.closeTask(
await Maproulette.singleton.closeTask(
Number(maproulette_id),
Number(status),
{

114
UI/ThemeViewGUI.svelte Normal file
View file

@ -0,0 +1,114 @@
<script lang="ts">
import { UIEventSource } from "../Logic/UIEventSource";
import { Map as MlMap } from "maplibre-gl";
import MaplibreMap from "./Map/MaplibreMap.svelte";
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning";
import { GeoLocationState } from "../Logic/State/GeoLocationState";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import { OsmConnection } from "../Logic/Osm/OsmConnection";
import { QueryParameters } from "../Logic/Web/QueryParameters";
import UserRelatedState from "../Logic/State/UserRelatedState";
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler";
import { ElementStorage } from "../Logic/ElementStorage";
import { Changes } from "../Logic/Osm/Changes";
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor";
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader";
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater";
import MapControlButton from "./Base/MapControlButton.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
import Svg from "../Svg";
import If from "./Base/If.svelte";
import { GeolocationControl } from "./BigComponents/GeolocationControl.js";
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
import { BBox } from "../Logic/BBox";
import ShowDataLayer from "./Map/ShowDataLayer";
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
export let layout: LayoutConfig;
const maplibremap: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
const initial = new InitialMapPositioning(layout);
const mapproperties = new MapLibreAdaptor(maplibremap, initial);
const geolocationState = new GeoLocationState();
const featureSwitches = new FeatureSwitchState(layout);
const osmConnection = new OsmConnection({
dryRun: featureSwitches.featureSwitchIsTesting,
fakeUser: featureSwitches.featureSwitchFakeUser.data,
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
undefined,
"Used to complete the login"
),
osmConfiguration: <"osm" | "osm-test">featureSwitches.featureSwitchApiURL.data
});
const userRelatedState = new UserRelatedState(osmConnection, layout?.language);
const selectedElement = new UIEventSource<any>(undefined, "Selected element");
const geolocation = new GeoLocationHandler(geolocationState, selectedElement, mapproperties, userRelatedState.gpsLocationHistoryRetentionTime);
const allElements = new ElementStorage();
const changes = new Changes({
allElements,
osmConnection,
historicalUserLocations: geolocation.historicalUserLocations
}, layout?.isLeftRightSensitive() ?? false);
Map
{
// Various actors that we don't need to reference
new ChangeToElementsActor(changes, allElements);
new PendingChangesUploader(changes, selectedElement);
new SelectedElementTagsUpdater({
allElements, changes, selectedElement, layoutToUse: layout, osmConnection
});
// Various initial setup
userRelatedState.markLayoutAsVisited(layout);
if(layout?.lockLocation){
const bbox = new BBox(layout.lockLocation)
mapproperties.maxbounds.setData(bbox)
ShowDataLayer.showRange(
maplibremap,
new StaticFeatureSource([bbox.asGeoJson({})]),
featureSwitches.featureSwitchIsTesting
)
}
}
</script>
<div class="h-screen w-screen absolute top-0 left-0 border-3 border-red-500">
<MaplibreMap class="w-full h-full border border-black" map={maplibremap}></MaplibreMap>
</div>
<div class="absolute top-0 left-0">
<!-- Top-left elements -->
</div>
<div class="absolute bottom-0 left-0">
</div>
<div class="absolute bottom-0 right-0 mb-4 mr-4">
<If condition={mapproperties.allowMoving}>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z+1)}>
<ToSvelte class="w-7 h-7 block" construct={Svg.plus_ui}></ToSvelte>
</MapControlButton>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z-1)}>
<ToSvelte class="w-7 h-7 block" construct={Svg.min_ui}></ToSvelte>
</MapControlButton>
</If>
<If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton>
<ToSvelte construct={() => new GeolocationControl(geolocation, mapproperties).SetClass("block w-8 h-8")}></ToSvelte>
</MapControlButton>
</If>
</div>
<div class="absolute top-0 right-0">
</div>

View file

@ -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<T> extends Translation {
export class TypedTranslation<T extends Record<string, any>> extends Translation {
constructor(translations: Record<string, string>, context?: string) {
super(translations, context)
}

View file

@ -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"

View file

@ -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": "<div class='rounded-full text-xl font-bold' style='width: 2rem; height: 2rem; background: white'>{showCount}</div>",
"mappings": [
{
"if": "showCount>1000",
"then": "<div class='rounded-full text-xl font-bold flex flex-col' style='width: 2.5rem; height: 2.5rem; background: white'>{kilocount}K</div>"
}
]
},
"location": [
"point"
]
},
{
"color": {
"render": "#3c3",
"mappings": [
{
"if": "showCount>200",
"then": "#f33"
},
{
"if": "showCount>100",
"then": "#c93"
},
{
"if": "showCount>50",
"then": "#cc3"
}
]
},
"width": {
"render": "1"
}
}
]
}

View file

@ -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": [

View file

@ -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": [],

View file

@ -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",

View file

@ -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": {

View file

@ -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": [
{

View file

@ -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"
},

View file

@ -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": {

View file

@ -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": [
{

View file

@ -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.<tagrendering>"
},
"#dont-translate": "*",
"source": {
"osmTags": "id~*"
},
"source": "special:library",
"title": null,
"mapRendering": null,
"tagRenderings": [

View file

@ -1,11 +1,7 @@
{
"id": "import_candidate",
"description": "Layer used in the importHelper",
"source": {
"osmTags": {
"and": []
}
},
"source":"special",
"mapRendering": [
{
"location": [

View file

@ -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"
}
]
}
}
]
}

View file

@ -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": [

View file

@ -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"
}
]
}

View file

@ -2,10 +2,7 @@
"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}",

View file

@ -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(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? ''",

View file

@ -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%;
}

View file

@ -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,18 +33,9 @@ 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()
}
}
}
document.getElementById("decoration-desktop").remove()
new Combine([

View file

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,754 +0,0 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > 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;
}

95
test.ts
View file

@ -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<MlMap>(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<RasterLayerPolygon>(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<boolean>(true),
appliedFilters: new UIEventSource<Map<string, FilterState>>(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((_) => {})

View file

@ -4,7 +4,6 @@
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
<link href="./public/vendor/leaflet.css" rel="stylesheet"/>
<link href="./css/userbadge.css" rel="stylesheet"/>
<link href="./css/tabbedComponent.css" rel="stylesheet"/>
<link href="./css/mobile.css" rel="stylesheet"/>