forked from MapComplete/MapComplete
Refactoring: split 'ThemeViewState' into many classes
This commit is contained in:
parent
2b858bd2aa
commit
dbcbf2787d
34 changed files with 1503 additions and 1227 deletions
File diff suppressed because it is too large
Load diff
292
src/Models/ThemeViewState/UserMapFeatureswitchState.ts
Normal file
292
src/Models/ThemeViewState/UserMapFeatureswitchState.ts
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
||||
import InitialMapPositioning from "../../Logic/Actors/InitialMapPositioning"
|
||||
import { MapLibreAdaptor } from "../../UI/Map/MapLibreAdaptor"
|
||||
import { ExportableMap, MapProperties } from "../MapProperties"
|
||||
import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
||||
import { PreferredRasterLayerSelector } from "../../Logic/Actors/PreferredRasterLayerSelector"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "../RasterLayers"
|
||||
import BackgroundLayerResetter from "../../Logic/Actors/BackgroundLayerResetter"
|
||||
import Hotkeys from "../../UI/Base/Hotkeys"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import { EliCategory } from "../RasterLayerProperties"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { Feature, Point, Polygon } from "geojson"
|
||||
import { FeatureSource, WritableFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { WithUserRelatedState } from "./WithUserRelatedState"
|
||||
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
|
||||
import { GeolocationControlState } from "../../UI/BigComponents/GeolocationControl"
|
||||
import ShowOverlayRasterLayer from "../../UI/Map/ShowOverlayRasterLayer"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||
|
||||
/**
|
||||
* The first core of the state management; everything related to:
|
||||
* - the OSM connection
|
||||
* - setting up the basemap
|
||||
* - the feature switches
|
||||
* - the GPS-location
|
||||
*
|
||||
* Anything that handles editable elements is _not_ done on this level.
|
||||
* Anything that handles the UI is not done on this level
|
||||
*/
|
||||
|
||||
export class UserMapFeatureswitchState extends WithUserRelatedState {
|
||||
|
||||
readonly map: UIEventSource<MlMap>
|
||||
|
||||
|
||||
readonly mapProperties: MapLibreAdaptor & MapProperties & ExportableMap
|
||||
readonly lastClickObject: LastClickFeatureSource
|
||||
|
||||
readonly geolocationState: GeoLocationState
|
||||
readonly geolocation: GeoLocationHandler
|
||||
readonly geolocationControl: GeolocationControlState
|
||||
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
||||
|
||||
|
||||
readonly availableLayers: { store: Store<RasterLayerPolygon[]> }
|
||||
readonly currentView: FeatureSource<Feature<Polygon>>
|
||||
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||
|
||||
|
||||
constructor(theme: ThemeConfig, selectedElement: Store<object>) {
|
||||
const rasterLayer: UIEventSource<RasterLayerPolygon> = new UIEventSource<RasterLayerPolygon>(undefined)
|
||||
super(theme, rasterLayer)
|
||||
this.geolocationState = new GeoLocationState()
|
||||
const initial = new InitialMapPositioning(theme, this.geolocationState, this.osmConnection)
|
||||
this.map = new UIEventSource<MlMap>(undefined)
|
||||
this.mapProperties = new MapLibreAdaptor(this.map, { rasterLayer, ...initial }, { correctClick: 20 })
|
||||
|
||||
this.geolocation = new GeoLocationHandler(
|
||||
this.geolocationState,
|
||||
selectedElement,
|
||||
this.mapProperties,
|
||||
this.userRelatedState.gpsLocationHistoryRetentionTime
|
||||
)
|
||||
this.geolocationControl = new GeolocationControlState(this.geolocation, this.mapProperties)
|
||||
this.historicalUserLocations = this.geolocation.historicalUserLocations
|
||||
|
||||
|
||||
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
|
||||
this.mapProperties.allowRotating.setData(fixated !== "yes")
|
||||
})
|
||||
|
||||
this.availableLayers = AvailableRasterLayers.layersAvailableAt(
|
||||
this.mapProperties.location,
|
||||
this.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
this.userRelatedState.markLayoutAsVisited(this.theme)
|
||||
|
||||
this.lastClickObject = new LastClickFeatureSource(
|
||||
this.theme,
|
||||
this.mapProperties.lastClickLocation,
|
||||
this.userRelatedState.addNewFeatureMode
|
||||
)
|
||||
{
|
||||
let currentViewIndex = 0
|
||||
const empty = []
|
||||
this.currentView = new StaticFeatureSource(
|
||||
this.mapProperties.bounds.map((bbox) => {
|
||||
if (!bbox) {
|
||||
return empty
|
||||
}
|
||||
currentViewIndex++
|
||||
return <Feature[]>[
|
||||
bbox.asGeoJson({
|
||||
zoom: this.mapProperties.zoom.data,
|
||||
...this.mapProperties.location.data,
|
||||
id: "current_view_" + currentViewIndex
|
||||
})
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (this.theme.layers.some((l) => l._needsFullNodeDatabase)) {
|
||||
this.fullNodeDatabase = new FullNodeDatabaseSource()
|
||||
}
|
||||
|
||||
|
||||
///////// Actors ///////////////
|
||||
|
||||
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers)
|
||||
|
||||
|
||||
this.userRelatedState.showScale.addCallbackAndRun((showScale) => {
|
||||
this.mapProperties.showScale.set(showScale)
|
||||
})
|
||||
new PreferredRasterLayerSelector(
|
||||
this.mapProperties.rasterLayer,
|
||||
this.availableLayers,
|
||||
this.featureSwitches.backgroundLayerId,
|
||||
this.userRelatedState.preferredBackgroundLayer
|
||||
)
|
||||
|
||||
|
||||
this.initHotkeys()
|
||||
this.drawOverlayLayers()
|
||||
this.drawLock()
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If the map is locked to a certain area _and_ we are in test mode, draw this on the map
|
||||
* @private
|
||||
*/
|
||||
private drawLock() {
|
||||
if (!this.theme?.lockLocation) {
|
||||
return
|
||||
}
|
||||
const bbox = new BBox(<[[number, number], [number, number]]>this.theme.lockLocation)
|
||||
this.mapProperties.maxbounds.setData(bbox)
|
||||
ShowDataLayer.showRange(
|
||||
this.map,
|
||||
new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
|
||||
this.featureSwitches.featureSwitchIsTesting
|
||||
)
|
||||
}
|
||||
|
||||
private drawOverlayLayers() {
|
||||
for (const rasterInfo of this.theme.tileLayerSources) {
|
||||
const state = this.overlayLayerStates.get(rasterInfo.id)
|
||||
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* By focussing on the map, the keyboard panning and zoom with '+' and '+' works */
|
||||
public focusOnMap() {
|
||||
if (this.map.data) {
|
||||
this.map.data.getCanvas().focus()
|
||||
return
|
||||
}
|
||||
this.map.addCallbackAndRunD((map) => {
|
||||
map.on("load", () => {
|
||||
map.getCanvas().focus()
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
private initHotkeys() {
|
||||
const docs = Translations.t.hotkeyDocumentation
|
||||
|
||||
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
|
||||
if (!enable) {
|
||||
return
|
||||
}
|
||||
|
||||
const setLayerCategory = (category: EliCategory, skipLayers: number = 0) => {
|
||||
const timeOfCall = new Date()
|
||||
this.availableLayers.store.addCallbackAndRunD((available) => {
|
||||
const now = new Date()
|
||||
const timeDiff = (now.getTime() - timeOfCall.getTime()) / 1000
|
||||
if (timeDiff > 3) {
|
||||
return true // unregister
|
||||
}
|
||||
const current = this.mapProperties.rasterLayer
|
||||
const best = RasterLayerUtils.SelectBestLayerAccordingTo(
|
||||
available,
|
||||
category,
|
||||
current.data,
|
||||
skipLayers
|
||||
)
|
||||
if (!best) {
|
||||
return
|
||||
}
|
||||
console.log("Best layer for category", category, "is", best?.properties?.id)
|
||||
current.setData(best)
|
||||
})
|
||||
}
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "O" },
|
||||
docs.selectOsmbasedmap,
|
||||
() => setLayerCategory("osmbasedmap")
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "M" },
|
||||
docs.selectMap,
|
||||
() => setLayerCategory("map")
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "P" },
|
||||
docs.selectAerial,
|
||||
() => setLayerCategory("photo")
|
||||
)
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ shift: "O" },
|
||||
docs.selectOsmbasedmap,
|
||||
() => setLayerCategory("osmbasedmap", 2)
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ shift: "M" },
|
||||
docs.selectMap,
|
||||
() => setLayerCategory("map", 2)
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ shift: "P" },
|
||||
docs.selectAerial,
|
||||
() => setLayerCategory("photo", 2)
|
||||
)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "L" },
|
||||
Translations.t.hotkeyDocumentation.geolocate,
|
||||
() => {
|
||||
this.geolocationControl.handleClick()
|
||||
}
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{
|
||||
shift: "T"
|
||||
},
|
||||
docs.translationMode,
|
||||
() => {
|
||||
const tm = this.userRelatedState.translationMode
|
||||
if (tm.data === "false") {
|
||||
tm.setData("true")
|
||||
} else {
|
||||
tm.setData("false")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the current GPS-location marker on the given map.
|
||||
* This is used to show the location on _other_ maps, e.g. on the map to add a new feature.
|
||||
*
|
||||
* This is _NOT_ to be used on the main map!
|
||||
*/
|
||||
public showCurrentLocationOn(map: Store<MlMap>) {
|
||||
const id = "gps_location"
|
||||
const layer = this.theme.getLayer(id)
|
||||
if (layer === undefined) {
|
||||
return
|
||||
}
|
||||
if (map === this.map) {
|
||||
throw "Invalid use of showCurrentLocationOn"
|
||||
}
|
||||
const features = this.geolocation.currentUserLocation
|
||||
return new ShowDataLayer(map, {
|
||||
features,
|
||||
layer,
|
||||
metaTags: this.userRelatedState.preferencesAsTags
|
||||
})
|
||||
}
|
||||
}
|
||||
236
src/Models/ThemeViewState/WithChangesState.ts
Normal file
236
src/Models/ThemeViewState/WithChangesState.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import {
|
||||
NewGeometryFromChangesFeatureSource
|
||||
} from "../../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
|
||||
import { WithLayoutSourceState } from "./WithLayoutSourceState"
|
||||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import Constants from "../Constants"
|
||||
import { WritableFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
|
||||
import ChangeToElementsActor from "../../Logic/Actors/ChangeToElementsActor"
|
||||
import MetaTagging from "../../Logic/MetaTagging"
|
||||
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import SaveFeatureSourceToLocalStorage from "../../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
|
||||
import ThemeSource from "../../Logic/FeatureSource/Sources/ThemeSource"
|
||||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import ChangeGeometryApplicator from "../../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource"
|
||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||
import SelectedElementTagsUpdater from "../../Logic/Actors/SelectedElementTagsUpdater"
|
||||
import NoElementsInViewDetector, { FeatureViewState } from "../../Logic/Actors/NoElementsInViewDetector"
|
||||
|
||||
export class WithChangesState extends WithLayoutSourceState {
|
||||
|
||||
readonly changes: Changes
|
||||
readonly newFeatures: WritableFeatureSource
|
||||
readonly osmObjectDownloader: OsmObjectDownloader
|
||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>
|
||||
readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage>
|
||||
/**
|
||||
* Indicates if there is _some_ data in view, even if it is not shown due to the filters
|
||||
*/
|
||||
readonly hasDataInView: Store<FeatureViewState>
|
||||
|
||||
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
|
||||
super(theme, mvtAvailableLayers)
|
||||
this.changes = new Changes(
|
||||
{
|
||||
featureSwitches: this.featureSwitches,
|
||||
allElements: this.indexedFeatures,
|
||||
osmConnection: this.osmConnection,
|
||||
featureProperties: this.featureProperties,
|
||||
historicalUserLocations: this.historicalUserLocations,
|
||||
reportError: this.reportError
|
||||
},
|
||||
theme?.isLeftRightSensitive() ?? false
|
||||
)
|
||||
this.newFeatures = new NewGeometryFromChangesFeatureSource(
|
||||
this.changes,
|
||||
this.indexedFeatures,
|
||||
this.featureProperties
|
||||
)
|
||||
this.indexedFeatures.addSource(this.newFeatures)
|
||||
|
||||
this.osmObjectDownloader = new OsmObjectDownloader(
|
||||
this.osmConnection.Backend(),
|
||||
this.changes
|
||||
)
|
||||
|
||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||
Array.from(this.layerState.filteredLayers.values()).filter(
|
||||
(l) => l.layerDef?.source !== null
|
||||
),
|
||||
new ChangeGeometryApplicator(this.indexedFeatures, this.changes),
|
||||
{
|
||||
constructStore: (features, layer) =>
|
||||
new GeoIndexedStoreForLayer(features, layer),
|
||||
handleLeftovers: (features) => {
|
||||
console.warn(
|
||||
"Got ",
|
||||
features.length,
|
||||
"leftover features, such as",
|
||||
features[0].properties
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
this.perLayer = perLayer.perLayer
|
||||
this.perLayerFiltered = this.showNormalDataOn(this.map)
|
||||
|
||||
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
|
||||
|
||||
this.toCacheSavers = theme.enableCache ? this.initSaveToLocalStorage() : undefined
|
||||
|
||||
|
||||
////// ACTORS ////////
|
||||
|
||||
new ChangeToElementsActor(this.changes, this.featureProperties)
|
||||
new SelectedElementTagsUpdater(this)
|
||||
|
||||
new MetaTagging({
|
||||
theme: this.theme,
|
||||
selectedElement: this.selectedElement,
|
||||
featureProperties: this.featureProperties,
|
||||
indexedFeatures: this.indexedFeatures,
|
||||
osmObjectDownloader: this.osmObjectDownloader,
|
||||
perLayer: this.perLayer
|
||||
})
|
||||
}
|
||||
|
||||
public async reportError(message: string | Error | XMLHttpRequest, extramessage: string = "") {
|
||||
if (Utils.runningFromConsole) {
|
||||
console.error("Got (in themeViewSTate.reportError):", message, extramessage)
|
||||
return
|
||||
}
|
||||
const isTesting = this.featureSwitchIsTesting?.data
|
||||
console.log(
|
||||
isTesting
|
||||
? ">>> _Not_ reporting error to report server as testmode is on"
|
||||
: ">>> Reporting error to",
|
||||
Constants.ErrorReportServer,
|
||||
message
|
||||
)
|
||||
if (isTesting) {
|
||||
return
|
||||
}
|
||||
|
||||
if ("" + message === "[object XMLHttpRequest]") {
|
||||
const req = <XMLHttpRequest>message
|
||||
let body = ""
|
||||
try {
|
||||
body = req.responseText
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
message =
|
||||
"XMLHttpRequest with status code " +
|
||||
req.status +
|
||||
", " +
|
||||
req.statusText +
|
||||
", received: " +
|
||||
body
|
||||
}
|
||||
|
||||
if (extramessage) {
|
||||
message += " (" + extramessage + ")"
|
||||
}
|
||||
|
||||
const stacktrace: string = new Error().stack
|
||||
|
||||
try {
|
||||
await fetch(Constants.ErrorReportServer, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
stacktrace,
|
||||
message: "" + message,
|
||||
theme: this.theme.id,
|
||||
version: Constants.vNumber,
|
||||
language: this.userRelatedState.language.data,
|
||||
username: this.osmConnection.userDetails.data?.name,
|
||||
userid: this.osmConnection.userDetails.data?.uid,
|
||||
pendingChanges: this.changes.pendingChanges.data,
|
||||
previousChanges: this.changes.allChanges.data,
|
||||
changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error("Could not upload an error report")
|
||||
}
|
||||
}
|
||||
|
||||
public initSaveToLocalStorage() {
|
||||
const toLocalStorage = new Map<string, SaveFeatureSourceToLocalStorage>()
|
||||
this.perLayer.forEach((fs, layerId) => {
|
||||
if (fs.layer.layerDef.source.geojsonSource !== undefined) {
|
||||
return // We don't cache external data layers
|
||||
}
|
||||
const storage = new SaveFeatureSourceToLocalStorage(
|
||||
this.osmConnection.Backend(),
|
||||
fs.layer.layerDef.id,
|
||||
ThemeSource.fromCacheZoomLevel,
|
||||
fs,
|
||||
this.featureProperties,
|
||||
fs.layer.layerDef.maxAgeOfCache
|
||||
)
|
||||
toLocalStorage.set(layerId, storage)
|
||||
})
|
||||
return toLocalStorage
|
||||
}
|
||||
|
||||
public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> {
|
||||
const filteringFeatureSource = new Map<string, FilteringFeatureSource>()
|
||||
this.perLayer.forEach((fs, layerName) => {
|
||||
const doShowLayer = this.mapProperties.zoom.map(
|
||||
(z) => {
|
||||
if (
|
||||
(fs.layer.isDisplayed?.data ?? true) &&
|
||||
z >= (fs.layer.layerDef?.minzoom ?? 0)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (this.layerState.globalFilters.data.some((f) => f.forceShowOnMatch)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
[fs.layer.isDisplayed, this.layerState.globalFilters]
|
||||
)
|
||||
|
||||
if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
|
||||
/* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
|
||||
*
|
||||
* This means that we don't have to filter it, nor do we have to display it
|
||||
*
|
||||
* Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden.
|
||||
* However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer!
|
||||
* */
|
||||
return
|
||||
}
|
||||
const filtered = new FilteringFeatureSource(
|
||||
fs.layer,
|
||||
fs,
|
||||
(id) => this.featureProperties.getStore(id),
|
||||
this.layerState.globalFilters,
|
||||
undefined,
|
||||
this.mapProperties.zoom,
|
||||
this.selectedElement
|
||||
)
|
||||
filteringFeatureSource.set(layerName, filtered)
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: fs.layer.layerDef,
|
||||
features: filtered,
|
||||
doShowLayer,
|
||||
metaTags: this.userRelatedState.preferencesAsTags,
|
||||
selectedElement: this.selectedElement,
|
||||
fetchStore: (id) => this.featureProperties.getStore(id)
|
||||
})
|
||||
})
|
||||
return filteringFeatureSource
|
||||
}
|
||||
|
||||
}
|
||||
72
src/Models/ThemeViewState/WithGuiState.ts
Normal file
72
src/Models/ThemeViewState/WithGuiState.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import { MenuState } from "../MenuState"
|
||||
import Hotkeys from "../../UI/Base/Hotkeys"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import { WithSpecialLayers } from "./WithSpecialLayers"
|
||||
|
||||
/**
|
||||
* Does all things related to:
|
||||
* - The UI state
|
||||
*/
|
||||
export class WithGuiState extends WithSpecialLayers {
|
||||
readonly guistate: MenuState
|
||||
|
||||
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
|
||||
super(theme, mvtAvailableLayers)
|
||||
this.guistate = new MenuState(
|
||||
this.featureSwitches.featureSwitchWelcomeMessage.data,
|
||||
theme.id
|
||||
)
|
||||
|
||||
Object.values(this.guistate.pageStates).forEach((toggle) => {
|
||||
toggle.addCallbackD((isOpened) => {
|
||||
// When a panel is closed: focus on the map again
|
||||
if (!isOpened) {
|
||||
if (!this.guistate.isSomethingOpen()) {
|
||||
this.focusOnMap()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.initHotkeysGui()
|
||||
}
|
||||
|
||||
|
||||
private initHotkeysGui() {
|
||||
const docs = Translations.t.hotkeyDocumentation
|
||||
|
||||
Hotkeys.RegisterHotkey({ nomod: "f" }, docs.selectFavourites, () => {
|
||||
this.guistate.pageStates.favourites.set(true)
|
||||
})
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{
|
||||
nomod: "b"
|
||||
},
|
||||
docs.openLayersPanel,
|
||||
() => {
|
||||
if (this.featureSwitches.featureSwitchBackgroundSelection.data) {
|
||||
this.guistate.pageStates.background.setData(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
Hotkeys.RegisterHotkey(
|
||||
{
|
||||
nomod: "s"
|
||||
},
|
||||
Translations.t.hotkeyDocumentation.openFilterPanel,
|
||||
() => {
|
||||
if (this.featureSwitches.featureSwitchFilter.data) {
|
||||
this.guistate.openFilterView()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public selectCurrentView() {
|
||||
this.guistate.closeAll()
|
||||
this.selectedElement.setData(this.currentView.features?.data?.[0])
|
||||
}
|
||||
|
||||
}
|
||||
68
src/Models/ThemeViewState/WithImageState.ts
Normal file
68
src/Models/ThemeViewState/WithImageState.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { ImageUploadManager } from "../../Logic/ImageProviders/ImageUploadManager"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
||||
import { CombinedFetcher } from "../../Logic/Web/NearbyImagesSearch"
|
||||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import { PanoramaxUploader } from "../../Logic/ImageProviders/Panoramax"
|
||||
import Constants from "../Constants"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
import ThemeViewStateHashActor from "../../Logic/Web/ThemeViewStateHashActor"
|
||||
import PendingChangesUploader from "../../Logic/Actors/PendingChangesUploader"
|
||||
import { WithGuiState } from "./WithGuiState"
|
||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
|
||||
export class WithImageState extends WithGuiState implements SpecialVisualizationState {
|
||||
|
||||
readonly imageUploadManager: ImageUploadManager
|
||||
readonly previewedImage = new UIEventSource<ProvidedImage>(undefined)
|
||||
readonly nearbyImageSearcher: CombinedFetcher
|
||||
|
||||
|
||||
constructor(layout: ThemeConfig, mvtAvailableLayers: Set<string>) {
|
||||
super(layout, mvtAvailableLayers)
|
||||
this.imageUploadManager = new ImageUploadManager(
|
||||
layout,
|
||||
new PanoramaxUploader(
|
||||
Constants.panoramax.url,
|
||||
Constants.panoramax.token,
|
||||
this.featureSwitchIsTesting.map((t) =>
|
||||
t ? Constants.panoramax.testsequence : Constants.panoramax.sequence
|
||||
)
|
||||
),
|
||||
this.featureProperties,
|
||||
this.osmConnection,
|
||||
this.changes,
|
||||
this.geolocation.geolocationState.currentGPSLocation,
|
||||
this.indexedFeatures,
|
||||
this.reportError
|
||||
)
|
||||
const longAgo = new Date()
|
||||
longAgo.setTime(new Date().getTime() - 5 * 365 * 24 * 60 * 60 * 1000)
|
||||
this.nearbyImageSearcher = new CombinedFetcher(50, longAgo, this.indexedFeatures)
|
||||
|
||||
|
||||
this.initActors()
|
||||
Hash.hash.addCallbackAndRunD((hash) => {
|
||||
if (hash === "current_view" || hash.match(/current_view_[0-9]+/)) {
|
||||
this.selectCurrentView()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup various services for which no reference are needed
|
||||
*/
|
||||
private initActors() {
|
||||
|
||||
new ThemeViewStateHashActor({
|
||||
previewedImage: this.previewedImage,
|
||||
selectedElement: this.selectedElement,
|
||||
indexedFeatures: this.indexedFeatures,
|
||||
guistate: this.guistate
|
||||
})
|
||||
new PendingChangesUploader(this.changes, this.selectedElement, this.imageUploadManager)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
129
src/Models/ThemeViewState/WithLayoutSourceState.ts
Normal file
129
src/Models/ThemeViewState/WithLayoutSourceState.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { WithSelectedElementState } from "./WithSelectedElementState"
|
||||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import ThemeSource from "../../Logic/FeatureSource/Sources/ThemeSource"
|
||||
import BBoxFeatureSource from "../../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||
import FeaturePropertiesStore from "../../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
||||
import LayerState from "../../Logic/State/LayerState"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { FeatureSource, IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
|
||||
export class WithLayoutSourceState extends WithSelectedElementState {
|
||||
|
||||
readonly layerState: LayerState
|
||||
readonly dataIsLoading: Store<boolean>
|
||||
|
||||
readonly featureProperties: FeaturePropertiesStore
|
||||
readonly indexedFeatures: IndexedFeatureSource & ThemeSource
|
||||
readonly featuresInView: FeatureSource
|
||||
/**
|
||||
* All 'level'-tags that are available with the current features
|
||||
*/
|
||||
readonly floors: Store<string[]>
|
||||
|
||||
|
||||
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
|
||||
super(theme)
|
||||
/* Set up the layout source
|
||||
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generates a stream of new and changed features too
|
||||
*/
|
||||
this.layerState = new LayerState(
|
||||
this.osmConnection,
|
||||
theme.layers,
|
||||
theme.id,
|
||||
this.featureSwitches.featureSwitchLayerDefault
|
||||
)
|
||||
|
||||
const layoutSource = new ThemeSource(
|
||||
theme.layers,
|
||||
this.featureSwitches,
|
||||
this.mapProperties,
|
||||
this.osmConnection.Backend(),
|
||||
(id) => this.layerState.filteredLayers.get(id).isDisplayed,
|
||||
mvtAvailableLayers,
|
||||
this.fullNodeDatabase
|
||||
)
|
||||
|
||||
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
|
||||
this.dataIsLoading = layoutSource.isLoading
|
||||
this.indexedFeatures = layoutSource
|
||||
this.featureProperties = new FeaturePropertiesStore(layoutSource)
|
||||
|
||||
this.floors = WithLayoutSourceState.initFloors(this.featuresInView)
|
||||
|
||||
this.initFilters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Special bypass: if "favourites" is set, we still show items marked as 'favourite' even though the main layer is disabled
|
||||
* @private
|
||||
*/
|
||||
private initFilters() {
|
||||
this.layerState.filteredLayers
|
||||
.get("favourite")
|
||||
?.isDisplayed?.addCallbackAndRunD((favouritesShown) => {
|
||||
const oldGlobal = this.layerState.globalFilters.data
|
||||
const key = "show-favourite"
|
||||
if (favouritesShown) {
|
||||
this.layerState.globalFilters.set([
|
||||
...oldGlobal,
|
||||
{
|
||||
forceShowOnMatch: true,
|
||||
id: key,
|
||||
osmTags: new Tag("_favourite", "yes"),
|
||||
state: 0,
|
||||
onNewPoint: undefined
|
||||
}
|
||||
])
|
||||
} else {
|
||||
this.layerState.globalFilters.set(oldGlobal.filter((gl) => gl.id !== key))
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private static initFloors(features: FeatureSource): Store<string[]> {
|
||||
return features.features.stabilized(500).map((features) => {
|
||||
if (!features) {
|
||||
return []
|
||||
}
|
||||
const floors = new Set<string>()
|
||||
for (const feature of features) {
|
||||
const level = feature.properties["_level"]
|
||||
if (level) {
|
||||
const levels = level.split(";")
|
||||
for (const l of levels) {
|
||||
floors.add(l)
|
||||
}
|
||||
} else {
|
||||
floors.add("0") // '0' is the default and is thus _always_ present
|
||||
}
|
||||
}
|
||||
const sorted = Array.from(floors)
|
||||
// Sort alphabetically first, to deal with floor "A", "B" and "C"
|
||||
sorted.sort()
|
||||
sorted.sort((a, b) => {
|
||||
// We use the laxer 'parseInt' to deal with floor '1A'
|
||||
const na = parseInt(a)
|
||||
const nb = parseInt(b)
|
||||
if (isNaN(na) || isNaN(nb)) {
|
||||
return 0
|
||||
}
|
||||
return na - nb
|
||||
})
|
||||
sorted.reverse(/* new list, no side-effects */)
|
||||
return sorted
|
||||
})
|
||||
}
|
||||
|
||||
public openNewDialog() {
|
||||
this.selectedElement.setData(undefined)
|
||||
|
||||
const { lon, lat } = this.mapProperties.location.data
|
||||
const feature = this.lastClickObject.createFeature(lon, lat)
|
||||
this.featureProperties.trackFeature(feature)
|
||||
this.selectedElement.setData(feature)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
72
src/Models/ThemeViewState/WithSearchState.ts
Normal file
72
src/Models/ThemeViewState/WithSearchState.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import SearchState from "../../Logic/State/SearchState"
|
||||
import Hotkeys from "../../UI/Base/Hotkeys"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import Zoomcontrol from "../../UI/Zoomcontrol"
|
||||
import { WithVisualFeedbackState } from "./WithVisualFeedbackState"
|
||||
import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions"
|
||||
import LayerConfig from "../ThemeConfig/LayerConfig"
|
||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||
|
||||
export class WithSearchState extends WithVisualFeedbackState {
|
||||
public readonly searchState: SearchState
|
||||
|
||||
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
|
||||
super(theme, mvtAvailableLayers)
|
||||
this.searchState = new SearchState(this)
|
||||
this.initHotkeysSearch()
|
||||
|
||||
{
|
||||
// Register the search layer on the map
|
||||
|
||||
const source = this.searchState.locationResults
|
||||
const flayer = this.layerState.filteredLayers.get("search")
|
||||
this.featureProperties.trackFeatureSource(source)
|
||||
const options: ShowDataLayerOptions & { layer: LayerConfig } = {
|
||||
features: source,
|
||||
doShowLayer: flayer.isDisplayed,
|
||||
layer: flayer.layerDef,
|
||||
metaTags: this.userRelatedState.preferencesAsTags,
|
||||
onClick: (feature) => {
|
||||
this.searchState.clickedOnMap(feature)
|
||||
}
|
||||
}
|
||||
new ShowDataLayer(this.map, options)
|
||||
}
|
||||
}
|
||||
|
||||
private initHotkeysSearch() {
|
||||
const docs = Translations.t.hotkeyDocumentation
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ ctrl: "F" },
|
||||
docs.selectSearch,
|
||||
() => {
|
||||
this.searchState.feedback.set(undefined)
|
||||
this.searchState.searchIsFocused.set(true)
|
||||
}
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey({ nomod: "Escape", onUp: true }, docs.closeSidebar, () => {
|
||||
if (this.previewedImage.data !== undefined) {
|
||||
this.previewedImage.setData(undefined)
|
||||
return
|
||||
}
|
||||
if (this.selectedElement.data) {
|
||||
this.selectedElement.setData(undefined)
|
||||
return
|
||||
}
|
||||
if (this.searchState.showSearchDrawer.data) {
|
||||
this.searchState.showSearchDrawer.set(false)
|
||||
return
|
||||
}
|
||||
if (this.guistate.closeAll()) {
|
||||
return
|
||||
}
|
||||
Zoomcontrol.resetzoom()
|
||||
this.focusOnMap()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
79
src/Models/ThemeViewState/WithSelectedElementState.ts
Normal file
79
src/Models/ThemeViewState/WithSelectedElementState.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { UserMapFeatureswitchState } from "./UserMapFeatureswitchState"
|
||||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Feature } from "geojson"
|
||||
import Zoomcontrol from "../../UI/Zoomcontrol"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
|
||||
|
||||
/**
|
||||
* The state interactions with a selected element, but is blind to loading elements
|
||||
*
|
||||
*
|
||||
* No GUI stuff
|
||||
*/
|
||||
export class WithSelectedElementState extends UserMapFeatureswitchState {
|
||||
|
||||
|
||||
readonly selectedElement: UIEventSource<Feature>
|
||||
|
||||
constructor(theme: ThemeConfig) {
|
||||
const selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
|
||||
super(theme, selectedElement)
|
||||
this.selectedElement = selectedElement
|
||||
this.selectedElement.addCallback((selected) => {
|
||||
if (selected === undefined) {
|
||||
Zoomcontrol.resetzoom()
|
||||
}
|
||||
})
|
||||
|
||||
this.mapProperties.lastClickLocation.addCallbackD((lastClick) => {
|
||||
if (lastClick.mode !== "left" || !lastClick.nearestFeature) {
|
||||
return
|
||||
}
|
||||
const f = lastClick.nearestFeature
|
||||
this.setSelectedElement(f)
|
||||
})
|
||||
|
||||
|
||||
// Add the selected element to the recently visited history
|
||||
this.selectedElement.addCallbackD((selected) => {
|
||||
const [osm_type, osm_id] = selected.properties.id.split("/")
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
||||
const layer = this.theme.getMatchingLayer(selected.properties)
|
||||
|
||||
const nameOptions = [
|
||||
selected?.properties?.name,
|
||||
selected?.properties?.alt_name,
|
||||
selected?.properties?.local_name,
|
||||
layer?.title.GetRenderValue(selected?.properties ?? {}).txt,
|
||||
selected.properties.display_name,
|
||||
selected.properties.id
|
||||
]
|
||||
const r = <GeocodeResult>{
|
||||
feature: selected,
|
||||
display_name: nameOptions.find((opt) => opt !== undefined),
|
||||
osm_id,
|
||||
osm_type,
|
||||
lon,
|
||||
lat
|
||||
}
|
||||
this.userRelatedState.recentlyVisitedSearch.add(r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
protected setSelectedElement(feature: Feature) {
|
||||
const current = this.selectedElement.data
|
||||
if (
|
||||
current?.properties?.id !== undefined &&
|
||||
current.properties.id === feature.properties.id
|
||||
) {
|
||||
console.log("Not setting selected, same id", current, feature)
|
||||
return // already set
|
||||
}
|
||||
this.selectedElement.setData(feature)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
227
src/Models/ThemeViewState/WithSpecialLayers.ts
Normal file
227
src/Models/ThemeViewState/WithSpecialLayers.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import { WithChangesState } from "./WithChangesState"
|
||||
import FavouritesFeatureSource from "../../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||
import Constants from "../Constants"
|
||||
import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||
import MetaTagging from "../../Logic/MetaTagging"
|
||||
import FilteredLayer from "../FilteredLayer"
|
||||
import LayerConfig from "../ThemeConfig/LayerConfig"
|
||||
import { LayerConfigJson } from "../ThemeConfig/Json/LayerConfigJson"
|
||||
import last_click_layerconfig from "../../assets/generated/layers/last_click.json"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import summaryLayer from "../../assets/generated/layers/summary.json"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import NearbyFeatureSource from "../../Logic/FeatureSource/Sources/NearbyFeatureSource"
|
||||
import {
|
||||
SummaryTileSource,
|
||||
SummaryTileSourceRewriter
|
||||
} from "../../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
|
||||
import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions"
|
||||
|
||||
export class WithSpecialLayers extends WithChangesState {
|
||||
|
||||
readonly favourites: FavouritesFeatureSource
|
||||
/**
|
||||
* When hovering (in the popup) an image, the location of the image will be revealed on the main map.
|
||||
* This store contains those images that should be shown, probably only the currently hovered image
|
||||
*/
|
||||
readonly geocodedImages: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
||||
/**
|
||||
* Contains a few (<10) >features that are near the center of the map.
|
||||
*/
|
||||
readonly closestFeatures: NearbyFeatureSource
|
||||
|
||||
readonly featureSummary: SummaryTileSourceRewriter
|
||||
/**
|
||||
* When using arrow keys to move, the accessibility mode is activated, which has a small rectangle set.
|
||||
* This is the 'viewport' which 'closestFeatures' uses to filter wilt
|
||||
*/
|
||||
readonly visualFeedbackViewportBounds: UIEventSource<BBox> = new UIEventSource<BBox>(undefined)
|
||||
|
||||
|
||||
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
|
||||
super(theme, mvtAvailableLayers)
|
||||
|
||||
this.favourites = new FavouritesFeatureSource(this)
|
||||
|
||||
this.closestFeatures = new NearbyFeatureSource(
|
||||
this.mapProperties.location,
|
||||
this.perLayerFiltered,
|
||||
{
|
||||
currentZoom: this.mapProperties.zoom,
|
||||
layerState: this.layerState,
|
||||
bounds: this.visualFeedbackViewportBounds.map(
|
||||
(bounds) => bounds ?? this.mapProperties.bounds?.data,
|
||||
[this.mapProperties.bounds]
|
||||
)
|
||||
}
|
||||
)
|
||||
this.closestFeatures.registerSource(this.favourites, "favourite")
|
||||
|
||||
this.featureSummary = this.setupSummaryLayer()
|
||||
this.initActorsSpecialLayers()
|
||||
this.drawSpecialLayers()
|
||||
this.drawLastClick()
|
||||
// Note: the lock-range is handled by UserMapFeatureSwitchState
|
||||
{
|
||||
// Activate metatagging for the 'current_view' layer
|
||||
console.log(">>>", this.layerState.filteredLayers)
|
||||
const currentViewLayer = this.layerState.filteredLayers.get("current_view")?.layerDef
|
||||
if (currentViewLayer?.tagRenderings?.length > 0) {
|
||||
const params = MetaTagging.createExtraFuncParams(this)
|
||||
this.currentView.features.addCallbackAndRunD((features) => {
|
||||
MetaTagging.addMetatags(
|
||||
features,
|
||||
params,
|
||||
currentViewLayer,
|
||||
this.theme,
|
||||
this.osmObjectDownloader,
|
||||
this.featureProperties
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private setupSummaryLayer(): SummaryTileSourceRewriter | undefined {
|
||||
/**
|
||||
* MaxZoom for the summary layer
|
||||
*/
|
||||
const normalLayers = this.theme.layers.filter((l) => l.isNormal())
|
||||
|
||||
const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom))
|
||||
|
||||
const layers = this.theme.layers.filter(
|
||||
(l) =>
|
||||
(<string[]>(<unknown>Constants.priviliged_layers)).indexOf(l.id) < 0 &&
|
||||
l.source.geojsonSource === undefined &&
|
||||
l.doCount
|
||||
)
|
||||
if (!Constants.SummaryServer || layers.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
const summaryTileSource = new SummaryTileSource(
|
||||
Constants.SummaryServer,
|
||||
layers.map((l) => l.id),
|
||||
this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)),
|
||||
this.mapProperties,
|
||||
{
|
||||
isActive: this.mapProperties.zoom.map((z) => z < maxzoom)
|
||||
}
|
||||
)
|
||||
|
||||
const source = new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers)
|
||||
|
||||
new ShowDataLayer(this.map, {
|
||||
features: source,
|
||||
layer: new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer"),
|
||||
// doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom),
|
||||
selectedElement: this.selectedElement
|
||||
})
|
||||
return source
|
||||
}
|
||||
|
||||
protected registerSpecialLayer(flayer: FilteredLayer, source: FeatureSource) {
|
||||
if (!source?.features) {
|
||||
return
|
||||
}
|
||||
|
||||
this.featureProperties.trackFeatureSource(source)
|
||||
const options: ShowDataLayerOptions & { layer: LayerConfig } = {
|
||||
features: source,
|
||||
doShowLayer: flayer.isDisplayed,
|
||||
layer: flayer.layerDef,
|
||||
metaTags: this.userRelatedState.preferencesAsTags,
|
||||
selectedElement: this.selectedElement
|
||||
}
|
||||
new ShowDataLayer(this.map, options)
|
||||
}
|
||||
|
||||
private drawLastClick() {
|
||||
const source = this.lastClickObject
|
||||
const lastClickLayerConfig = new LayerConfig(
|
||||
<LayerConfigJson>last_click_layerconfig,
|
||||
"last_click"
|
||||
)
|
||||
const lastClickFiltered =
|
||||
lastClickLayerConfig.isShown === undefined
|
||||
? source
|
||||
: source.features.mapD((fs) =>
|
||||
fs.filter((f) => {
|
||||
const matches = lastClickLayerConfig.isShown.matchesProperties(
|
||||
f.properties
|
||||
)
|
||||
console.debug("LastClick ", f, "matches", matches)
|
||||
return matches
|
||||
})
|
||||
)
|
||||
// show last click = new point/note marker
|
||||
new ShowDataLayer(this.map, {
|
||||
features: new StaticFeatureSource(lastClickFiltered),
|
||||
layer: lastClickLayerConfig,
|
||||
onClick: (feature) => {
|
||||
if (this.mapProperties.zoom.data >= Constants.minZoomLevelToAddNewPoint) {
|
||||
this.selectedElement.setData(feature)
|
||||
return
|
||||
}
|
||||
this.map.data.flyTo({
|
||||
zoom: Constants.minZoomLevelToAddNewPoint,
|
||||
center: GeoOperations.centerpointCoordinates(feature)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private drawSpecialLayers() {
|
||||
|
||||
type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
|
||||
type LayersToAdd = "current_view" | Exclude<AddedByDefaultTypes,
|
||||
"search" // Handled by WithSearchState
|
||||
| "last_click" // handled by this.drawLastClick()
|
||||
| "summary" // handled by setupSummaryLayer
|
||||
| "range" // handled by UserMapFeatureSwitchState
|
||||
>
|
||||
const empty = []
|
||||
/**
|
||||
* A listing which maps the layerId onto the featureSource
|
||||
*/
|
||||
const specialLayers: Record<LayersToAdd, FeatureSource> = {
|
||||
home_location: this.userRelatedState.homeLocation,
|
||||
gps_location: this.geolocation.currentUserLocation,
|
||||
gps_location_history: this.geolocation.historicalUserLocations,
|
||||
gps_track: this.geolocation.historicalUserLocationsTrack,
|
||||
current_view: this.currentView,
|
||||
favourite: this.favourites,
|
||||
geocoded_image: new StaticFeatureSource(this.geocodedImages),
|
||||
selected_element: new StaticFeatureSource(
|
||||
this.selectedElement.map((f) => (f === undefined ? empty : [f]))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// enumerate all 'normal' layers and match them with the appropriate 'special' layer - if applicable
|
||||
this.layerState.filteredLayers.forEach((flayer) => {
|
||||
this.registerSpecialLayer(flayer, specialLayers[flayer.layerDef.id])
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private initActorsSpecialLayers() {
|
||||
this.selectedElement.addCallback((selected) => {
|
||||
if (selected === undefined) {
|
||||
this.focusOnMap()
|
||||
this.geocodedImages.set([])
|
||||
} else {
|
||||
this.lastClickObject.clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
107
src/Models/ThemeViewState/WithUserRelatedState.ts
Normal file
107
src/Models/ThemeViewState/WithUserRelatedState.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
||||
import FeatureSwitchState from "../../Logic/State/FeatureSwitchState"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import LayerConfig from "../ThemeConfig/LayerConfig"
|
||||
import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
||||
import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
|
||||
|
||||
export class WithUserRelatedState {
|
||||
readonly theme: ThemeConfig
|
||||
|
||||
readonly featureSwitches: FeatureSwitchState
|
||||
readonly featureSwitchIsTesting: Store<boolean>
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
|
||||
readonly osmConnection: OsmConnection
|
||||
readonly userRelatedState: UserRelatedState
|
||||
readonly overlayLayerStates: ReadonlyMap<
|
||||
string,
|
||||
{ readonly isDisplayed: UIEventSource<boolean> }
|
||||
>
|
||||
|
||||
constructor(theme: ThemeConfig, rasterLayer: Store<{ properties: { id: string } }>) {
|
||||
{
|
||||
// Some weird setups
|
||||
Utils.initDomPurify()
|
||||
if (!Utils.runningFromConsole && theme.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) {
|
||||
Utils.LoadCustomCss(theme.customCss)
|
||||
}
|
||||
}
|
||||
this.theme = theme
|
||||
this.featureSwitches = new FeatureSwitchState(theme)
|
||||
this.osmConnection = new OsmConnection({
|
||||
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
||||
fakeUser: this.featureSwitches.featureSwitchFakeUser.data,
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
"oauth_token",
|
||||
undefined,
|
||||
"Used to complete the login"
|
||||
)
|
||||
})
|
||||
|
||||
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
||||
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
|
||||
|
||||
this.userRelatedState = new UserRelatedState(
|
||||
this.osmConnection,
|
||||
theme,
|
||||
this.featureSwitches,
|
||||
rasterLayer
|
||||
)
|
||||
|
||||
if (!this.theme.official) {
|
||||
// Add custom themes to the "visited custom themes"
|
||||
const th = this.theme
|
||||
this.userRelatedState.addUnofficialTheme({
|
||||
id: th.id,
|
||||
icon: th.icon,
|
||||
title: th.title.translations,
|
||||
shortDescription: th.shortDescription.translations,
|
||||
layers: th.layers.filter((l) => l.isNormal()).map((l) => l.id)
|
||||
})
|
||||
}
|
||||
|
||||
const overlayLayerStates = new Map<string, { isDisplayed: UIEventSource<boolean> }>()
|
||||
this.overlayLayerStates = overlayLayerStates
|
||||
for (const rasterInfo of this.theme.tileLayerSources) {
|
||||
const isDisplayed = QueryParameters.GetBooleanQueryParameter(
|
||||
"overlay-" + rasterInfo.id,
|
||||
rasterInfo.defaultState ?? true,
|
||||
"Whether or not overlay layer " + rasterInfo.id + " is shown"
|
||||
)
|
||||
const state = { isDisplayed }
|
||||
overlayLayerStates.set(rasterInfo.id, state)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the theme
|
||||
*/
|
||||
public getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined {
|
||||
const id = properties.id
|
||||
|
||||
if (id.startsWith("summary_")) {
|
||||
// We don't select 'summary'-objects
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (id === "settings") {
|
||||
return UserRelatedState.usersettingsConfig
|
||||
}
|
||||
if (id.startsWith(LastClickFeatureSource.newPointElementId)) {
|
||||
return this.theme.layers.find((l) => l.id === "last_click")
|
||||
}
|
||||
if (id.startsWith("search_result")) {
|
||||
return GeocodingUtils.searchLayer
|
||||
}
|
||||
if (id === "location_track") {
|
||||
return this.theme.layers.find((l) => l.id === "gps_track")
|
||||
}
|
||||
return this.theme.getMatchingLayer(properties)
|
||||
}
|
||||
|
||||
}
|
||||
108
src/Models/ThemeViewState/WithVisualFeedbackState.ts
Normal file
108
src/Models/ThemeViewState/WithVisualFeedbackState.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Hotkeys from "../../UI/Base/Hotkeys"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import ThemeViewState from "../ThemeViewState"
|
||||
|
||||
export class WithVisualFeedbackState extends ThemeViewState {
|
||||
/**
|
||||
* If true, the user interface will toggle some extra aids for people using screenreaders and keyboard navigation
|
||||
* Triggered by navigating the map with arrows or by pressing 'space' or 'enter'
|
||||
*/
|
||||
public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
|
||||
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
|
||||
super(theme, mvtAvailableLayers)
|
||||
this.initHotkeysVisualFeedback()
|
||||
|
||||
///// ACTORS /////
|
||||
|
||||
this.userRelatedState.a11y.addCallbackAndRunD((a11y) => {
|
||||
if (a11y === "always") {
|
||||
this.visualFeedback.setData(true)
|
||||
} else if (a11y === "never") {
|
||||
this.visualFeedback.setData(false)
|
||||
}
|
||||
})
|
||||
this.mapProperties.onKeyNavigationEvent((keyEvent) => {
|
||||
if (this.userRelatedState.a11y.data === "never") {
|
||||
return
|
||||
}
|
||||
if (["north", "east", "south", "west"].indexOf(keyEvent.key) >= 0) {
|
||||
this.visualFeedback.setData(true)
|
||||
return true // Our job is done, unregister
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Selects the feature that is 'i' closest to the map center
|
||||
*/
|
||||
private selectClosestAtCenter(i: number = 0) {
|
||||
console.log("Selecting closest", i)
|
||||
if (this.userRelatedState.a11y.data !== "never") {
|
||||
this.visualFeedback.setData(true)
|
||||
}
|
||||
|
||||
const toSelect = this.closestFeatures.features?.data?.[i]
|
||||
if (!toSelect) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const toSelect = this.closestFeatures.features?.data?.[i]
|
||||
if (!toSelect) {
|
||||
return
|
||||
}
|
||||
this.setSelectedElement(toSelect)
|
||||
})
|
||||
return
|
||||
}
|
||||
this.setSelectedElement(toSelect)
|
||||
}
|
||||
|
||||
private initHotkeysVisualFeedback() {
|
||||
const docs = Translations.t.hotkeyDocumentation
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{
|
||||
nomod: " ",
|
||||
onUp: true
|
||||
},
|
||||
docs.selectItem,
|
||||
() => {
|
||||
if (this.selectedElement.data !== undefined) {
|
||||
return false
|
||||
}
|
||||
if (this.guistate.isSomethingOpen() || this.previewedImage.data !== undefined) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
document.activeElement.tagName === "button" ||
|
||||
document.activeElement.tagName === "input"
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.selectClosestAtCenter(0)
|
||||
}
|
||||
)
|
||||
|
||||
for (let i = 1; i < 9; i++) {
|
||||
let doc = docs.selectItemI.Subs({ i })
|
||||
if (i === 1) {
|
||||
doc = docs.selectItem
|
||||
} else if (i === 2) {
|
||||
doc = docs.selectItem2
|
||||
} else if (i === 3) {
|
||||
doc = docs.selectItem3
|
||||
}
|
||||
Hotkeys.RegisterHotkey(
|
||||
{
|
||||
nomod: "" + i,
|
||||
onUp: true
|
||||
},
|
||||
doc,
|
||||
() => this.selectClosestAtCenter(i - 1)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue