forked from MapComplete/MapComplete
refactoring: fix basic flow to add a new point
This commit is contained in:
parent
52a0810ea9
commit
0241f89d3d
109 changed files with 1931 additions and 1446 deletions
|
@ -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 =
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
64
Models/MenuState.ts
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue