forked from MapComplete/MapComplete
refactoring(maplibre): WIP
This commit is contained in:
parent
231d67361e
commit
4d48b1cf2b
89 changed files with 1166 additions and 3973 deletions
|
@ -1,476 +0,0 @@
|
|||
import BaseUIElement from "./BaseUIElement"
|
||||
import Combine from "./Base/Combine"
|
||||
import Svg from "../Svg"
|
||||
import Title from "./Base/Title"
|
||||
import Toggle from "./Input/Toggle"
|
||||
import { SubtleButton } from "./Base/SubtleButton"
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import ValidatedTextField from "./Input/ValidatedTextField"
|
||||
import { Utils } from "../Utils"
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import { FixedUiElement } from "./Base/FixedUiElement"
|
||||
import { Tiles } from "../Models/TileRange"
|
||||
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
|
||||
import { DropDown } from "./Input/DropDown"
|
||||
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
|
||||
import MinimapImplementation from "./Base/MinimapImplementation"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import MapState from "../Logic/State/MapState"
|
||||
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
|
||||
import FeatureSource from "../Logic/FeatureSource/FeatureSource"
|
||||
import List from "./Base/List"
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
||||
import { SubstitutedTranslation } from "./SubstitutedTranslation"
|
||||
import { AutoAction } from "./Popup/AutoApplyButton"
|
||||
import DynamicGeoJsonTileSource from "../Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource"
|
||||
import themeOverview from "../assets/generated/theme_overview.json"
|
||||
|
||||
class AutomationPanel extends Combine {
|
||||
private static readonly openChangeset = new UIEventSource<number>(undefined)
|
||||
|
||||
constructor(
|
||||
layoutToUse: LayoutConfig,
|
||||
indices: number[],
|
||||
extraCommentText: UIEventSource<string>,
|
||||
tagRenderingToAutomate: { layer: LayerConfig; tagRendering: TagRenderingConfig }
|
||||
) {
|
||||
const layerId = tagRenderingToAutomate.layer.id
|
||||
const trId = tagRenderingToAutomate.tagRendering.id
|
||||
const tileState = LocalStorageSource.GetParsed(
|
||||
"automation-tile_state-" + layerId + "-" + trId,
|
||||
{}
|
||||
)
|
||||
const logMessages = new UIEventSource<string[]>([])
|
||||
if (indices === undefined) {
|
||||
throw "No tiles loaded - can not automate"
|
||||
}
|
||||
const openChangeset = AutomationPanel.openChangeset
|
||||
|
||||
openChangeset.addCallbackAndRun((cs) =>
|
||||
console.trace("Sync current open changeset to:", cs)
|
||||
)
|
||||
|
||||
const nextTileToHandle = tileState.map((handledTiles) => {
|
||||
for (const index of indices) {
|
||||
if (handledTiles[index] !== undefined) {
|
||||
// Already handled
|
||||
continue
|
||||
}
|
||||
return index
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
nextTileToHandle.addCallback((t) => console.warn("Next tile to handle is", t))
|
||||
|
||||
const neededTimes = new UIEventSource<number[]>([])
|
||||
const automaton = new VariableUiElement(
|
||||
nextTileToHandle.map((tileIndex) => {
|
||||
if (tileIndex === undefined) {
|
||||
return new FixedUiElement("All done!").SetClass("thanks")
|
||||
}
|
||||
console.warn("Triggered map on nextTileToHandle", tileIndex)
|
||||
const start = new Date()
|
||||
return AutomationPanel.TileHandler(
|
||||
layoutToUse,
|
||||
tileIndex,
|
||||
layerId,
|
||||
tagRenderingToAutomate.tagRendering,
|
||||
extraCommentText,
|
||||
(result, logMessage) => {
|
||||
const end = new Date()
|
||||
const timeNeeded = (end.getTime() - start.getTime()) / 1000
|
||||
neededTimes.data.push(timeNeeded)
|
||||
neededTimes.ping()
|
||||
tileState.data[tileIndex] = result
|
||||
tileState.ping()
|
||||
if (logMessage !== undefined) {
|
||||
logMessages.data.push(logMessage)
|
||||
logMessages.ping()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const statistics = new VariableUiElement(
|
||||
tileState.map((states) => {
|
||||
let total = 0
|
||||
const perResult = new Map<string, number>()
|
||||
for (const key in states) {
|
||||
total++
|
||||
const result = states[key]
|
||||
perResult.set(result, (perResult.get(result) ?? 0) + 1)
|
||||
}
|
||||
|
||||
let sum = 0
|
||||
neededTimes.data.forEach((v) => {
|
||||
sum = sum + v
|
||||
})
|
||||
let timePerTile = sum / neededTimes.data.length
|
||||
|
||||
return new Combine([
|
||||
"Handled " + total + "/" + indices.length + " tiles: ",
|
||||
new List(
|
||||
Array.from(perResult.keys()).map((key) => key + ": " + perResult.get(key))
|
||||
),
|
||||
"Handling one tile needs " +
|
||||
Math.floor(timePerTile * 100) / 100 +
|
||||
"s on average. Estimated time left: " +
|
||||
Utils.toHumanTime((indices.length - total) * timePerTile),
|
||||
]).SetClass("flex flex-col")
|
||||
})
|
||||
)
|
||||
|
||||
super([
|
||||
statistics,
|
||||
automaton,
|
||||
new SubtleButton(undefined, "Clear fixed").onClick(() => {
|
||||
const st = tileState.data
|
||||
for (const tileIndex in st) {
|
||||
if (st[tileIndex] === "fixed") {
|
||||
delete st[tileIndex]
|
||||
}
|
||||
}
|
||||
|
||||
tileState.ping()
|
||||
}),
|
||||
new VariableUiElement(logMessages.map((logMessages) => new List(logMessages))),
|
||||
])
|
||||
this.SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
private static TileHandler(
|
||||
layoutToUse: LayoutConfig,
|
||||
tileIndex: number,
|
||||
targetLayer: string,
|
||||
targetAction: TagRenderingConfig,
|
||||
extraCommentText: UIEventSource<string>,
|
||||
whenDone: (result: string, logMessage?: string) => void
|
||||
): BaseUIElement {
|
||||
const state = new MapState(layoutToUse, { attemptLogin: false })
|
||||
extraCommentText.syncWith(state.changes.extraComment)
|
||||
const [z, x, y] = Tiles.tile_from_index(tileIndex)
|
||||
state.locationControl.setData({
|
||||
zoom: z,
|
||||
lon: x,
|
||||
lat: y,
|
||||
})
|
||||
state.currentBounds.setData(BBox.fromTileIndex(tileIndex))
|
||||
|
||||
let targetTiles: UIEventSource<FeatureSource[]> = new UIEventSource<FeatureSource[]>([])
|
||||
const pipeline = new FeaturePipeline((tile) => {
|
||||
const layerId = tile.layer.layerDef.id
|
||||
if (layerId === targetLayer) {
|
||||
targetTiles.data.push(tile)
|
||||
targetTiles.ping()
|
||||
}
|
||||
}, state)
|
||||
|
||||
state.locationControl.ping()
|
||||
state.currentBounds.ping()
|
||||
const stateToShow = new UIEventSource("")
|
||||
|
||||
pipeline.runningQuery.map(
|
||||
async (isRunning) => {
|
||||
if (targetTiles.data.length === 0) {
|
||||
stateToShow.setData("No data loaded yet...")
|
||||
return
|
||||
}
|
||||
if (isRunning) {
|
||||
stateToShow.setData(
|
||||
"Waiting for all layers to be loaded... Has " +
|
||||
targetTiles.data.length +
|
||||
" tiles already"
|
||||
)
|
||||
return
|
||||
}
|
||||
if (targetTiles.data.length === 0) {
|
||||
stateToShow.setData("No features found to apply the action")
|
||||
whenDone("empty")
|
||||
return true
|
||||
}
|
||||
stateToShow.setData("Gathering applicable elements")
|
||||
|
||||
let handled = 0
|
||||
let inspected = 0
|
||||
let log = []
|
||||
for (const targetTile of targetTiles.data) {
|
||||
for (const ffs of targetTile.features.data) {
|
||||
inspected++
|
||||
if (inspected % 10 === 0) {
|
||||
stateToShow.setData(
|
||||
"Inspected " +
|
||||
inspected +
|
||||
" features, updated " +
|
||||
handled +
|
||||
" features"
|
||||
)
|
||||
}
|
||||
const feature = ffs.feature
|
||||
const renderingTr = targetAction.GetRenderValue(feature.properties)
|
||||
const rendering = renderingTr.txt
|
||||
log.push(
|
||||
"<a href='https://openstreetmap.org/" +
|
||||
feature.properties.id +
|
||||
"' target='_blank'>" +
|
||||
feature.properties.id +
|
||||
"</a>: " +
|
||||
new SubstitutedTranslation(
|
||||
renderingTr,
|
||||
new UIEventSource<any>(feature.properties),
|
||||
undefined
|
||||
).ConstructElement().textContent
|
||||
)
|
||||
const actions = Utils.NoNull(
|
||||
SubstitutedTranslation.ExtractSpecialComponents(rendering).map(
|
||||
(obj) => obj.special
|
||||
)
|
||||
)
|
||||
for (const action of actions) {
|
||||
const auto = <AutoAction>action.func
|
||||
if (auto.supportsAutoAction !== true) {
|
||||
continue
|
||||
}
|
||||
|
||||
await auto.applyActionOn(
|
||||
{
|
||||
layoutToUse: state.layoutToUse,
|
||||
changes: state.changes,
|
||||
},
|
||||
state.allElements.getEventSourceById(feature.properties.id),
|
||||
action.args
|
||||
)
|
||||
handled++
|
||||
}
|
||||
}
|
||||
}
|
||||
stateToShow.setData(
|
||||
"Done! Inspected " + inspected + " features, updated " + handled + " features"
|
||||
)
|
||||
|
||||
if (inspected === 0) {
|
||||
whenDone("empty")
|
||||
return true
|
||||
}
|
||||
|
||||
if (handled === 0) {
|
||||
whenDone("no-action", "Inspected " + inspected + " elements: " + log.join("; "))
|
||||
} else {
|
||||
state.osmConnection.AttemptLogin()
|
||||
state.changes.flushChanges("handled tile automatically, time to flush!")
|
||||
whenDone(
|
||||
"fixed",
|
||||
"Updated " +
|
||||
handled +
|
||||
" elements, inspected " +
|
||||
inspected +
|
||||
": " +
|
||||
log.join("; ")
|
||||
)
|
||||
}
|
||||
return true
|
||||
},
|
||||
[targetTiles]
|
||||
)
|
||||
|
||||
return new Combine([
|
||||
new Title("Performing action for tile " + tileIndex, 1),
|
||||
new VariableUiElement(stateToShow),
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
}
|
||||
|
||||
class AutomatonGui {
|
||||
constructor() {
|
||||
const osmConnection = new OsmConnection({
|
||||
singlePage: false,
|
||||
oauth_token: QueryParameters.GetQueryParameter("oauth_token", "OAuth token"),
|
||||
})
|
||||
|
||||
new Combine([
|
||||
new Combine([
|
||||
Svg.robot_svg().SetClass("w-24 h-24 p-4 rounded-full subtle-background"),
|
||||
new Combine([
|
||||
new Title("MapComplete Automaton", 1),
|
||||
"This page helps to automate certain tasks for a theme. Expert use only.",
|
||||
]).SetClass("flex flex-col m-4"),
|
||||
]).SetClass("flex"),
|
||||
new Toggle(
|
||||
AutomatonGui.GenerateMainPanel(),
|
||||
new SubtleButton(Svg.osm_logo_svg(), "Login to get started").onClick(() =>
|
||||
osmConnection.AttemptLogin()
|
||||
),
|
||||
osmConnection.isLoggedIn
|
||||
),
|
||||
])
|
||||
.SetClass("block p-4")
|
||||
.AttachTo("main")
|
||||
}
|
||||
|
||||
private static GenerateMainPanel(): BaseUIElement {
|
||||
const themeSelect = new DropDown<string>(
|
||||
"Select a theme",
|
||||
Array.from(themeOverview).map((l) => ({ value: l.id, shown: l.id }))
|
||||
)
|
||||
|
||||
LocalStorageSource.Get("automation-theme-id", "missing_streets").syncWith(
|
||||
themeSelect.GetValue()
|
||||
)
|
||||
|
||||
const tilepath = ValidatedTextField.ForType("url").ConstructInputElement({
|
||||
placeholder: "Specifiy the path of the overview",
|
||||
inputStyle: "width: 100%",
|
||||
})
|
||||
tilepath.SetClass("w-full")
|
||||
LocalStorageSource.Get("automation-tile_path").syncWith(tilepath.GetValue(), true)
|
||||
|
||||
let tilesToRunOver = tilepath.GetValue().bind((path) => {
|
||||
if (path === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return UIEventSource.FromPromiseWithErr(Utils.downloadJsonCached(path, 1000 * 60 * 60))
|
||||
})
|
||||
|
||||
const targetZoom = 14
|
||||
|
||||
const tilesPerIndex = tilesToRunOver.map((tiles) => {
|
||||
if (tiles === undefined || tiles["error"] !== undefined) {
|
||||
return undefined
|
||||
}
|
||||
let indexes: number[] = []
|
||||
const tilesS = tiles["success"]
|
||||
DynamicGeoJsonTileSource.RegisterWhitelist(tilepath.GetValue().data, tilesS)
|
||||
const z = Number(tilesS["zoom"])
|
||||
for (const key in tilesS) {
|
||||
if (key === "zoom") {
|
||||
continue
|
||||
}
|
||||
const x = Number(key)
|
||||
const ys = tilesS[key]
|
||||
indexes.push(...ys.map((y) => Tiles.tile_index(z, x, y)))
|
||||
}
|
||||
|
||||
console.log("Got ", indexes.length, "indexes")
|
||||
let rezoomed = new Set<number>()
|
||||
for (const index of indexes) {
|
||||
let [z, x, y] = Tiles.tile_from_index(index)
|
||||
while (z > targetZoom) {
|
||||
z--
|
||||
x = Math.floor(x / 2)
|
||||
y = Math.floor(y / 2)
|
||||
}
|
||||
rezoomed.add(Tiles.tile_index(z, x, y))
|
||||
}
|
||||
|
||||
return Array.from(rezoomed)
|
||||
})
|
||||
|
||||
const extraComment = ValidatedTextField.ForType("text").ConstructInputElement()
|
||||
LocalStorageSource.Get("automaton-extra-comment").syncWith(extraComment.GetValue())
|
||||
|
||||
return new Combine([
|
||||
themeSelect,
|
||||
"Specify the path to a tile overview. This is a hosted .json of the format {x : [y0, y1, y2], x1: [y0, ...]} where x is a string and y are numbers",
|
||||
tilepath,
|
||||
"Add an extra comment:",
|
||||
extraComment,
|
||||
new VariableUiElement(
|
||||
extraComment
|
||||
.GetValue()
|
||||
.map((c) => "Your comment is " + (c?.length ?? 0) + "/200 characters long")
|
||||
).SetClass("subtle"),
|
||||
new VariableUiElement(
|
||||
tilesToRunOver.map((t) => {
|
||||
if (t === undefined) {
|
||||
return "No path given or still loading..."
|
||||
}
|
||||
if (t["error"] !== undefined) {
|
||||
return new FixedUiElement("Invalid URL or data: " + t["error"]).SetClass(
|
||||
"alert"
|
||||
)
|
||||
}
|
||||
|
||||
return new FixedUiElement(
|
||||
"Loaded " + tilesPerIndex.data.length + " tiles to automated over"
|
||||
).SetClass("thanks")
|
||||
})
|
||||
),
|
||||
new VariableUiElement(
|
||||
themeSelect
|
||||
.GetValue()
|
||||
.map((id) => AllKnownLayouts.allKnownLayouts.get(id))
|
||||
.map(
|
||||
(layoutToUse) => {
|
||||
if (layoutToUse === undefined) {
|
||||
return new FixedUiElement("Select a valid layout")
|
||||
}
|
||||
if (
|
||||
tilesPerIndex.data === undefined ||
|
||||
tilesPerIndex.data.length === 0
|
||||
) {
|
||||
return "No tiles given"
|
||||
}
|
||||
|
||||
const automatableTagRenderings: {
|
||||
layer: LayerConfig
|
||||
tagRendering: TagRenderingConfig
|
||||
}[] = []
|
||||
for (const layer of layoutToUse.layers) {
|
||||
for (const tagRendering of layer.tagRenderings) {
|
||||
if (tagRendering.group === "auto") {
|
||||
automatableTagRenderings.push({
|
||||
layer,
|
||||
tagRendering: tagRendering,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Automatable tag renderings:", automatableTagRenderings)
|
||||
if (automatableTagRenderings.length === 0) {
|
||||
return new FixedUiElement(
|
||||
'This theme does not have any tagRendering with "group": "auto" set'
|
||||
).SetClass("alert")
|
||||
}
|
||||
const pickAuto = new DropDown("Pick the action to automate", [
|
||||
{
|
||||
value: undefined,
|
||||
shown: "Pick an option",
|
||||
},
|
||||
...automatableTagRenderings.map((config) => ({
|
||||
shown: config.layer.id + " - " + config.tagRendering.id,
|
||||
value: config,
|
||||
})),
|
||||
])
|
||||
|
||||
return new Combine([
|
||||
pickAuto,
|
||||
new VariableUiElement(
|
||||
pickAuto
|
||||
.GetValue()
|
||||
.map((auto) =>
|
||||
auto === undefined
|
||||
? undefined
|
||||
: new AutomationPanel(
|
||||
layoutToUse,
|
||||
tilesPerIndex.data,
|
||||
extraComment.GetValue(),
|
||||
auto
|
||||
)
|
||||
)
|
||||
),
|
||||
])
|
||||
},
|
||||
[tilesPerIndex]
|
||||
)
|
||||
).SetClass("flex flex-col"),
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
}
|
||||
|
||||
MinimapImplementation.initialize()
|
||||
|
||||
new AutomatonGui()
|
14
UI/Base/If.svelte
Normal file
14
UI/Base/If.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
|
||||
/**
|
||||
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
|
||||
*/
|
||||
export let condition: UIEventSource<boolean>;
|
||||
let _c = condition.data;
|
||||
condition.addCallback(c => _c = c)
|
||||
</script>
|
||||
|
||||
{#if _c}
|
||||
<slot></slot>
|
||||
{/if}
|
13
UI/Base/MapControlButton.svelte
Normal file
13
UI/Base/MapControlButton.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
/**
|
||||
* A round button with an icon and possible a small text, which hovers above the map
|
||||
*/
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
|
||||
<div on:click={e => dispatch("click", e)} class="subtle-background block rounded-full min-w-10 h-10 pointer-events-auto m-0.5 md:m-1 p-1">
|
||||
<slot class="m-4"></slot>
|
||||
</div>
|
|
@ -1,47 +0,0 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import Loc from "../../Models/Loc"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
|
||||
export interface MinimapOptions {
|
||||
background?: UIEventSource<BaseLayer>
|
||||
location?: UIEventSource<Loc>
|
||||
bounds?: UIEventSource<BBox>
|
||||
allowMoving?: boolean
|
||||
leafletOptions?: any
|
||||
attribution?: BaseUIElement | boolean
|
||||
onFullyLoaded?: (leaflet: L.Map) => void
|
||||
leafletMap?: UIEventSource<any>
|
||||
lastClickLocation?: UIEventSource<{ lat: number; lon: number }>
|
||||
addLayerControl?: boolean | false
|
||||
}
|
||||
|
||||
export interface MinimapObj {
|
||||
readonly leafletMap: UIEventSource<any>
|
||||
readonly location: UIEventSource<Loc>
|
||||
readonly bounds: UIEventSource<BBox>
|
||||
|
||||
installBounds(factor: number | BBox, showRange?: boolean): void
|
||||
|
||||
TakeScreenshot(format): Promise<string>
|
||||
TakeScreenshot(format: "image"): Promise<string>
|
||||
TakeScreenshot(format: "blob"): Promise<Blob>
|
||||
TakeScreenshot(format?: "image" | "blob"): Promise<string | Blob>
|
||||
}
|
||||
|
||||
export default class Minimap {
|
||||
/**
|
||||
* A stub implementation. The actual implementation is injected later on, but only in the browser.
|
||||
* importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it
|
||||
*/
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Construct a minimap
|
||||
*/
|
||||
public static createMiniMap: (options?: MinimapOptions) => BaseUIElement & MinimapObj = (_) => {
|
||||
throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()"
|
||||
}
|
||||
}
|
|
@ -1,422 +0,0 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
||||
import * as L from "leaflet"
|
||||
import { LeafletMouseEvent, Map } from "leaflet"
|
||||
import Minimap, { MinimapObj, MinimapOptions } from "./Minimap"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import "leaflet-polylineoffset"
|
||||
import { SimpleMapScreenshoter } from "leaflet-simple-map-screenshoter"
|
||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import ShowDataLayerImplementation from "../ShowDataLayer/ShowDataLayerImplementation"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import ScrollableFullScreen from "./ScrollableFullScreen"
|
||||
import Constants from "../../Models/Constants"
|
||||
import StrayClickHandler from "../../Logic/Actors/StrayClickHandler"
|
||||
|
||||
/**
|
||||
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
|
||||
* Shows the given uiToShow-element in the messagebox
|
||||
*/
|
||||
class StrayClickHandlerImplementation {
|
||||
private _lastMarker
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
|
||||
selectedElement: UIEventSource<string>
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
leafletMap: UIEventSource<L.Map>
|
||||
},
|
||||
uiToShow: ScrollableFullScreen,
|
||||
iconToShow: BaseUIElement
|
||||
) {
|
||||
const self = this
|
||||
const leafletMap = state.leafletMap
|
||||
state.filteredLayers.data.forEach((filteredLayer) => {
|
||||
filteredLayer.isDisplayed.addCallback((isEnabled) => {
|
||||
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
|
||||
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
|
||||
// This reclick might be at a location where a feature now appeared...
|
||||
state.leafletMap.data.removeLayer(self._lastMarker)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
state.LastClickLocation.addCallback(function (lastClick) {
|
||||
if (self._lastMarker !== undefined) {
|
||||
state.leafletMap.data?.removeLayer(self._lastMarker)
|
||||
}
|
||||
|
||||
if (lastClick === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
state.selectedElement.setData(undefined)
|
||||
const clickCoor: [number, number] = [lastClick.lat, lastClick.lon]
|
||||
self._lastMarker = L.marker(clickCoor, {
|
||||
icon: L.divIcon({
|
||||
html: iconToShow.ConstructElement(),
|
||||
iconSize: [50, 50],
|
||||
iconAnchor: [25, 50],
|
||||
popupAnchor: [0, -45],
|
||||
}),
|
||||
})
|
||||
|
||||
self._lastMarker.addTo(leafletMap.data)
|
||||
|
||||
self._lastMarker.on("click", () => {
|
||||
if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) {
|
||||
leafletMap.data.flyTo(
|
||||
clickCoor,
|
||||
Constants.userJourney.minZoomLevelToAddNewPoints
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
uiToShow.Activate()
|
||||
})
|
||||
})
|
||||
|
||||
state.selectedElement.addCallback(() => {
|
||||
if (self._lastMarker !== undefined) {
|
||||
leafletMap.data.removeLayer(self._lastMarker)
|
||||
this._lastMarker = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
|
||||
private static _nextId = 0
|
||||
public readonly leafletMap: UIEventSource<Map>
|
||||
public readonly location: UIEventSource<Loc>
|
||||
public readonly bounds: UIEventSource<BBox> | undefined
|
||||
private readonly _id: string
|
||||
private readonly _background: UIEventSource<BaseLayer>
|
||||
private _isInited = false
|
||||
private _allowMoving: boolean
|
||||
private readonly _leafletoptions: any
|
||||
private readonly _onFullyLoaded: (leaflet: L.Map) => void
|
||||
private readonly _attribution: BaseUIElement | boolean
|
||||
private readonly _addLayerControl: boolean
|
||||
private readonly _options: MinimapOptions
|
||||
|
||||
private constructor(options?: MinimapOptions) {
|
||||
super()
|
||||
options = options ?? {}
|
||||
this._id = "minimap" + MinimapImplementation._nextId
|
||||
MinimapImplementation._nextId++
|
||||
this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
|
||||
this._background =
|
||||
options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
|
||||
this.location = options?.location ?? new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
|
||||
this.bounds = options?.bounds
|
||||
this._allowMoving = options.allowMoving ?? true
|
||||
this._leafletoptions = options.leafletOptions ?? {}
|
||||
this._onFullyLoaded = options.onFullyLoaded
|
||||
this._attribution = options.attribution
|
||||
this._addLayerControl = options.addLayerControl ?? false
|
||||
this._options = options
|
||||
this.SetClass("relative")
|
||||
}
|
||||
|
||||
public static initialize() {
|
||||
Minimap.createMiniMap = (options) => new MinimapImplementation(options)
|
||||
ShowDataLayer.actualContstructor = (options) => new ShowDataLayerImplementation(options)
|
||||
StrayClickHandler.construct = (
|
||||
state: {
|
||||
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
|
||||
selectedElement: UIEventSource<string>
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
leafletMap: UIEventSource<L.Map>
|
||||
},
|
||||
uiToShow: ScrollableFullScreen,
|
||||
iconToShow: BaseUIElement
|
||||
) => {
|
||||
return new StrayClickHandlerImplementation(state, uiToShow, iconToShow)
|
||||
}
|
||||
}
|
||||
|
||||
public installBounds(factor: number | BBox, showRange?: boolean) {
|
||||
this.leafletMap.addCallbackD((leaflet) => {
|
||||
let bounds: { getEast(); getNorth(); getWest(); getSouth() }
|
||||
if (typeof factor === "number") {
|
||||
const lbounds = leaflet.getBounds().pad(factor)
|
||||
leaflet.setMaxBounds(lbounds)
|
||||
bounds = lbounds
|
||||
} else {
|
||||
// @ts-ignore
|
||||
leaflet.setMaxBounds(factor.toLeaflet())
|
||||
bounds = factor
|
||||
}
|
||||
|
||||
if (showRange) {
|
||||
const data = {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[bounds.getEast(), bounds.getNorth()],
|
||||
[bounds.getWest(), bounds.getNorth()],
|
||||
[bounds.getWest(), bounds.getSouth()],
|
||||
|
||||
[bounds.getEast(), bounds.getSouth()],
|
||||
[bounds.getEast(), bounds.getNorth()],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
// @ts-ignore
|
||||
L.geoJSON(data, {
|
||||
style: {
|
||||
color: "#f44",
|
||||
weight: 4,
|
||||
opacity: 0.7,
|
||||
},
|
||||
}).addTo(leaflet)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Destroy() {
|
||||
super.Destroy()
|
||||
console.warn("Decomissioning minimap", this._id)
|
||||
const mp = this.leafletMap.data
|
||||
this.leafletMap.setData(null)
|
||||
mp.off()
|
||||
mp.remove()
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a screenshot of the current map
|
||||
* @param format: image: give a base64 encoded png image;
|
||||
* @constructor
|
||||
*/
|
||||
public async TakeScreenshot(): Promise<string>
|
||||
public async TakeScreenshot(format: "image"): Promise<string>
|
||||
public async TakeScreenshot(format: "blob"): Promise<Blob>
|
||||
public async TakeScreenshot(format: "image" | "blob"): Promise<string | Blob>
|
||||
public async TakeScreenshot(format: "image" | "blob" = "image"): Promise<string | Blob> {
|
||||
console.log("Taking a screenshot...")
|
||||
const screenshotter = new SimpleMapScreenshoter()
|
||||
screenshotter.addTo(this.leafletMap.data)
|
||||
const result = <any>await screenshotter.takeScreen(<any>format ?? "image")
|
||||
if (format === "image" && typeof result === "string") {
|
||||
return result
|
||||
}
|
||||
if (format === "blob" && result instanceof Blob) {
|
||||
return result
|
||||
}
|
||||
throw "Something went wrong while creating the screenshot: " + result
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const div = document.createElement("div")
|
||||
div.id = this._id
|
||||
div.style.height = "100%"
|
||||
div.style.width = "100%"
|
||||
div.style.minWidth = "40px"
|
||||
div.style.minHeight = "40px"
|
||||
div.style.position = "relative"
|
||||
const wrapper = document.createElement("div")
|
||||
wrapper.appendChild(div)
|
||||
const self = this
|
||||
// @ts-ignore
|
||||
const resizeObserver = new ResizeObserver((_) => {
|
||||
if (wrapper.clientHeight === 0 || wrapper.clientWidth === 0) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
wrapper.offsetParent === null ||
|
||||
window.getComputedStyle(wrapper).display === "none"
|
||||
) {
|
||||
// Not visible
|
||||
return
|
||||
}
|
||||
try {
|
||||
self.InitMap()
|
||||
} catch (e) {
|
||||
console.debug("Could not construct a minimap:", e)
|
||||
}
|
||||
|
||||
try {
|
||||
self.leafletMap?.data?.invalidateSize()
|
||||
} catch (e) {
|
||||
console.debug("Could not invalidate size of a minimap:", e)
|
||||
}
|
||||
})
|
||||
|
||||
resizeObserver.observe(div)
|
||||
|
||||
if (this._addLayerControl) {
|
||||
const switcher = new BackgroundMapSwitch(
|
||||
{
|
||||
locationControl: this.location,
|
||||
backgroundLayer: this._background,
|
||||
},
|
||||
this._background
|
||||
).SetClass("top-0 right-0 z-above-map absolute")
|
||||
wrapper.appendChild(switcher.ConstructElement())
|
||||
}
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
private InitMap() {
|
||||
if (this._constructedHtmlElement === undefined) {
|
||||
// This element isn't initialized yet
|
||||
return
|
||||
}
|
||||
|
||||
if (document.getElementById(this._id) === null) {
|
||||
// not yet attached, we probably got some other event
|
||||
return
|
||||
}
|
||||
|
||||
if (this._isInited) {
|
||||
return
|
||||
}
|
||||
this._isInited = true
|
||||
const location = this.location
|
||||
const self = this
|
||||
let currentLayer = this._background.data.layer()
|
||||
let latLon = <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0]
|
||||
if (isNaN(latLon[0]) || isNaN(latLon[1])) {
|
||||
latLon = [0, 0]
|
||||
}
|
||||
const options = {
|
||||
center: latLon,
|
||||
zoom: location.data?.zoom ?? 2,
|
||||
layers: [currentLayer],
|
||||
zoomControl: false,
|
||||
attributionControl: this._attribution !== undefined,
|
||||
dragging: this._allowMoving,
|
||||
scrollWheelZoom: this._allowMoving,
|
||||
doubleClickZoom: this._allowMoving,
|
||||
keyboard: this._allowMoving,
|
||||
touchZoom: this._allowMoving,
|
||||
// Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving,
|
||||
fadeAnimation: this._allowMoving,
|
||||
maxZoom: 21,
|
||||
}
|
||||
|
||||
Utils.Merge(this._leafletoptions, options)
|
||||
/*
|
||||
* Somehow, the element gets '_leaflet_id' set on chrome.
|
||||
* When attempting to init this leaflet map, it'll throw an exception and the map won't show up.
|
||||
* Simply removing '_leaflet_id' fixes the issue.
|
||||
* See https://github.com/pietervdvn/MapComplete/issues/726
|
||||
* */
|
||||
delete document.getElementById(this._id)["_leaflet_id"]
|
||||
|
||||
const map = L.map(this._id, options)
|
||||
if (self._onFullyLoaded !== undefined) {
|
||||
currentLayer.on("load", () => {
|
||||
console.log("Fully loaded all tiles!")
|
||||
self._onFullyLoaded(map)
|
||||
})
|
||||
}
|
||||
|
||||
// Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then
|
||||
// We give a bit of leeway for people on the edges
|
||||
// Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/
|
||||
|
||||
map.setMaxBounds([
|
||||
[-100, -200],
|
||||
[100, 200],
|
||||
])
|
||||
|
||||
if (this._attribution !== undefined) {
|
||||
if (this._attribution === true) {
|
||||
map.attributionControl.setPrefix(false)
|
||||
} else {
|
||||
map.attributionControl.setPrefix("<span id='leaflet-attribution'></span>")
|
||||
}
|
||||
}
|
||||
|
||||
this._background.addCallbackAndRun((layer) => {
|
||||
const newLayer = layer.layer()
|
||||
if (currentLayer !== undefined) {
|
||||
map.removeLayer(currentLayer)
|
||||
}
|
||||
currentLayer = newLayer
|
||||
if (self._onFullyLoaded !== undefined) {
|
||||
currentLayer.on("load", () => {
|
||||
console.log("Fully loaded all tiles!")
|
||||
self._onFullyLoaded(map)
|
||||
})
|
||||
}
|
||||
map.addLayer(newLayer)
|
||||
if (self._attribution !== true && self._attribution !== false) {
|
||||
self._attribution?.AttachTo("leaflet-attribution")
|
||||
}
|
||||
})
|
||||
|
||||
let isRecursing = false
|
||||
map.on("moveend", function () {
|
||||
if (isRecursing) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
map.getZoom() === location.data.zoom &&
|
||||
map.getCenter().lat === location.data.lat &&
|
||||
map.getCenter().lng === location.data.lon
|
||||
) {
|
||||
return
|
||||
}
|
||||
location.data.zoom = map.getZoom()
|
||||
location.data.lat = map.getCenter().lat
|
||||
location.data.lon = map.getCenter().lng
|
||||
isRecursing = true
|
||||
location.ping()
|
||||
|
||||
if (self.bounds !== undefined) {
|
||||
self.bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
|
||||
}
|
||||
|
||||
isRecursing = false // This is ugly, I know
|
||||
})
|
||||
|
||||
location.addCallback((loc) => {
|
||||
const mapLoc = map.getCenter()
|
||||
const dlat = Math.abs(loc.lat - mapLoc[0])
|
||||
const dlon = Math.abs(loc.lon - mapLoc[1])
|
||||
|
||||
if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) {
|
||||
return
|
||||
}
|
||||
map.setView([loc.lat, loc.lon], loc.zoom)
|
||||
})
|
||||
|
||||
if (self.bounds !== undefined) {
|
||||
self.bounds.setData(BBox.fromLeafletBounds(map.getBounds()))
|
||||
}
|
||||
|
||||
if (this._options.lastClickLocation) {
|
||||
const lastClickLocation = this._options.lastClickLocation
|
||||
map.addEventListener("click", function (e: LeafletMouseEvent) {
|
||||
if (e.originalEvent["dismissed"]) {
|
||||
return
|
||||
}
|
||||
lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng })
|
||||
})
|
||||
|
||||
map.on("contextmenu", function (e) {
|
||||
// @ts-ignore
|
||||
lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng })
|
||||
map.setZoom(map.getZoom() + 1)
|
||||
})
|
||||
}
|
||||
|
||||
this.leafletMap.setData(map)
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import { SvelteComponentTyped } from "svelte"
|
|||
|
||||
/**
|
||||
* The SvelteUIComponent serves as a translating class which which wraps a SvelteElement into the BaseUIElement framework.
|
||||
* Also see ToSvelte.svelte for the opposite conversion
|
||||
*/
|
||||
export default class SvelteUIElement<
|
||||
Props extends Record<string, any> = any,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
|
@ -24,13 +24,13 @@ export default class AddNewMarker extends Combine {
|
|||
for (const preset of filteredLayer.layerDef.presets) {
|
||||
const tags = TagUtils.KVtoProperties(preset.tags)
|
||||
const icon = layer.mapRendering[0]
|
||||
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
|
||||
.RenderIcon(new ImmutableStore<any>(tags), false)
|
||||
.html.SetClass("block relative")
|
||||
.SetStyle("width: 42px; height: 42px;")
|
||||
icons.push(icon)
|
||||
if (last === undefined) {
|
||||
last = layer.mapRendering[0]
|
||||
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
|
||||
.RenderIcon(new ImmutableStore<any>(tags), false)
|
||||
.html.SetClass("block relative")
|
||||
.SetStyle("width: 42px; height: 42px;")
|
||||
}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
import Link from "../Base/Link"
|
||||
import Svg from "../../Svg"
|
||||
import Combine from "../Base/Combine"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection"
|
||||
import Constants from "../../Models/Constants"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { Utils } from "../../Utils"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
/**
|
||||
* The bottom right attribution panel in the leaflet map
|
||||
*/
|
||||
export default class Attribution extends Combine {
|
||||
constructor(
|
||||
location: UIEventSource<Loc>,
|
||||
userDetails: UIEventSource<UserDetails>,
|
||||
layoutToUse: LayoutConfig,
|
||||
currentBounds: UIEventSource<BBox>
|
||||
) {
|
||||
const mapComplete = new Link(
|
||||
`Mapcomplete ${Constants.vNumber}`,
|
||||
"https://github.com/pietervdvn/MapComplete",
|
||||
true
|
||||
)
|
||||
const reportBug = new Link(
|
||||
Svg.bug_ui().SetClass("small-image"),
|
||||
"https://github.com/pietervdvn/MapComplete/issues",
|
||||
true
|
||||
)
|
||||
|
||||
const layoutId = layoutToUse?.id
|
||||
const stats = new Link(
|
||||
Svg.statistics_ui().SetClass("small-image"),
|
||||
Utils.OsmChaLinkFor(31, layoutId),
|
||||
true
|
||||
)
|
||||
|
||||
const idLink = location.map(
|
||||
(location) =>
|
||||
`https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${
|
||||
location?.lat ?? 0
|
||||
}/${location?.lon ?? 0}`
|
||||
)
|
||||
const editHere = new Link(Svg.pencil_ui().SetClass("small-image"), idLink, true)
|
||||
|
||||
const mapillaryLink = location.map(
|
||||
(location) =>
|
||||
`https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${
|
||||
location?.lon ?? 0
|
||||
}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
|
||||
)
|
||||
const mapillary = new Link(
|
||||
Svg.mapillary_black_ui().SetClass("small-image"),
|
||||
mapillaryLink,
|
||||
true
|
||||
)
|
||||
|
||||
const mapDataByOsm = new Link(
|
||||
Translations.t.general.attribution.mapDataByOsm,
|
||||
"https://openstreetmap.org/copyright",
|
||||
true
|
||||
)
|
||||
|
||||
const editWithJosm = new VariableUiElement(
|
||||
userDetails.map(
|
||||
(userDetails) => {
|
||||
if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
|
||||
return undefined
|
||||
}
|
||||
const bounds: any = currentBounds.data
|
||||
if (bounds === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const top = bounds.getNorth()
|
||||
const bottom = bounds.getSouth()
|
||||
const right = bounds.getEast()
|
||||
const left = bounds.getWest()
|
||||
|
||||
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
|
||||
return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true)
|
||||
},
|
||||
[location, currentBounds]
|
||||
)
|
||||
)
|
||||
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary, mapDataByOsm])
|
||||
this.SetClass("flex")
|
||||
}
|
||||
}
|
|
@ -1,25 +1,19 @@
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Svg from "../../Svg"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import Loc from "../../Models/Loc"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
|
||||
/**
|
||||
* Displays an icon depending on the state of the geolocation.
|
||||
* Will set the 'lock' if clicked twice
|
||||
*/
|
||||
export class GeolocationControl extends VariableUiElement {
|
||||
constructor(
|
||||
geolocationHandler: GeoLocationHandler,
|
||||
state: {
|
||||
locationControl: UIEventSource<Loc>
|
||||
currentBounds: UIEventSource<BBox>
|
||||
}
|
||||
) {
|
||||
constructor(geolocationHandler: GeoLocationHandler, state: MapProperties) {
|
||||
const lastClick = new UIEventSource<Date>(undefined)
|
||||
lastClick.addCallbackD((date) => {
|
||||
geolocationHandler.geolocationState.requestMoment.setData(date)
|
||||
|
@ -48,7 +42,7 @@ export class GeolocationControl extends VariableUiElement {
|
|||
if (permission === "denied") {
|
||||
return Svg.location_refused_svg()
|
||||
}
|
||||
if (geolocationState.isLocked.data) {
|
||||
if (!geolocationState.allowMoving.data) {
|
||||
return Svg.location_locked_svg()
|
||||
}
|
||||
|
||||
|
@ -77,7 +71,7 @@ export class GeolocationControl extends VariableUiElement {
|
|||
},
|
||||
[
|
||||
geolocationState.currentGPSLocation,
|
||||
geolocationState.isLocked,
|
||||
geolocationState.allowMoving,
|
||||
geolocationHandler.mapHasMoved,
|
||||
lastClickWithinThreeSecs,
|
||||
lastRequestWithinTimeout,
|
||||
|
@ -95,9 +89,9 @@ export class GeolocationControl extends VariableUiElement {
|
|||
await geolocationState.requestPermission()
|
||||
}
|
||||
|
||||
if (geolocationState.isLocked.data === true) {
|
||||
if (geolocationState.allowMoving.data === false) {
|
||||
// Unlock
|
||||
geolocationState.isLocked.setData(false)
|
||||
geolocationState.allowMoving.setData(true)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -109,21 +103,17 @@ export class GeolocationControl extends VariableUiElement {
|
|||
|
||||
// A location _is_ known! Let's move to this location
|
||||
const currentLocation = geolocationState.currentGPSLocation.data
|
||||
const inBounds = state.currentBounds.data.contains([
|
||||
const inBounds = state.bounds.data.contains([
|
||||
currentLocation.longitude,
|
||||
currentLocation.latitude,
|
||||
])
|
||||
geolocationHandler.MoveMapToCurrentLocation()
|
||||
if (inBounds) {
|
||||
const lc = state.locationControl.data
|
||||
state.locationControl.setData({
|
||||
...lc,
|
||||
zoom: lc.zoom + 3,
|
||||
})
|
||||
state.zoom.update((z) => z + 3)
|
||||
}
|
||||
|
||||
if (lastClickWithinThreeSecs.data) {
|
||||
geolocationState.isLocked.setData(true)
|
||||
geolocationState.allowMoving.setData(false)
|
||||
lastClick.setData(undefined)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import BackgroundMapSwitch from "./BackgroundMapSwitch"
|
|||
import Lazy from "../Base/Lazy"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox"
|
||||
import CopyrightPanel from "./CopyrightPanel"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
|
@ -21,7 +20,7 @@ export default class LeftControls extends Combine {
|
|||
const currentViewFL = state.currentView?.layer
|
||||
const currentViewAction = new Toggle(
|
||||
new Lazy(() => {
|
||||
const feature: Store<any> = state.currentView.features.map((ffs) => ffs[0]?.feature)
|
||||
const feature: Store<any> = state.currentView.features.map((ffs) => ffs[0])
|
||||
const icon = new VariableUiElement(
|
||||
feature.map((feature) => {
|
||||
const defaultIcon = Svg.checkbox_empty_svg()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import FloorLevelInputElement from "../Input/FloorLevelInputElement"
|
||||
import MapState, { GlobalFilter } from "../../Logic/State/MapState"
|
||||
import MapState from "../../Logic/State/MapState"
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
import { RegexTag } from "../../Logic/Tags/RegexTag"
|
||||
import { Or } from "../../Logic/Tags/Or"
|
||||
|
@ -11,6 +11,7 @@ import { BBox } from "../../Logic/BBox"
|
|||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { GlobalFilter } from "../../Logic/State/GlobalFilter"
|
||||
|
||||
/***
|
||||
* The element responsible for the level input element and picking the right level, showing and hiding at the right time, ...
|
||||
|
|
|
@ -9,30 +9,9 @@ import LevelSelector from "./LevelSelector"
|
|||
import { GeolocationControl } from "./GeolocationControl"
|
||||
|
||||
export default class RightControls extends Combine {
|
||||
constructor(
|
||||
state: MapState & { featurePipeline: FeaturePipeline },
|
||||
geolocationHandler: GeoLocationHandler
|
||||
) {
|
||||
const geolocationButton = Toggle.If(state.featureSwitchGeolocation, () =>
|
||||
new MapControlButton(new GeolocationControl(geolocationHandler, state), {
|
||||
dontStyle: true,
|
||||
}).SetClass("p-1")
|
||||
)
|
||||
|
||||
const plus = new MapControlButton(Svg.plus_svg()).onClick(() => {
|
||||
state.locationControl.data.zoom++
|
||||
state.locationControl.ping()
|
||||
})
|
||||
|
||||
const min = new MapControlButton(Svg.min_svg()).onClick(() => {
|
||||
state.locationControl.data.zoom--
|
||||
state.locationControl.ping()
|
||||
})
|
||||
|
||||
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
|
||||
const levelSelector = new LevelSelector(state)
|
||||
super(
|
||||
[levelSelector, plus, min, geolocationButton].map((el) => el.SetClass("m-0.5 md:m-1"))
|
||||
)
|
||||
super([levelSelector].map((el) => el.SetClass("m-0.5 md:m-1")))
|
||||
this.SetClass("flex flex-col items-center")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Svg from "../../Svg"
|
||||
import { TextField } from "../Input/TextField"
|
||||
|
@ -7,10 +7,15 @@ import Translations from "../i18n/Translations"
|
|||
import Hash from "../../Logic/Web/Hash"
|
||||
import Combine from "../Base/Combine"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
|
||||
export default class SearchAndGo extends Combine {
|
||||
private readonly _searchField: TextField
|
||||
constructor(state: { leafletMap: UIEventSource<any>; selectedElement?: UIEventSource<any> }) {
|
||||
constructor(state: {
|
||||
leafletMap: UIEventSource<any>
|
||||
selectedElement?: UIEventSource<any>
|
||||
bounds?: Store<BBox>
|
||||
}) {
|
||||
const goButton = Svg.search_ui().SetClass("w-8 h-8 full-rounded border-black float-right")
|
||||
|
||||
const placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
|
||||
|
@ -49,7 +54,7 @@ export default class SearchAndGo extends Combine {
|
|||
searchField.GetValue().setData("")
|
||||
placeholder.setData(Translations.t.general.search.searching)
|
||||
try {
|
||||
const result = await Geocoding.Search(searchString)
|
||||
const result = await Geocoding.Search(searchString, state.bounds.data)
|
||||
|
||||
console.log("Search result", result)
|
||||
if (result.length == 0) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
|
||||
*/
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Svg from "../../Svg"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Combine from "../Base/Combine"
|
||||
|
@ -22,13 +22,12 @@ import { Changes } from "../../Logic/Osm/Changes"
|
|||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import { ElementStorage } from "../../Logic/ElementStorage"
|
||||
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import Loading from "../Base/Loading"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
import { GlobalFilter } from "../../Logic/State/MapState"
|
||||
import { WayId } from "../../Models/OsmFeature"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import { GlobalFilter } from "../../Models/GlobalFilter"
|
||||
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
|
@ -288,7 +287,7 @@ export default class SimpleAddUI extends LoginToggle {
|
|||
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
|
||||
let icon: () => BaseUIElement = () =>
|
||||
layer.layerDef.mapRendering[0]
|
||||
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
|
||||
.RenderIcon(new ImmutableStore<any>(tags), false)
|
||||
.html.SetClass("w-12 h-12 block relative")
|
||||
const presetInfo: PresetInfo = {
|
||||
layerToAddTo: layer,
|
||||
|
|
|
@ -1,305 +0,0 @@
|
|||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import { FixedUiElement } from "./Base/FixedUiElement"
|
||||
import { Utils } from "../Utils"
|
||||
import Combine from "./Base/Combine"
|
||||
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import home_location_json from "../assets/layers/home_location/home_location.json"
|
||||
import State from "../State"
|
||||
import Title from "./Base/Title"
|
||||
import { MinimapObj } from "./Base/Minimap"
|
||||
import BaseUIElement from "./BaseUIElement"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import { GeoOperations } from "../Logic/GeoOperations"
|
||||
import { OsmFeature } from "../Models/OsmFeature"
|
||||
import SearchAndGo from "./BigComponents/SearchAndGo"
|
||||
import FeatureInfoBox from "./Popup/FeatureInfoBox"
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import LanguagePicker from "./LanguagePicker"
|
||||
import Lazy from "./Base/Lazy"
|
||||
import TagRenderingAnswer from "./Popup/TagRenderingAnswer"
|
||||
import Hash from "../Logic/Web/Hash"
|
||||
import FilterView from "./BigComponents/FilterView"
|
||||
import Translations from "./i18n/Translations"
|
||||
import Constants from "../Models/Constants"
|
||||
import SimpleAddUI from "./BigComponents/SimpleAddUI"
|
||||
import BackToIndex from "./BigComponents/BackToIndex"
|
||||
import StatisticsPanel from "./BigComponents/StatisticsPanel"
|
||||
|
||||
export default class DashboardGui {
|
||||
private readonly state: FeaturePipelineState
|
||||
private readonly currentView: UIEventSource<{
|
||||
title: string | BaseUIElement
|
||||
contents: string | BaseUIElement
|
||||
}> = new UIEventSource(undefined)
|
||||
|
||||
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
|
||||
this.state = state
|
||||
}
|
||||
|
||||
private viewSelector(
|
||||
shown: BaseUIElement,
|
||||
title: string | BaseUIElement,
|
||||
contents: string | BaseUIElement,
|
||||
hash?: string
|
||||
): BaseUIElement {
|
||||
const currentView = this.currentView
|
||||
const v = { title, contents }
|
||||
shown.SetClass("pl-1 pr-1 rounded-md")
|
||||
shown.onClick(() => {
|
||||
currentView.setData(v)
|
||||
})
|
||||
Hash.hash.addCallbackAndRunD((h) => {
|
||||
if (h === hash) {
|
||||
currentView.setData(v)
|
||||
}
|
||||
})
|
||||
currentView.addCallbackAndRunD((cv) => {
|
||||
if (cv == v) {
|
||||
shown.SetClass("bg-unsubtle")
|
||||
Hash.hash.setData(hash)
|
||||
} else {
|
||||
shown.RemoveClass("bg-unsubtle")
|
||||
}
|
||||
})
|
||||
return shown
|
||||
}
|
||||
|
||||
private singleElementCache: Record<string, BaseUIElement> = {}
|
||||
|
||||
private singleElementView(
|
||||
element: OsmFeature,
|
||||
layer: LayerConfig,
|
||||
distance: number
|
||||
): BaseUIElement {
|
||||
if (this.singleElementCache[element.properties.id] !== undefined) {
|
||||
return this.singleElementCache[element.properties.id]
|
||||
}
|
||||
const tags = this.state.allElements.getEventSourceById(element.properties.id)
|
||||
const title = new Combine([
|
||||
new Title(new TagRenderingAnswer(tags, layer.title, this.state), 4),
|
||||
distance < 900
|
||||
? Math.floor(distance) + "m away"
|
||||
: Utils.Round(distance / 1000) + "km away",
|
||||
]).SetClass("flex justify-between")
|
||||
|
||||
return (this.singleElementCache[element.properties.id] = this.viewSelector(
|
||||
title,
|
||||
new Lazy(() => FeatureInfoBox.GenerateTitleBar(tags, layer, this.state)),
|
||||
new Lazy(() => FeatureInfoBox.GenerateContent(tags, layer, this.state))
|
||||
// element.properties.id
|
||||
))
|
||||
}
|
||||
|
||||
private mainElementsView(
|
||||
elements: { element: OsmFeature; layer: LayerConfig; distance: number }[]
|
||||
): BaseUIElement {
|
||||
const self = this
|
||||
if (elements === undefined) {
|
||||
return new FixedUiElement("Initializing")
|
||||
}
|
||||
if (elements.length == 0) {
|
||||
return new FixedUiElement("No elements in view")
|
||||
}
|
||||
return new Combine(
|
||||
elements.map((e) => self.singleElementView(e.element, e.layer, e.distance))
|
||||
)
|
||||
}
|
||||
|
||||
private documentationButtonFor(layerConfig: LayerConfig): BaseUIElement {
|
||||
return this.viewSelector(
|
||||
Translations.W(layerConfig.name?.Clone() ?? layerConfig.id),
|
||||
new Combine(["Documentation about ", layerConfig.name?.Clone() ?? layerConfig.id]),
|
||||
layerConfig.GenerateDocumentation([]),
|
||||
"documentation-" + layerConfig.id
|
||||
)
|
||||
}
|
||||
|
||||
private allDocumentationButtons(): BaseUIElement {
|
||||
const layers = this.state.layoutToUse.layers
|
||||
.filter((l) => Constants.priviliged_layers.indexOf(l.id) < 0)
|
||||
.filter((l) => !l.id.startsWith("note_import_"))
|
||||
|
||||
if (layers.length === 1) {
|
||||
return this.documentationButtonFor(layers[0])
|
||||
}
|
||||
return this.viewSelector(
|
||||
new FixedUiElement("Documentation"),
|
||||
"Documentation",
|
||||
new Combine(layers.map((l) => this.documentationButtonFor(l).SetClass("flex flex-col")))
|
||||
)
|
||||
}
|
||||
|
||||
public setup(): void {
|
||||
const state = this.state
|
||||
|
||||
if (this.state.layoutToUse.customCss !== undefined) {
|
||||
if (window.location.pathname.indexOf("index") >= 0) {
|
||||
Utils.LoadCustomCss(this.state.layoutToUse.customCss)
|
||||
}
|
||||
}
|
||||
const map = this.SetupMap()
|
||||
|
||||
Utils.downloadJson("./service-worker-version")
|
||||
.then((data) => console.log("Service worker", data))
|
||||
.catch((_) => console.log("Service worker not active"))
|
||||
|
||||
document.getElementById("centermessage").classList.add("hidden")
|
||||
|
||||
const layers: Record<string, LayerConfig> = {}
|
||||
for (const layer of state.layoutToUse.layers) {
|
||||
layers[layer.id] = layer
|
||||
}
|
||||
|
||||
const self = this
|
||||
const elementsInview = new UIEventSource<
|
||||
{
|
||||
distance: number
|
||||
center: [number, number]
|
||||
element: OsmFeature
|
||||
layer: LayerConfig
|
||||
}[]
|
||||
>([])
|
||||
|
||||
function update() {
|
||||
const mapCenter = <[number, number]>[
|
||||
self.state.locationControl.data.lon,
|
||||
self.state.locationControl.data.lon,
|
||||
]
|
||||
const elements = self.state.featurePipeline
|
||||
.getAllVisibleElementsWithmeta(self.state.currentBounds.data)
|
||||
.map((el) => {
|
||||
const distance = GeoOperations.distanceBetween(el.center, mapCenter)
|
||||
return { ...el, distance }
|
||||
})
|
||||
elements.sort((e0, e1) => e0.distance - e1.distance)
|
||||
elementsInview.setData(elements)
|
||||
}
|
||||
|
||||
map.bounds.addCallbackAndRun(update)
|
||||
state.featurePipeline.newDataLoadedSignal.addCallback(update)
|
||||
state.filteredLayers.addCallbackAndRun((fls) => {
|
||||
for (const fl of fls) {
|
||||
fl.isDisplayed.addCallback(update)
|
||||
fl.appliedFilters.addCallback(update)
|
||||
}
|
||||
})
|
||||
|
||||
const filterView = new Lazy(() => {
|
||||
return new FilterView(state.filteredLayers, state.overlayToggles, state)
|
||||
})
|
||||
const welcome = new Combine([
|
||||
state.layoutToUse.description,
|
||||
state.layoutToUse.descriptionTail,
|
||||
])
|
||||
self.currentView.setData({ title: state.layoutToUse.title, contents: welcome })
|
||||
const filterViewIsOpened = new UIEventSource(false)
|
||||
filterViewIsOpened.addCallback((_) =>
|
||||
self.currentView.setData({ title: "filters", contents: filterView })
|
||||
)
|
||||
|
||||
const newPointIsShown = new UIEventSource(false)
|
||||
const addNewPoint = new SimpleAddUI(
|
||||
new UIEventSource(true),
|
||||
new UIEventSource(undefined),
|
||||
filterViewIsOpened,
|
||||
state,
|
||||
state.locationControl
|
||||
)
|
||||
const addNewPointTitle = "Add a missing point"
|
||||
this.currentView.addCallbackAndRunD((cv) => {
|
||||
newPointIsShown.setData(cv.contents === addNewPoint)
|
||||
})
|
||||
newPointIsShown.addCallbackAndRun((isShown) => {
|
||||
if (isShown) {
|
||||
if (self.currentView.data.contents !== addNewPoint) {
|
||||
self.currentView.setData({ title: addNewPointTitle, contents: addNewPoint })
|
||||
}
|
||||
} else {
|
||||
if (self.currentView.data.contents === addNewPoint) {
|
||||
self.currentView.setData(undefined)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
new Combine([
|
||||
new Combine([
|
||||
this.viewSelector(
|
||||
new Title(state.layoutToUse.title.Clone(), 2),
|
||||
state.layoutToUse.title.Clone(),
|
||||
welcome,
|
||||
"welcome"
|
||||
),
|
||||
map.SetClass("w-full h-64 shrink-0 rounded-lg"),
|
||||
new SearchAndGo(state),
|
||||
this.viewSelector(
|
||||
new Title(
|
||||
new VariableUiElement(
|
||||
elementsInview.map(
|
||||
(elements) => "There are " + elements?.length + " elements in view"
|
||||
)
|
||||
)
|
||||
),
|
||||
"Statistics",
|
||||
new StatisticsPanel(elementsInview, this.state),
|
||||
"statistics"
|
||||
),
|
||||
|
||||
this.viewSelector(new FixedUiElement("Filter"), "Filters", filterView, "filters"),
|
||||
this.viewSelector(
|
||||
new Combine(["Add a missing point"]),
|
||||
addNewPointTitle,
|
||||
addNewPoint
|
||||
),
|
||||
|
||||
new VariableUiElement(
|
||||
elementsInview.map((elements) =>
|
||||
this.mainElementsView(elements).SetClass("block m-2")
|
||||
)
|
||||
).SetClass(
|
||||
"block shrink-2 overflow-x-auto h-full border-2 border-subtle rounded-lg"
|
||||
),
|
||||
this.allDocumentationButtons(),
|
||||
new LanguagePicker(Object.keys(state.layoutToUse.title.translations)).SetClass(
|
||||
"mt-2"
|
||||
),
|
||||
new BackToIndex(),
|
||||
]).SetClass("w-1/2 lg:w-1/4 m-4 flex flex-col shrink-0 grow-0"),
|
||||
new VariableUiElement(
|
||||
this.currentView.map(({ title, contents }) => {
|
||||
return new Combine([
|
||||
new Title(Translations.W(title), 2).SetClass(
|
||||
"shrink-0 border-b-4 border-subtle"
|
||||
),
|
||||
Translations.W(contents).SetClass("shrink-2 overflow-y-auto block"),
|
||||
]).SetClass("flex flex-col h-full")
|
||||
})
|
||||
).SetClass(
|
||||
"w-1/2 lg:w-3/4 m-4 p-2 border-2 border-subtle rounded-xl m-4 ml-0 mr-8 shrink-0 grow-0"
|
||||
),
|
||||
])
|
||||
.SetClass("flex h-full")
|
||||
.AttachTo("leafletDiv")
|
||||
}
|
||||
|
||||
private SetupMap(): MinimapObj & BaseUIElement {
|
||||
const state = this.state
|
||||
|
||||
new ShowDataLayer({
|
||||
leafletMap: state.leafletMap,
|
||||
layerToShow: new LayerConfig(home_location_json, "home_location", true),
|
||||
features: state.homeLocation,
|
||||
state,
|
||||
})
|
||||
|
||||
state.leafletMap.addCallbackAndRunD((_) => {
|
||||
// Lets assume that all showDataLayers are initialized at this point
|
||||
state.selectedElement.ping()
|
||||
State.state.locationControl.ping()
|
||||
return true
|
||||
})
|
||||
|
||||
return state.mainMapObject
|
||||
}
|
||||
}
|
|
@ -171,9 +171,6 @@ export default class DefaultGUI {
|
|||
const state = this.state
|
||||
const guiState = this.guiState
|
||||
|
||||
// Attach the map
|
||||
state.mainMapObject.SetClass("w-full h-full").AttachTo("leafletDiv")
|
||||
|
||||
this.setupClickDialogOnMap(guiState.filterViewIsOpened, state)
|
||||
|
||||
new ShowDataLayer({
|
||||
|
|
|
@ -138,12 +138,6 @@ export default class ConflationChecker
|
|||
location,
|
||||
background,
|
||||
bounds: currentBounds,
|
||||
attribution: new Attribution(
|
||||
location,
|
||||
state.osmConnection.userDetails,
|
||||
undefined,
|
||||
currentBounds
|
||||
),
|
||||
})
|
||||
osmLiveData.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
|
|
|
@ -9,10 +9,6 @@ import { DropDown } from "../Input/DropDown"
|
|||
import { Utils } from "../../Utils"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Loc from "../../Models/Loc"
|
||||
import Minimap from "../Base/Minimap"
|
||||
import Attribution from "../BigComponents/Attribution"
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
|
||||
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
|
@ -21,12 +17,14 @@ import { FlowStep } from "./FlowStep"
|
|||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import Title from "../Base/Title"
|
||||
import CheckBoxes from "../Input/Checkboxes"
|
||||
import AllTagsPanel from "../AllTagsPanel.svelte"
|
||||
import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
|
||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
||||
import { Feature, Point } from "geojson"
|
||||
import DivContainer from "../Base/DivContainer"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
|
||||
class PreviewPanel extends ScrollableFullScreen {
|
||||
constructor(tags: UIEventSource<any>) {
|
||||
|
@ -110,21 +108,11 @@ export class MapPreview
|
|||
|
||||
return matching
|
||||
})
|
||||
const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
|
||||
const background = new UIEventSource<RasterLayerPolygon>(AvailableRasterLayers.osmCarto)
|
||||
const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
|
||||
const currentBounds = new UIEventSource<BBox>(undefined)
|
||||
const map = Minimap.createMiniMap({
|
||||
allowMoving: true,
|
||||
location,
|
||||
background,
|
||||
bounds: currentBounds,
|
||||
attribution: new Attribution(
|
||||
location,
|
||||
state.osmConnection.userDetails,
|
||||
undefined,
|
||||
currentBounds
|
||||
),
|
||||
})
|
||||
const { ui, mapproperties, map } = MapLibreAdaptor.construct()
|
||||
|
||||
const layerControl = new BackgroundMapSwitch(
|
||||
{
|
||||
backgroundLayer: background,
|
||||
|
@ -132,15 +120,14 @@ export class MapPreview
|
|||
},
|
||||
background
|
||||
)
|
||||
map.SetClass("w-full").SetStyle("height: 500px")
|
||||
ui.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
layerPicker.GetValue().addCallbackAndRunD((layerToShow) => {
|
||||
new ShowDataLayer({
|
||||
layerToShow,
|
||||
new ShowDataLayer(map, {
|
||||
layer: layerToShow,
|
||||
zoomToFeatures: true,
|
||||
features: new StaticFeatureSource(matching),
|
||||
leafletMap: map.leafletMap,
|
||||
popup: (tag) => new PreviewPanel(tag),
|
||||
buildPopup: (tag) => new PreviewPanel(tag),
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -171,9 +158,8 @@ export class MapPreview
|
|||
new Title(t.title, 1),
|
||||
layerPicker,
|
||||
new Toggle(t.autodetected.SetClass("thanks"), undefined, autodetected),
|
||||
|
||||
mismatchIndicator,
|
||||
map,
|
||||
ui,
|
||||
new DivContainer("fullscreen"),
|
||||
layerControl,
|
||||
confirm,
|
||||
|
|
|
@ -1,34 +1,56 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MLMap } from "maplibre-gl"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "./MaplibreMap.svelte"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
export interface MapState {
|
||||
/**
|
||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||
*/
|
||||
export class MapLibreAdaptor implements MapProperties {
|
||||
private static maplibre_control_handlers = [
|
||||
"scrollZoom",
|
||||
"boxZoom",
|
||||
"dragRotate",
|
||||
"dragPan",
|
||||
"keyboard",
|
||||
"doubleClickZoom",
|
||||
"touchZoomRotate",
|
||||
]
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
readonly zoom: UIEventSource<number>
|
||||
readonly bounds: Store<BBox>
|
||||
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
|
||||
}
|
||||
export class MapLibreAdaptor implements MapState {
|
||||
readonly maxbounds: UIEventSource<BBox | undefined>
|
||||
readonly allowMoving: UIEventSource<true | boolean | undefined>
|
||||
private readonly _maplibreMap: Store<MLMap>
|
||||
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
readonly zoom: UIEventSource<number>
|
||||
readonly bounds: Store<BBox>
|
||||
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
|
||||
private readonly _bounds: UIEventSource<BBox>
|
||||
|
||||
/**
|
||||
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
||||
* @private
|
||||
*/
|
||||
private _currentRasterLayer: string
|
||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapState, "bounds">>) {
|
||||
|
||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapProperties, "bounds">>) {
|
||||
this._maplibreMap = maplibreMap
|
||||
|
||||
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })
|
||||
this.zoom = state?.zoom ?? new UIEventSource(1)
|
||||
this.zoom.addCallbackAndRunD((z) => {
|
||||
if (z < 0) {
|
||||
this.zoom.setData(0)
|
||||
}
|
||||
if (z > 24) {
|
||||
this.zoom.setData(24)
|
||||
}
|
||||
})
|
||||
this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined)
|
||||
this.allowMoving = state?.allowMoving ?? new UIEventSource(true)
|
||||
this._bounds = new UIEventSource(BBox.global)
|
||||
this.bounds = this._bounds
|
||||
this.rasterLayer =
|
||||
|
@ -38,20 +60,26 @@ export class MapLibreAdaptor implements MapState {
|
|||
maplibreMap.addCallbackAndRunD((map) => {
|
||||
map.on("load", () => {
|
||||
self.setBackground()
|
||||
self.MoveMapToCurrentLoc(self.location.data)
|
||||
self.SetZoom(self.zoom.data)
|
||||
self.setMaxBounds(self.maxbounds.data)
|
||||
self.setAllowMoving(self.allowMoving.data)
|
||||
})
|
||||
self.MoveMapToCurrentLoc(this.location.data)
|
||||
self.SetZoom(this.zoom.data)
|
||||
self.MoveMapToCurrentLoc(self.location.data)
|
||||
self.SetZoom(self.zoom.data)
|
||||
self.setMaxBounds(self.maxbounds.data)
|
||||
self.setAllowMoving(self.allowMoving.data)
|
||||
map.on("moveend", () => {
|
||||
const dt = this.location.data
|
||||
dt.lon = map.getCenter().lng
|
||||
dt.lat = map.getCenter().lat
|
||||
this.location.ping()
|
||||
this.zoom.setData(map.getZoom())
|
||||
this.zoom.setData(Math.round(map.getZoom() * 10) / 10)
|
||||
})
|
||||
})
|
||||
|
||||
this.rasterLayer.addCallback((_) =>
|
||||
self.setBackground().catch((e) => {
|
||||
self.setBackground().catch((_) => {
|
||||
console.error("Could not set background")
|
||||
})
|
||||
)
|
||||
|
@ -60,25 +88,25 @@ export class MapLibreAdaptor implements MapState {
|
|||
self.MoveMapToCurrentLoc(loc)
|
||||
})
|
||||
this.zoom.addCallbackAndRunD((z) => self.SetZoom(z))
|
||||
this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox))
|
||||
this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving))
|
||||
}
|
||||
private SetZoom(z: number) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined || z === undefined) {
|
||||
return
|
||||
}
|
||||
if (map.getZoom() !== z) {
|
||||
map.setZoom(z)
|
||||
}
|
||||
}
|
||||
private MoveMapToCurrentLoc(loc: { lat: number; lon: number }) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined || loc === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const center = map.getCenter()
|
||||
if (center.lng !== loc.lon || center.lat !== loc.lat) {
|
||||
map.setCenter({ lng: loc.lon, lat: loc.lat })
|
||||
/**
|
||||
* Convenience constructor
|
||||
*/
|
||||
public static construct(): {
|
||||
map: Store<MLMap>
|
||||
ui: SvelteUIElement
|
||||
mapproperties: MapProperties
|
||||
} {
|
||||
const mlmap = new UIEventSource<MlMap>(undefined)
|
||||
return {
|
||||
map: mlmap,
|
||||
ui: new SvelteUIElement(MaplibreMap, {
|
||||
map: mlmap,
|
||||
}),
|
||||
mapproperties: new MapLibreAdaptor(mlmap),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,7 +131,6 @@ export class MapLibreAdaptor implements MapState {
|
|||
|
||||
const subdomains = url.match(/\{switch:([a-zA-Z0-9,]*)}/)
|
||||
if (subdomains !== null) {
|
||||
console.log("Found a switch:", subdomains)
|
||||
const options = subdomains[1].split(",")
|
||||
const option = options[Math.floor(Math.random() * options.length)]
|
||||
url = url.replace(subdomains[0], option)
|
||||
|
@ -112,6 +139,28 @@ export class MapLibreAdaptor implements MapState {
|
|||
return url
|
||||
}
|
||||
|
||||
private SetZoom(z: number) {
|
||||
const map = this._maplibreMap.data
|
||||
if (!map || z === undefined) {
|
||||
return
|
||||
}
|
||||
if (Math.abs(map.getZoom() - z) > 0.01) {
|
||||
map.setZoom(z)
|
||||
}
|
||||
}
|
||||
|
||||
private MoveMapToCurrentLoc(loc: { lat: number; lon: number }) {
|
||||
const map = this._maplibreMap.data
|
||||
if (!map || loc === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const center = map.getCenter()
|
||||
if (center.lng !== loc.lon || center.lat !== loc.lat) {
|
||||
map.setCenter({ lng: loc.lon, lat: loc.lat })
|
||||
}
|
||||
}
|
||||
|
||||
private async awaitStyleIsLoaded(): Promise<void> {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
|
@ -125,7 +174,6 @@ export class MapLibreAdaptor implements MapState {
|
|||
private removeCurrentLayer(map: MLMap) {
|
||||
if (this._currentRasterLayer) {
|
||||
// hide the previous layer
|
||||
console.log("Removing previous layer", this._currentRasterLayer)
|
||||
map.removeLayer(this._currentRasterLayer)
|
||||
map.removeSource(this._currentRasterLayer)
|
||||
}
|
||||
|
@ -185,4 +233,32 @@ export class MapLibreAdaptor implements MapState {
|
|||
this.removeCurrentLayer(map)
|
||||
this._currentRasterLayer = background?.id
|
||||
}
|
||||
|
||||
private setMaxBounds(bbox: undefined | BBox) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
return
|
||||
}
|
||||
if (bbox) {
|
||||
map.setMaxBounds(bbox.toLngLat())
|
||||
} else {
|
||||
map.setMaxBounds(null)
|
||||
}
|
||||
}
|
||||
|
||||
private setAllowMoving(allow: true | boolean | undefined) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
return
|
||||
}
|
||||
if (allow === false) {
|
||||
for (const id of MapLibreAdaptor.maplibre_control_handlers) {
|
||||
map[id].disable()
|
||||
}
|
||||
} else {
|
||||
for (const id of MapLibreAdaptor.maplibre_control_handlers) {
|
||||
map[id].enable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
import { Map } from "@onsvisual/svelte-maps";
|
||||
import type { Map as MaplibreMap } from "maplibre-gl";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type Loc from "../../Models/Loc";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -30,7 +28,6 @@
|
|||
<main>
|
||||
<Map bind:center={center}
|
||||
bind:map={$map}
|
||||
controls="true"
|
||||
id="map" location={{lng: 0, lat: 0, zoom: 0}} maxzoom=24 style={styleUrl} />
|
||||
</main>
|
||||
|
||||
|
|
|
@ -1,108 +1,294 @@
|
|||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import type { Map as MlMap } from "maplibre-gl"
|
||||
import { Marker } from "maplibre-gl"
|
||||
import { ShowDataLayerOptions } from "../ShowDataLayer/ShowDataLayerOptions"
|
||||
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
|
||||
import { OsmFeature, OsmTags } from "../../Models/OsmFeature"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
|
||||
import { Feature, LineString } from "geojson"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import * as range_layer from "../../assets/layers/range/range.json"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
class PointRenderingLayer {
|
||||
private readonly _config: PointRenderingConfig
|
||||
private readonly _fetchStore?: (id: string) => Store<OsmTags>
|
||||
private readonly _map: MlMap
|
||||
private readonly _onClick: (id: string) => void
|
||||
private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>()
|
||||
|
||||
constructor(
|
||||
map: MlMap,
|
||||
features: FeatureSource,
|
||||
config: PointRenderingConfig,
|
||||
fetchStore?: (id: string) => Store<OsmTags>
|
||||
visibility?: Store<boolean>,
|
||||
fetchStore?: (id: string) => Store<OsmTags>,
|
||||
onClick?: (id: string) => void
|
||||
) {
|
||||
this._config = config
|
||||
this._map = map
|
||||
this._fetchStore = fetchStore
|
||||
const cache: Map<string, Marker> = new Map<string, Marker>()
|
||||
this._onClick = onClick
|
||||
const self = this
|
||||
features.features.addCallbackAndRunD((features) => {
|
||||
const unseenKeys = new Set(cache.keys())
|
||||
for (const { feature } of features) {
|
||||
const id = feature.properties.id
|
||||
|
||||
features.features.addCallbackAndRunD((features) => self.updateFeatures(features))
|
||||
visibility?.addCallbackAndRunD((visible) => self.setVisibility(visible))
|
||||
}
|
||||
|
||||
private updateFeatures(features: Feature[]) {
|
||||
const cache = this._allMarkers
|
||||
const unseenKeys = new Set(cache.keys())
|
||||
for (const location of this._config.location) {
|
||||
for (const feature of features) {
|
||||
const loc = GeoOperations.featureToCoordinateWithRenderingType(
|
||||
<any>feature,
|
||||
location
|
||||
)
|
||||
if (loc === undefined) {
|
||||
continue
|
||||
}
|
||||
const id = feature.properties.id + "-" + location
|
||||
unseenKeys.delete(id)
|
||||
const loc = GeoOperations.centerpointCoordinates(feature)
|
||||
|
||||
if (cache.has(id)) {
|
||||
console.log("Not creating a marker for ", id)
|
||||
const cached = cache.get(id)
|
||||
const oldLoc = cached.getLngLat()
|
||||
console.log("OldLoc vs newLoc", oldLoc, loc)
|
||||
if (loc[0] !== oldLoc.lng && loc[1] !== oldLoc.lat) {
|
||||
cached.setLngLat(loc)
|
||||
console.log("MOVED")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
console.log("Creating a marker for ", id)
|
||||
const marker = self.addPoint(feature)
|
||||
const marker = this.addPoint(feature, loc)
|
||||
cache.set(id, marker)
|
||||
}
|
||||
}
|
||||
|
||||
for (const unseenKey of unseenKeys) {
|
||||
cache.get(unseenKey).remove()
|
||||
cache.delete(unseenKey)
|
||||
}
|
||||
})
|
||||
for (const unseenKey of unseenKeys) {
|
||||
cache.get(unseenKey).remove()
|
||||
cache.delete(unseenKey)
|
||||
}
|
||||
}
|
||||
|
||||
private addPoint(feature: OsmFeature): Marker {
|
||||
private setVisibility(visible: boolean) {
|
||||
for (const marker of this._allMarkers.values()) {
|
||||
if (visible) {
|
||||
marker.getElement().classList.remove("hidden")
|
||||
} else {
|
||||
marker.getElement().classList.add("hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addPoint(feature: Feature, loc: [number, number]): Marker {
|
||||
let store: Store<OsmTags>
|
||||
if (this._fetchStore) {
|
||||
store = this._fetchStore(feature.properties.id)
|
||||
} else {
|
||||
store = new ImmutableStore(feature.properties)
|
||||
store = new ImmutableStore(<OsmTags>feature.properties)
|
||||
}
|
||||
const { html, iconAnchor } = this._config.GenerateLeafletStyle(store, true)
|
||||
const { html, iconAnchor } = this._config.RenderIcon(store, true)
|
||||
html.SetClass("marker")
|
||||
const el = html.ConstructElement()
|
||||
|
||||
el.addEventListener("click", function () {
|
||||
window.alert("Hello world!")
|
||||
})
|
||||
if (this._onClick) {
|
||||
const self = this
|
||||
el.addEventListener("click", function () {
|
||||
self._onClick(feature.properties.id)
|
||||
})
|
||||
}
|
||||
|
||||
return new Marker(el)
|
||||
.setLngLat(GeoOperations.centerpointCoordinates(feature))
|
||||
.setOffset(iconAnchor)
|
||||
.addTo(this._map)
|
||||
return new Marker(el).setLngLat(loc).setOffset(iconAnchor).addTo(this._map)
|
||||
}
|
||||
}
|
||||
|
||||
export class ShowDataLayer {
|
||||
class LineRenderingLayer {
|
||||
/**
|
||||
* These are dynamic properties
|
||||
* @private
|
||||
*/
|
||||
private static readonly lineConfigKeys = [
|
||||
"color",
|
||||
"width",
|
||||
"lineCap",
|
||||
"offset",
|
||||
"fill",
|
||||
"fillColor",
|
||||
]
|
||||
private readonly _map: MlMap
|
||||
private readonly _config: LineRenderingConfig
|
||||
private readonly _visibility?: Store<boolean>
|
||||
private readonly _fetchStore?: (id: string) => Store<OsmTags>
|
||||
private readonly _onClick?: (id: string) => void
|
||||
private readonly _layername: string
|
||||
|
||||
constructor(
|
||||
map: MlMap,
|
||||
features: FeatureSource,
|
||||
layername: string,
|
||||
config: LineRenderingConfig,
|
||||
visibility?: Store<boolean>,
|
||||
fetchStore?: (id: string) => Store<OsmTags>,
|
||||
onClick?: (id: string) => void
|
||||
) {
|
||||
this._layername = layername
|
||||
this._map = map
|
||||
this._config = config
|
||||
this._visibility = visibility
|
||||
this._fetchStore = fetchStore
|
||||
this._onClick = onClick
|
||||
const self = this
|
||||
features.features.addCallbackAndRunD((features) => self.update(features))
|
||||
}
|
||||
|
||||
private async update(features: Feature[]) {
|
||||
const map = this._map
|
||||
while (!map.isStyleLoaded()) {
|
||||
await Utils.waitFor(100)
|
||||
}
|
||||
map.addSource(this._layername, {
|
||||
type: "geojson",
|
||||
data: {
|
||||
type: "FeatureCollection",
|
||||
features,
|
||||
},
|
||||
promoteId: "id",
|
||||
})
|
||||
for (let i = 0; i < features.length; i++) {
|
||||
const feature = features[i]
|
||||
const id = feature.properties.id ?? "" + i
|
||||
const tags = this._fetchStore(id)
|
||||
tags.addCallbackAndRunD((properties) => {
|
||||
const config = this._config
|
||||
|
||||
const calculatedProps = {}
|
||||
for (const key of LineRenderingLayer.lineConfigKeys) {
|
||||
const v = config[key]?.GetRenderValue(properties)?.Subs(properties).txt
|
||||
calculatedProps[key] = v
|
||||
}
|
||||
|
||||
map.setFeatureState({ source: this._layername, id }, calculatedProps)
|
||||
})
|
||||
}
|
||||
|
||||
map.addLayer({
|
||||
source: this._layername,
|
||||
id: this._layername + "_line",
|
||||
type: "line",
|
||||
filter: ["in", ["geometry-type"], ["literal", ["LineString", "MultiLineString"]]],
|
||||
layout: {},
|
||||
paint: {
|
||||
"line-color": ["feature-state", "color"],
|
||||
"line-width": ["feature-state", "width"],
|
||||
"line-offset": ["feature-state", "offset"],
|
||||
},
|
||||
})
|
||||
|
||||
/*[
|
||||
"color",
|
||||
"width",
|
||||
"dashArray",
|
||||
"lineCap",
|
||||
"offset",
|
||||
"fill",
|
||||
"fillColor",
|
||||
]*/
|
||||
map.addLayer({
|
||||
source: this._layername,
|
||||
id: this._layername + "_polygon",
|
||||
type: "fill",
|
||||
filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]],
|
||||
layout: {},
|
||||
paint: {
|
||||
"fill-color": ["feature-state", "fillColor"],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default class ShowDataLayer {
|
||||
private readonly _map: Store<MlMap>
|
||||
private _options: ShowDataLayerOptions & { layer: LayerConfig }
|
||||
private readonly _options: ShowDataLayerOptions & { layer: LayerConfig }
|
||||
private readonly _popupCache: Map<string, ScrollableFullScreen>
|
||||
|
||||
constructor(map: Store<MlMap>, options: ShowDataLayerOptions & { layer: LayerConfig }) {
|
||||
this._map = map
|
||||
this._options = options
|
||||
this._popupCache = new Map()
|
||||
const self = this
|
||||
map.addCallbackAndRunD((map) => self.initDrawFeatures(map))
|
||||
}
|
||||
|
||||
private initDrawFeatures(map: MlMap) {
|
||||
for (const pointRenderingConfig of this._options.layer.mapRendering) {
|
||||
new PointRenderingLayer(
|
||||
map,
|
||||
this._options.features,
|
||||
pointRenderingConfig,
|
||||
this._options.fetchStore
|
||||
)
|
||||
private static rangeLayer = new LayerConfig(
|
||||
<LayerConfigJson>range_layer,
|
||||
"ShowDataLayer.ts:range.json"
|
||||
)
|
||||
|
||||
public static showRange(
|
||||
map: Store<MlMap>,
|
||||
features: FeatureSource,
|
||||
doShowLayer?: Store<boolean>
|
||||
): ShowDataLayer {
|
||||
return new ShowDataLayer(map, {
|
||||
layer: ShowDataLayer.rangeLayer,
|
||||
features,
|
||||
doShowLayer,
|
||||
})
|
||||
}
|
||||
|
||||
private openOrReusePopup(id: string): void {
|
||||
if (this._popupCache.has(id)) {
|
||||
this._popupCache.get(id).Activate()
|
||||
return
|
||||
}
|
||||
const tags = this._options.fetchStore(id)
|
||||
if (!tags) {
|
||||
return
|
||||
}
|
||||
const popup = this._options.buildPopup(tags, this._options.layer)
|
||||
this._popupCache.set(id, popup)
|
||||
popup.Activate()
|
||||
}
|
||||
|
||||
private zoomToCurrentFeatures(map: MlMap) {
|
||||
if (this._options.zoomToFeatures) {
|
||||
const features = this._options.features.features.data
|
||||
const bbox = BBox.bboxAroundAll(features.map((f) => BBox.get(f.feature)))
|
||||
const bbox = BBox.bboxAroundAll(features.map(BBox.get))
|
||||
map.fitBounds(bbox.toLngLat(), {
|
||||
padding: { top: 10, bottom: 10, left: 10, right: 10 },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private initDrawFeatures(map: MlMap) {
|
||||
const { features, doShowLayer, fetchStore, buildPopup } = this._options
|
||||
const onClick = buildPopup === undefined ? undefined : (id) => this.openOrReusePopup(id)
|
||||
for (const lineRenderingConfig of this._options.layer.lineRendering) {
|
||||
new LineRenderingLayer(
|
||||
map,
|
||||
features,
|
||||
"test",
|
||||
lineRenderingConfig,
|
||||
doShowLayer,
|
||||
fetchStore,
|
||||
onClick
|
||||
)
|
||||
}
|
||||
|
||||
for (const pointRenderingConfig of this._options.layer.mapRendering) {
|
||||
new PointRenderingLayer(
|
||||
map,
|
||||
features,
|
||||
pointRenderingConfig,
|
||||
doShowLayer,
|
||||
fetchStore,
|
||||
onClick
|
||||
)
|
||||
}
|
||||
features.features.addCallbackAndRunD((_) => this.zoomToCurrentFeatures(map))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,5 +33,5 @@ export interface ShowDataLayerOptions {
|
|||
/**
|
||||
* Function which fetches the relevant store
|
||||
*/
|
||||
fetchStore?: (id: string) => Store<OsmTags>
|
||||
fetchStore?: (id: string) => UIEventSource<OsmTags>
|
||||
}
|
|
@ -6,18 +6,21 @@ import ShowDataLayer from "./ShowDataLayer"
|
|||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
export default class ShowDataMultiLayer {
|
||||
constructor(options: ShowDataLayerOptions & { layers: Store<FilteredLayer[]> }) {
|
||||
constructor(
|
||||
map: Store<MlMap>,
|
||||
options: ShowDataLayerOptions & { layers: Store<FilteredLayer[]> }
|
||||
) {
|
||||
new PerLayerFeatureSourceSplitter(
|
||||
options.layers,
|
||||
(perLayer) => {
|
||||
const newOptions = {
|
||||
...options,
|
||||
layerToShow: perLayer.layer.layerDef,
|
||||
layer: perLayer.layer.layerDef,
|
||||
features: perLayer,
|
||||
}
|
||||
new ShowDataLayer(newOptions)
|
||||
new ShowDataLayer(map, newOptions)
|
||||
},
|
||||
options.features
|
||||
)
|
|
@ -13,13 +13,13 @@ import Toggle from "../Input/Toggle"
|
|||
import SimpleAddUI, { PresetInfo } from "../BigComponents/SimpleAddUI"
|
||||
import Img from "../Base/Img"
|
||||
import Title from "../Base/Title"
|
||||
import { GlobalFilter } from "../../Logic/State/MapState"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import { WayId } from "../../Models/OsmFeature"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { Feature } from "geojson";
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers";
|
||||
import { Feature } from "geojson"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { GlobalFilter } from "../../Logic/State/GlobalFilter"
|
||||
|
||||
export default class ConfirmLocationOfPoint extends Combine {
|
||||
constructor(
|
||||
|
@ -69,7 +69,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
let snapToFeatures: UIEventSource<Feature[]> = undefined
|
||||
let mapBounds: UIEventSource<BBox> = undefined
|
||||
if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) {
|
||||
snapToFeatures = new UIEventSource< Feature[]>([])
|
||||
snapToFeatures = new UIEventSource<Feature[]>([])
|
||||
mapBounds = new UIEventSource<BBox>(undefined)
|
||||
}
|
||||
|
||||
|
@ -110,9 +110,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
console.log("Snapping to", layerId)
|
||||
state.featurePipeline
|
||||
.GetFeaturesWithin(layerId, bbox)
|
||||
?.forEach((feats) =>
|
||||
allFeatures.push(...<any[]>feats)
|
||||
)
|
||||
?.forEach((feats) => allFeatures.push(...(<any[]>feats)))
|
||||
})
|
||||
console.log("Snapping to", allFeatures)
|
||||
snapToFeatures.setData(allFeatures)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
||||
import Table from "./Base/Table"
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Table from "../Base/Table"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
//Svelte props
|
||||
export let tags: UIEventSource<any>
|
|
@ -45,6 +45,7 @@ import { ElementStorage } from "../../Logic/ElementStorage"
|
|||
import Hash from "../../Logic/Web/Hash"
|
||||
import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
import Maproulette from "../../Logic/Maproulette";
|
||||
|
||||
/**
|
||||
* A helper class for the various import-flows.
|
||||
|
@ -720,7 +721,7 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
)
|
||||
} else {
|
||||
console.log("Marking maproulette task as fixed")
|
||||
await state.maprouletteConnection.closeTask(Number(maproulette_id))
|
||||
await Maproulette.singleton.closeTask(Number(maproulette_id))
|
||||
originalFeatureTags.data["mr_taskStatus"] = "Fixed"
|
||||
originalFeatureTags.ping()
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import Minimap from "../Base/Minimap"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import left_right_style_json from "../../assets/layers/left_right_style/left_right_style.json"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { SpecialVisualization } from "../SpecialVisualization"
|
||||
|
||||
export class SidedMinimap implements SpecialVisualization {
|
||||
funcName = "sided_minimap"
|
||||
docs =
|
||||
"A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced"
|
||||
args = [
|
||||
{
|
||||
doc: "The side to show, either `left` or `right`",
|
||||
name: "side",
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
example = "`{sided_minimap(left)}`"
|
||||
|
||||
public constr(state, tagSource, args) {
|
||||
const properties = tagSource.data
|
||||
const locationSource = new UIEventSource<Loc>({
|
||||
lat: Number(properties._lat),
|
||||
lon: Number(properties._lon),
|
||||
zoom: 18,
|
||||
})
|
||||
const minimap = Minimap.createMiniMap({
|
||||
background: state.backgroundLayer,
|
||||
location: locationSource,
|
||||
allowMoving: false,
|
||||
})
|
||||
const side = args[0]
|
||||
const feature = state.allElements.ContainingFeatures.get(tagSource.data.id)
|
||||
const copy = { ...feature }
|
||||
copy.properties = {
|
||||
id: side,
|
||||
}
|
||||
new ShowDataLayer({
|
||||
leafletMap: minimap["leafletMap"],
|
||||
zoomToFeatures: true,
|
||||
layerToShow: new LayerConfig(left_right_style_json, "all_known_layers", true),
|
||||
features: StaticFeatureSource.fromGeojson([copy]),
|
||||
state,
|
||||
})
|
||||
|
||||
minimap.SetStyle("overflow: hidden; pointer-events: none;")
|
||||
return minimap
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* The data layer shows all the given geojson elements with the appropriate icon etc
|
||||
*/
|
||||
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
|
||||
export default class ShowDataLayer {
|
||||
public static actualContstructor: (
|
||||
options: ShowDataLayerOptions & { layerToShow: LayerConfig }
|
||||
) => void = undefined
|
||||
|
||||
/**
|
||||
* Creates a datalayer.
|
||||
*
|
||||
* If 'createPopup' is set, this function is called every time that 'popupOpen' is called
|
||||
* @param options
|
||||
*/
|
||||
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
|
||||
if (ShowDataLayer.actualContstructor === undefined) {
|
||||
console.error(
|
||||
"Show data layer is called, but it isn't initialized yet. Call ` ShowDataLayer.actualContstructor = (options => new ShowDataLayerImplementation(options)) ` somewhere, e.g. in your init"
|
||||
)
|
||||
return
|
||||
}
|
||||
ShowDataLayer.actualContstructor(options)
|
||||
}
|
||||
}
|
|
@ -1,407 +0,0 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||
import { ElementStorage } from "../../Logic/ElementStorage"
|
||||
import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import { LeafletMouseEvent, PathOptions } from "leaflet"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { Utils } from "../../Utils"
|
||||
/*
|
||||
// import 'leaflet-polylineoffset';
|
||||
We don't actually import it here. It is imported in the 'MinimapImplementation'-class, which'll result in a patched 'L' object.
|
||||
Even though actually importing this here would seem cleaner, we don't do this as this breaks some scripts:
|
||||
- Scripts are ran in ts-node
|
||||
- ts-node doesn't define the 'window'-object
|
||||
- Importing this will execute some code which needs the window object
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* The data layer shows all the given geojson elements with the appropriate icon etc
|
||||
*/
|
||||
export default class ShowDataLayerImplementation {
|
||||
private static dataLayerIds = 0
|
||||
private readonly _leafletMap: Store<L.Map>
|
||||
private readonly _enablePopups: boolean
|
||||
private readonly _features: RenderingMultiPlexerFeatureSource
|
||||
private readonly _layerToShow: LayerConfig
|
||||
private readonly _selectedElement: UIEventSource<any>
|
||||
private readonly allElements: ElementStorage
|
||||
// Used to generate a fresh ID when needed
|
||||
private _cleanCount = 0
|
||||
private geoLayer = undefined
|
||||
|
||||
/**
|
||||
* A collection of functions to call when the current geolayer is unregistered
|
||||
*/
|
||||
private unregister: (() => void)[] = []
|
||||
private isDirty = false
|
||||
/**
|
||||
* If the selected element triggers, this is used to lookup the correct layer and to open the popup
|
||||
* Used to avoid a lot of callbacks on the selected element
|
||||
*
|
||||
* Note: the key of this dictionary is 'feature.properties.id+features.geometry.type' as one feature might have multiple presentations
|
||||
* @private
|
||||
*/
|
||||
private readonly leafletLayersPerId = new Map<
|
||||
string,
|
||||
{ feature: any; activateFunc: (event: LeafletMouseEvent) => void }
|
||||
>()
|
||||
|
||||
private readonly showDataLayerid: number
|
||||
private readonly createPopup: (
|
||||
tags: UIEventSource<any>,
|
||||
layer: LayerConfig
|
||||
) => ScrollableFullScreen
|
||||
|
||||
/**
|
||||
* Creates a datalayer.
|
||||
*
|
||||
* If 'createPopup' is set, this function is called every time that 'popupOpen' is called
|
||||
* @param options
|
||||
*/
|
||||
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
|
||||
this._leafletMap = options.leafletMap
|
||||
this.showDataLayerid = ShowDataLayerImplementation.dataLayerIds
|
||||
ShowDataLayerImplementation.dataLayerIds++
|
||||
if (options.features === undefined) {
|
||||
console.error("Invalid ShowDataLayer invocation: options.features is undefed")
|
||||
throw "Invalid ShowDataLayer invocation: options.features is undefed"
|
||||
}
|
||||
this._features = new RenderingMultiPlexerFeatureSource(
|
||||
options.features,
|
||||
options.layerToShow
|
||||
)
|
||||
this._layerToShow = options.layerToShow
|
||||
this._selectedElement = options.selectedElement
|
||||
this.allElements = options.state?.allElements
|
||||
this.createPopup = undefined
|
||||
this._enablePopups = options.popup !== undefined
|
||||
if (options.popup !== undefined) {
|
||||
this.createPopup = options.popup
|
||||
}
|
||||
const self = this
|
||||
|
||||
options.leafletMap.addCallback(() => {
|
||||
return self.update(options)
|
||||
})
|
||||
|
||||
this._features.features.addCallback((_) => self.update(options))
|
||||
options.doShowLayer?.addCallback((doShow) => {
|
||||
const mp = options.leafletMap.data
|
||||
if (mp === null) {
|
||||
self.Destroy()
|
||||
return true
|
||||
}
|
||||
if (mp == undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (doShow) {
|
||||
if (self.isDirty) {
|
||||
return self.update(options)
|
||||
} else {
|
||||
mp.addLayer(this.geoLayer)
|
||||
}
|
||||
} else {
|
||||
if (this.geoLayer !== undefined) {
|
||||
mp.removeLayer(this.geoLayer)
|
||||
this.unregister.forEach((f) => f())
|
||||
this.unregister = []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this._selectedElement?.addCallbackAndRunD((selected) => {
|
||||
if (selected === undefined) {
|
||||
ScrollableFullScreen.collapse()
|
||||
return
|
||||
}
|
||||
self.openPopupOfSelectedElement(selected)
|
||||
})
|
||||
|
||||
this.update(options)
|
||||
}
|
||||
|
||||
private Destroy() {
|
||||
this.unregister.forEach((f) => f())
|
||||
}
|
||||
|
||||
private openPopupOfSelectedElement(selected) {
|
||||
if (selected === undefined) {
|
||||
return
|
||||
}
|
||||
if (this._leafletMap.data === undefined) {
|
||||
return
|
||||
}
|
||||
const v = this.leafletLayersPerId.get(selected.properties.id)
|
||||
if (v === undefined) {
|
||||
return
|
||||
}
|
||||
const feature = v.feature
|
||||
if (selected.properties.id !== feature.properties.id) {
|
||||
return
|
||||
}
|
||||
|
||||
if (feature.id !== feature.properties.id) {
|
||||
// Probably a feature which has renamed
|
||||
// the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too
|
||||
console.log("Not opening the popup for", feature, "as probably renamed")
|
||||
return
|
||||
}
|
||||
v.activateFunc(null)
|
||||
}
|
||||
|
||||
private update(options: ShowDataLayerOptions): boolean {
|
||||
if (this._features.features.data === undefined) {
|
||||
return
|
||||
}
|
||||
this.isDirty = true
|
||||
if (options?.doShowLayer?.data === false) {
|
||||
return
|
||||
}
|
||||
const mp = options.leafletMap.data
|
||||
|
||||
if (mp === null) {
|
||||
return true // Unregister as the map has been destroyed
|
||||
}
|
||||
if (mp === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this._cleanCount++
|
||||
// clean all the old stuff away, if any
|
||||
if (this.geoLayer !== undefined) {
|
||||
mp.removeLayer(this.geoLayer)
|
||||
}
|
||||
|
||||
const self = this
|
||||
|
||||
this.geoLayer = new L.LayerGroup()
|
||||
|
||||
const selfLayer = this.geoLayer
|
||||
const allFeats = this._features.features.data
|
||||
for (const feat of allFeats) {
|
||||
if (feat === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Why not one geojson layer with _all_ features, and attaching a right-click onto every feature individually?
|
||||
// Because that somehow doesn't work :(
|
||||
const feature = feat
|
||||
const geojsonLayer = L.geoJSON(feature, {
|
||||
style: (feature) => <PathOptions>self.createStyleFor(feature),
|
||||
pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng),
|
||||
onEachFeature: (feature, leafletLayer) =>
|
||||
self.postProcessFeature(feature, leafletLayer),
|
||||
})
|
||||
if (feature.geometry.type === "Point") {
|
||||
geojsonLayer.on({
|
||||
contextmenu: (e) => {
|
||||
const o = self.leafletLayersPerId.get(feature?.properties?.id)
|
||||
o?.activateFunc(<LeafletMouseEvent>e)
|
||||
Utils.preventDefaultOnMouseEvent(e.originalEvent)
|
||||
},
|
||||
dblclick: (e) => {
|
||||
const o = self.leafletLayersPerId.get(feature?.properties?.id)
|
||||
o?.activateFunc(<LeafletMouseEvent>e)
|
||||
Utils.preventDefaultOnMouseEvent(e.originalEvent)
|
||||
},
|
||||
})
|
||||
}
|
||||
this.geoLayer.addLayer(geojsonLayer)
|
||||
try {
|
||||
if (feat.geometry.type === "LineString") {
|
||||
const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates)
|
||||
const tagsSource =
|
||||
this.allElements?.addOrGetElement(feat) ??
|
||||
new UIEventSource<any>(feat.properties)
|
||||
let offsettedLine
|
||||
tagsSource
|
||||
.map((tags) =>
|
||||
this._layerToShow.lineRendering[
|
||||
feat.lineRenderingIndex
|
||||
].GenerateLeafletStyle(tags)
|
||||
)
|
||||
.withEqualityStabilized((a, b) => {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
if (a === undefined || b === undefined) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
a.offset === b.offset &&
|
||||
a.color === b.color &&
|
||||
a.weight === b.weight &&
|
||||
a.dashArray === b.dashArray
|
||||
)
|
||||
})
|
||||
.addCallbackAndRunD((lineStyle) => {
|
||||
if (offsettedLine !== undefined) {
|
||||
self.geoLayer.removeLayer(offsettedLine)
|
||||
}
|
||||
// @ts-ignore
|
||||
offsettedLine = L.polyline(coords, lineStyle)
|
||||
this.postProcessFeature(feat, offsettedLine)
|
||||
offsettedLine.addTo(this.geoLayer)
|
||||
|
||||
// If 'self.geoLayer' is not the same as the layer the feature is added to, we can safely remove this callback
|
||||
return self.geoLayer !== selfLayer
|
||||
})
|
||||
} else {
|
||||
geojsonLayer.addData(feat)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Could not add ",
|
||||
feat,
|
||||
"to the geojson layer in leaflet due to",
|
||||
e,
|
||||
e.stack
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if ((options.zoomToFeatures ?? false) && allFeats.length > 0) {
|
||||
let bound = undefined
|
||||
for (const feat of allFeats) {
|
||||
const fbound = BBox.get(feat)
|
||||
bound = bound?.unionWith(fbound) ?? fbound
|
||||
}
|
||||
if (bound !== undefined) {
|
||||
mp.fitBounds(bound?.toLeaflet(), { animate: false })
|
||||
}
|
||||
}
|
||||
|
||||
if (options.doShowLayer?.data ?? true) {
|
||||
mp.addLayer(this.geoLayer)
|
||||
}
|
||||
this.isDirty = false
|
||||
this.openPopupOfSelectedElement(this._selectedElement?.data)
|
||||
}
|
||||
|
||||
private createStyleFor(feature) {
|
||||
const tagsSource =
|
||||
this.allElements?.addOrGetElement(feature) ?? new UIEventSource<any>(feature.properties)
|
||||
// Every object is tied to exactly one layer
|
||||
const layer = this._layerToShow
|
||||
|
||||
const pointRenderingIndex = feature.pointRenderingIndex
|
||||
const lineRenderingIndex = feature.lineRenderingIndex
|
||||
|
||||
if (pointRenderingIndex !== undefined) {
|
||||
const style = layer.mapRendering[pointRenderingIndex].GenerateLeafletStyle(
|
||||
tagsSource,
|
||||
this._enablePopups
|
||||
)
|
||||
return {
|
||||
icon: style,
|
||||
}
|
||||
}
|
||||
if (lineRenderingIndex !== undefined) {
|
||||
return layer.lineRendering[lineRenderingIndex].GenerateLeafletStyle(tagsSource.data)
|
||||
}
|
||||
|
||||
throw "Neither lineRendering nor mapRendering defined for " + feature
|
||||
}
|
||||
|
||||
private pointToLayer(feature, latLng): L.Layer {
|
||||
// Leaflet cannot handle geojson points natively
|
||||
// We have to convert them to the appropriate icon
|
||||
// Click handling is done in the next step
|
||||
const layer: LayerConfig = this._layerToShow
|
||||
if (layer === undefined) {
|
||||
return
|
||||
}
|
||||
let tagSource =
|
||||
this.allElements?.getEventSourceById(feature.properties.id) ??
|
||||
new UIEventSource<any>(feature.properties)
|
||||
const clickable =
|
||||
!(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) &&
|
||||
this._enablePopups
|
||||
let style: any = layer.mapRendering[feature.pointRenderingIndex].GenerateLeafletStyle(
|
||||
tagSource,
|
||||
clickable
|
||||
)
|
||||
const baseElement = style.html
|
||||
if (!this._enablePopups) {
|
||||
baseElement.SetStyle("cursor: initial !important")
|
||||
}
|
||||
style.html = style.html.ConstructElement()
|
||||
return L.marker(latLng, {
|
||||
icon: L.divIcon(style),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function which, for the given feature, will open the featureInfoBox (and lazyly create it)
|
||||
* This function is cached
|
||||
* @param feature
|
||||
* @param key
|
||||
* @param layer
|
||||
* @private
|
||||
*/
|
||||
private createActivateFunction(feature, key: string, layer: LayerConfig): (event) => void {
|
||||
if (this.leafletLayersPerId.has(key)) {
|
||||
return this.leafletLayersPerId.get(key).activateFunc
|
||||
}
|
||||
|
||||
let infobox: ScrollableFullScreen = undefined
|
||||
const self = this
|
||||
|
||||
function activate(event: LeafletMouseEvent) {
|
||||
Utils.preventDefaultOnMouseEvent(event)
|
||||
if (infobox === undefined) {
|
||||
const tags =
|
||||
self.allElements?.getEventSourceById(key) ??
|
||||
new UIEventSource<any>(feature.properties)
|
||||
infobox = self.createPopup(tags, layer)
|
||||
|
||||
self.unregister.push(() => {
|
||||
console.log("Destroying infobox")
|
||||
infobox.Destroy()
|
||||
})
|
||||
}
|
||||
infobox.Activate()
|
||||
self._selectedElement.setData(
|
||||
self.allElements.ContainingFeatures.get(feature.id) ?? feature
|
||||
)
|
||||
}
|
||||
return activate
|
||||
}
|
||||
/**
|
||||
* Post processing - basically adding the popup
|
||||
* @param feature
|
||||
* @param leafletLayer
|
||||
* @private
|
||||
*/
|
||||
private postProcessFeature(feature, leafletLayer: L.Evented) {
|
||||
const layer: LayerConfig = this._layerToShow
|
||||
if (layer.title === undefined || !this._enablePopups) {
|
||||
// No popup action defined -> Don't do anything
|
||||
// or probably a map in the popup - no popups needed!
|
||||
return
|
||||
}
|
||||
const key = feature.properties.id
|
||||
const activate = this.createActivateFunction(feature, key, layer)
|
||||
|
||||
// We also have to open on rightclick, doubleclick, ... as users sometimes do this. See #1219
|
||||
leafletLayer.on({
|
||||
dblclick: activate,
|
||||
contextmenu: activate,
|
||||
// click: activate,
|
||||
})
|
||||
leafletLayer.addEventListener("click", activate)
|
||||
// Add the feature to the index to open the popup when needed
|
||||
this.leafletLayersPerId.set(key, {
|
||||
feature: feature,
|
||||
activateFunc: activate,
|
||||
})
|
||||
if (Hash.hash.data === key) {
|
||||
activate(null)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import FeatureSource, { Tiled } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ShowDataLayer from "./ShowDataLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import clusterstyle from "../../assets/layers/cluster_style/cluster_style.json"
|
||||
|
||||
export default class ShowTileInfo {
|
||||
public static readonly styling = new LayerConfig(clusterstyle, "ShowTileInfo", true)
|
||||
|
||||
constructor(
|
||||
options: {
|
||||
source: FeatureSource & Tiled
|
||||
leafletMap: UIEventSource<any>
|
||||
layer?: LayerConfig
|
||||
doShowLayer?: UIEventSource<boolean>
|
||||
},
|
||||
state
|
||||
) {
|
||||
const source = options.source
|
||||
const metaFeature: Store<{ feature; freshness: Date }[]> = source.features.map(
|
||||
(features) => {
|
||||
const bbox = source.bbox
|
||||
const [z, x, y] = Tiles.tile_from_index(source.tileIndex)
|
||||
const box = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
z: z,
|
||||
x: x,
|
||||
y: y,
|
||||
tileIndex: source.tileIndex,
|
||||
source: source.name,
|
||||
count: features.length,
|
||||
tileId: source.name + "/" + source.tileIndex,
|
||||
},
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [
|
||||
[
|
||||
[bbox.minLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.maxLat],
|
||||
[bbox.maxLon, bbox.minLat],
|
||||
[bbox.minLon, bbox.minLat],
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
const center = GeoOperations.centerpoint(box)
|
||||
return [box, center].map((feature) => ({ feature, freshness: new Date() }))
|
||||
}
|
||||
)
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: ShowTileInfo.styling,
|
||||
features: new StaticFeatureSource(metaFeature),
|
||||
leafletMap: options.leafletMap,
|
||||
doShowLayer: options.doShowLayer,
|
||||
state,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -5,9 +5,9 @@ import FeatureSource, {
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
/**
|
||||
* A feature source containing but a single feature, which keeps stats about a tile
|
||||
|
@ -17,16 +17,14 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
public totalValue: number = 0
|
||||
public showCount: number = 0
|
||||
public hiddenCount: number = 0
|
||||
public readonly features = new UIEventSource<{ feature: any; freshness: Date }[]>(
|
||||
TileHierarchyAggregator.empty
|
||||
)
|
||||
public readonly features = new UIEventSource<Feature[]>(TileHierarchyAggregator.empty)
|
||||
public readonly name
|
||||
private _parent: TileHierarchyAggregator
|
||||
private _root: TileHierarchyAggregator
|
||||
private _z: number
|
||||
private _x: number
|
||||
private _y: number
|
||||
private _tileIndex: number
|
||||
private readonly _z: number
|
||||
private readonly _x: number
|
||||
private readonly _y: number
|
||||
private readonly _tileIndex: number
|
||||
private _counter: SingleTileCounter
|
||||
private _subtiles: [
|
||||
TileHierarchyAggregator,
|
||||
|
@ -158,42 +156,6 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
}
|
||||
this.updateSignal.setData(source)
|
||||
}
|
||||
|
||||
getCountsForZoom(
|
||||
clusteringConfig: { maxZoom: number },
|
||||
locationControl: UIEventSource<{ zoom: number }>,
|
||||
cutoff: number = 0
|
||||
): FeatureSource {
|
||||
const self = this
|
||||
const empty = []
|
||||
const features = locationControl
|
||||
.map((loc) => loc.zoom)
|
||||
.map(
|
||||
(targetZoom) => {
|
||||
if (targetZoom - 1 > clusteringConfig.maxZoom) {
|
||||
return empty
|
||||
}
|
||||
|
||||
const features: { feature: any; freshness: Date }[] = []
|
||||
self.visitSubTiles((aggr) => {
|
||||
if (aggr.showCount < cutoff) {
|
||||
return false
|
||||
}
|
||||
if (aggr._z === targetZoom) {
|
||||
features.push(...aggr.features.data)
|
||||
return false
|
||||
}
|
||||
return aggr._z <= targetZoom
|
||||
})
|
||||
|
||||
return features
|
||||
},
|
||||
[this.updateSignal.stabilized(500)]
|
||||
)
|
||||
|
||||
return new StaticFeatureSource(features)
|
||||
}
|
||||
|
||||
private update() {
|
||||
const newMap = new Map<string, number>()
|
||||
let total = 0
|
||||
|
@ -254,13 +216,6 @@ export class TileHierarchyAggregator implements FeatureSource {
|
|||
this.features.ping()
|
||||
}
|
||||
}
|
||||
|
||||
private visitSubTiles(f: (aggr: TileHierarchyAggregator) => boolean) {
|
||||
const visitFurther = f(this)
|
||||
if (visitFurther) {
|
||||
this._subtiles.forEach((tile) => tile?.visitSubTiles(f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,6 @@ import { SpecialVisualization } from "./SpecialVisualization"
|
|||
import { HistogramViz } from "./Popup/HistogramViz"
|
||||
import { StealViz } from "./Popup/StealViz"
|
||||
import { MinimapViz } from "./Popup/MinimapViz"
|
||||
import { SidedMinimap } from "./Popup/SidedMinimap"
|
||||
import { ShareLinkViz } from "./Popup/ShareLinkViz"
|
||||
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
|
||||
import { MultiApplyViz } from "./Popup/MultiApplyViz"
|
||||
|
@ -20,7 +19,7 @@ import { CloseNoteButton } from "./Popup/CloseNoteButton"
|
|||
import { NearbyImageVis } from "./Popup/NearbyImageVis"
|
||||
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
|
||||
import { Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||
import AllTagsPanel from "./AllTagsPanel.svelte"
|
||||
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
|
||||
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
|
||||
import { ImageCarousel } from "./Image/ImageCarousel"
|
||||
import { ImageUploadFlow } from "./Image/ImageUploadFlow"
|
||||
|
@ -142,7 +141,6 @@ export default class SpecialVisualizations {
|
|||
new HistogramViz(),
|
||||
new StealViz(),
|
||||
new MinimapViz(),
|
||||
new SidedMinimap(),
|
||||
new ShareLinkViz(),
|
||||
new UploadToOsmViz(),
|
||||
new MultiApplyViz(),
|
||||
|
@ -664,7 +662,7 @@ export default class SpecialVisualizations {
|
|||
const maproulette_id =
|
||||
tagsSource.data[maproulette_id_key] ?? tagsSource.data.id
|
||||
try {
|
||||
await state.maprouletteConnection.closeTask(
|
||||
await Maproulette.singleton.closeTask(
|
||||
Number(maproulette_id),
|
||||
Number(status),
|
||||
{
|
||||
|
|
114
UI/ThemeViewGUI.svelte
Normal file
114
UI/ThemeViewGUI.svelte
Normal file
|
@ -0,0 +1,114 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor";
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning";
|
||||
import { GeoLocationState } from "../Logic/State/GeoLocationState";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters";
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler";
|
||||
import { ElementStorage } from "../Logic/ElementStorage";
|
||||
import { Changes } from "../Logic/Osm/Changes";
|
||||
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor";
|
||||
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader";
|
||||
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater";
|
||||
import MapControlButton from "./Base/MapControlButton.svelte";
|
||||
import ToSvelte from "./Base/ToSvelte.svelte";
|
||||
import Svg from "../Svg";
|
||||
import If from "./Base/If.svelte";
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl.js";
|
||||
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
|
||||
import { BBox } from "../Logic/BBox";
|
||||
import ShowDataLayer from "./Map/ShowDataLayer";
|
||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
|
||||
export let layout: LayoutConfig;
|
||||
|
||||
const maplibremap: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
const initial = new InitialMapPositioning(layout);
|
||||
const mapproperties = new MapLibreAdaptor(maplibremap, initial);
|
||||
const geolocationState = new GeoLocationState();
|
||||
|
||||
const featureSwitches = new FeatureSwitchState(layout);
|
||||
|
||||
const osmConnection = new OsmConnection({
|
||||
dryRun: featureSwitches.featureSwitchIsTesting,
|
||||
fakeUser: featureSwitches.featureSwitchFakeUser.data,
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
"oauth_token",
|
||||
undefined,
|
||||
"Used to complete the login"
|
||||
),
|
||||
osmConfiguration: <"osm" | "osm-test">featureSwitches.featureSwitchApiURL.data
|
||||
});
|
||||
const userRelatedState = new UserRelatedState(osmConnection, layout?.language);
|
||||
const selectedElement = new UIEventSource<any>(undefined, "Selected element");
|
||||
const geolocation = new GeoLocationHandler(geolocationState, selectedElement, mapproperties, userRelatedState.gpsLocationHistoryRetentionTime);
|
||||
|
||||
const allElements = new ElementStorage();
|
||||
const changes = new Changes({
|
||||
allElements,
|
||||
osmConnection,
|
||||
historicalUserLocations: geolocation.historicalUserLocations
|
||||
}, layout?.isLeftRightSensitive() ?? false);
|
||||
|
||||
Map
|
||||
|
||||
{
|
||||
// Various actors that we don't need to reference
|
||||
new ChangeToElementsActor(changes, allElements);
|
||||
new PendingChangesUploader(changes, selectedElement);
|
||||
new SelectedElementTagsUpdater({
|
||||
allElements, changes, selectedElement, layoutToUse: layout, osmConnection
|
||||
});
|
||||
// Various initial setup
|
||||
userRelatedState.markLayoutAsVisited(layout);
|
||||
if(layout?.lockLocation){
|
||||
const bbox = new BBox(layout.lockLocation)
|
||||
mapproperties.maxbounds.setData(bbox)
|
||||
ShowDataLayer.showRange(
|
||||
maplibremap,
|
||||
new StaticFeatureSource([bbox.asGeoJson({})]),
|
||||
featureSwitches.featureSwitchIsTesting
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="h-screen w-screen absolute top-0 left-0 border-3 border-red-500">
|
||||
<MaplibreMap class="w-full h-full border border-black" map={maplibremap}></MaplibreMap>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-0 left-0">
|
||||
<!-- Top-left elements -->
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0">
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 right-0 mb-4 mr-4">
|
||||
|
||||
<If condition={mapproperties.allowMoving}>
|
||||
<MapControlButton on:click={() => mapproperties.zoom.update(z => z+1)}>
|
||||
<ToSvelte class="w-7 h-7 block" construct={Svg.plus_ui}></ToSvelte>
|
||||
</MapControlButton>
|
||||
<MapControlButton on:click={() => mapproperties.zoom.update(z => z-1)}>
|
||||
<ToSvelte class="w-7 h-7 block" construct={Svg.min_ui}></ToSvelte>
|
||||
</MapControlButton>
|
||||
</If>
|
||||
<If condition={featureSwitches.featureSwitchGeolocation}>
|
||||
<MapControlButton>
|
||||
<ToSvelte construct={() => new GeolocationControl(geolocation, mapproperties).SetClass("block w-8 h-8")}></ToSvelte>
|
||||
</MapControlButton>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-0 right-0">
|
||||
</div>
|
||||
|
|
@ -2,7 +2,6 @@ import Locale from "./Locale"
|
|||
import { Utils } from "../../Utils"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import LinkToWeblate from "../Base/LinkToWeblate"
|
||||
import { SvelteComponent } from "svelte"
|
||||
|
||||
export class Translation extends BaseUIElement {
|
||||
public static forcedLanguage = undefined
|
||||
|
@ -299,7 +298,7 @@ export class Translation extends BaseUIElement {
|
|||
}
|
||||
}
|
||||
|
||||
export class TypedTranslation<T> extends Translation {
|
||||
export class TypedTranslation<T extends Record<string, any>> extends Translation {
|
||||
constructor(translations: Record<string, string>, context?: string) {
|
||||
super(translations, context)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue