refactoring

This commit is contained in:
Pieter Vander Vennet 2023-03-28 05:13:48 +02:00
parent b94a8f5745
commit 5d0fe31c41
114 changed files with 2412 additions and 2958 deletions

View file

@ -1,14 +1,90 @@
import { UIEventSource } from "../Logic/UIEventSource"
import LayerConfig from "./ThemeConfig/LayerConfig"
import { TagsFilter } from "../Logic/Tags/TagsFilter"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import { QueryParameters } from "../Logic/Web/QueryParameters"
export interface FilterState {
currentFilter: TagsFilter
state: string | number
}
export default interface FilteredLayer {
export default class FilteredLayer {
/**
* Wether or not the specified layer is shown
*/
readonly isDisplayed: UIEventSource<boolean>
readonly appliedFilters: UIEventSource<Map<string, FilterState>>
/**
* Maps the filter.option.id onto the actual used state
*/
readonly appliedFilters: Map<string, UIEventSource<undefined | number | string>>
readonly layerDef: LayerConfig
constructor(
layer: LayerConfig,
appliedFilters?: Map<string, UIEventSource<undefined | number | string>>,
isDisplayed?: UIEventSource<boolean>
) {
this.layerDef = layer
this.isDisplayed = isDisplayed ?? new UIEventSource(true)
this.appliedFilters =
appliedFilters ?? new Map<string, UIEventSource<number | string | undefined>>()
}
/**
* Creates a FilteredLayer which is tied into the QueryParameters and/or user preferences
*/
public static initLinkedState(
layer: LayerConfig,
context: string,
osmConnection: OsmConnection
) {
let isDisplayed: UIEventSource<boolean>
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.GetParsed(
context + "-layer-" + layer.id + "-enabled",
layer.shownByDefault
)
} else if (layer.syncSelection === "theme-only") {
isDisplayed = FilteredLayer.getPref(
osmConnection,
context + "-layer-" + layer.id + "-enabled",
layer
)
} else if (layer.syncSelection === "global") {
isDisplayed = FilteredLayer.getPref(
osmConnection,
"layer-" + layer.id + "-enabled",
layer
)
} else {
isDisplayed = QueryParameters.GetBooleanQueryParameter(
"layer-" + layer.id,
layer.shownByDefault,
"Whether or not layer " + layer.id + " is shown"
)
}
const appliedFilters = new Map<string, UIEventSource<undefined | number | string>>()
for (const subfilter of layer.filters) {
appliedFilters.set(subfilter.id, subfilter.initState())
}
return new FilteredLayer(layer, appliedFilters, isDisplayed)
}
private static getPref(
osmConnection: OsmConnection,
key: string,
layer: LayerConfig
): UIEventSource<boolean> {
return osmConnection.GetPreference(key, layer.shownByDefault + "").sync(
(v) => {
if (v === undefined) {
return undefined
}
return v === "true"
},
[],
(b) => {
if (b === undefined) {
return undefined
}
return "" + b
}
)
}
}

View file

@ -1,9 +1,10 @@
import { Translation, TypedTranslation } from "../UI/i18n/Translation"
import { FilterState } from "./FilteredLayer"
import { Tag } from "../Logic/Tags/Tag"
import { TagsFilter } from "../Logic/Tags/TagsFilter"
export interface GlobalFilter {
filter: FilterState
osmTags: TagsFilter
state: number | string | undefined
id: string
onNewPoint: {
safetyCheck: Translation

View file

@ -5,8 +5,10 @@ import { RasterLayerPolygon } from "./RasterLayers"
export interface MapProperties {
readonly location: UIEventSource<{ lon: number; lat: number }>
readonly zoom: UIEventSource<number>
readonly bounds: Store<BBox>
readonly bounds: UIEventSource<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
readonly maxbounds: UIEventSource<undefined | BBox>
readonly allowMoving: UIEventSource<true | boolean>
readonly allowZooming: UIEventSource<true | boolean>
}

View file

@ -36,6 +36,14 @@ export class AvailableRasterLayers {
geometry: BBox.global.asGeometry(),
}
public static readonly maplibre: RasterLayerPolygon = {
type: "Feature",
properties: <any>{
name: "MapLibre",
url: null,
},
geometry: BBox.global.asGeometry(),
}
public static layersAvailableAt(
location: Store<{ lon: number; lat: number }>
): Store<RasterLayerPolygon[]> {
@ -58,6 +66,7 @@ export class AvailableRasterLayers {
return GeoOperations.inside(lonlat, eliPolygon)
})
matching.unshift(AvailableRasterLayers.osmCarto)
matching.unshift(AvailableRasterLayers.maplibre)
matching.push(...AvailableRasterLayers.globalLayers)
return matching
})

View file

@ -5,7 +5,6 @@ import Translations from "../../UI/i18n/Translations"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { TagConfigJson } from "./Json/TagConfigJson"
import { UIEventSource } from "../../Logic/UIEventSource"
import { FilterState } from "../FilteredLayer"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import { Utils } from "../../Utils"
import { RegexTag } from "../../Logic/Tags/RegexTag"
@ -144,14 +143,7 @@ export default class FilterConfig {
})
}
public initState(): UIEventSource<FilterState> {
function reset(state: FilterState): string {
if (state === undefined) {
return ""
}
return "" + state.state
}
public initState(): UIEventSource<undefined | number | string> {
let defaultValue = ""
if (this.options.length > 1) {
defaultValue = "" + (this.defaultSelection ?? 0)
@ -159,6 +151,8 @@ export default class FilterConfig {
// Only a single option
if (this.defaultSelection === 0) {
defaultValue = "true"
} else {
defaultValue = "false"
}
}
const qp = QueryParameters.GetQueryParameter(
@ -168,12 +162,6 @@ export default class FilterConfig {
)
if (this.options.length > 1) {
// This is a multi-option filter; state should be a number which selects the correct entry
const possibleStates: FilterState[] = this.options.map((opt, i) => ({
currentFilter: opt.osmTags,
state: i,
}))
// We map the query parameter for this case
return qp.sync(
(str) => {
@ -182,62 +170,29 @@ export default class FilterConfig {
// Nope, not a correct number!
return undefined
}
return possibleStates[parsed]
return parsed
},
[],
reset
(n) => "" + n
)
}
const option = this.options[0]
if (option.fields.length > 0) {
return qp.sync(
(str) => {
// There are variables in play!
// str should encode a json-hash
try {
const props = JSON.parse(str)
const origTags = option.originalTagsSpec
const rewrittenTags = Utils.WalkJson(origTags, (v) => {
if (typeof v !== "string") {
return v
}
for (const key in props) {
v = (<string>v).replace("{" + key + "}", props[key])
}
return v
})
const parsed = TagUtils.Tag(rewrittenTags)
return <FilterState>{
currentFilter: parsed,
state: str,
}
} catch (e) {
return undefined
}
},
[],
reset
)
return qp
}
// The last case is pretty boring: it is checked or it isn't
const filterState: FilterState = {
currentFilter: option.osmTags,
state: "true",
}
return qp.sync(
(str) => {
// Only a single option exists here
if (str === "true") {
return filterState
return 0
}
return undefined
},
[],
reset
(n) => (n === undefined ? "false" : "true")
)
}

View file

@ -205,25 +205,6 @@ export interface LayoutConfigJson {
}
)[]
/**
* If defined, data will be clustered.
* Defaults to {maxZoom: 16, minNeeded: 500}
*/
clustering?:
| {
/**
* All zoom levels above 'maxzoom' are not clustered anymore.
* Defaults to 18
*/
maxZoom?: number
/**
* The number of elements per tile needed to start clustering
* If clustering is defined, defaults to 250
*/
minNeededElements?: number
}
| false
/**
* The URL of a custom CSS stylesheet to modify the layout
*/

View file

@ -40,10 +40,6 @@ export default class LayoutConfig implements LayoutInformation {
public defaultBackgroundId?: string
public layers: LayerConfig[]
public tileLayerSources: TilesourceConfig[]
public readonly clustering?: {
maxZoom: number
minNeededElements: number
}
public readonly hideFromOverview: boolean
public lockLocation: boolean | [[number, number], [number, number]]
public readonly enableUserBadge: boolean
@ -188,22 +184,6 @@ export default class LayoutConfig implements LayoutInformation {
context + ".extraLink"
)
this.clustering = {
maxZoom: 16,
minNeededElements: 250,
}
if (json.clustering === false) {
this.clustering = {
maxZoom: 0,
minNeededElements: 100000,
}
} else if (json.clustering) {
this.clustering = {
maxZoom: json.clustering.maxZoom ?? 18,
minNeededElements: json.clustering.minNeededElements ?? 250,
}
}
this.hideFromOverview = json.hideFromOverview ?? false
this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined
this.enableUserBadge = json.enableUserBadge ?? true

View file

@ -11,8 +11,6 @@ import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import Img from "../../UI/Base/Img"
import Combine from "../../UI/Base/Combine"
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
import { OsmTags } from "../OsmFeature"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
export default class PointRenderingConfig extends WithContextLoader {
private static readonly allowed_location_codes = new Set<string>([
@ -176,7 +174,7 @@ export default class PointRenderingConfig extends WithContextLoader {
return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin)
}
public GetSimpleIcon(tags: Store<OsmTags>): BaseUIElement {
public GetSimpleIcon(tags: Store<Record<string, string>>): BaseUIElement {
const self = this
if (this.icon === undefined) {
return undefined
@ -187,7 +185,7 @@ export default class PointRenderingConfig extends WithContextLoader {
}
public RenderIcon(
tags: Store<OsmTags>,
tags: Store<Record<string, string>>,
clickable: boolean,
options?: {
noSize?: false | boolean
@ -277,7 +275,7 @@ export default class PointRenderingConfig extends WithContextLoader {
}
}
private GetBadges(tags: Store<OsmTags>): BaseUIElement {
private GetBadges(tags: Store<Record<string, string>>): BaseUIElement {
if (this.iconBadges.length === 0) {
return undefined
}
@ -309,7 +307,7 @@ export default class PointRenderingConfig extends WithContextLoader {
).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0")
}
private GetLabel(tags: Store<OsmTags>): BaseUIElement {
private GetLabel(tags: Store<Record<string, string>>): BaseUIElement {
if (this.label === undefined) {
return undefined
}

278
Models/ThemeViewState.ts Normal file
View file

@ -0,0 +1,278 @@
import LayoutConfig from "./ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import FeatureSource, {
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { DefaultGuiState } from "../UI/DefaultGuiState"
import { MapProperties } from "./MapProperties"
import LayerState from "../Logic/State/LayerState"
import { Feature } from "geojson"
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { Map as MlMap } from "maplibre-gl"
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"
import { GeoLocationState } from "../Logic/State/GeoLocationState"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LayerConfig from "./ThemeConfig/LayerConfig"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { AvailableRasterLayers, RasterLayerPolygon } from "./RasterLayers"
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
import ShowDataLayer from "../UI/Map/ShowDataLayer"
import TitleHandler from "../Logic/Actors/TitleHandler"
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"
import { BBox } from "../Logic/BBox"
import Constants from "./Constants"
import Hotkeys from "../UI/Base/Hotkeys"
import Translations from "../UI/i18n/Translations"
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
/**
*
* The themeviewState contains all the state needed for the themeViewGUI.
*
* This is pretty much the 'brain' or the HQ of MapComplete
*
* It ties up all the needed elements and starts some actors.
*/
export default class ThemeViewState implements SpecialVisualizationState {
readonly layout: LayoutConfig
readonly map: UIEventSource<MlMap>
readonly changes: Changes
readonly featureSwitches: FeatureSwitchState
readonly featureSwitchIsTesting: Store<boolean>
readonly featureSwitchUserbadge: Store<boolean>
readonly featureProperties: FeaturePropertiesStore
readonly osmConnection: OsmConnection
readonly selectedElement: UIEventSource<Feature>
readonly mapProperties: MapProperties
readonly dataIsLoading: Store<boolean> // TODO
readonly guistate: DefaultGuiState
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
readonly historicalUserLocations: WritableFeatureSource
readonly indexedFeatures: IndexedFeatureSource
readonly layerState: LayerState
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly availableLayers: Store<RasterLayerPolygon[]>
readonly selectedLayer: UIEventSource<LayerConfig>
readonly userRelatedState: UserRelatedState
readonly geolocation: GeoLocationHandler
constructor(layout: LayoutConfig) {
this.layout = layout
this.guistate = new DefaultGuiState()
this.map = new UIEventSource<MlMap>(undefined)
const initial = new InitialMapPositioning(layout)
this.mapProperties = new MapLibreAdaptor(this.map, initial)
const geolocationState = new GeoLocationState()
this.featureSwitches = new FeatureSwitchState(layout)
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchUserbadge
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"
),
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data,
})
this.userRelatedState = new UserRelatedState(this.osmConnection, layout?.language)
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
this.geolocation = new GeoLocationHandler(
geolocationState,
this.selectedElement,
this.mapProperties,
this.userRelatedState.gpsLocationHistoryRetentionTime
)
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
const indexedElements = new LayoutSource(
layout.layers,
this.featureSwitches,
new StaticFeatureSource([]),
this.mapProperties,
this.osmConnection.Backend(),
(id) => this.layerState.filteredLayers.get(id).isDisplayed
)
this.featureProperties = new FeaturePropertiesStore(indexedElements)
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()),
indexedElements,
{
constructStore: (features, layer) => new GeoIndexedStoreForLayer(features, layer),
}
)
this.perLayer = perLayer.perLayer
this.perLayer.forEach((fs) => {
new SaveFeatureSourceToLocalStorage(fs.layer.layerDef.id, 15, fs)
const filtered = new FilteringFeatureSource(
fs.layer,
fs,
(id) => this.featureProperties.getStore(id),
this.layerState.globalFilters
)
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed]
)
doShowLayer.addCallbackAndRunD((doShow) =>
console.log(
"Layer",
fs.layer.layerDef.id,
"is",
doShow,
this.mapProperties.zoom.data,
fs.layer.layerDef.minzoom
)
)
new ShowDataLayer(this.map, {
layer: fs.layer.layerDef,
features: filtered,
doShowLayer,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
fetchStore: (id) => this.featureProperties.getStore(id),
})
})
this.changes = new Changes(
{
dryRun: this.featureSwitches.featureSwitchIsTesting,
allElements: indexedElements,
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations,
},
layout?.isLeftRightSensitive() ?? false
)
this.initActors()
this.drawSpecialLayers()
this.initHotkeys()
this.miscSetup()
}
/**
* Various small methods that need to be called
*/
private miscSetup() {
this.userRelatedState.markLayoutAsVisited(this.layout)
}
private initHotkeys() {
Hotkeys.RegisterHotkey(
{ nomod: "Escape", onUp: true },
Translations.t.hotkeyDocumentation.closeSidebar,
() => {
this.selectedElement.setData(undefined)
this.guistate.closeAll()
}
)
}
/**
* Add the special layers to the map
* @private
*/
private drawSpecialLayers() {
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
/**
* A listing which maps the layerId onto the featureSource
*/
const empty = []
const specialLayers: Record<AddedByDefaultTypes | "current_view", FeatureSource> = {
home_location: this.userRelatedState.homeLocation,
gps_location: this.geolocation.currentUserLocation,
gps_location_history: this.geolocation.historicalUserLocations,
gps_track: this.geolocation.historicalUserLocationsTrack,
selected_element: new StaticFeatureSource(
this.selectedElement.map((f) => (f === undefined ? empty : [f]))
),
range: new StaticFeatureSource(
this.mapProperties.maxbounds.map((bbox) =>
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
)
),
current_view: new StaticFeatureSource(
this.mapProperties.bounds.map((bbox) =>
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "current_view" })]
)
),
}
if (this.layout?.lockLocation) {
const bbox = new BBox(this.layout.lockLocation)
this.mapProperties.maxbounds.setData(bbox)
ShowDataLayer.showRange(
this.map,
new StaticFeatureSource([bbox.asGeoJson({})]),
this.featureSwitches.featureSwitchIsTesting
)
}
this.layerState.filteredLayers
.get("range")
?.isDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
this.layerState.filteredLayers.forEach((flayer) => {
const features = specialLayers[flayer.layerDef.id]
if (features === undefined) {
return
}
new ShowDataLayer(this.map, {
features,
doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
})
})
}
/**
* Setup various services for which no reference are needed
* @private
*/
private initActors() {
// Various actors that we don't need to reference
new TitleHandler(
this.selectedElement,
this.selectedLayer,
this.featureProperties,
this.layout
)
new ChangeToElementsActor(this.changes, this.featureProperties)
new PendingChangesUploader(this.changes, this.selectedElement)
new SelectedElementTagsUpdater({
allElements: this.featureProperties,
changes: this.changes,
selectedElement: this.selectedElement,
layoutToUse: this.layout,
osmConnection: this.osmConnection,
})
}
}