refactoring(maplibre): WIP
This commit is contained in:
parent
231d67361e
commit
4d48b1cf2b
89 changed files with 1166 additions and 3973 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
64
Logic/Actors/InitialMapPositioning.ts
Normal file
64
Logic/Actors/InitialMapPositioning.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
)
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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++) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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[] = []
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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=" +
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
public markLayoutAsVisited(layout: LayoutConfig) {
|
||||
if (!layout) {
|
||||
console.error("Trying to mark a layout as visited, but ", layout, " got passed")
|
||||
return
|
||||
}
|
||||
if (layout.hideFromOverview) {
|
||||
this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
this.osmConnection
|
||||
.GetPreference("hidden-theme-" + layout?.id + "-enabled")
|
||||
.setData("true")
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!layout.official) {
|
||||
this.osmConnection.GetLongPreference("unofficial-theme-" + layout.id).setData(
|
||||
JSON.stringify({
|
||||
id: layout.id,
|
||||
icon: layout.icon,
|
||||
title: layout.title.translations,
|
||||
shortDescription: layout.shortDescription.translations,
|
||||
definition: layout["definition"],
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private InitializeLanguage(availableLanguages?: string[]) {
|
||||
Locale.language.syncWith(this.osmConnection.GetPreference("language"))
|
||||
Locale.language.addCallback((currentLanguage) => {
|
||||
if (layoutToUse === undefined) {
|
||||
return
|
||||
}
|
||||
if (Locale.showLinkToWeblate.data) {
|
||||
return true // Disable auto switching as we are in translators mode
|
||||
}
|
||||
if (this.layoutToUse.language.indexOf(currentLanguage) < 0) {
|
||||
if (availableLanguages?.indexOf(currentLanguage) < 0) {
|
||||
console.log(
|
||||
"Resetting language to",
|
||||
layoutToUse.language[0],
|
||||
availableLanguages[0],
|
||||
"as",
|
||||
currentLanguage,
|
||||
" is unsupported"
|
||||
)
|
||||
// The current language is not supported -> switch to a supported one
|
||||
Locale.language.setData(layoutToUse.language[0])
|
||||
Locale.language.setData(availableLanguages[0])
|
||||
}
|
||||
})
|
||||
Locale.language.ping()
|
||||
|
@ -193,4 +179,43 @@ export default class UserRelatedState extends ElementsState {
|
|||
.map((k) => k.substring(prefix.length, k.length - postfix.length))
|
||||
)
|
||||
}
|
||||
|
||||
private initHomeLocation(): FeatureSource {
|
||||
const empty = []
|
||||
const feature = Stores.ListStabilized(
|
||||
this.osmConnection.userDetails.map((userDetails) => {
|
||||
if (userDetails === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const home = userDetails.home
|
||||
if (home === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return [home.lon, home.lat]
|
||||
})
|
||||
).map((homeLonLat) => {
|
||||
if (homeLonLat === undefined) {
|
||||
return empty
|
||||
}
|
||||
return [
|
||||
{
|
||||
feature: {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "home",
|
||||
"user:home": "yes",
|
||||
_lon: homeLonLat[0],
|
||||
_lat: homeLonLat[1],
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: homeLonLat,
|
||||
},
|
||||
},
|
||||
freshness: new Date(),
|
||||
},
|
||||
]
|
||||
})
|
||||
return new StaticFeatureSource(feature)
|
||||
}
|
||||
}
|
||||
|
|
13
Models/GlobalFilter.ts
Normal file
13
Models/GlobalFilter.ts
Normal 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[]
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export default interface LeafletMap {
|
||||
getBounds(): [[number, number], [number, number]]
|
||||
}
|
14
Models/MapProperties.ts
Normal file
14
Models/MapProperties.ts
Normal 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>
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
@ -430,7 +430,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
constructor(state: DesugaringContext) {
|
||||
super(
|
||||
`If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)
|
||||
|
||||
|
||||
Note that these layers are added _at the start_ of the layer list, meaning that they will see _every_ feature.
|
||||
Furthermore, \`passAllFeatures\` will be set, so that they won't steal away features from further layers.
|
||||
Some layers (e.g. \`all_buildings_and_walls\' or \'streets_with_a_name\') are invisible, so by default, \'force_load\' is set too.
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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}]`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)[]
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
**/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
14
UI/Base/If.svelte
Normal 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}
|
13
UI/Base/MapControlButton.svelte
Normal file
13
UI/Base/MapControlButton.svelte
Normal 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>
|
|
@ -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()"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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, ...
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox))
|
||||
this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving))
|
||||
}
|
||||
private SetZoom(z: number) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined || z === undefined) {
|
||||
return
|
||||
}
|
||||
if (map.getZoom() !== z) {
|
||||
map.setZoom(z)
|
||||
}
|
||||
}
|
||||
private MoveMapToCurrentLoc(loc: { lat: number; lon: number }) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined || loc === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const center = map.getCenter()
|
||||
if (center.lng !== loc.lon || center.lat !== loc.lat) {
|
||||
map.setCenter({ lng: loc.lon, lat: loc.lat })
|
||||
/**
|
||||
* Convenience constructor
|
||||
*/
|
||||
public static construct(): {
|
||||
map: Store<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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
const unseenKeys = new Set(cache.keys())
|
||||
for (const { feature } of features) {
|
||||
const id = feature.properties.id
|
||||
|
||||
features.features.addCallbackAndRunD((features) => self.updateFeatures(features))
|
||||
visibility?.addCallbackAndRunD((visible) => self.setVisibility(visible))
|
||||
}
|
||||
|
||||
private updateFeatures(features: Feature[]) {
|
||||
const cache = this._allMarkers
|
||||
const unseenKeys = new Set(cache.keys())
|
||||
for (const location of this._config.location) {
|
||||
for (const feature of features) {
|
||||
const loc = GeoOperations.featureToCoordinateWithRenderingType(
|
||||
<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)
|
||||
}
|
||||
})
|
||||
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()
|
||||
|
||||
el.addEventListener("click", function () {
|
||||
window.alert("Hello world!")
|
||||
})
|
||||
if (this._onClick) {
|
||||
const self = this
|
||||
el.addEventListener("click", function () {
|
||||
self._onClick(feature.properties.id)
|
||||
})
|
||||
}
|
||||
|
||||
return new Marker(el)
|
||||
.setLngLat(GeoOperations.centerpointCoordinates(feature))
|
||||
.setOffset(iconAnchor)
|
||||
.addTo(this._map)
|
||||
return new Marker(el).setLngLat(loc).setOffset(iconAnchor).addTo(this._map)
|
||||
}
|
||||
}
|
||||
|
||||
export class ShowDataLayer {
|
||||
class LineRenderingLayer {
|
||||
/**
|
||||
* These are dynamic properties
|
||||
* @private
|
||||
*/
|
||||
private static readonly lineConfigKeys = [
|
||||
"color",
|
||||
"width",
|
||||
"lineCap",
|
||||
"offset",
|
||||
"fill",
|
||||
"fillColor",
|
||||
]
|
||||
private readonly _map: MlMap
|
||||
private readonly _config: LineRenderingConfig
|
||||
private readonly _visibility?: Store<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"],
|
||||
},
|
||||
})
|
||||
|
||||
/*[
|
||||
"color",
|
||||
"width",
|
||||
"dashArray",
|
||||
"lineCap",
|
||||
"offset",
|
||||
"fill",
|
||||
"fillColor",
|
||||
]*/
|
||||
map.addLayer({
|
||||
source: this._layername,
|
||||
id: this._layername + "_polygon",
|
||||
type: "fill",
|
||||
filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]],
|
||||
layout: {},
|
||||
paint: {
|
||||
"fill-color": ["feature-state", "fillColor"],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default class ShowDataLayer {
|
||||
private readonly _map: Store<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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,5 +33,5 @@ export interface ShowDataLayerOptions {
|
|||
/**
|
||||
* Function which fetches the relevant store
|
||||
*/
|
||||
fetchStore?: (id: string) => Store<OsmTags>
|
||||
fetchStore?: (id: string) => UIEventSource<OsmTags>
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -13,13 +13,13 @@ import Toggle from "../Input/Toggle"
|
|||
import SimpleAddUI, { PresetInfo } from "../BigComponents/SimpleAddUI"
|
||||
import Img from "../Base/Img"
|
||||
import Title from "../Base/Title"
|
||||
import { GlobalFilter } from "../../Logic/State/MapState"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { WayId } from "../../Models/OsmFeature"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { Feature } from "geojson";
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers";
|
||||
import { Feature } from "geojson"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { GlobalFilter } from "../../Logic/State/GlobalFilter"
|
||||
|
||||
export default class ConfirmLocationOfPoint extends Combine {
|
||||
constructor(
|
||||
|
@ -69,7 +69,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
let snapToFeatures: UIEventSource<Feature[]> = undefined
|
||||
let mapBounds: UIEventSource<BBox> = undefined
|
||||
if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) {
|
||||
snapToFeatures = new UIEventSource< Feature[]>([])
|
||||
snapToFeatures = new UIEventSource<Feature[]>([])
|
||||
mapBounds = new UIEventSource<BBox>(undefined)
|
||||
}
|
||||
|
||||
|
@ -110,9 +110,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
console.log("Snapping to", layerId)
|
||||
state.featurePipeline
|
||||
.GetFeaturesWithin(layerId, bbox)
|
||||
?.forEach((feats) =>
|
||||
allFeatures.push(...<any[]>feats)
|
||||
)
|
||||
?.forEach((feats) => allFeatures.push(...(<any[]>feats)))
|
||||
})
|
||||
console.log("Snapping to", allFeatures)
|
||||
snapToFeatures.setData(allFeatures)
|
||||
|
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
114
UI/ThemeViewGUI.svelte
Normal 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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -2,14 +2,7 @@
|
|||
"id": "conflation",
|
||||
"description": "If the import-button moves OSM points, the imported way points or conflates, a preview is shown. This layer defines how this preview is rendered. This layer cannot be included in a theme.",
|
||||
"minzoom": 1,
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"move=yes",
|
||||
"newpoint=yes"
|
||||
]
|
||||
}
|
||||
},
|
||||
"source": "special",
|
||||
"name": "Conflation",
|
||||
"title": "Conflation",
|
||||
"mapRendering": [
|
||||
|
@ -86,4 +79,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
{
|
||||
"id": "current_view",
|
||||
"description": "A meta-layer which contains one single feature, namely the BBOX of the current map view. This can be used to trigger special actions. If a popup is defined for this layer, this popup will be accessible via an extra button on screen.\n\nThe icon on the button is the default icon of the layer, but can be customized by detecting 'button=yes'.",
|
||||
"source": {
|
||||
"osmTags": "current_view=yes",
|
||||
"maxCacheAge": 0
|
||||
},
|
||||
"source": "special",
|
||||
"shownByDefault": false,
|
||||
"title": "Current View",
|
||||
"tagRenderings": [],
|
||||
|
@ -13,4 +10,4 @@
|
|||
"color": "#cccc0088"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
"id": "gps_location",
|
||||
"description": "Meta layer showing the current location of the user. Add this to your theme and override the icon to change the appearance of the current location. The object will always have `id=gps` and will have _all_ the properties included in the [`Coordinates`-object](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates) returned by the browser.",
|
||||
"minzoom": 0,
|
||||
"source": {
|
||||
"osmTags": "id=gps",
|
||||
"maxCacheAge": 0
|
||||
},
|
||||
"source": "special",
|
||||
"mapRendering": [
|
||||
{
|
||||
"icon": {
|
||||
|
@ -38,4 +35,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,7 @@
|
|||
"description": "Meta layer which contains the previous locations of the user as single points. This is mainly for technical reasons, e.g. to keep match the distance to the modified object",
|
||||
"minzoom": 1,
|
||||
"name": null,
|
||||
"source": {
|
||||
"osmTags": "user:location=yes",
|
||||
"#": "Cache is disabled here as these points are kept seperately",
|
||||
"maxCacheAge": 0
|
||||
},
|
||||
"source": "special",
|
||||
"shownByDefault": false,
|
||||
"mapRendering": [
|
||||
{
|
||||
|
@ -19,4 +15,4 @@
|
|||
"iconSize": "5,5,center"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
"id": "home_location",
|
||||
"description": "Meta layer showing the home location of the user. The home location can be set in the [profile settings](https://www.openstreetmap.org/profile/edit) of OpenStreetMap.",
|
||||
"minzoom": 0,
|
||||
"source": {
|
||||
"osmTags": "user:home=yes",
|
||||
"maxCacheAge": 0
|
||||
},
|
||||
"source":"special",
|
||||
"mapRendering": [
|
||||
{
|
||||
"icon": {
|
||||
|
@ -20,4 +17,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
"description": {
|
||||
"en": "A layer acting as library for icon-tagrenderings, especially to show as badge next to a POI"
|
||||
},
|
||||
"source": {
|
||||
"osmTags": "id~*"
|
||||
},
|
||||
"source":"special:library",
|
||||
"title": null,
|
||||
"tagRenderings": [
|
||||
{
|
||||
|
@ -127,4 +125,4 @@
|
|||
}
|
||||
],
|
||||
"mapRendering": null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
@ -20217,4 +20215,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
{
|
||||
"id": "import_candidate",
|
||||
"description": "Layer used in the importHelper",
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": []
|
||||
}
|
||||
},
|
||||
"source":"special",
|
||||
"mapRendering": [
|
||||
{
|
||||
"location": [
|
||||
|
@ -23,4 +19,4 @@
|
|||
"render": "{all_tags()}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
{
|
||||
"id": "matchpoint",
|
||||
"description": "The default rendering for a locationInput which snaps onto another object",
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": []
|
||||
}
|
||||
},
|
||||
"source":"special",
|
||||
"mapRendering": [
|
||||
{
|
||||
"location": [
|
||||
|
@ -15,4 +11,4 @@
|
|||
"icon": "./assets/svg/crosshair-empty.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
14
assets/layers/range/range.json
Normal file
14
assets/layers/range/range.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -2,12 +2,9 @@
|
|||
"id": "type_node",
|
||||
"description": "This is a priviliged meta_layer which exports _every_ point in OSM. This only works if zoomed below the point that the full tile is loaded (and not loaded via Overpass). Note that this point will also contain a property `parent_ways` which contains all the ways this node is part of as a list. This is mainly used for extremely specialized themes, which do advanced conflations. Expert use only.",
|
||||
"minzoom": 18,
|
||||
"source": {
|
||||
"osmTags": "id~node/.*",
|
||||
"maxCacheAge": 0
|
||||
},
|
||||
"source": "special",
|
||||
"mapRendering": null,
|
||||
"name": "All OSM Nodes",
|
||||
"title": "OSM node {id}",
|
||||
"tagRendering": []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
"nl": "Een speciale lag die niet getoond wordt op de kaart, maar die de instellingen van de gebruiker weergeeft"
|
||||
},
|
||||
"title": null,
|
||||
"source": {
|
||||
"osmTags": "id~*"
|
||||
},
|
||||
"source": "special",
|
||||
"calculatedTags": [
|
||||
"_mastodon_candidate_md=feat.properties._description.match(/\\[[^\\]]*\\]\\((.*(mastodon|en.osm.town).*)\\).*/)?.at(1)",
|
||||
"_d=feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? ''",
|
||||
|
@ -320,4 +318,4 @@
|
|||
}
|
||||
],
|
||||
"mapRendering": null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
16
index.ts
16
index.ts
|
@ -1,6 +1,5 @@
|
|||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||
import Combine from "./UI/Base/Combine"
|
||||
import MinimapImplementation from "./UI/Base/MinimapImplementation"
|
||||
import { Utils } from "./Utils"
|
||||
import AllThemesGui from "./UI/AllThemesGui"
|
||||
import DetermineLayout from "./Logic/DetermineLayout"
|
||||
|
@ -9,11 +8,7 @@ import DefaultGUI from "./UI/DefaultGUI"
|
|||
import State from "./State"
|
||||
import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"
|
||||
import { DefaultGuiState } from "./UI/DefaultGuiState"
|
||||
import { QueryParameters } from "./Logic/Web/QueryParameters"
|
||||
import DashboardGui from "./UI/DashboardGui"
|
||||
|
||||
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts running from console
|
||||
MinimapImplementation.initialize()
|
||||
ShowOverlayLayerImplementation.Implement()
|
||||
// Miscelleanous
|
||||
Utils.DisableLongPresses()
|
||||
|
@ -38,16 +33,7 @@ class Init {
|
|||
// @ts-ignore
|
||||
window.mapcomplete_state = State.state
|
||||
|
||||
const mode = QueryParameters.GetQueryParameter(
|
||||
"mode",
|
||||
"map",
|
||||
"The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'"
|
||||
)
|
||||
if (mode.data === "dashboard") {
|
||||
new DashboardGui(State.state, guiState).setup()
|
||||
} else {
|
||||
new DefaultGUI(State.state, guiState).setup()
|
||||
}
|
||||
new DefaultGUI(State.state, guiState).setup()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
BIN
public/vendor/images/layers-2x.png
vendored
BIN
public/vendor/images/layers-2x.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
BIN
public/vendor/images/layers.png
vendored
BIN
public/vendor/images/layers.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 696 B |
BIN
public/vendor/images/marker-icon.png
vendored
BIN
public/vendor/images/marker-icon.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
754
public/vendor/leaflet.css
vendored
754
public/vendor/leaflet.css
vendored
|
@ -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
95
test.ts
|
@ -1,94 +1,15 @@
|
|||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||
import MaplibreMap from "./UI/Map/MaplibreMap.svelte"
|
||||
import { UIEventSource } from "./Logic/UIEventSource"
|
||||
import { MapLibreAdaptor } from "./UI/Map/MapLibreAdaptor"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "./Models/RasterLayers"
|
||||
import type { Map as MlMap } from "maplibre-gl"
|
||||
import { ShowDataLayer } from "./UI/Map/ShowDataLayer"
|
||||
import LayerConfig from "./Models/ThemeConfig/LayerConfig"
|
||||
import * as bench from "./assets/generated/layers/bench.json"
|
||||
import { Utils } from "./Utils"
|
||||
import SimpleFeatureSource from "./Logic/FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { FilterState } from "./Models/FilteredLayer"
|
||||
import ThemeViewGUI from "./UI/ThemeViewGUI.svelte"
|
||||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||
import { QueryParameters } from "./Logic/Web/QueryParameters"
|
||||
import { AllKnownLayoutsLazy } from "./Customizations/AllKnownLayouts"
|
||||
|
||||
async function main() {
|
||||
const mlmap = new UIEventSource<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((_) => {})
|
||||
|
|
|
@ -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"/>
|
||||
|
|
Loading…
Reference in a new issue