refactoring: fix basic flow to add a new point

This commit is contained in:
Pieter Vander Vennet 2023-04-06 01:33:08 +02:00
parent 52a0810ea9
commit 0241f89d3d
109 changed files with 1931 additions and 1446 deletions

View file

@ -1,7 +1,7 @@
import { Utils } from "../Utils"
export default class Constants {
public static vNumber = "0.27.0"
public static vNumber = "0.30.0"
public static ImgurApiKey = "7070e7167f0a25a"
public static readonly mapillary_client_token_v4 =

View file

@ -1,8 +1,13 @@
import { UIEventSource } from "../Logic/UIEventSource"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import LayerConfig from "./ThemeConfig/LayerConfig"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import { FilterConfigOption } from "./ThemeConfig/FilterConfig"
import { TagsFilter } from "../Logic/Tags/TagsFilter"
import { Utils } from "../Utils"
import { TagUtils } from "../Logic/Tags/TagUtils"
import { And } from "../Logic/Tags/And"
export default class FilteredLayer {
/**
@ -10,11 +15,22 @@ export default class FilteredLayer {
*/
readonly isDisplayed: UIEventSource<boolean>
/**
* Maps the filter.option.id onto the actual used state
* Maps the filter.option.id onto the actual used state.
* This state is either the chosen option (as number) or a representation of the fields
*/
readonly appliedFilters: Map<string, UIEventSource<undefined | number | string>>
readonly appliedFilters: ReadonlyMap<string, UIEventSource<undefined | number | string>>
readonly layerDef: LayerConfig
/**
* Indicates if some filter is set.
* If this is the case, adding a new element of this type might be a bad idea
*/
readonly hasFilter: Store<boolean>
/**
* Contains the current properties a feature should fulfill in order to match the filter
*/
readonly currentFilter: Store<TagsFilter | undefined>
constructor(
layer: LayerConfig,
appliedFilters?: Map<string, UIEventSource<undefined | number | string>>,
@ -24,6 +40,105 @@ export default class FilteredLayer {
this.isDisplayed = isDisplayed ?? new UIEventSource(true)
this.appliedFilters =
appliedFilters ?? new Map<string, UIEventSource<number | string | undefined>>()
const hasFilter = new UIEventSource<boolean>(false)
const self = this
const currentTags = new UIEventSource<TagsFilter>(undefined)
this.appliedFilters.forEach((filterSrc) => {
filterSrc.addCallbackAndRun((filter) => {
if ((filter ?? 0) !== 0) {
hasFilter.setData(true)
currentTags.setData(self.calculateCurrentTags())
return
}
const hf = Array.from(self.appliedFilters.values()).some((f) => (f.data ?? 0) !== 0)
if (hf) {
currentTags.setData(self.calculateCurrentTags())
} else {
currentTags.setData(undefined)
}
hasFilter.setData(hf)
})
})
currentTags.addCallbackAndRunD((t) => console.log("Current filter is", t))
this.currentFilter = currentTags
}
private calculateCurrentTags(): TagsFilter {
let needed: TagsFilter[] = []
for (const filter of this.layerDef.filters) {
const state = this.appliedFilters.get(filter.id)
if (state.data === undefined) {
continue
}
if (filter.options[0].fields.length > 0) {
const fieldProperties = FilteredLayer.stringToFieldProperties(<string>state.data)
const asTags = FilteredLayer.fieldsToTags(filter.options[0], fieldProperties)
needed.push(asTags)
continue
}
needed.push(filter.options[state.data].osmTags)
}
needed = Utils.NoNull(needed)
if (needed.length == 0) {
return undefined
}
let tags: TagsFilter
if (needed.length == 1) {
tags = needed[1]
} else {
tags = new And(needed)
}
let optimized = tags.optimize()
if (optimized === true) {
return undefined
}
if (optimized === false) {
return tags
}
return optimized
}
public static fieldsToString(values: Record<string, string>): string {
return JSON.stringify(values)
}
public static stringToFieldProperties(value: string): Record<string, string> {
return JSON.parse(value)
}
private static fieldsToTags(
option: FilterConfigOption,
fieldstate: string | Record<string, string>
): TagsFilter {
let properties: Record<string, string>
if (typeof fieldstate === "string") {
properties = FilteredLayer.stringToFieldProperties(fieldstate)
} else {
properties = fieldstate
}
console.log("Building tagsspec with properties", properties)
const tagsSpec = Utils.WalkJson(option.originalTagsSpec, (v) => {
if (typeof v !== "string") {
return v
}
for (const key in properties) {
v = (<string>v).replace("{" + key + "}", properties[key])
}
return v
})
return TagUtils.Tag(tagsSpec)
}
public disableAllFilters(): void {
this.appliedFilters.forEach((value) => value.setData(undefined))
}
/**

View file

@ -5,6 +5,7 @@ import { RasterLayerPolygon } from "./RasterLayers"
export interface MapProperties {
readonly location: UIEventSource<{ lon: number; lat: number }>
readonly zoom: UIEventSource<number>
readonly minzoom: UIEventSource<number>
readonly bounds: UIEventSource<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
readonly maxbounds: UIEventSource<undefined | BBox>

64
Models/MenuState.ts Normal file
View file

@ -0,0 +1,64 @@
import LayerConfig from "./ThemeConfig/LayerConfig"
import { UIEventSource } from "../Logic/UIEventSource"
/**
* Indicates if a menu is open, and if so, which tab is selected;
* Some tabs allow to highlight an element.
*
* Some convenience methods are provided for this as well
*/
export class MenuState {
private static readonly _themeviewTabs = ["intro", "filters"] as const
public readonly themeIsOpened = new UIEventSource(true)
public readonly themeViewTabIndex: UIEventSource<number>
public readonly themeViewTab: UIEventSource<typeof MenuState._themeviewTabs[number]>
private static readonly _menuviewTabs = ["about", "settings", "community", "privacy"] as const
public readonly menuIsOpened = new UIEventSource(false)
public readonly menuViewTabIndex: UIEventSource<number>
public readonly menuViewTab: UIEventSource<typeof MenuState._menuviewTabs[number]>
public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>(
undefined
)
constructor() {
this.themeViewTabIndex = new UIEventSource(0)
this.themeViewTab = this.themeViewTabIndex.sync(
(i) => MenuState._themeviewTabs[i],
[],
(str) => MenuState._themeviewTabs.indexOf(<any>str)
)
this.menuViewTabIndex = new UIEventSource(1)
this.menuViewTab = this.menuViewTabIndex.sync(
(i) => MenuState._menuviewTabs[i],
[],
(str) => MenuState._menuviewTabs.indexOf(<any>str)
)
this.themeIsOpened.addCallbackAndRun((isOpen) => {
if (!isOpen) {
this.highlightedLayerInFilters.setData(undefined)
}
})
this.themeViewTab.addCallbackAndRun((tab) => {
if (tab !== "filters") {
this.highlightedLayerInFilters.setData(undefined)
}
})
}
public openFilterView(highlightLayer?: LayerConfig | string) {
this.themeIsOpened.setData(true)
this.themeViewTab.setData("filters")
if (highlightLayer) {
if (typeof highlightLayer !== "string") {
highlightLayer = highlightLayer.id
}
this.highlightedLayerInFilters.setData(highlightLayer)
}
}
public closeAll() {
this.menuIsOpened.setData(false)
this.themeIsOpened.setData(false)
}
}

View file

@ -11,15 +11,16 @@ import { RegexTag } from "../../Logic/Tags/RegexTag"
import BaseUIElement from "../../UI/BaseUIElement"
import Table from "../../UI/Base/Table"
import Combine from "../../UI/Base/Combine"
export type FilterConfigOption = {
question: Translation
osmTags: TagsFilter | undefined
/* Only set if fields are present. Used to create `osmTags` (which are used to _actually_ filter) when the field is written*/
readonly originalTagsSpec: TagConfigJson
fields: { name: string; type: string }[]
}
export default class FilterConfig {
public readonly id: string
public readonly options: {
question: Translation
osmTags: TagsFilter | undefined
originalTagsSpec: TagConfigJson
fields: { name: string; type: string }[]
}[]
public readonly options: FilterConfigOption[]
public readonly defaultSelection?: number
constructor(json: FilterConfigJson, context: string) {

View file

@ -2,12 +2,12 @@ import LayoutConfig from "./ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes"
import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource"
import FeatureSource, {
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"
@ -39,6 +39,8 @@ import Hotkeys from "../UI/Base/Hotkeys"
import Translations from "../UI/i18n/Translations"
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
import SimpleFeatureSource from "../Logic/FeatureSource/Sources/SimpleFeatureSource"
import { MenuState } from "./MenuState"
/**
*
@ -63,11 +65,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
readonly mapProperties: MapProperties
readonly dataIsLoading: Store<boolean> // TODO
readonly guistate: DefaultGuiState
readonly guistate: MenuState
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
readonly historicalUserLocations: WritableFeatureSource
readonly indexedFeatures: IndexedFeatureSource
readonly newFeatures: WritableFeatureSource
readonly layerState: LayerState
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly availableLayers: Store<RasterLayerPolygon[]>
@ -75,9 +78,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
readonly userRelatedState: UserRelatedState
readonly geolocation: GeoLocationHandler
readonly lastClickObject: WritableFeatureSource
constructor(layout: LayoutConfig) {
this.layout = layout
this.guistate = new DefaultGuiState()
this.guistate = new MenuState()
this.map = new UIEventSource<MlMap>(undefined)
const initial = new InitialMapPositioning(layout)
this.mapProperties = new MapLibreAdaptor(this.map, initial)
@ -109,20 +113,26 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
const self = this
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
this.newFeatures = new SimpleFeatureSource(undefined)
this.indexedFeatures = new LayoutSource(
layout.layers,
this.featureSwitches,
new StaticFeatureSource([]),
this.newFeatures,
this.mapProperties,
this.osmConnection.Backend(),
(id) => this.layerState.filteredLayers.get(id).isDisplayed
(id) => self.layerState.filteredLayers.get(id).isDisplayed
)
const lastClick = (this.lastClickObject = new LastClickFeatureSource(
this.mapProperties.lastClickLocation,
this.layout
))
const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements)
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()).filter(
(l) => l.layerDef.source !== null
(l) => l.layerDef?.source !== null
),
indexedElements,
{
@ -176,9 +186,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
)
this.initActors()
this.drawSpecialLayers()
this.drawSpecialLayers(lastClick)
this.initHotkeys()
this.miscSetup()
console.log("State setup completed", this)
}
/**
@ -197,21 +208,30 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.guistate.closeAll()
}
)
Hotkeys.RegisterHotkey(
{
nomod: "b",
},
Translations.t.hotkeyDocumentation.openLayersPanel,
() => {
if (this.featureSwitches.featureSwitchFilter.data) {
this.guistate.openFilterView()
}
}
)
}
/**
* Add the special layers to the map
* @private
*/
private drawSpecialLayers() {
private drawSpecialLayers(last_click: LastClickFeatureSource) {
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
const empty = []
{
// The last_click gets a _very_ special treatment
const last_click = new LastClickFeatureSource(
this.mapProperties.lastClickLocation,
this.layout
)
const last_click_layer = this.layerState.filteredLayers.get("last_click")
this.featureProperties.addSpecial(
"last_click",

View file

@ -84,7 +84,7 @@ export class Tiles {
* Return x, y of the tile containing (lat, lon) on the given zoom level
*/
static embedded_tile(lat: number, lon: number, z: number): { x: number; y: number; z: number } {
return { x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z: z }
return { x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z }
}
static tileRangeFrom(bbox: BBox, zoomlevel: number) {