refactoring(maplibre): WIP

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

View file

@ -1,476 +0,0 @@
import BaseUIElement from "./BaseUIElement"
import Combine from "./Base/Combine"
import Svg from "../Svg"
import Title from "./Base/Title"
import Toggle from "./Input/Toggle"
import { SubtleButton } from "./Base/SubtleButton"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import ValidatedTextField from "./Input/ValidatedTextField"
import { Utils } from "../Utils"
import { UIEventSource } from "../Logic/UIEventSource"
import { VariableUiElement } from "./Base/VariableUIElement"
import { FixedUiElement } from "./Base/FixedUiElement"
import { Tiles } from "../Models/TileRange"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import { DropDown } from "./Input/DropDown"
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
import MinimapImplementation from "./Base/MinimapImplementation"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { BBox } from "../Logic/BBox"
import MapState from "../Logic/State/MapState"
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import FeatureSource from "../Logic/FeatureSource/FeatureSource"
import List from "./Base/List"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import { SubstitutedTranslation } from "./SubstitutedTranslation"
import { AutoAction } from "./Popup/AutoApplyButton"
import DynamicGeoJsonTileSource from "../Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource"
import themeOverview from "../assets/generated/theme_overview.json"
class AutomationPanel extends Combine {
private static readonly openChangeset = new UIEventSource<number>(undefined)
constructor(
layoutToUse: LayoutConfig,
indices: number[],
extraCommentText: UIEventSource<string>,
tagRenderingToAutomate: { layer: LayerConfig; tagRendering: TagRenderingConfig }
) {
const layerId = tagRenderingToAutomate.layer.id
const trId = tagRenderingToAutomate.tagRendering.id
const tileState = LocalStorageSource.GetParsed(
"automation-tile_state-" + layerId + "-" + trId,
{}
)
const logMessages = new UIEventSource<string[]>([])
if (indices === undefined) {
throw "No tiles loaded - can not automate"
}
const openChangeset = AutomationPanel.openChangeset
openChangeset.addCallbackAndRun((cs) =>
console.trace("Sync current open changeset to:", cs)
)
const nextTileToHandle = tileState.map((handledTiles) => {
for (const index of indices) {
if (handledTiles[index] !== undefined) {
// Already handled
continue
}
return index
}
return undefined
})
nextTileToHandle.addCallback((t) => console.warn("Next tile to handle is", t))
const neededTimes = new UIEventSource<number[]>([])
const automaton = new VariableUiElement(
nextTileToHandle.map((tileIndex) => {
if (tileIndex === undefined) {
return new FixedUiElement("All done!").SetClass("thanks")
}
console.warn("Triggered map on nextTileToHandle", tileIndex)
const start = new Date()
return AutomationPanel.TileHandler(
layoutToUse,
tileIndex,
layerId,
tagRenderingToAutomate.tagRendering,
extraCommentText,
(result, logMessage) => {
const end = new Date()
const timeNeeded = (end.getTime() - start.getTime()) / 1000
neededTimes.data.push(timeNeeded)
neededTimes.ping()
tileState.data[tileIndex] = result
tileState.ping()
if (logMessage !== undefined) {
logMessages.data.push(logMessage)
logMessages.ping()
}
}
)
})
)
const statistics = new VariableUiElement(
tileState.map((states) => {
let total = 0
const perResult = new Map<string, number>()
for (const key in states) {
total++
const result = states[key]
perResult.set(result, (perResult.get(result) ?? 0) + 1)
}
let sum = 0
neededTimes.data.forEach((v) => {
sum = sum + v
})
let timePerTile = sum / neededTimes.data.length
return new Combine([
"Handled " + total + "/" + indices.length + " tiles: ",
new List(
Array.from(perResult.keys()).map((key) => key + ": " + perResult.get(key))
),
"Handling one tile needs " +
Math.floor(timePerTile * 100) / 100 +
"s on average. Estimated time left: " +
Utils.toHumanTime((indices.length - total) * timePerTile),
]).SetClass("flex flex-col")
})
)
super([
statistics,
automaton,
new SubtleButton(undefined, "Clear fixed").onClick(() => {
const st = tileState.data
for (const tileIndex in st) {
if (st[tileIndex] === "fixed") {
delete st[tileIndex]
}
}
tileState.ping()
}),
new VariableUiElement(logMessages.map((logMessages) => new List(logMessages))),
])
this.SetClass("flex flex-col")
}
private static TileHandler(
layoutToUse: LayoutConfig,
tileIndex: number,
targetLayer: string,
targetAction: TagRenderingConfig,
extraCommentText: UIEventSource<string>,
whenDone: (result: string, logMessage?: string) => void
): BaseUIElement {
const state = new MapState(layoutToUse, { attemptLogin: false })
extraCommentText.syncWith(state.changes.extraComment)
const [z, x, y] = Tiles.tile_from_index(tileIndex)
state.locationControl.setData({
zoom: z,
lon: x,
lat: y,
})
state.currentBounds.setData(BBox.fromTileIndex(tileIndex))
let targetTiles: UIEventSource<FeatureSource[]> = new UIEventSource<FeatureSource[]>([])
const pipeline = new FeaturePipeline((tile) => {
const layerId = tile.layer.layerDef.id
if (layerId === targetLayer) {
targetTiles.data.push(tile)
targetTiles.ping()
}
}, state)
state.locationControl.ping()
state.currentBounds.ping()
const stateToShow = new UIEventSource("")
pipeline.runningQuery.map(
async (isRunning) => {
if (targetTiles.data.length === 0) {
stateToShow.setData("No data loaded yet...")
return
}
if (isRunning) {
stateToShow.setData(
"Waiting for all layers to be loaded... Has " +
targetTiles.data.length +
" tiles already"
)
return
}
if (targetTiles.data.length === 0) {
stateToShow.setData("No features found to apply the action")
whenDone("empty")
return true
}
stateToShow.setData("Gathering applicable elements")
let handled = 0
let inspected = 0
let log = []
for (const targetTile of targetTiles.data) {
for (const ffs of targetTile.features.data) {
inspected++
if (inspected % 10 === 0) {
stateToShow.setData(
"Inspected " +
inspected +
" features, updated " +
handled +
" features"
)
}
const feature = ffs.feature
const renderingTr = targetAction.GetRenderValue(feature.properties)
const rendering = renderingTr.txt
log.push(
"<a href='https://openstreetmap.org/" +
feature.properties.id +
"' target='_blank'>" +
feature.properties.id +
"</a>: " +
new SubstitutedTranslation(
renderingTr,
new UIEventSource<any>(feature.properties),
undefined
).ConstructElement().textContent
)
const actions = Utils.NoNull(
SubstitutedTranslation.ExtractSpecialComponents(rendering).map(
(obj) => obj.special
)
)
for (const action of actions) {
const auto = <AutoAction>action.func
if (auto.supportsAutoAction !== true) {
continue
}
await auto.applyActionOn(
{
layoutToUse: state.layoutToUse,
changes: state.changes,
},
state.allElements.getEventSourceById(feature.properties.id),
action.args
)
handled++
}
}
}
stateToShow.setData(
"Done! Inspected " + inspected + " features, updated " + handled + " features"
)
if (inspected === 0) {
whenDone("empty")
return true
}
if (handled === 0) {
whenDone("no-action", "Inspected " + inspected + " elements: " + log.join("; "))
} else {
state.osmConnection.AttemptLogin()
state.changes.flushChanges("handled tile automatically, time to flush!")
whenDone(
"fixed",
"Updated " +
handled +
" elements, inspected " +
inspected +
": " +
log.join("; ")
)
}
return true
},
[targetTiles]
)
return new Combine([
new Title("Performing action for tile " + tileIndex, 1),
new VariableUiElement(stateToShow),
]).SetClass("flex flex-col")
}
}
class AutomatonGui {
constructor() {
const osmConnection = new OsmConnection({
singlePage: false,
oauth_token: QueryParameters.GetQueryParameter("oauth_token", "OAuth token"),
})
new Combine([
new Combine([
Svg.robot_svg().SetClass("w-24 h-24 p-4 rounded-full subtle-background"),
new Combine([
new Title("MapComplete Automaton", 1),
"This page helps to automate certain tasks for a theme. Expert use only.",
]).SetClass("flex flex-col m-4"),
]).SetClass("flex"),
new Toggle(
AutomatonGui.GenerateMainPanel(),
new SubtleButton(Svg.osm_logo_svg(), "Login to get started").onClick(() =>
osmConnection.AttemptLogin()
),
osmConnection.isLoggedIn
),
])
.SetClass("block p-4")
.AttachTo("main")
}
private static GenerateMainPanel(): BaseUIElement {
const themeSelect = new DropDown<string>(
"Select a theme",
Array.from(themeOverview).map((l) => ({ value: l.id, shown: l.id }))
)
LocalStorageSource.Get("automation-theme-id", "missing_streets").syncWith(
themeSelect.GetValue()
)
const tilepath = ValidatedTextField.ForType("url").ConstructInputElement({
placeholder: "Specifiy the path of the overview",
inputStyle: "width: 100%",
})
tilepath.SetClass("w-full")
LocalStorageSource.Get("automation-tile_path").syncWith(tilepath.GetValue(), true)
let tilesToRunOver = tilepath.GetValue().bind((path) => {
if (path === undefined) {
return undefined
}
return UIEventSource.FromPromiseWithErr(Utils.downloadJsonCached(path, 1000 * 60 * 60))
})
const targetZoom = 14
const tilesPerIndex = tilesToRunOver.map((tiles) => {
if (tiles === undefined || tiles["error"] !== undefined) {
return undefined
}
let indexes: number[] = []
const tilesS = tiles["success"]
DynamicGeoJsonTileSource.RegisterWhitelist(tilepath.GetValue().data, tilesS)
const z = Number(tilesS["zoom"])
for (const key in tilesS) {
if (key === "zoom") {
continue
}
const x = Number(key)
const ys = tilesS[key]
indexes.push(...ys.map((y) => Tiles.tile_index(z, x, y)))
}
console.log("Got ", indexes.length, "indexes")
let rezoomed = new Set<number>()
for (const index of indexes) {
let [z, x, y] = Tiles.tile_from_index(index)
while (z > targetZoom) {
z--
x = Math.floor(x / 2)
y = Math.floor(y / 2)
}
rezoomed.add(Tiles.tile_index(z, x, y))
}
return Array.from(rezoomed)
})
const extraComment = ValidatedTextField.ForType("text").ConstructInputElement()
LocalStorageSource.Get("automaton-extra-comment").syncWith(extraComment.GetValue())
return new Combine([
themeSelect,
"Specify the path to a tile overview. This is a hosted .json of the format {x : [y0, y1, y2], x1: [y0, ...]} where x is a string and y are numbers",
tilepath,
"Add an extra comment:",
extraComment,
new VariableUiElement(
extraComment
.GetValue()
.map((c) => "Your comment is " + (c?.length ?? 0) + "/200 characters long")
).SetClass("subtle"),
new VariableUiElement(
tilesToRunOver.map((t) => {
if (t === undefined) {
return "No path given or still loading..."
}
if (t["error"] !== undefined) {
return new FixedUiElement("Invalid URL or data: " + t["error"]).SetClass(
"alert"
)
}
return new FixedUiElement(
"Loaded " + tilesPerIndex.data.length + " tiles to automated over"
).SetClass("thanks")
})
),
new VariableUiElement(
themeSelect
.GetValue()
.map((id) => AllKnownLayouts.allKnownLayouts.get(id))
.map(
(layoutToUse) => {
if (layoutToUse === undefined) {
return new FixedUiElement("Select a valid layout")
}
if (
tilesPerIndex.data === undefined ||
tilesPerIndex.data.length === 0
) {
return "No tiles given"
}
const automatableTagRenderings: {
layer: LayerConfig
tagRendering: TagRenderingConfig
}[] = []
for (const layer of layoutToUse.layers) {
for (const tagRendering of layer.tagRenderings) {
if (tagRendering.group === "auto") {
automatableTagRenderings.push({
layer,
tagRendering: tagRendering,
})
}
}
}
console.log("Automatable tag renderings:", automatableTagRenderings)
if (automatableTagRenderings.length === 0) {
return new FixedUiElement(
'This theme does not have any tagRendering with "group": "auto" set'
).SetClass("alert")
}
const pickAuto = new DropDown("Pick the action to automate", [
{
value: undefined,
shown: "Pick an option",
},
...automatableTagRenderings.map((config) => ({
shown: config.layer.id + " - " + config.tagRendering.id,
value: config,
})),
])
return new Combine([
pickAuto,
new VariableUiElement(
pickAuto
.GetValue()
.map((auto) =>
auto === undefined
? undefined
: new AutomationPanel(
layoutToUse,
tilesPerIndex.data,
extraComment.GetValue(),
auto
)
)
),
])
},
[tilesPerIndex]
)
).SetClass("flex flex-col"),
]).SetClass("flex flex-col")
}
}
MinimapImplementation.initialize()
new AutomatonGui()

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

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

View file

@ -0,0 +1,13 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
/**
* A round button with an icon and possible a small text, which hovers above the map
*/
const dispatch = createEventDispatcher()
</script>
<div on:click={e => dispatch("click", e)} class="subtle-background block rounded-full min-w-10 h-10 pointer-events-auto m-0.5 md:m-1 p-1">
<slot class="m-4"></slot>
</div>

View file

@ -1,47 +0,0 @@
import BaseUIElement from "../BaseUIElement"
import Loc from "../../Models/Loc"
import BaseLayer from "../../Models/BaseLayer"
import { UIEventSource } from "../../Logic/UIEventSource"
import { BBox } from "../../Logic/BBox"
export interface MinimapOptions {
background?: UIEventSource<BaseLayer>
location?: UIEventSource<Loc>
bounds?: UIEventSource<BBox>
allowMoving?: boolean
leafletOptions?: any
attribution?: BaseUIElement | boolean
onFullyLoaded?: (leaflet: L.Map) => void
leafletMap?: UIEventSource<any>
lastClickLocation?: UIEventSource<{ lat: number; lon: number }>
addLayerControl?: boolean | false
}
export interface MinimapObj {
readonly leafletMap: UIEventSource<any>
readonly location: UIEventSource<Loc>
readonly bounds: UIEventSource<BBox>
installBounds(factor: number | BBox, showRange?: boolean): void
TakeScreenshot(format): Promise<string>
TakeScreenshot(format: "image"): Promise<string>
TakeScreenshot(format: "blob"): Promise<Blob>
TakeScreenshot(format?: "image" | "blob"): Promise<string | Blob>
}
export default class Minimap {
/**
* A stub implementation. The actual implementation is injected later on, but only in the browser.
* importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it
*/
private constructor() {}
/**
* Construct a minimap
*/
public static createMiniMap: (options?: MinimapOptions) => BaseUIElement & MinimapObj = (_) => {
throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()"
}
}

View file

@ -1,422 +0,0 @@
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import Loc from "../../Models/Loc"
import BaseLayer from "../../Models/BaseLayer"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import * as L from "leaflet"
import { LeafletMouseEvent, Map } from "leaflet"
import Minimap, { MinimapObj, MinimapOptions } from "./Minimap"
import { BBox } from "../../Logic/BBox"
import "leaflet-polylineoffset"
import { SimpleMapScreenshoter } from "leaflet-simple-map-screenshoter"
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import ShowDataLayerImplementation from "../ShowDataLayer/ShowDataLayerImplementation"
import FilteredLayer from "../../Models/FilteredLayer"
import ScrollableFullScreen from "./ScrollableFullScreen"
import Constants from "../../Models/Constants"
import StrayClickHandler from "../../Logic/Actors/StrayClickHandler"
/**
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
* Shows the given uiToShow-element in the messagebox
*/
class StrayClickHandlerImplementation {
private _lastMarker
constructor(
state: {
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
selectedElement: UIEventSource<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
},
uiToShow: ScrollableFullScreen,
iconToShow: BaseUIElement
) {
const self = this
const leafletMap = state.leafletMap
state.filteredLayers.data.forEach((filteredLayer) => {
filteredLayer.isDisplayed.addCallback((isEnabled) => {
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
// This reclick might be at a location where a feature now appeared...
state.leafletMap.data.removeLayer(self._lastMarker)
}
})
})
state.LastClickLocation.addCallback(function (lastClick) {
if (self._lastMarker !== undefined) {
state.leafletMap.data?.removeLayer(self._lastMarker)
}
if (lastClick === undefined) {
return
}
state.selectedElement.setData(undefined)
const clickCoor: [number, number] = [lastClick.lat, lastClick.lon]
self._lastMarker = L.marker(clickCoor, {
icon: L.divIcon({
html: iconToShow.ConstructElement(),
iconSize: [50, 50],
iconAnchor: [25, 50],
popupAnchor: [0, -45],
}),
})
self._lastMarker.addTo(leafletMap.data)
self._lastMarker.on("click", () => {
if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) {
leafletMap.data.flyTo(
clickCoor,
Constants.userJourney.minZoomLevelToAddNewPoints
)
return
}
uiToShow.Activate()
})
})
state.selectedElement.addCallback(() => {
if (self._lastMarker !== undefined) {
leafletMap.data.removeLayer(self._lastMarker)
this._lastMarker = undefined
}
})
}
}
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
private static _nextId = 0
public readonly leafletMap: UIEventSource<Map>
public readonly location: UIEventSource<Loc>
public readonly bounds: UIEventSource<BBox> | undefined
private readonly _id: string
private readonly _background: UIEventSource<BaseLayer>
private _isInited = false
private _allowMoving: boolean
private readonly _leafletoptions: any
private readonly _onFullyLoaded: (leaflet: L.Map) => void
private readonly _attribution: BaseUIElement | boolean
private readonly _addLayerControl: boolean
private readonly _options: MinimapOptions
private constructor(options?: MinimapOptions) {
super()
options = options ?? {}
this._id = "minimap" + MinimapImplementation._nextId
MinimapImplementation._nextId++
this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
this._background =
options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
this.location = options?.location ?? new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
this.bounds = options?.bounds
this._allowMoving = options.allowMoving ?? true
this._leafletoptions = options.leafletOptions ?? {}
this._onFullyLoaded = options.onFullyLoaded
this._attribution = options.attribution
this._addLayerControl = options.addLayerControl ?? false
this._options = options
this.SetClass("relative")
}
public static initialize() {
Minimap.createMiniMap = (options) => new MinimapImplementation(options)
ShowDataLayer.actualContstructor = (options) => new ShowDataLayerImplementation(options)
StrayClickHandler.construct = (
state: {
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
selectedElement: UIEventSource<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
},
uiToShow: ScrollableFullScreen,
iconToShow: BaseUIElement
) => {
return new StrayClickHandlerImplementation(state, uiToShow, iconToShow)
}
}
public installBounds(factor: number | BBox, showRange?: boolean) {
this.leafletMap.addCallbackD((leaflet) => {
let bounds: { getEast(); getNorth(); getWest(); getSouth() }
if (typeof factor === "number") {
const lbounds = leaflet.getBounds().pad(factor)
leaflet.setMaxBounds(lbounds)
bounds = lbounds
} else {
// @ts-ignore
leaflet.setMaxBounds(factor.toLeaflet())
bounds = factor
}
if (showRange) {
const data = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "LineString",
coordinates: [
[bounds.getEast(), bounds.getNorth()],
[bounds.getWest(), bounds.getNorth()],
[bounds.getWest(), bounds.getSouth()],
[bounds.getEast(), bounds.getSouth()],
[bounds.getEast(), bounds.getNorth()],
],
},
},
],
}
// @ts-ignore
L.geoJSON(data, {
style: {
color: "#f44",
weight: 4,
opacity: 0.7,
},
}).addTo(leaflet)
}
})
}
Destroy() {
super.Destroy()
console.warn("Decomissioning minimap", this._id)
const mp = this.leafletMap.data
this.leafletMap.setData(null)
mp.off()
mp.remove()
}
/**
* Takes a screenshot of the current map
* @param format: image: give a base64 encoded png image;
* @constructor
*/
public async TakeScreenshot(): Promise<string>
public async TakeScreenshot(format: "image"): Promise<string>
public async TakeScreenshot(format: "blob"): Promise<Blob>
public async TakeScreenshot(format: "image" | "blob"): Promise<string | Blob>
public async TakeScreenshot(format: "image" | "blob" = "image"): Promise<string | Blob> {
console.log("Taking a screenshot...")
const screenshotter = new SimpleMapScreenshoter()
screenshotter.addTo(this.leafletMap.data)
const result = <any>await screenshotter.takeScreen(<any>format ?? "image")
if (format === "image" && typeof result === "string") {
return result
}
if (format === "blob" && result instanceof Blob) {
return result
}
throw "Something went wrong while creating the screenshot: " + result
}
protected InnerConstructElement(): HTMLElement {
const div = document.createElement("div")
div.id = this._id
div.style.height = "100%"
div.style.width = "100%"
div.style.minWidth = "40px"
div.style.minHeight = "40px"
div.style.position = "relative"
const wrapper = document.createElement("div")
wrapper.appendChild(div)
const self = this
// @ts-ignore
const resizeObserver = new ResizeObserver((_) => {
if (wrapper.clientHeight === 0 || wrapper.clientWidth === 0) {
return
}
if (
wrapper.offsetParent === null ||
window.getComputedStyle(wrapper).display === "none"
) {
// Not visible
return
}
try {
self.InitMap()
} catch (e) {
console.debug("Could not construct a minimap:", e)
}
try {
self.leafletMap?.data?.invalidateSize()
} catch (e) {
console.debug("Could not invalidate size of a minimap:", e)
}
})
resizeObserver.observe(div)
if (this._addLayerControl) {
const switcher = new BackgroundMapSwitch(
{
locationControl: this.location,
backgroundLayer: this._background,
},
this._background
).SetClass("top-0 right-0 z-above-map absolute")
wrapper.appendChild(switcher.ConstructElement())
}
return wrapper
}
private InitMap() {
if (this._constructedHtmlElement === undefined) {
// This element isn't initialized yet
return
}
if (document.getElementById(this._id) === null) {
// not yet attached, we probably got some other event
return
}
if (this._isInited) {
return
}
this._isInited = true
const location = this.location
const self = this
let currentLayer = this._background.data.layer()
let latLon = <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0]
if (isNaN(latLon[0]) || isNaN(latLon[1])) {
latLon = [0, 0]
}
const options = {
center: latLon,
zoom: location.data?.zoom ?? 2,
layers: [currentLayer],
zoomControl: false,
attributionControl: this._attribution !== undefined,
dragging: this._allowMoving,
scrollWheelZoom: this._allowMoving,
doubleClickZoom: this._allowMoving,
keyboard: this._allowMoving,
touchZoom: this._allowMoving,
// Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving,
fadeAnimation: this._allowMoving,
maxZoom: 21,
}
Utils.Merge(this._leafletoptions, options)
/*
* Somehow, the element gets '_leaflet_id' set on chrome.
* When attempting to init this leaflet map, it'll throw an exception and the map won't show up.
* Simply removing '_leaflet_id' fixes the issue.
* See https://github.com/pietervdvn/MapComplete/issues/726
* */
delete document.getElementById(this._id)["_leaflet_id"]
const map = L.map(this._id, options)
if (self._onFullyLoaded !== undefined) {
currentLayer.on("load", () => {
console.log("Fully loaded all tiles!")
self._onFullyLoaded(map)
})
}
// Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then
// We give a bit of leeway for people on the edges
// Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/
map.setMaxBounds([
[-100, -200],
[100, 200],
])
if (this._attribution !== undefined) {
if (this._attribution === true) {
map.attributionControl.setPrefix(false)
} else {
map.attributionControl.setPrefix("<span id='leaflet-attribution'></span>")
}
}
this._background.addCallbackAndRun((layer) => {
const newLayer = layer.layer()
if (currentLayer !== undefined) {
map.removeLayer(currentLayer)
}
currentLayer = newLayer
if (self._onFullyLoaded !== undefined) {
currentLayer.on("load", () => {
console.log("Fully loaded all tiles!")
self._onFullyLoaded(map)
})
}
map.addLayer(newLayer)
if (self._attribution !== true && self._attribution !== false) {
self._attribution?.AttachTo("leaflet-attribution")
}
})
let isRecursing = false
map.on("moveend", function () {
if (isRecursing) {
return
}
if (
map.getZoom() === location.data.zoom &&
map.getCenter().lat === location.data.lat &&
map.getCenter().lng === location.data.lon
) {
return
}
location.data.zoom = map.getZoom()
location.data.lat = map.getCenter().lat
location.data.lon = map.getCenter().lng
isRecursing = true
location.ping()
if (self.bounds !== undefined) {
self.bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
}
isRecursing = false // This is ugly, I know
})
location.addCallback((loc) => {
const mapLoc = map.getCenter()
const dlat = Math.abs(loc.lat - mapLoc[0])
const dlon = Math.abs(loc.lon - mapLoc[1])
if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) {
return
}
map.setView([loc.lat, loc.lon], loc.zoom)
})
if (self.bounds !== undefined) {
self.bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
}
if (this._options.lastClickLocation) {
const lastClickLocation = this._options.lastClickLocation
map.addEventListener("click", function (e: LeafletMouseEvent) {
if (e.originalEvent["dismissed"]) {
return
}
lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng })
})
map.on("contextmenu", function (e) {
// @ts-ignore
lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng })
map.setZoom(map.getZoom() + 1)
})
}
this.leafletMap.setData(map)
}
}

View file

@ -4,6 +4,7 @@ import { SvelteComponentTyped } from "svelte"
/**
* The SvelteUIComponent serves as a translating class which which wraps a SvelteElement into the BaseUIElement framework.
* Also see ToSvelte.svelte for the opposite conversion
*/
export default class SvelteUIElement<
Props extends Record<string, any> = any,

View file

@ -1,4 +1,4 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource";
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
@ -24,13 +24,13 @@ export default class AddNewMarker extends Combine {
for (const preset of filteredLayer.layerDef.presets) {
const tags = TagUtils.KVtoProperties(preset.tags)
const icon = layer.mapRendering[0]
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
.RenderIcon(new ImmutableStore<any>(tags), false)
.html.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;")
icons.push(icon)
if (last === undefined) {
last = layer.mapRendering[0]
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
.RenderIcon(new ImmutableStore<any>(tags), false)
.html.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;")
}

View file

@ -1,92 +0,0 @@
import Link from "../Base/Link"
import Svg from "../../Svg"
import Combine from "../Base/Combine"
import { UIEventSource } from "../../Logic/UIEventSource"
import UserDetails from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants"
import Loc from "../../Models/Loc"
import { VariableUiElement } from "../Base/VariableUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { BBox } from "../../Logic/BBox"
import { Utils } from "../../Utils"
import Translations from "../i18n/Translations"
/**
* The bottom right attribution panel in the leaflet map
*/
export default class Attribution extends Combine {
constructor(
location: UIEventSource<Loc>,
userDetails: UIEventSource<UserDetails>,
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>
) {
const mapComplete = new Link(
`Mapcomplete ${Constants.vNumber}`,
"https://github.com/pietervdvn/MapComplete",
true
)
const reportBug = new Link(
Svg.bug_ui().SetClass("small-image"),
"https://github.com/pietervdvn/MapComplete/issues",
true
)
const layoutId = layoutToUse?.id
const stats = new Link(
Svg.statistics_ui().SetClass("small-image"),
Utils.OsmChaLinkFor(31, layoutId),
true
)
const idLink = location.map(
(location) =>
`https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${
location?.lat ?? 0
}/${location?.lon ?? 0}`
)
const editHere = new Link(Svg.pencil_ui().SetClass("small-image"), idLink, true)
const mapillaryLink = location.map(
(location) =>
`https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${
location?.lon ?? 0
}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
)
const mapillary = new Link(
Svg.mapillary_black_ui().SetClass("small-image"),
mapillaryLink,
true
)
const mapDataByOsm = new Link(
Translations.t.general.attribution.mapDataByOsm,
"https://openstreetmap.org/copyright",
true
)
const editWithJosm = new VariableUiElement(
userDetails.map(
(userDetails) => {
if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
return undefined
}
const bounds: any = currentBounds.data
if (bounds === undefined) {
return undefined
}
const top = bounds.getNorth()
const bottom = bounds.getSouth()
const right = bounds.getEast()
const left = bounds.getWest()
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true)
},
[location, currentBounds]
)
)
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary, mapDataByOsm])
this.SetClass("flex")
}
}

View file

@ -1,25 +1,19 @@
import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc"
import Hotkeys from "../Base/Hotkeys"
import Translations from "../i18n/Translations"
import Constants from "../../Models/Constants"
import { MapProperties } from "../../Models/MapProperties"
/**
* Displays an icon depending on the state of the geolocation.
* Will set the 'lock' if clicked twice
*/
export class GeolocationControl extends VariableUiElement {
constructor(
geolocationHandler: GeoLocationHandler,
state: {
locationControl: UIEventSource<Loc>
currentBounds: UIEventSource<BBox>
}
) {
constructor(geolocationHandler: GeoLocationHandler, state: MapProperties) {
const lastClick = new UIEventSource<Date>(undefined)
lastClick.addCallbackD((date) => {
geolocationHandler.geolocationState.requestMoment.setData(date)
@ -48,7 +42,7 @@ export class GeolocationControl extends VariableUiElement {
if (permission === "denied") {
return Svg.location_refused_svg()
}
if (geolocationState.isLocked.data) {
if (!geolocationState.allowMoving.data) {
return Svg.location_locked_svg()
}
@ -77,7 +71,7 @@ export class GeolocationControl extends VariableUiElement {
},
[
geolocationState.currentGPSLocation,
geolocationState.isLocked,
geolocationState.allowMoving,
geolocationHandler.mapHasMoved,
lastClickWithinThreeSecs,
lastRequestWithinTimeout,
@ -95,9 +89,9 @@ export class GeolocationControl extends VariableUiElement {
await geolocationState.requestPermission()
}
if (geolocationState.isLocked.data === true) {
if (geolocationState.allowMoving.data === false) {
// Unlock
geolocationState.isLocked.setData(false)
geolocationState.allowMoving.setData(true)
return
}
@ -109,21 +103,17 @@ export class GeolocationControl extends VariableUiElement {
// A location _is_ known! Let's move to this location
const currentLocation = geolocationState.currentGPSLocation.data
const inBounds = state.currentBounds.data.contains([
const inBounds = state.bounds.data.contains([
currentLocation.longitude,
currentLocation.latitude,
])
geolocationHandler.MoveMapToCurrentLocation()
if (inBounds) {
const lc = state.locationControl.data
state.locationControl.setData({
...lc,
zoom: lc.zoom + 3,
})
state.zoom.update((z) => z + 3)
}
if (lastClickWithinThreeSecs.data) {
geolocationState.isLocked.setData(true)
geolocationState.allowMoving.setData(false)
lastClick.setData(undefined)
return
}

View file

@ -11,7 +11,6 @@ import BackgroundMapSwitch from "./BackgroundMapSwitch"
import Lazy from "../Base/Lazy"
import { VariableUiElement } from "../Base/VariableUIElement"
import FeatureInfoBox from "../Popup/FeatureInfoBox"
import CopyrightPanel from "./CopyrightPanel"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import Hotkeys from "../Base/Hotkeys"
import { DefaultGuiState } from "../DefaultGuiState"
@ -21,7 +20,7 @@ export default class LeftControls extends Combine {
const currentViewFL = state.currentView?.layer
const currentViewAction = new Toggle(
new Lazy(() => {
const feature: Store<any> = state.currentView.features.map((ffs) => ffs[0]?.feature)
const feature: Store<any> = state.currentView.features.map((ffs) => ffs[0])
const icon = new VariableUiElement(
feature.map((feature) => {
const defaultIcon = Svg.checkbox_empty_svg()

View file

@ -1,5 +1,5 @@
import FloorLevelInputElement from "../Input/FloorLevelInputElement"
import MapState, { GlobalFilter } from "../../Logic/State/MapState"
import MapState from "../../Logic/State/MapState"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import { RegexTag } from "../../Logic/Tags/RegexTag"
import { Or } from "../../Logic/Tags/Or"
@ -11,6 +11,7 @@ import { BBox } from "../../Logic/BBox"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import { Store } from "../../Logic/UIEventSource"
import { GlobalFilter } from "../../Logic/State/GlobalFilter"
/***
* The element responsible for the level input element and picking the right level, showing and hiding at the right time, ...

View file

@ -9,30 +9,9 @@ import LevelSelector from "./LevelSelector"
import { GeolocationControl } from "./GeolocationControl"
export default class RightControls extends Combine {
constructor(
state: MapState & { featurePipeline: FeaturePipeline },
geolocationHandler: GeoLocationHandler
) {
const geolocationButton = Toggle.If(state.featureSwitchGeolocation, () =>
new MapControlButton(new GeolocationControl(geolocationHandler, state), {
dontStyle: true,
}).SetClass("p-1")
)
const plus = new MapControlButton(Svg.plus_svg()).onClick(() => {
state.locationControl.data.zoom++
state.locationControl.ping()
})
const min = new MapControlButton(Svg.min_svg()).onClick(() => {
state.locationControl.data.zoom--
state.locationControl.ping()
})
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
const levelSelector = new LevelSelector(state)
super(
[levelSelector, plus, min, geolocationButton].map((el) => el.SetClass("m-0.5 md:m-1"))
)
super([levelSelector].map((el) => el.SetClass("m-0.5 md:m-1")))
this.SetClass("flex flex-col items-center")
}
}

View file

@ -1,4 +1,4 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Translation } from "../i18n/Translation"
import Svg from "../../Svg"
import { TextField } from "../Input/TextField"
@ -7,10 +7,15 @@ import Translations from "../i18n/Translations"
import Hash from "../../Logic/Web/Hash"
import Combine from "../Base/Combine"
import Locale from "../i18n/Locale"
import { BBox } from "../../Logic/BBox"
export default class SearchAndGo extends Combine {
private readonly _searchField: TextField
constructor(state: { leafletMap: UIEventSource<any>; selectedElement?: UIEventSource<any> }) {
constructor(state: {
leafletMap: UIEventSource<any>
selectedElement?: UIEventSource<any>
bounds?: Store<BBox>
}) {
const goButton = Svg.search_ui().SetClass("w-8 h-8 full-rounded border-black float-right")
const placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
@ -49,7 +54,7 @@ export default class SearchAndGo extends Combine {
searchField.GetValue().setData("")
placeholder.setData(Translations.t.general.search.searching)
try {
const result = await Geocoding.Search(searchString)
const result = await Geocoding.Search(searchString, state.bounds.data)
console.log("Search result", result)
if (result.length == 0) {

View file

@ -1,7 +1,7 @@
/**
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
*/
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import Svg from "../../Svg"
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
@ -22,13 +22,12 @@ import { Changes } from "../../Logic/Osm/Changes"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import { ElementStorage } from "../../Logic/ElementStorage"
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"
import BaseLayer from "../../Models/BaseLayer"
import Loading from "../Base/Loading"
import Hash from "../../Logic/Web/Hash"
import { GlobalFilter } from "../../Logic/State/MapState"
import { WayId } from "../../Models/OsmFeature"
import { Tag } from "../../Logic/Tags/Tag"
import { LoginToggle } from "../Popup/LoginButton"
import { GlobalFilter } from "../../Models/GlobalFilter"
/*
* The SimpleAddUI is a single panel, which can have multiple states:
@ -288,7 +287,7 @@ export default class SimpleAddUI extends LoginToggle {
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
let icon: () => BaseUIElement = () =>
layer.layerDef.mapRendering[0]
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
.RenderIcon(new ImmutableStore<any>(tags), false)
.html.SetClass("w-12 h-12 block relative")
const presetInfo: PresetInfo = {
layerToAddTo: layer,

View file

@ -1,305 +0,0 @@
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
import { DefaultGuiState } from "./DefaultGuiState"
import { FixedUiElement } from "./Base/FixedUiElement"
import { Utils } from "../Utils"
import Combine from "./Base/Combine"
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import home_location_json from "../assets/layers/home_location/home_location.json"
import State from "../State"
import Title from "./Base/Title"
import { MinimapObj } from "./Base/Minimap"
import BaseUIElement from "./BaseUIElement"
import { VariableUiElement } from "./Base/VariableUIElement"
import { GeoOperations } from "../Logic/GeoOperations"
import { OsmFeature } from "../Models/OsmFeature"
import SearchAndGo from "./BigComponents/SearchAndGo"
import FeatureInfoBox from "./Popup/FeatureInfoBox"
import { UIEventSource } from "../Logic/UIEventSource"
import LanguagePicker from "./LanguagePicker"
import Lazy from "./Base/Lazy"
import TagRenderingAnswer from "./Popup/TagRenderingAnswer"
import Hash from "../Logic/Web/Hash"
import FilterView from "./BigComponents/FilterView"
import Translations from "./i18n/Translations"
import Constants from "../Models/Constants"
import SimpleAddUI from "./BigComponents/SimpleAddUI"
import BackToIndex from "./BigComponents/BackToIndex"
import StatisticsPanel from "./BigComponents/StatisticsPanel"
export default class DashboardGui {
private readonly state: FeaturePipelineState
private readonly currentView: UIEventSource<{
title: string | BaseUIElement
contents: string | BaseUIElement
}> = new UIEventSource(undefined)
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
this.state = state
}
private viewSelector(
shown: BaseUIElement,
title: string | BaseUIElement,
contents: string | BaseUIElement,
hash?: string
): BaseUIElement {
const currentView = this.currentView
const v = { title, contents }
shown.SetClass("pl-1 pr-1 rounded-md")
shown.onClick(() => {
currentView.setData(v)
})
Hash.hash.addCallbackAndRunD((h) => {
if (h === hash) {
currentView.setData(v)
}
})
currentView.addCallbackAndRunD((cv) => {
if (cv == v) {
shown.SetClass("bg-unsubtle")
Hash.hash.setData(hash)
} else {
shown.RemoveClass("bg-unsubtle")
}
})
return shown
}
private singleElementCache: Record<string, BaseUIElement> = {}
private singleElementView(
element: OsmFeature,
layer: LayerConfig,
distance: number
): BaseUIElement {
if (this.singleElementCache[element.properties.id] !== undefined) {
return this.singleElementCache[element.properties.id]
}
const tags = this.state.allElements.getEventSourceById(element.properties.id)
const title = new Combine([
new Title(new TagRenderingAnswer(tags, layer.title, this.state), 4),
distance < 900
? Math.floor(distance) + "m away"
: Utils.Round(distance / 1000) + "km away",
]).SetClass("flex justify-between")
return (this.singleElementCache[element.properties.id] = this.viewSelector(
title,
new Lazy(() => FeatureInfoBox.GenerateTitleBar(tags, layer, this.state)),
new Lazy(() => FeatureInfoBox.GenerateContent(tags, layer, this.state))
// element.properties.id
))
}
private mainElementsView(
elements: { element: OsmFeature; layer: LayerConfig; distance: number }[]
): BaseUIElement {
const self = this
if (elements === undefined) {
return new FixedUiElement("Initializing")
}
if (elements.length == 0) {
return new FixedUiElement("No elements in view")
}
return new Combine(
elements.map((e) => self.singleElementView(e.element, e.layer, e.distance))
)
}
private documentationButtonFor(layerConfig: LayerConfig): BaseUIElement {
return this.viewSelector(
Translations.W(layerConfig.name?.Clone() ?? layerConfig.id),
new Combine(["Documentation about ", layerConfig.name?.Clone() ?? layerConfig.id]),
layerConfig.GenerateDocumentation([]),
"documentation-" + layerConfig.id
)
}
private allDocumentationButtons(): BaseUIElement {
const layers = this.state.layoutToUse.layers
.filter((l) => Constants.priviliged_layers.indexOf(l.id) < 0)
.filter((l) => !l.id.startsWith("note_import_"))
if (layers.length === 1) {
return this.documentationButtonFor(layers[0])
}
return this.viewSelector(
new FixedUiElement("Documentation"),
"Documentation",
new Combine(layers.map((l) => this.documentationButtonFor(l).SetClass("flex flex-col")))
)
}
public setup(): void {
const state = this.state
if (this.state.layoutToUse.customCss !== undefined) {
if (window.location.pathname.indexOf("index") >= 0) {
Utils.LoadCustomCss(this.state.layoutToUse.customCss)
}
}
const map = this.SetupMap()
Utils.downloadJson("./service-worker-version")
.then((data) => console.log("Service worker", data))
.catch((_) => console.log("Service worker not active"))
document.getElementById("centermessage").classList.add("hidden")
const layers: Record<string, LayerConfig> = {}
for (const layer of state.layoutToUse.layers) {
layers[layer.id] = layer
}
const self = this
const elementsInview = new UIEventSource<
{
distance: number
center: [number, number]
element: OsmFeature
layer: LayerConfig
}[]
>([])
function update() {
const mapCenter = <[number, number]>[
self.state.locationControl.data.lon,
self.state.locationControl.data.lon,
]
const elements = self.state.featurePipeline
.getAllVisibleElementsWithmeta(self.state.currentBounds.data)
.map((el) => {
const distance = GeoOperations.distanceBetween(el.center, mapCenter)
return { ...el, distance }
})
elements.sort((e0, e1) => e0.distance - e1.distance)
elementsInview.setData(elements)
}
map.bounds.addCallbackAndRun(update)
state.featurePipeline.newDataLoadedSignal.addCallback(update)
state.filteredLayers.addCallbackAndRun((fls) => {
for (const fl of fls) {
fl.isDisplayed.addCallback(update)
fl.appliedFilters.addCallback(update)
}
})
const filterView = new Lazy(() => {
return new FilterView(state.filteredLayers, state.overlayToggles, state)
})
const welcome = new Combine([
state.layoutToUse.description,
state.layoutToUse.descriptionTail,
])
self.currentView.setData({ title: state.layoutToUse.title, contents: welcome })
const filterViewIsOpened = new UIEventSource(false)
filterViewIsOpened.addCallback((_) =>
self.currentView.setData({ title: "filters", contents: filterView })
)
const newPointIsShown = new UIEventSource(false)
const addNewPoint = new SimpleAddUI(
new UIEventSource(true),
new UIEventSource(undefined),
filterViewIsOpened,
state,
state.locationControl
)
const addNewPointTitle = "Add a missing point"
this.currentView.addCallbackAndRunD((cv) => {
newPointIsShown.setData(cv.contents === addNewPoint)
})
newPointIsShown.addCallbackAndRun((isShown) => {
if (isShown) {
if (self.currentView.data.contents !== addNewPoint) {
self.currentView.setData({ title: addNewPointTitle, contents: addNewPoint })
}
} else {
if (self.currentView.data.contents === addNewPoint) {
self.currentView.setData(undefined)
}
}
})
new Combine([
new Combine([
this.viewSelector(
new Title(state.layoutToUse.title.Clone(), 2),
state.layoutToUse.title.Clone(),
welcome,
"welcome"
),
map.SetClass("w-full h-64 shrink-0 rounded-lg"),
new SearchAndGo(state),
this.viewSelector(
new Title(
new VariableUiElement(
elementsInview.map(
(elements) => "There are " + elements?.length + " elements in view"
)
)
),
"Statistics",
new StatisticsPanel(elementsInview, this.state),
"statistics"
),
this.viewSelector(new FixedUiElement("Filter"), "Filters", filterView, "filters"),
this.viewSelector(
new Combine(["Add a missing point"]),
addNewPointTitle,
addNewPoint
),
new VariableUiElement(
elementsInview.map((elements) =>
this.mainElementsView(elements).SetClass("block m-2")
)
).SetClass(
"block shrink-2 overflow-x-auto h-full border-2 border-subtle rounded-lg"
),
this.allDocumentationButtons(),
new LanguagePicker(Object.keys(state.layoutToUse.title.translations)).SetClass(
"mt-2"
),
new BackToIndex(),
]).SetClass("w-1/2 lg:w-1/4 m-4 flex flex-col shrink-0 grow-0"),
new VariableUiElement(
this.currentView.map(({ title, contents }) => {
return new Combine([
new Title(Translations.W(title), 2).SetClass(
"shrink-0 border-b-4 border-subtle"
),
Translations.W(contents).SetClass("shrink-2 overflow-y-auto block"),
]).SetClass("flex flex-col h-full")
})
).SetClass(
"w-1/2 lg:w-3/4 m-4 p-2 border-2 border-subtle rounded-xl m-4 ml-0 mr-8 shrink-0 grow-0"
),
])
.SetClass("flex h-full")
.AttachTo("leafletDiv")
}
private SetupMap(): MinimapObj & BaseUIElement {
const state = this.state
new ShowDataLayer({
leafletMap: state.leafletMap,
layerToShow: new LayerConfig(home_location_json, "home_location", true),
features: state.homeLocation,
state,
})
state.leafletMap.addCallbackAndRunD((_) => {
// Lets assume that all showDataLayers are initialized at this point
state.selectedElement.ping()
State.state.locationControl.ping()
return true
})
return state.mainMapObject
}
}

View file

@ -171,9 +171,6 @@ export default class DefaultGUI {
const state = this.state
const guiState = this.guiState
// Attach the map
state.mainMapObject.SetClass("w-full h-full").AttachTo("leafletDiv")
this.setupClickDialogOnMap(guiState.filterViewIsOpened, state)
new ShowDataLayer({

View file

@ -138,12 +138,6 @@ export default class ConflationChecker
location,
background,
bounds: currentBounds,
attribution: new Attribution(
location,
state.osmConnection.userDetails,
undefined,
currentBounds
),
})
osmLiveData.SetClass("w-full").SetStyle("height: 500px")

View file

@ -9,10 +9,6 @@ import { DropDown } from "../Input/DropDown"
import { Utils } from "../../Utils"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Loc from "../../Models/Loc"
import Minimap from "../Base/Minimap"
import Attribution from "../BigComponents/Attribution"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import Toggle from "../Input/Toggle"
import { VariableUiElement } from "../Base/VariableUIElement"
@ -21,12 +17,14 @@ import { FlowStep } from "./FlowStep"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import Title from "../Base/Title"
import CheckBoxes from "../Input/Checkboxes"
import AllTagsPanel from "../AllTagsPanel.svelte"
import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
import { Feature, Point } from "geojson"
import DivContainer from "../Base/DivContainer"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import SvelteUIElement from "../Base/SvelteUIElement"
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import ShowDataLayer from "../Map/ShowDataLayer"
class PreviewPanel extends ScrollableFullScreen {
constructor(tags: UIEventSource<any>) {
@ -110,21 +108,11 @@ export class MapPreview
return matching
})
const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
const background = new UIEventSource<RasterLayerPolygon>(AvailableRasterLayers.osmCarto)
const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
const currentBounds = new UIEventSource<BBox>(undefined)
const map = Minimap.createMiniMap({
allowMoving: true,
location,
background,
bounds: currentBounds,
attribution: new Attribution(
location,
state.osmConnection.userDetails,
undefined,
currentBounds
),
})
const { ui, mapproperties, map } = MapLibreAdaptor.construct()
const layerControl = new BackgroundMapSwitch(
{
backgroundLayer: background,
@ -132,15 +120,14 @@ export class MapPreview
},
background
)
map.SetClass("w-full").SetStyle("height: 500px")
ui.SetClass("w-full").SetStyle("height: 500px")
layerPicker.GetValue().addCallbackAndRunD((layerToShow) => {
new ShowDataLayer({
layerToShow,
new ShowDataLayer(map, {
layer: layerToShow,
zoomToFeatures: true,
features: new StaticFeatureSource(matching),
leafletMap: map.leafletMap,
popup: (tag) => new PreviewPanel(tag),
buildPopup: (tag) => new PreviewPanel(tag),
})
})
@ -171,9 +158,8 @@ export class MapPreview
new Title(t.title, 1),
layerPicker,
new Toggle(t.autodetected.SetClass("thanks"), undefined, autodetected),
mismatchIndicator,
map,
ui,
new DivContainer("fullscreen"),
layerControl,
confirm,

View file

@ -1,34 +1,56 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Map as MLMap } from "maplibre-gl"
import { Map as MlMap } from "maplibre-gl"
import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers"
import { Utils } from "../../Utils"
import { BBox } from "../../Logic/BBox"
import { MapProperties } from "../../Models/MapProperties"
import SvelteUIElement from "../Base/SvelteUIElement"
import MaplibreMap from "./MaplibreMap.svelte"
import Constants from "../../Models/Constants"
export interface MapState {
/**
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
*/
export class MapLibreAdaptor implements MapProperties {
private static maplibre_control_handlers = [
"scrollZoom",
"boxZoom",
"dragRotate",
"dragPan",
"keyboard",
"doubleClickZoom",
"touchZoomRotate",
]
readonly location: UIEventSource<{ lon: number; lat: number }>
readonly zoom: UIEventSource<number>
readonly bounds: Store<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
}
export class MapLibreAdaptor implements MapState {
readonly maxbounds: UIEventSource<BBox | undefined>
readonly allowMoving: UIEventSource<true | boolean | undefined>
private readonly _maplibreMap: Store<MLMap>
readonly location: UIEventSource<{ lon: number; lat: number }>
readonly zoom: UIEventSource<number>
readonly bounds: Store<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
private readonly _bounds: UIEventSource<BBox>
/**
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
* @private
*/
private _currentRasterLayer: string
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapState, "bounds">>) {
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapProperties, "bounds">>) {
this._maplibreMap = maplibreMap
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })
this.zoom = state?.zoom ?? new UIEventSource(1)
this.zoom.addCallbackAndRunD((z) => {
if (z < 0) {
this.zoom.setData(0)
}
if (z > 24) {
this.zoom.setData(24)
}
})
this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined)
this.allowMoving = state?.allowMoving ?? new UIEventSource(true)
this._bounds = new UIEventSource(BBox.global)
this.bounds = this._bounds
this.rasterLayer =
@ -38,20 +60,26 @@ export class MapLibreAdaptor implements MapState {
maplibreMap.addCallbackAndRunD((map) => {
map.on("load", () => {
self.setBackground()
self.MoveMapToCurrentLoc(self.location.data)
self.SetZoom(self.zoom.data)
self.setMaxBounds(self.maxbounds.data)
self.setAllowMoving(self.allowMoving.data)
})
self.MoveMapToCurrentLoc(this.location.data)
self.SetZoom(this.zoom.data)
self.MoveMapToCurrentLoc(self.location.data)
self.SetZoom(self.zoom.data)
self.setMaxBounds(self.maxbounds.data)
self.setAllowMoving(self.allowMoving.data)
map.on("moveend", () => {
const dt = this.location.data
dt.lon = map.getCenter().lng
dt.lat = map.getCenter().lat
this.location.ping()
this.zoom.setData(map.getZoom())
this.zoom.setData(Math.round(map.getZoom() * 10) / 10)
})
})
this.rasterLayer.addCallback((_) =>
self.setBackground().catch((e) => {
self.setBackground().catch((_) => {
console.error("Could not set background")
})
)
@ -60,25 +88,25 @@ export class MapLibreAdaptor implements MapState {
self.MoveMapToCurrentLoc(loc)
})
this.zoom.addCallbackAndRunD((z) => self.SetZoom(z))
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()
}
}
}
}

View file

@ -8,8 +8,6 @@
import { Map } from "@onsvisual/svelte-maps";
import type { Map as MaplibreMap } from "maplibre-gl";
import type { Writable } from "svelte/store";
import type Loc from "../../Models/Loc";
import { UIEventSource } from "../../Logic/UIEventSource";
/**
@ -30,7 +28,6 @@
<main>
<Map bind:center={center}
bind:map={$map}
controls="true"
id="map" location={{lng: 0, lat: 0, zoom: 0}} maxzoom=24 style={styleUrl} />
</main>

View file

@ -1,108 +1,294 @@
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import type { Map as MlMap } from "maplibre-gl"
import { Marker } from "maplibre-gl"
import { ShowDataLayerOptions } from "../ShowDataLayer/ShowDataLayerOptions"
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
import { GeoOperations } from "../../Logic/GeoOperations"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
import { OsmFeature, OsmTags } from "../../Models/OsmFeature"
import { OsmTags } from "../../Models/OsmFeature"
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
import { BBox } from "../../Logic/BBox"
import { Feature, LineString } from "geojson"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
import { Utils } from "../../Utils"
import * as range_layer from "../../assets/layers/range/range.json"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
class PointRenderingLayer {
private readonly _config: PointRenderingConfig
private readonly _fetchStore?: (id: string) => Store<OsmTags>
private readonly _map: MlMap
private readonly _onClick: (id: string) => void
private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>()
constructor(
map: MlMap,
features: FeatureSource,
config: PointRenderingConfig,
fetchStore?: (id: string) => Store<OsmTags>
visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<OsmTags>,
onClick?: (id: string) => void
) {
this._config = config
this._map = map
this._fetchStore = fetchStore
const cache: Map<string, Marker> = new Map<string, Marker>()
this._onClick = onClick
const self = this
features.features.addCallbackAndRunD((features) => {
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))
}
}

View file

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

View file

@ -6,18 +6,21 @@ import ShowDataLayer from "./ShowDataLayer"
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteredLayer from "../../Models/FilteredLayer"
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
import { Map as MlMap } from "maplibre-gl"
export default class ShowDataMultiLayer {
constructor(options: ShowDataLayerOptions & { layers: Store<FilteredLayer[]> }) {
constructor(
map: Store<MlMap>,
options: ShowDataLayerOptions & { layers: Store<FilteredLayer[]> }
) {
new PerLayerFeatureSourceSplitter(
options.layers,
(perLayer) => {
const newOptions = {
...options,
layerToShow: perLayer.layer.layerDef,
layer: perLayer.layer.layerDef,
features: perLayer,
}
new ShowDataLayer(newOptions)
new ShowDataLayer(map, newOptions)
},
options.features
)

View file

@ -13,13 +13,13 @@ import Toggle from "../Input/Toggle"
import SimpleAddUI, { PresetInfo } from "../BigComponents/SimpleAddUI"
import Img from "../Base/Img"
import Title from "../Base/Title"
import { GlobalFilter } from "../../Logic/State/MapState"
import { VariableUiElement } from "../Base/VariableUIElement"
import { Tag } from "../../Logic/Tags/Tag"
import { WayId } from "../../Models/OsmFeature"
import { Translation } from "../i18n/Translation"
import { Feature } from "geojson";
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers";
import { Feature } from "geojson"
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
import { GlobalFilter } from "../../Logic/State/GlobalFilter"
export default class ConfirmLocationOfPoint extends Combine {
constructor(
@ -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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

114
UI/ThemeViewGUI.svelte Normal file
View file

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

View file

@ -2,7 +2,6 @@ import Locale from "./Locale"
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import LinkToWeblate from "../Base/LinkToWeblate"
import { SvelteComponent } from "svelte"
export class Translation extends BaseUIElement {
public static forcedLanguage = undefined
@ -299,7 +298,7 @@ export class Translation extends BaseUIElement {
}
}
export class TypedTranslation<T> extends Translation {
export class TypedTranslation<T extends Record<string, any>> extends Translation {
constructor(translations: Record<string, string>, context?: string) {
super(translations, context)
}