refactoring: split all the states

This commit is contained in:
Pieter Vander Vennet 2023-03-25 02:48:24 +01:00
parent 4d48b1cf2b
commit 8e2f04c0d0
32 changed files with 411 additions and 395 deletions

View file

@ -148,7 +148,7 @@ export default class OverpassFeatureSource implements FeatureSource {
if (typeof layer === "string") {
throw "A layer was not expanded!"
}
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
if (layer.source === undefined) {
continue
}
if (this.state.locationControl.data.zoom < layer.minzoom) {

View file

@ -7,14 +7,9 @@ import { ElementStorage } from "../ElementStorage"
import { Utils } from "../../Utils"
export default class TitleHandler {
constructor(state: {
selectedElement: Store<any>
layoutToUse: LayoutConfig
allElements: ElementStorage
}) {
const currentTitle: Store<string> = state.selectedElement.map(
constructor(selectedElement: Store<any>, layout: LayoutConfig, allElements: ElementStorage) {
const currentTitle: Store<string> = selectedElement.map(
(selected) => {
const layout = state.layoutToUse
const defaultTitle = layout?.title?.txt ?? "MapComplete"
if (selected === undefined) {
@ -28,8 +23,7 @@ export default class TitleHandler {
}
if (layer.source.osmTags.matchesProperties(tags)) {
const tagsSource =
state.allElements.getEventSourceById(tags.id) ??
new UIEventSource<any>(tags)
allElements.getEventSourceById(tags.id) ?? new UIEventSource<any>(tags)
const title = new TagRenderingAnswer(tagsSource, layer.title, {})
return (
new Combine([defaultTitle, " | ", title]).ConstructElement()

View file

@ -189,7 +189,7 @@ export class BBox {
public asGeoJson<T>(properties: T): Feature<Polygon, T> {
return {
type: "Feature",
properties: properties,
properties,
geometry: this.asGeometry(),
}
}

View file

@ -50,7 +50,7 @@ export default class FeaturePipeline {
public readonly relationTracker: RelationsTracker
/**
* Keeps track of all raw OSM-nodes.
* Only initialized if 'type_node' is defined as layer
* Only initialized if `ReplaceGeometryAction` is needed somewhere
*/
public readonly fullNodeDatabase?: FullNodeDatabaseSource
private readonly overpassUpdater: OverpassFeatureSource
@ -132,14 +132,6 @@ export default class FeaturePipeline {
// We do not mark as visited here, this is the responsability of the code near the actual loader (e.g. overpassLoader and OSMApiFeatureLoader)
}
function handlePriviligedFeatureSource(src: FeatureSourceForLayer & Tiled) {
// Passthrough to passed function, except that it registers as well
handleFeatureSource(src)
src.features.addCallbackAndRunD((fs) => {
fs.forEach((ff) => state.allElements.addOrGetElement(<any>ff))
})
}
for (const filteredLayer of state.filteredLayers.data) {
const id = filteredLayer.layerDef.id
const source = filteredLayer.layerDef.source
@ -160,36 +152,6 @@ export default class FeaturePipeline {
continue
}
if (id === "selected_element") {
handlePriviligedFeatureSource(state.selectedElementsLayer)
continue
}
if (id === "gps_location") {
handlePriviligedFeatureSource(state.currentUserLocation)
continue
}
if (id === "gps_location_history") {
handlePriviligedFeatureSource(state.historicalUserLocations)
continue
}
if (id === "gps_track") {
handlePriviligedFeatureSource(state.historicalUserLocationsTrack)
continue
}
if (id === "home_location") {
handlePriviligedFeatureSource(state.homeLocation)
continue
}
if (id === "current_view") {
handlePriviligedFeatureSource(state.currentView)
continue
}
const localTileSaver = new SaveTileToLocalStorageActor(filteredLayer)
this.localStorageSavers.set(filteredLayer.layerDef.id, localTileSaver)

View file

@ -14,7 +14,7 @@ import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
export default class FeaturePipelineState extends MapState {
export default class FeaturePipelineState {
/**
* The piece of code which fetches data from various sources and shows it on the background map
*/
@ -27,8 +27,6 @@ export default class FeaturePipelineState extends MapState {
>()
constructor(layoutToUse: LayoutConfig) {
super(layoutToUse)
const clustering = layoutToUse?.clustering
this.featureAggregator = TileHierarchyAggregator.createHierarchy(this)
const clusterCounter = this.featureAggregator

145
Logic/State/LayerState.ts Normal file
View file

@ -0,0 +1,145 @@
import { UIEventSource } from "../UIEventSource"
import { GlobalFilter } from "../../Models/GlobalFilter"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { OsmConnection } from "../Osm/OsmConnection"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { QueryParameters } from "../Web/QueryParameters"
/**
* The layer state keeps track of:
* - Which layers are enabled
* - Which filters are used, including 'global' filters
*/
export default class LayerState {
/**
* Filters which apply onto all layers
*/
public readonly globalFilters: UIEventSource<GlobalFilter[]> = new UIEventSource(
[],
"globalFilters"
)
/**
* Which layers are enabled in the current theme and what filters are applied onto them
*/
public readonly filteredLayers: Map<string, FilteredLayer>
private readonly osmConnection: OsmConnection
/**
*
* @param osmConnection
* @param layers
* @param context: the context, probably the name of the theme. Used to disambiguate the upstream user preference
*/
constructor(osmConnection: OsmConnection, layers: LayerConfig[], context: string) {
this.osmConnection = osmConnection
this.filteredLayers = new Map()
for (const layer of layers) {
this.filteredLayers.set(layer.id, this.initFilteredLayer(layer, context))
}
layers.forEach((l) => this.linkFilterStates(l))
}
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
}
)
}
/**
* INitializes a filtered layer for the given layer.
* @param layer
* @param context: probably the theme-name. This is used to disambiguate the user settings; e.g. when using the same layer in different contexts
* @private
*/
private initFilteredLayer(layer: LayerConfig, context: string): FilteredLayer | undefined {
let isDisplayed: UIEventSource<boolean>
const osmConnection = this.osmConnection
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.GetParsed(
context + "-layer-" + layer.id + "-enabled",
layer.shownByDefault
)
} else if (layer.syncSelection === "theme-only") {
isDisplayed = LayerState.getPref(
osmConnection,
context + "-layer-" + layer.id + "-enabled",
layer
)
} else if (layer.syncSelection === "global") {
isDisplayed = LayerState.getPref(osmConnection, "layer-" + layer.id + "-enabled", layer)
} else {
isDisplayed = QueryParameters.GetBooleanQueryParameter(
"layer-" + layer.id,
layer.shownByDefault,
"Wether or not layer " + layer.id + " is shown"
)
}
const flayer: FilteredLayer = {
isDisplayed,
layerDef: layer,
appliedFilters: new UIEventSource<Map<string, FilterState>>(
new Map<string, FilterState>()
),
}
layer.filters?.forEach((filterConfig) => {
const stateSrc = filterConfig.initState()
stateSrc.addCallbackAndRun((state) =>
flayer.appliedFilters.data.set(filterConfig.id, state)
)
flayer.appliedFilters
.map((dict) => dict.get(filterConfig.id))
.addCallback((state) => stateSrc.setData(state))
})
return flayer
}
/**
* Some layers copy the filter state of another layer - this is quite often the case for 'sibling'-layers,
* (where two variations of the same layer are used, e.g. a specific type of shop on all zoom levels and all shops on high zoom).
*
* This methods links those states for the given layer
*/
private linkFilterStates(layer: LayerConfig) {
if (layer.filterIsSameAs === undefined) {
return
}
const toReuse = this.filteredLayers.get(layer.filterIsSameAs)
if (toReuse === undefined) {
throw (
"Error in layer " +
layer.id +
": it defines that it should be use the filters of " +
layer.filterIsSameAs +
", but this layer was not loaded"
)
}
console.warn(
"Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs
)
this.filteredLayers.set(layer.id, {
isDisplayed: toReuse.isDisplayed,
layerDef: layer,
appliedFilters: toReuse.appliedFilters,
})
}
}

View file

@ -1,31 +1,19 @@
import { Store, UIEventSource } from "../UIEventSource"
import Attribution from "../../UI/BigComponents/Attribution"
import BaseUIElement from "../../UI/BaseUIElement"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import FilteredLayer from "../../Models/FilteredLayer"
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
import { QueryParameters } from "../Web/QueryParameters"
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"
import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import TitleHandler from "../Actors/TitleHandler"
import { BBox } from "../BBox"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
import StaticFeatureSource, {
TiledStaticFeatureSource,
} from "../FeatureSource/Sources/StaticFeatureSource"
import { OsmConnection } from "../Osm/OsmConnection"
import { Feature } from "geojson"
import { Map as MlMap } from "maplibre-gl"
import { GlobalFilter } from "../../Models/GlobalFilter"
import { MapProperties } from "../../Models/MapProperties"
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
/**
* Contains all the leaflet-map related state
*/
export default class MapState {
/**
* Last location where a click was registered
*/
@ -46,21 +34,6 @@ export default class MapState {
*/
public selectedElementsLayer: FeatureSourceForLayer & Tiled
public readonly mainMapObject: BaseUIElement
/**
* Which layers are enabled in the current theme and what filters are applied onto them
*/
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(
[],
"filteredLayers"
)
/**
* Filters which apply onto all layers
*/
public globalFilters: UIEventSource<GlobalFilter[]> = new UIEventSource([], "globalFilters")
/**
* Which overlays are shown
*/
@ -80,16 +53,6 @@ export default class MapState {
this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer)
this.backgroundLayer.addCallbackAndRunD((layer) => self.backgroundLayerId.setData(layer.id))
// Will write into this.leafletMap
this.mainMapObject = Minimap.createMiniMap({
background: this.backgroundLayer,
location: this.locationControl,
leafletMap: this.leafletMap,
bounds: this.currentBounds,
attribution: attr,
lastClickLocation: this.LastClickLocation,
})
this.overlayToggles =
this.layoutToUse?.tileLayerSources
?.filter((c) => c.name !== undefined)
@ -101,16 +64,11 @@ export default class MapState {
"Wether or not the overlay " + c.id + " is shown"
),
})) ?? []
this.filteredLayers = new UIEventSource<FilteredLayer[]>(
MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection)
)
this.AddAllOverlaysToMap(this.leafletMap)
this.initCurrentView()
this.initSelectedElement()
new TitleHandler(this)
}
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) {
@ -128,48 +86,23 @@ export default class MapState {
}
}
private initCurrentView() {
let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter(
(l) => l.layerDef.id === "current_view"
)[0]
if (currentViewLayer === undefined) {
// This layer is not needed by the theme and thus unloaded
return
}
private static initCurrentView(mapproperties: MapProperties): FeatureSource {
let i = 0
const self = this
const features: Store<Feature[]> = this.currentBounds.map((bounds) => {
const features: Store<Feature[]> = mapproperties.bounds.map((bounds) => {
if (bounds === undefined) {
return []
}
i++
const feature = {
type: "Feature",
properties: {
return [
bounds.asGeoJson({
id: "current_view-" + i,
current_view: "yes",
zoom: "" + self.locationControl.data.zoom,
},
geometry: {
type: "Polygon",
coordinates: [
[
[bounds.maxLon, bounds.maxLat],
[bounds.minLon, bounds.maxLat],
[bounds.minLon, bounds.minLat],
[bounds.maxLon, bounds.minLat],
[bounds.maxLon, bounds.maxLat],
],
],
},
}
return [feature]
zoom: "" + mapproperties.zoom.data,
}),
]
})
this.currentView = new TiledStaticFeatureSource(features, currentViewLayer)
return new StaticFeatureSource(features)
}
private initSelectedElement() {
@ -197,113 +130,4 @@ export default class MapState {
})
this.selectedElementsLayer = new TiledStaticFeatureSource(store, layerDef)
}
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
}
)
}
public static InitializeFilteredLayers(
layoutToUse: { layers: LayerConfig[]; id: string },
osmConnection: OsmConnection
): FilteredLayer[] {
if (layoutToUse === undefined) {
return []
}
const flayers: FilteredLayer[] = []
for (const layer of layoutToUse.layers) {
let isDisplayed: UIEventSource<boolean>
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.GetParsed(
layoutToUse.id + "-layer-" + layer.id + "-enabled",
layer.shownByDefault
)
} else if (layer.syncSelection === "theme-only") {
isDisplayed = MapState.getPref(
osmConnection,
layoutToUse.id + "-layer-" + layer.id + "-enabled",
layer
)
} else if (layer.syncSelection === "global") {
isDisplayed = MapState.getPref(
osmConnection,
"layer-" + layer.id + "-enabled",
layer
)
} else {
isDisplayed = QueryParameters.GetBooleanQueryParameter(
"layer-" + layer.id,
layer.shownByDefault,
"Wether or not layer " + layer.id + " is shown"
)
}
const flayer: FilteredLayer = {
isDisplayed,
layerDef: layer,
appliedFilters: new UIEventSource<Map<string, FilterState>>(
new Map<string, FilterState>()
),
}
layer.filters.forEach((filterConfig) => {
const stateSrc = filterConfig.initState()
stateSrc.addCallbackAndRun((state) =>
flayer.appliedFilters.data.set(filterConfig.id, state)
)
flayer.appliedFilters
.map((dict) => dict.get(filterConfig.id))
.addCallback((state) => stateSrc.setData(state))
})
flayers.push(flayer)
}
for (const layer of layoutToUse.layers) {
if (layer.filterIsSameAs === undefined) {
continue
}
const toReuse = flayers.find((l) => l.layerDef.id === layer.filterIsSameAs)
if (toReuse === undefined) {
throw (
"Error in layer " +
layer.id +
": it defines that it should be use the filters of " +
layer.filterIsSameAs +
", but this layer was not loaded"
)
}
console.warn(
"Linking filter and isDisplayed-states of " +
layer.id +
" and " +
layer.filterIsSameAs
)
const selfLayer = flayers.findIndex((l) => l.layerDef.id === layer.id)
flayers[selfLayer] = {
isDisplayed: toReuse.isDisplayed,
layerDef: layer,
appliedFilters: toReuse.appliedFilters,
}
}
return flayers
}
}

View file

@ -6,6 +6,7 @@ import Locale from "../../UI/i18n/Locale"
import { Changes } from "../Osm/Changes"
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
import FeatureSource from "../FeatureSource/FeatureSource"
import { Feature } from "geojson"
/**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
@ -182,7 +183,7 @@ export default class UserRelatedState {
private initHomeLocation(): FeatureSource {
const empty = []
const feature = Stores.ListStabilized(
const feature: Store<Feature[]> = Stores.ListStabilized(
this.osmConnection.userDetails.map((userDetails) => {
if (userDetails === undefined) {
return undefined
@ -198,21 +199,18 @@ export default class UserRelatedState {
return empty
}
return [
{
feature: {
type: "Feature",
properties: {
id: "home",
"user:home": "yes",
_lon: homeLonLat[0],
_lat: homeLonLat[1],
},
geometry: {
type: "Point",
coordinates: homeLonLat,
},
<Feature>{
type: "Feature",
properties: {
id: "home",
"user:home": "yes",
_lon: homeLonLat[0],
_lat: homeLonLat[1],
},
geometry: {
type: "Point",
coordinates: homeLonLat,
},
freshness: new Date(),
},
]
})

View file

@ -26,31 +26,30 @@ export default class Constants {
// Doesn't support nwr: "https://overpass.openstreetmap.fr/api/interpreter"
]
public static readonly added_by_default: string[] = [
public static readonly added_by_default = [
"selected_element",
"gps_location",
"gps_location_history",
"home_location",
"gps_track",
]
public static readonly no_include: string[] = [
"range",
] as const
/**
* Special layers which are not included in a theme by default
*/
public static readonly no_include = [
"conflation",
"left_right_style",
"split_point",
"current_view",
"matchpoint",
]
] as const
/**
* Layer IDs of layers which have special properties through built-in hooks
*/
public static readonly priviliged_layers: string[] = [
public static readonly priviliged_layers = [
...Constants.added_by_default,
"type_node",
"note",
"import_candidate",
"direction",
...Constants.no_include,
]
] as const
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {

View file

@ -255,7 +255,7 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
const creator = new CreateNoteImportLayer()
for (let i1 = 0; i1 < allLayers.length; i1++) {
const layer = allLayers[i1]
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
if (layer.source === undefined) {
// Priviliged layers are skipped
continue
}
@ -600,7 +600,7 @@ class PreparePersonalTheme extends DesugaringStep<LayoutConfigJson> {
// All other preparations are done by the 'override-all'-block in personal.json
json.layers = Array.from(this._state.sharedLayers.keys())
.filter((l) => Constants.priviliged_layers.indexOf(l) < 0)
.filter((l) => this._state.sharedLayers.get(l).source !== null)
.filter((l) => this._state.publicLayers.has(l))
return {
result: json,

View file

@ -845,7 +845,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}
if (json.description === undefined) {
if (Constants.priviliged_layers.indexOf(json.id) >= 0) {
if (typeof json.source === null) {
errors.push(context + ": A priviliged layer must have a description")
} else {
warnings.push(context + ": A builtin layer should have a description")
@ -882,6 +882,9 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}
if (json.presets !== undefined) {
if (typeof json.source === "string") {
throw "A special layer cannot have presets"
}
// Check that a preset will be picked up by the layer itself
const baseTags = TagUtils.Tag(json.source.osmTags)
for (let i = 0; i < json.presets.length; i++) {

View file

@ -77,4 +77,15 @@ export default interface PointRenderingConfigJson {
* A snippet of css-classes. They can be space-separated
*/
cssClasses?: string | TagRenderingConfigJson
/**
* If the map is pitched, the marker will stay parallel to the screen.
* Set to 'map' if you want to put it flattened on the map
*/
pitchAlignment?: "canvas" | "map" | TagRenderingConfigJson
/**
* If the map is rotated, the icon will still point to the north if no rotation was applied
*/
rotationAlignment?: "map" | "canvas" | TagRenderingConfigJson
}

View file

@ -86,9 +86,9 @@ export default class LayerConfig extends WithContextLoader {
throw "Layer " + this.id + " does not define a source section (" + context + ")"
}
if(json.source === "special" || json.source === "special:library"){
if (json.source === "special" || json.source === "special:library") {
this.source = null
}else if (json.source.osmTags === undefined) {
} else if (json.source.osmTags === undefined) {
throw (
"Layer " +
this.id +
@ -105,7 +105,6 @@ export default class LayerConfig extends WithContextLoader {
throw `${context}: The id of a layer should match [a-z0-9-_]*: ${json.id}`
}
this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30
if (
json.syncSelection !== undefined &&
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
@ -120,13 +119,28 @@ export default class LayerConfig extends WithContextLoader {
)
}
this.syncSelection = json.syncSelection ?? "no"
const osmTags = TagUtils.Tag(json.source.osmTags, context + "source.osmTags")
if (typeof json.source !== "string") {
this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30
const osmTags = TagUtils.Tag(json.source.osmTags, context + "source.osmTags")
if (osmTags.isNegative()) {
throw (
context +
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
osmTags.asHumanString(false, false, {})
)
}
if (Constants.priviliged_layers.indexOf(this.id) < 0 && osmTags.isNegative()) {
throw (
context +
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
osmTags.asHumanString(false, false, {})
this.source = new SourceConfig(
{
osmTags: osmTags,
geojsonSource: json.source["geoJson"],
geojsonSourceLevel: json.source["geoJsonZoomLevel"],
overpassScript: json.source["overpassScript"],
isOsmCache: json.source["isOsmCache"],
mercatorCrs: json.source["mercatorCrs"],
idKey: json.source["idKey"],
},
json.id
)
}
@ -138,20 +152,6 @@ export default class LayerConfig extends WithContextLoader {
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"
}
this.source = new SourceConfig(
{
osmTags: osmTags,
geojsonSource: json.source["geoJson"],
geojsonSourceLevel: json.source["geoJsonZoomLevel"],
overpassScript: json.source["overpassScript"],
isOsmCache: json.source["isOsmCache"],
mercatorCrs: json.source["mercatorCrs"],
idKey: json.source["idKey"],
},
Constants.priviliged_layers.indexOf(this.id) > 0,
json.id
)
this.allowSplit = json.allowSplit ?? false
this.name = Translations.T(json.name, translationContext + ".name")
if (json.units !== undefined && !Array.isArray(json.units)) {
@ -250,7 +250,7 @@ export default class LayerConfig extends WithContextLoader {
| "osmbasedmap"
| "historicphoto"
| string
)[]
)[]
if (typeof pr.preciseInput.preferredBackground === "string") {
preferredBackground = [pr.preciseInput.preferredBackground]
} else {
@ -597,7 +597,7 @@ export default class LayerConfig extends WithContextLoader {
}
let overpassLink: BaseUIElement = undefined
if (Constants.priviliged_layers.indexOf(this.id) < 0) {
if (this.source !== undefined) {
try {
overpassLink = new Link(
"Execute on overpass",

View file

@ -12,6 +12,7 @@ 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>([
@ -32,6 +33,8 @@ export default class PointRenderingConfig extends WithContextLoader {
public readonly rotation: TagRenderingConfig
public readonly cssDef: TagRenderingConfig
public readonly cssClasses?: TagRenderingConfig
public readonly pitchAlignment?: TagRenderingConfig
public readonly rotationAlignment?: TagRenderingConfig
constructor(json: PointRenderingConfigJson, context: string) {
super(json, context)
@ -88,6 +91,14 @@ export default class PointRenderingConfig extends WithContextLoader {
this.iconSize = this.tr("iconSize", "40,40,center")
this.label = this.tr("label", undefined)
this.rotation = this.tr("rotation", "0")
if (json.pitchAlignment) {
console.log("Got a pitch alignment!", json.pitchAlignment)
}
this.pitchAlignment = this.tr("pitchAlignment", "canvas")
this.rotationAlignment = this.tr(
"rotationAlignment",
json.pitchAlignment === "map" ? "map" : "canvas"
)
}
/**

View file

@ -20,7 +20,6 @@ export default class SourceConfig {
geojsonSourceLevel?: number
idKey?: string
},
isSpecialLayer: boolean,
context?: string
) {
let defined = 0
@ -51,7 +50,7 @@ export default class SourceConfig {
throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})`
}
}
if (params.osmTags !== undefined && !isSpecialLayer) {
if (params.osmTags !== undefined) {
const optimized = params.osmTags.optimize()
if (optimized === false) {
throw (

View file

@ -228,7 +228,7 @@ export class DownloadPanel extends Toggle {
new Set(neededLayers)
)
for (const tile of featureList) {
if (Constants.priviliged_layers.indexOf(tile.layer) >= 0) {
if (tile.layer !== undefined) {
continue
}

View file

@ -31,7 +31,6 @@ export class GeolocationControl extends VariableUiElement {
return false
}
const timeDiff = (new Date().getTime() - date.getTime()) / 1000
console.log("Timediff", timeDiff)
return timeDiff <= Constants.zoomToLocationTimeout
}
)

View file

@ -59,7 +59,7 @@ export class MapPreview
}
const availableLayers = AllKnownLayouts.AllPublicLayers().filter(
(l) => l.name !== undefined && Constants.priviliged_layers.indexOf(l.id) < 0
(l) => l.name !== undefined && l.source !== undefined
)
const layerPicker = new DropDown(
t.selectLayer,

View file

@ -14,12 +14,12 @@ import Constants from "../../Models/Constants"
*/
export class MapLibreAdaptor implements MapProperties {
private static maplibre_control_handlers = [
"scrollZoom",
"boxZoom",
// "scrollZoom",
// "boxZoom",
// "doubleClickZoom",
"dragRotate",
"dragPan",
"keyboard",
"doubleClickZoom",
"touchZoomRotate",
]
readonly location: UIEventSource<{ lon: number; lat: number }>

View file

@ -8,12 +8,13 @@ import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
import { OsmTags } from "../../Models/OsmFeature"
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
import { BBox } from "../../Logic/BBox"
import { Feature, LineString } from "geojson"
import { Feature } from "geojson"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
import { Utils } from "../../Utils"
import * as range_layer from "../../assets/layers/range/range.json"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
class PointRenderingLayer {
private readonly _config: PointRenderingConfig
private readonly _fetchStore?: (id: string) => Store<OsmTags>
@ -44,6 +45,16 @@ class PointRenderingLayer {
const unseenKeys = new Set(cache.keys())
for (const location of this._config.location) {
for (const feature of features) {
if (feature?.geometry === undefined) {
console.warn(
"Got an invalid feature:",
features,
" while rendering",
location,
"of",
this._config
)
}
const loc = GeoOperations.featureToCoordinateWithRenderingType(
<any>feature,
location
@ -102,7 +113,14 @@ class PointRenderingLayer {
})
}
return new Marker(el).setLngLat(loc).setOffset(iconAnchor).addTo(this._map)
const marker = new Marker(el).setLngLat(loc).setOffset(iconAnchor).addTo(this._map)
store
.map((tags) => this._config.pitchAlignment.GetRenderValue(tags).Subs(tags).txt)
.addCallbackAndRun((pitchAligment) => marker.setPitchAlignment(pitchAligment))
store
.map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt)
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(pitchAligment))
return marker
}
}
@ -118,13 +136,17 @@ class LineRenderingLayer {
"offset",
"fill",
"fillColor",
]
] as const
private static readonly lineConfigKeysColor = ["color", "fillColor"] as const
private static readonly lineConfigKeysNumber = ["width", "offset"] as const
private readonly _map: MlMap
private readonly _config: LineRenderingConfig
private readonly _visibility?: Store<boolean>
private readonly _fetchStore?: (id: string) => Store<OsmTags>
private readonly _onClick?: (id: string) => void
private readonly _layername: string
private readonly _listenerInstalledOn: Set<string> = new Set<string>()
constructor(
map: MlMap,
@ -145,6 +167,39 @@ class LineRenderingLayer {
features.features.addCallbackAndRunD((features) => self.update(features))
}
private calculatePropsFor(
properties: Record<string, string>
): Partial<Record<typeof LineRenderingLayer.lineConfigKeys[number], string>> {
const calculatedProps = {}
const config = this._config
for (const key of LineRenderingLayer.lineConfigKeys) {
const v = config[key]?.GetRenderValue(properties)?.Subs(properties).txt
calculatedProps[key] = v
}
for (const key of LineRenderingLayer.lineConfigKeysColor) {
let v = config[key]?.GetRenderValue(properties)?.Subs(properties).txt
if (v === undefined) {
continue
}
console.log("Color", v)
if (v.length == 9 && v.startsWith("#")) {
// This includes opacity
calculatedProps[key + "-opacity"] = parseInt(v.substring(7), 16) / 256
v = v.substring(0, 7)
console.log("Color >", v, calculatedProps[key + "-opacity"])
}
calculatedProps[key] = v
}
for (const key of LineRenderingLayer.lineConfigKeysNumber) {
const v = config[key]?.GetRenderValue(properties)?.Subs(properties).txt
calculatedProps[key] = Number(v)
}
console.log("Calculated props:", calculatedProps, "for", properties.id)
return calculatedProps
}
private async update(features: Feature[]) {
const map = this._map
while (!map.isStyleLoaded()) {
@ -158,31 +213,14 @@ class LineRenderingLayer {
},
promoteId: "id",
})
for (let i = 0; i < features.length; i++) {
const feature = features[i]
const id = feature.properties.id ?? "" + i
const tags = this._fetchStore(id)
tags.addCallbackAndRunD((properties) => {
const config = this._config
const calculatedProps = {}
for (const key of LineRenderingLayer.lineConfigKeys) {
const v = config[key]?.GetRenderValue(properties)?.Subs(properties).txt
calculatedProps[key] = v
}
map.setFeatureState({ source: this._layername, id }, calculatedProps)
})
}
map.addLayer({
source: this._layername,
id: this._layername + "_line",
type: "line",
filter: ["in", ["geometry-type"], ["literal", ["LineString", "MultiLineString"]]],
layout: {},
paint: {
"line-color": ["feature-state", "color"],
"line-opacity": ["feature-state", "color-opacity"],
"line-width": ["feature-state", "width"],
"line-offset": ["feature-state", "offset"],
},
@ -205,12 +243,49 @@ class LineRenderingLayer {
layout: {},
paint: {
"fill-color": ["feature-state", "fillColor"],
"fill-opacity": 0.1,
},
})
for (let i = 0; i < features.length; i++) {
const feature = features[i]
const id = feature.properties.id ?? feature.id
console.log("ID is", id)
if (id === undefined) {
console.trace(
"Got a feature without ID; this causes rendering bugs:",
feature,
"from"
)
continue
}
if (this._listenerInstalledOn.has(id)) {
continue
}
if (this._fetchStore === undefined) {
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(feature.properties)
)
} else {
const tags = this._fetchStore(id)
this._listenerInstalledOn.add(id)
tags.addCallbackAndRunD((properties) => {
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(properties)
)
})
}
}
}
}
export default class ShowDataLayer {
private static rangeLayer = new LayerConfig(
<LayerConfigJson>range_layer,
"ShowDataLayer.ts:range.json"
)
private readonly _map: Store<MlMap>
private readonly _options: ShowDataLayerOptions & { layer: LayerConfig }
private readonly _popupCache: Map<string, ScrollableFullScreen>
@ -223,11 +298,6 @@ export default class ShowDataLayer {
map.addCallbackAndRunD((map) => self.initDrawFeatures(map))
}
private static rangeLayer = new LayerConfig(
<LayerConfigJson>range_layer,
"ShowDataLayer.ts:range.json"
)
public static showRange(
map: Store<MlMap>,
features: FeatureSource,
@ -241,6 +311,9 @@ export default class ShowDataLayer {
}
private openOrReusePopup(id: string): void {
if (!this._popupCache || !this._options.fetchStore) {
return
}
if (this._popupCache.has(id)) {
this._popupCache.get(id).Activate()
return
@ -267,11 +340,12 @@ export default class ShowDataLayer {
private initDrawFeatures(map: MlMap) {
const { features, doShowLayer, fetchStore, buildPopup } = this._options
const onClick = buildPopup === undefined ? undefined : (id) => this.openOrReusePopup(id)
for (const lineRenderingConfig of this._options.layer.lineRendering) {
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
const lineRenderingConfig = this._options.layer.lineRendering[i]
new LineRenderingLayer(
map,
features,
"test",
this._options.layer.id + "_linerendering_" + i,
lineRenderingConfig,
doShowLayer,
fetchStore,

View file

@ -21,11 +21,13 @@
import Svg from "../Svg";
import If from "./Base/If.svelte";
import { GeolocationControl } from "./BigComponents/GeolocationControl.js";
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
import { BBox } from "../Logic/BBox";
import ShowDataLayer from "./Map/ShowDataLayer";
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
import type FeatureSource from "../Logic/FeatureSource/FeatureSource";
import LayerState from "../Logic/State/LayerState";
import Constants from "../Models/Constants";
import type { Feature } from "geojson";
export let layout: LayoutConfig;
const maplibremap: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
@ -46,7 +48,7 @@
osmConfiguration: <"osm" | "osm-test">featureSwitches.featureSwitchApiURL.data
});
const userRelatedState = new UserRelatedState(osmConnection, layout?.language);
const selectedElement = new UIEventSource<any>(undefined, "Selected element");
const selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element");
const geolocation = new GeoLocationHandler(geolocationState, selectedElement, mapproperties, userRelatedState.gpsLocationHistoryRetentionTime);
const allElements = new ElementStorage();
@ -55,16 +57,19 @@
osmConnection,
historicalUserLocations: geolocation.historicalUserLocations
}, layout?.isLeftRightSensitive() ?? false);
Map
console.log("Setting up layerstate...")
const layerState = new LayerState(osmConnection, layout.layers, layout.id)
{
// Various actors that we don't need to reference
// TODO enable new TitleHandler(selectedElement,layout,allElements)
new ChangeToElementsActor(changes, allElements);
new PendingChangesUploader(changes, selectedElement);
new SelectedElementTagsUpdater({
allElements, changes, selectedElement, layoutToUse: layout, osmConnection
});
// Various initial setup
userRelatedState.markLayoutAsVisited(layout);
if(layout?.lockLocation){
@ -76,7 +81,37 @@
featureSwitches.featureSwitchIsTesting
)
}
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": userRelatedState.homeLocation,
gps_location: geolocation.currentUserLocation,
gps_location_history: geolocation.historicalUserLocations,
gps_track: geolocation.historicalUserLocationsTrack,
selected_element: new StaticFeatureSource(selectedElement.map(f => f === undefined ? empty : [f])),
range: new StaticFeatureSource(mapproperties.maxbounds.map(bbox => bbox === undefined ? empty : <Feature[]> [bbox.asGeoJson({id:"range"})])) ,
current_view: new StaticFeatureSource(mapproperties.bounds.map(bbox => bbox === undefined ? empty : <Feature[]> [bbox.asGeoJson({id:"current_view"})])),
}
layerState.filteredLayers.get("range")?.isDisplayed?.syncWith(featureSwitches.featureSwitchIsTesting, true)
console.log("RAnge fs", specialLayers.range)
specialLayers.range.features.addCallbackAndRun(fs => console.log("Range.features:", JSON.stringify(fs)))
layerState.filteredLayers.forEach((flayer) => {
const features = specialLayers[flayer.layerDef.id]
if(features === undefined){
return
}
new ShowDataLayer(maplibremap, {
features,
doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef,
selectedElement
})
})
}
</script>
@ -93,15 +128,12 @@
</div>
<div class="absolute bottom-0 right-0 mb-4 mr-4">
<If condition={mapproperties.allowMoving}>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z+1)}>
<ToSvelte class="w-7 h-7 block" construct={Svg.plus_ui}></ToSvelte>
</MapControlButton>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z-1)}>
<ToSvelte class="w-7 h-7 block" construct={Svg.min_ui}></ToSvelte>
</MapControlButton>
</If>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z+1)}>
<ToSvelte class="w-7 h-7 block" construct={Svg.plus_ui}></ToSvelte>
</MapControlButton>
<MapControlButton on:click={() => mapproperties.zoom.update(z => z-1)}>
<ToSvelte class="w-7 h-7 block" construct={Svg.min_ui}></ToSvelte>
</MapControlButton>
<If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton>
<ToSvelte construct={() => new GeolocationControl(geolocation, mapproperties).SetClass("block w-8 h-8")}></ToSvelte>

View file

@ -15,6 +15,7 @@
]
},
"iconSize": "40,40,center",
"pitchAlignment": "map",
"rotation": {
"render": "0deg",
"mappings": [

View file

@ -2,7 +2,7 @@
"id": "home_location",
"description": "Meta layer showing the home location of the user. The home location can be set in the [profile settings](https://www.openstreetmap.org/profile/edit) of OpenStreetMap.",
"minzoom": 0,
"source":"special",
"source": "special",
"mapRendering": [
{
"icon": {

View file

@ -1,6 +1,6 @@
{
"id": "import_candidate",
"description": "Layer used in the importHelper",
"description": "Layer used as template in the importHelper",
"source":"special",
"mapRendering": [
{

View file

@ -6,9 +6,9 @@
"name": null,
"mapRendering": [
{
"width": 4,
"width": 3,
"fill": "no",
"color": "#ff000088"
"color": "#cc00cc"
}
]
}

View file

@ -2,9 +2,7 @@
"id": "split_point",
"description": "Layer rendering the little scissors for the minimap in the 'splitRoadWizard'",
"minzoom": 1,
"source": {
"osmTags": "_split_point=yes"
},
"source": "special",
"name": "Split point",
"title": "Split point",
"mapRendering": [
@ -17,4 +15,4 @@
"iconSize": "30,30,center"
}
]
}
}

View file

@ -1,10 +0,0 @@
{
"id": "type_node",
"description": "This is a priviliged meta_layer which exports _every_ point in OSM. This only works if zoomed below the point that the full tile is loaded (and not loaded via Overpass). Note that this point will also contain a property `parent_ways` which contains all the ways this node is part of as a list. This is mainly used for extremely specialized themes, which do advanced conflations. Expert use only.",
"minzoom": 18,
"source": "special",
"mapRendering": null,
"name": "All OSM Nodes",
"title": "OSM node {id}",
"tagRendering": []
}

View file

@ -28,29 +28,6 @@
"minzoom": 19
},
"layers": [
{
"builtin": "type_node",
"override": {
"calculatedTags": [
"_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false",
"_is_part_of_grb_building=feat.get('parent_ways')?.some(p => p['source:geometry:ref'] !== undefined) ?? false",
"_is_part_of_building_passage=feat.get('parent_ways')?.some(p => p.tunnel === 'building_passage') ?? false",
"_is_part_of_highway=!feat.get('is_part_of_building_passage') && (feat.get('parent_ways')?.some(p => p.highway !== undefined && p.highway !== '') ?? false)",
"_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false",
"_moveable=feat.get('_is_part_of_building') && !feat.get('_is_part_of_grb_building')"
],
"mapRendering": [
{
"icon": "square:#cc0",
"iconSize": "5,5,center",
"location": [
"point"
]
}
],
"passAllFeatures": true
}
},
{
"id": "osm-buildings",
"name": "All OSM-buildings",
@ -771,4 +748,4 @@
"overpassMaxZoom": 17,
"osmApiTileSize": 17,
"credits": "Pieter Vander Vennet"
}
}

View file

@ -114,7 +114,7 @@ function GenLayerOverviewText(): BaseUIElement {
}
const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
(layer) => Constants.priviliged_layers.indexOf(layer.id) < 0
(layer) => layer.source === null
)
const builtinLayerIds: Set<string> = new Set<string>()
@ -183,7 +183,7 @@ function GenOverviewsForSingleLayer(
callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void
): void {
const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
(layer) => Constants.priviliged_layers.indexOf(layer.id) < 0
(layer) => layer.source !== null
)
const builtinLayerIds: Set<string> = new Set<string>()
allLayers.forEach((l) => builtinLayerIds.add(l.id))
@ -195,7 +195,7 @@ function GenOverviewsForSingleLayer(
}
for (const layer of layout.layers) {
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
if (layer.source === null) {
continue
}
if (builtinLayerIds.has(layer.id)) {

View file

@ -18,7 +18,7 @@ async function main(includeTags = true) {
if (layer.source["geoJson"] !== undefined && !layer.source["isOsmCache"]) {
continue
}
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
if (layer.source == null || typeof layer.source === "string") {
continue
}

View file

@ -132,7 +132,7 @@ function generateLayerUsage(layer: LayerConfig, layout: LayoutConfig): any[] {
function generateTagInfoEntry(layout: LayoutConfig): any {
const usedTags = []
for (const layer of layout.layers) {
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
if (layer.source === null) {
continue
}
if (layer.source.geojsonSource !== undefined && layer.source.isOsmCacheLayer !== true) {

View file

@ -3,11 +3,12 @@ import ThemeViewGUI from "./UI/ThemeViewGUI.svelte"
import { FixedUiElement } from "./UI/Base/FixedUiElement"
import { QueryParameters } from "./Logic/Web/QueryParameters"
import { AllKnownLayoutsLazy } from "./Customizations/AllKnownLayouts"
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
import * as benches from "./assets/generated/themes/benches.json"
async function main() {
new FixedUiElement("Determining layout...").AttachTo("maindiv")
const qp = QueryParameters.GetQueryParameter("layout", "benches")
const layout = new AllKnownLayoutsLazy().get(qp.data)
const qp = QueryParameters.GetQueryParameter("layout", "")
const layout = new LayoutConfig(<any>benches, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
console.log("Using layout", layout.id)
new SvelteUIElement(ThemeViewGUI, { layout }).AttachTo("maindiv")
}