forked from MapComplete/MapComplete
refactoring(maplibre): add pointRendering
This commit is contained in:
parent
4f2bbf4b54
commit
1b3609b13f
10 changed files with 316 additions and 122 deletions
|
@ -186,6 +186,14 @@ export class BBox {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toLngLat(): [[number, number], [number, number]] {
|
||||||
|
return [
|
||||||
|
[this.minLon, this.minLat],
|
||||||
|
[this.maxLon, this.maxLat],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public asGeoJson<T>(properties: T): Feature<Polygon, T> {
|
public asGeoJson<T>(properties: T): Feature<Polygon, T> {
|
||||||
return {
|
return {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import UserRelatedState from "./UserRelatedState"
|
import UserRelatedState from "./UserRelatedState"
|
||||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||||
import BaseLayer from "../../Models/BaseLayer"
|
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
import AvailableBaseLayers from "../Actors/AvailableBaseLayers"
|
|
||||||
import Attribution from "../../UI/BigComponents/Attribution"
|
import Attribution from "../../UI/BigComponents/Attribution"
|
||||||
import Minimap, { MinimapObj } from "../../UI/Base/Minimap"
|
import Minimap, { MinimapObj } from "../../UI/Base/Minimap"
|
||||||
import { Tiles } from "../../Models/TileRange"
|
import { Tiles } from "../../Models/TileRange"
|
||||||
|
@ -43,10 +41,6 @@ export default class MapState extends UserRelatedState {
|
||||||
The leaflet instance of the big basemap
|
The leaflet instance of the big basemap
|
||||||
*/
|
*/
|
||||||
public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap")
|
public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap")
|
||||||
/**
|
|
||||||
* A list of currently available background layers
|
|
||||||
*/
|
|
||||||
public availableBackgroundLayers: Store<BaseLayer[]>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current background layer
|
* The current background layer
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { Feature, Polygon } from "geojson"
|
||||||
import * as editorlayerindex from "../assets/editor-layer-index.json"
|
import * as editorlayerindex from "../assets/editor-layer-index.json"
|
||||||
import * as globallayers from "../assets/global-raster-layers.json"
|
import * as globallayers from "../assets/global-raster-layers.json"
|
||||||
import { BBox } from "../Logic/BBox"
|
import { BBox } from "../Logic/BBox"
|
||||||
|
import { Store, Stores } from "../Logic/UIEventSource"
|
||||||
|
import { GeoOperations } from "../Logic/GeoOperations"
|
||||||
|
|
||||||
export class AvailableRasterLayers {
|
export class AvailableRasterLayers {
|
||||||
public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> &
|
public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> &
|
||||||
|
@ -33,6 +35,35 @@ export class AvailableRasterLayers {
|
||||||
properties: AvailableRasterLayers.osmCartoProperties,
|
properties: AvailableRasterLayers.osmCartoProperties,
|
||||||
geometry: BBox.global.asGeometry(),
|
geometry: BBox.global.asGeometry(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static layersAvailableAt(
|
||||||
|
location: Store<{ lon: number; lat: number }>
|
||||||
|
): Store<RasterLayerPolygon[]> {
|
||||||
|
const availableLayersBboxes = Stores.ListStabilized(
|
||||||
|
location.mapD((loc) => {
|
||||||
|
const lonlat: [number, number] = [loc.lon, loc.lat]
|
||||||
|
return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) =>
|
||||||
|
BBox.get(eliPolygon).contains(lonlat)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const available = Stores.ListStabilized(
|
||||||
|
availableLayersBboxes.map((eliPolygons) => {
|
||||||
|
const loc = location.data
|
||||||
|
const lonlat: [number, number] = [loc.lon, loc.lat]
|
||||||
|
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
|
||||||
|
if (eliPolygon.geometry === null) {
|
||||||
|
return true // global ELI-layer
|
||||||
|
}
|
||||||
|
return GeoOperations.inside(lonlat, eliPolygon)
|
||||||
|
})
|
||||||
|
matching.unshift(AvailableRasterLayers.osmCarto)
|
||||||
|
matching.push(...AvailableRasterLayers.globalLayers)
|
||||||
|
return matching
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return available
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RasterLayerUtils {
|
export class RasterLayerUtils {
|
||||||
|
|
|
@ -5,12 +5,13 @@ import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg"
|
||||||
import WithContextLoader from "./WithContextLoader"
|
import WithContextLoader from "./WithContextLoader"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
import BaseUIElement from "../../UI/BaseUIElement"
|
import BaseUIElement from "../../UI/BaseUIElement"
|
||||||
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
|
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
|
||||||
import Img from "../../UI/Base/Img"
|
import Img from "../../UI/Base/Img"
|
||||||
import Combine from "../../UI/Base/Combine"
|
import Combine from "../../UI/Base/Combine"
|
||||||
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
|
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
|
||||||
|
import { OsmTags } from "../OsmFeature"
|
||||||
|
|
||||||
export default class PointRenderingConfig extends WithContextLoader {
|
export default class PointRenderingConfig extends WithContextLoader {
|
||||||
private static readonly allowed_location_codes = new Set<string>([
|
private static readonly allowed_location_codes = new Set<string>([
|
||||||
|
@ -164,7 +165,7 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin)
|
return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin)
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetSimpleIcon(tags: UIEventSource<any>): BaseUIElement {
|
public GetSimpleIcon(tags: Store<OsmTags>): BaseUIElement {
|
||||||
const self = this
|
const self = this
|
||||||
if (this.icon === undefined) {
|
if (this.icon === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -175,7 +176,7 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
public GenerateLeafletStyle(
|
public GenerateLeafletStyle(
|
||||||
tags: UIEventSource<any>,
|
tags: Store<OsmTags>,
|
||||||
clickable: boolean,
|
clickable: boolean,
|
||||||
options?: {
|
options?: {
|
||||||
noSize?: false | boolean
|
noSize?: false | boolean
|
||||||
|
@ -183,11 +184,7 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
}
|
}
|
||||||
): {
|
): {
|
||||||
html: BaseUIElement
|
html: BaseUIElement
|
||||||
iconSize: [number, number]
|
|
||||||
iconAnchor: [number, number]
|
iconAnchor: [number, number]
|
||||||
popupAnchor: [number, number]
|
|
||||||
iconUrl: string
|
|
||||||
className: string
|
|
||||||
} {
|
} {
|
||||||
function num(str, deflt = 40) {
|
function num(str, deflt = 40) {
|
||||||
const n = Number(str)
|
const n = Number(str)
|
||||||
|
@ -211,20 +208,21 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
let iconH = num(iconSize[1])
|
let iconH = num(iconSize[1])
|
||||||
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"
|
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"
|
||||||
|
|
||||||
let anchorW = iconW / 2
|
// 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 = iconH / 2
|
||||||
if (mode === "left") {
|
if (mode === "left") {
|
||||||
anchorW = 0
|
anchorW = -iconW / 2
|
||||||
}
|
}
|
||||||
if (mode === "right") {
|
if (mode === "right") {
|
||||||
anchorW = iconW
|
anchorW = iconW / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "top") {
|
if (mode === "top") {
|
||||||
anchorH = 0
|
anchorH = -iconH / 2
|
||||||
}
|
}
|
||||||
if (mode === "bottom") {
|
if (mode === "bottom") {
|
||||||
anchorH = iconH
|
anchorH = iconH / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = this.GetSimpleIcon(tags)
|
const icon = this.GetSimpleIcon(tags)
|
||||||
|
@ -264,15 +262,11 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
html: htmlEl,
|
html: htmlEl,
|
||||||
iconSize: [iconW, iconH],
|
|
||||||
iconAnchor: [anchorW, anchorH],
|
iconAnchor: [anchorW, anchorH],
|
||||||
popupAnchor: [0, 3 - anchorH],
|
|
||||||
iconUrl: undefined,
|
|
||||||
className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private GetBadges(tags: UIEventSource<any>): BaseUIElement {
|
private GetBadges(tags: Store<OsmTags>): BaseUIElement {
|
||||||
if (this.iconBadges.length === 0) {
|
if (this.iconBadges.length === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -304,7 +298,7 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
|
).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
|
||||||
}
|
}
|
||||||
|
|
||||||
private GetLabel(tags: UIEventSource<any>): BaseUIElement {
|
private GetLabel(tags: Store<OsmTags>): BaseUIElement {
|
||||||
if (this.label === undefined) {
|
if (this.label === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import Loc from "../../Models/Loc"
|
import Loc from "../../Models/Loc"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
import BaseLayer from "../../Models/BaseLayer"
|
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
import Hotkeys from "../Base/Hotkeys"
|
import Hotkeys from "../Base/Hotkeys"
|
||||||
|
|
|
@ -18,14 +18,12 @@ import { Unit } from "../../Models/Unit"
|
||||||
import { FixedInputElement } from "./FixedInputElement"
|
import { FixedInputElement } from "./FixedInputElement"
|
||||||
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"
|
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"
|
||||||
import Wikidata from "../../Logic/Web/Wikidata"
|
import Wikidata from "../../Logic/Web/Wikidata"
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
|
||||||
import Table from "../Base/Table"
|
import Table from "../Base/Table"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import Title from "../Base/Title"
|
import Title from "../Base/Title"
|
||||||
import InputElementMap from "./InputElementMap"
|
import InputElementMap from "./InputElementMap"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import { Translation } from "../i18n/Translation"
|
import { Translation } from "../i18n/Translation"
|
||||||
import BaseLayer from "../../Models/BaseLayer"
|
|
||||||
import Locale from "../i18n/Locale"
|
import Locale from "../i18n/Locale"
|
||||||
|
|
||||||
export class TextFieldDef {
|
export class TextFieldDef {
|
||||||
|
|
|
@ -1,65 +1,81 @@
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import type { Map as MLMap } from "maplibre-gl"
|
import type { Map as MLMap } from "maplibre-gl"
|
||||||
import {
|
import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers"
|
||||||
EditorLayerIndexProperties,
|
|
||||||
RasterLayerPolygon,
|
|
||||||
RasterLayerProperties,
|
|
||||||
} from "../../Models/RasterLayers"
|
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import Loc from "../../Models/Loc"
|
import { BBox } from "../../Logic/BBox"
|
||||||
|
|
||||||
export class MapLibreAdaptor {
|
export interface MapState {
|
||||||
|
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 {
|
||||||
private readonly _maplibreMap: Store<MLMap>
|
private readonly _maplibreMap: Store<MLMap>
|
||||||
private readonly _backgroundLayer?: Store<RasterLayerPolygon>
|
|
||||||
|
|
||||||
private _currentRasterLayer: string = undefined
|
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>
|
||||||
|
|
||||||
constructor(
|
/**
|
||||||
maplibreMap: Store<MLMap>,
|
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
||||||
state?: {
|
* @private
|
||||||
// availableBackgroundLayers: Store<BaseLayer[]>
|
*/
|
||||||
/**
|
private _currentRasterLayer: string
|
||||||
* The current background layer
|
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapState, "bounds">>) {
|
||||||
*/
|
|
||||||
readonly backgroundLayer?: Store<RasterLayerPolygon>
|
|
||||||
readonly locationControl?: UIEventSource<Loc>
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
this._maplibreMap = maplibreMap
|
this._maplibreMap = maplibreMap
|
||||||
this._backgroundLayer = state.backgroundLayer
|
|
||||||
|
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })
|
||||||
|
this.zoom = state?.zoom ?? new UIEventSource(1)
|
||||||
|
this._bounds = new UIEventSource(BBox.global)
|
||||||
|
this.bounds = this._bounds
|
||||||
|
this.rasterLayer =
|
||||||
|
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
this._backgroundLayer?.addCallback((_) => self.setBackground())
|
|
||||||
|
|
||||||
maplibreMap.addCallbackAndRunD((map) => {
|
maplibreMap.addCallbackAndRunD((map) => {
|
||||||
map.on("load", () => {
|
map.on("load", () => {
|
||||||
self.setBackground()
|
self.setBackground()
|
||||||
})
|
})
|
||||||
if (state.locationControl) {
|
self.MoveMapToCurrentLoc(this.location.data)
|
||||||
self.MoveMapToCurrentLoc(state.locationControl.data)
|
self.SetZoom(this.zoom.data)
|
||||||
map.on("moveend", () => {
|
map.on("moveend", () => {
|
||||||
const dt = state.locationControl.data
|
const dt = this.location.data
|
||||||
dt.lon = map.getCenter().lng
|
dt.lon = map.getCenter().lng
|
||||||
dt.lat = map.getCenter().lat
|
dt.lat = map.getCenter().lat
|
||||||
dt.zoom = map.getZoom()
|
this.location.ping()
|
||||||
state.locationControl.ping()
|
this.zoom.setData(map.getZoom())
|
||||||
})
|
})
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
state.locationControl.addCallbackAndRunD((loc) => {
|
this.rasterLayer.addCallback((_) =>
|
||||||
|
self.setBackground().catch((e) => {
|
||||||
|
console.error("Could not set background")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
this.location.addCallbackAndRunD((loc) => {
|
||||||
self.MoveMapToCurrentLoc(loc)
|
self.MoveMapToCurrentLoc(loc)
|
||||||
})
|
})
|
||||||
|
this.zoom.addCallbackAndRunD((z) => self.SetZoom(z))
|
||||||
}
|
}
|
||||||
|
private SetZoom(z: number) {
|
||||||
private MoveMapToCurrentLoc(loc: Loc) {
|
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
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined || loc === undefined) {
|
if (map === undefined || loc === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (map.getZoom() !== loc.zoom) {
|
|
||||||
map.setZoom(loc.zoom)
|
|
||||||
}
|
|
||||||
const center = map.getCenter()
|
const center = map.getCenter()
|
||||||
if (center.lng !== loc.lon || center.lat !== loc.lat) {
|
if (center.lng !== loc.lon || center.lat !== loc.lat) {
|
||||||
map.setCenter({ lng: loc.lon, lat: loc.lat })
|
map.setCenter({ lng: loc.lon, lat: loc.lat })
|
||||||
|
@ -120,14 +136,14 @@ export class MapLibreAdaptor {
|
||||||
if (map === undefined) {
|
if (map === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const background: RasterLayerProperties = this._backgroundLayer?.data?.properties
|
const background: RasterLayerProperties = this.rasterLayer?.data?.properties
|
||||||
if (background !== undefined && this._currentRasterLayer === background.id) {
|
if (background !== undefined && this._currentRasterLayer === background.id) {
|
||||||
// already the correct background layer, nothing to do
|
// already the correct background layer, nothing to do
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await this.awaitStyleIsLoaded()
|
await this.awaitStyleIsLoaded()
|
||||||
|
|
||||||
if (background !== this._backgroundLayer?.data?.properties) {
|
if (background !== this.rasterLayer?.data?.properties) {
|
||||||
// User selected another background in the meantime... abort
|
// User selected another background in the meantime... abort
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
108
UI/Map/ShowDataLayer.ts
Normal file
108
UI/Map/ShowDataLayer.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
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 { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
|
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
|
||||||
|
import { OsmFeature, OsmTags } from "../../Models/OsmFeature"
|
||||||
|
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
||||||
|
import { BBox } from "../../Logic/BBox"
|
||||||
|
|
||||||
|
class PointRenderingLayer {
|
||||||
|
private readonly _config: PointRenderingConfig
|
||||||
|
private readonly _fetchStore?: (id: string) => Store<OsmTags>
|
||||||
|
private readonly _map: MlMap
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
map: MlMap,
|
||||||
|
features: FeatureSource,
|
||||||
|
config: PointRenderingConfig,
|
||||||
|
fetchStore?: (id: string) => Store<OsmTags>
|
||||||
|
) {
|
||||||
|
this._config = config
|
||||||
|
this._map = map
|
||||||
|
this._fetchStore = fetchStore
|
||||||
|
const cache: Map<string, Marker> = new Map<string, Marker>()
|
||||||
|
const self = this
|
||||||
|
features.features.addCallbackAndRunD((features) => {
|
||||||
|
const unseenKeys = new Set(cache.keys())
|
||||||
|
for (const { feature } of features) {
|
||||||
|
const id = feature.properties.id
|
||||||
|
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)
|
||||||
|
cache.set(id, marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const unseenKey of unseenKeys) {
|
||||||
|
cache.get(unseenKey).remove()
|
||||||
|
cache.delete(unseenKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private addPoint(feature: OsmFeature): Marker {
|
||||||
|
let store: Store<OsmTags>
|
||||||
|
if (this._fetchStore) {
|
||||||
|
store = this._fetchStore(feature.properties.id)
|
||||||
|
} else {
|
||||||
|
store = new ImmutableStore(feature.properties)
|
||||||
|
}
|
||||||
|
const { html, iconAnchor } = this._config.GenerateLeafletStyle(store, true)
|
||||||
|
html.SetClass("marker")
|
||||||
|
const el = html.ConstructElement()
|
||||||
|
|
||||||
|
el.addEventListener("click", function () {
|
||||||
|
window.alert("Hello world!")
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Marker(el)
|
||||||
|
.setLngLat(GeoOperations.centerpointCoordinates(feature))
|
||||||
|
.setOffset(iconAnchor)
|
||||||
|
.addTo(this._map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShowDataLayer {
|
||||||
|
private readonly _map: Store<MlMap>
|
||||||
|
private _options: ShowDataLayerOptions & { layer: LayerConfig }
|
||||||
|
|
||||||
|
constructor(map: Store<MlMap>, options: ShowDataLayerOptions & { layer: LayerConfig }) {
|
||||||
|
this._map = map
|
||||||
|
this._options = options
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (this._options.zoomToFeatures) {
|
||||||
|
const features = this._options.features.features.data
|
||||||
|
const bbox = BBox.bboxAroundAll(features.map((f) => BBox.get(f.feature)))
|
||||||
|
map.fitBounds(bbox.toLngLat(), {
|
||||||
|
padding: { top: 10, bottom: 10, left: 10, right: 10 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,13 +3,35 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { ElementStorage } from "../../Logic/ElementStorage"
|
import { ElementStorage } from "../../Logic/ElementStorage"
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||||
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
|
|
||||||
export interface ShowDataLayerOptions {
|
export interface ShowDataLayerOptions {
|
||||||
|
/**
|
||||||
|
* Features to show
|
||||||
|
*/
|
||||||
features: FeatureSource
|
features: FeatureSource
|
||||||
|
/**
|
||||||
|
* Indication of the current selected element; overrides some filters
|
||||||
|
*/
|
||||||
selectedElement?: UIEventSource<any>
|
selectedElement?: UIEventSource<any>
|
||||||
leafletMap: Store<L.Map>
|
/**
|
||||||
popup?: undefined | ((tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen)
|
* What popup to build when a feature is selected
|
||||||
|
*/
|
||||||
|
buildPopup?:
|
||||||
|
| undefined
|
||||||
|
| ((tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set, zoom to the features when initially loaded and when they are changed
|
||||||
|
*/
|
||||||
zoomToFeatures?: false | boolean
|
zoomToFeatures?: false | boolean
|
||||||
doShowLayer?: Store<boolean>
|
/**
|
||||||
state?: { allElements?: ElementStorage }
|
* Toggles the layer on/off
|
||||||
|
*/
|
||||||
|
doShowLayer?: Store<true | boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function which fetches the relevant store
|
||||||
|
*/
|
||||||
|
fetchStore?: (id: string) => Store<OsmTags>
|
||||||
}
|
}
|
||||||
|
|
123
test.ts
123
test.ts
|
@ -1,24 +1,23 @@
|
||||||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||||
import MaplibreMap from "./UI/Map/MaplibreMap.svelte"
|
import MaplibreMap from "./UI/Map/MaplibreMap.svelte"
|
||||||
import { Store, Stores, UIEventSource } from "./Logic/UIEventSource"
|
import { ImmutableStore, UIEventSource } from "./Logic/UIEventSource"
|
||||||
import { MapLibreAdaptor } from "./UI/Map/MapLibreAdaptor"
|
import { MapLibreAdaptor } from "./UI/Map/MapLibreAdaptor"
|
||||||
import {
|
import { AvailableRasterLayers, RasterLayerPolygon } from "./Models/RasterLayers"
|
||||||
EditorLayerIndexProperties,
|
|
||||||
RasterLayerPolygon,
|
|
||||||
RasterLayerProperties,
|
|
||||||
} from "./Models/RasterLayers"
|
|
||||||
import type { Map as MlMap } from "maplibre-gl"
|
import type { Map as MlMap } from "maplibre-gl"
|
||||||
import { AvailableRasterLayers } from "./Models/RasterLayers"
|
|
||||||
import Loc from "./Models/Loc"
|
|
||||||
import { BBox } from "./Logic/BBox"
|
|
||||||
import { GeoOperations } from "./Logic/GeoOperations"
|
|
||||||
import RasterLayerPicker from "./UI/Map/RasterLayerPicker.svelte"
|
import RasterLayerPicker from "./UI/Map/RasterLayerPicker.svelte"
|
||||||
import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter"
|
import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter"
|
||||||
|
import { ShowDataLayer } from "./UI/Map/ShowDataLayer"
|
||||||
|
import StaticFeatureSource from "./Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||||
|
import { Layer } from "leaflet"
|
||||||
|
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 { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||||
async function main() {
|
async function main() {
|
||||||
const mlmap = new UIEventSource<MlMap>(undefined)
|
const mlmap = new UIEventSource<MlMap>(undefined)
|
||||||
const locationControl = new UIEventSource<Loc>({
|
const location = new UIEventSource<{ lon: number; lat: number }>({
|
||||||
zoom: 14,
|
|
||||||
lat: 51.1,
|
lat: 51.1,
|
||||||
lon: 3.1,
|
lon: 3.1,
|
||||||
})
|
})
|
||||||
|
@ -29,44 +28,70 @@ async function main() {
|
||||||
.SetStyle("height: 50vh; width: 90%; margin: 1%")
|
.SetStyle("height: 50vh; width: 90%; margin: 1%")
|
||||||
.AttachTo("maindiv")
|
.AttachTo("maindiv")
|
||||||
const bg = new UIEventSource<RasterLayerPolygon>(undefined)
|
const bg = new UIEventSource<RasterLayerPolygon>(undefined)
|
||||||
new MapLibreAdaptor(mlmap, {
|
const mla = new MapLibreAdaptor(mlmap, {
|
||||||
backgroundLayer: bg,
|
rasterLayer: bg,
|
||||||
locationControl,
|
location,
|
||||||
})
|
})
|
||||||
|
|
||||||
const availableLayersBboxes = Stores.ListStabilized(
|
const features = new UIEventSource([
|
||||||
locationControl.mapD((loc) => {
|
{
|
||||||
const lonlat: [number, number] = [loc.lon, loc.lat]
|
feature: {
|
||||||
return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) =>
|
type: "Feature",
|
||||||
BBox.get(eliPolygon).contains(lonlat)
|
properties: {
|
||||||
)
|
hello: "world",
|
||||||
})
|
id: "" + 1,
|
||||||
)
|
},
|
||||||
const availableLayers: Store<RasterLayerPolygon[]> = Stores.ListStabilized(
|
geometry: {
|
||||||
availableLayersBboxes.map((eliPolygons) => {
|
type: "Point",
|
||||||
const loc = locationControl.data
|
coordinates: [3.1, 51.2],
|
||||||
const lonlat: [number, number] = [loc.lon, loc.lat]
|
},
|
||||||
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
|
},
|
||||||
if (eliPolygon.geometry === null) {
|
freshness: new Date(),
|
||||||
return true // global ELI-layer
|
},
|
||||||
}
|
])
|
||||||
return GeoOperations.inside(lonlat, eliPolygon)
|
const layer = new LayerConfig(bench)
|
||||||
})
|
const options = {
|
||||||
matching.unshift(AvailableRasterLayers.osmCarto)
|
zoomToFeatures: false,
|
||||||
matching.push(...AvailableRasterLayers.globalLayers)
|
features: new SimpleFeatureSource(
|
||||||
return matching
|
{
|
||||||
})
|
layerDef: layer,
|
||||||
)
|
isDisplayed: new UIEventSource<boolean>(true),
|
||||||
|
appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined),
|
||||||
availableLayers.map((a) =>
|
},
|
||||||
console.log(
|
0,
|
||||||
"Availabe layers at current location:",
|
features
|
||||||
a.map((al) => al.properties.id)
|
),
|
||||||
)
|
layer,
|
||||||
)
|
}
|
||||||
|
new ShowDataLayer(mlmap, options)
|
||||||
new BackgroundLayerResetter(bg, availableLayers)
|
mla.zoom.set(9)
|
||||||
new SvelteUIElement(RasterLayerPicker, { availableLayers, value: bg }).AttachTo("extradiv")
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
main().then((_) => {})
|
main().then((_) => {})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue