Merge develop

This commit is contained in:
Pieter Vander Vennet 2025-01-23 13:41:06 +01:00
commit 9b7052767a
49 changed files with 1744 additions and 1414 deletions

View file

@ -2,18 +2,17 @@ import { QueryParameters } from "../Web/QueryParameters"
import { BBox } from "../BBox"
import Constants from "../../Models/Constants"
import { GeoLocationState } from "../State/GeoLocationState"
import { UIEventSource } from "../UIEventSource"
import { Store, UIEventSource } from "../UIEventSource"
import { Feature, LineString, Point } from "geojson"
import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSource"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeoOperations } from "../GeoOperations"
import { OsmTags } from "../../Models/OsmFeature"
import StaticFeatureSource, {
WritableStaticFeatureSource,
} from "../FeatureSource/Sources/StaticFeatureSource"
import StaticFeatureSource, { WritableStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource"
import { MapProperties } from "../../Models/MapProperties"
import { Orientation } from "../../Sensors/Orientation"
;("use strict")
("use strict")
/**
* The geolocation-handler takes a map-location and a geolocation state.
* It'll move the map as appropriate given the state of the geolocation-API
@ -45,14 +44,14 @@ export default class GeoLocationHandler {
public readonly mapHasMoved: UIEventSource<Date | undefined> = new UIEventSource<
Date | undefined
>(undefined)
private readonly selectedElement: UIEventSource<Feature>
private readonly mapProperties?: MapProperties
private readonly selectedElement: Store<object>
private readonly mapProperties: MapProperties
private readonly gpsLocationHistoryRetentionTime?: UIEventSource<number>
constructor(
geolocationState: GeoLocationState,
selectedElement: UIEventSource<Feature>,
mapProperties?: MapProperties,
selectedElement: Store<object>,
mapProperties: MapProperties,
gpsLocationHistoryRetentionTime?: UIEventSource<number>
) {
this.geolocationState = geolocationState
@ -62,7 +61,7 @@ export default class GeoLocationHandler {
this.gpsLocationHistoryRetentionTime = gpsLocationHistoryRetentionTime
// Did an interaction move the map?
const initTime = new Date()
mapLocation.addCallbackD(() => {
mapLocation?.addCallbackD(() => {
if (new Date().getTime() - initTime.getTime() < 250) {
return
}
@ -139,7 +138,7 @@ export default class GeoLocationHandler {
}
}
mapLocation.setData({
mapLocation?.setData({
lon: newLocation.longitude,
lat: newLocation.latitude,
})

View file

@ -1,7 +1,7 @@
import { BBox } from "../BBox"
import { Store } from "../UIEventSource"
import ThemeViewState from "../../Models/ThemeViewState"
import Constants from "../../Models/Constants"
import { WithChangesState } from "../../Models/ThemeViewState/WithChangesState"
export type FeatureViewState =
| "no-data"
@ -11,7 +11,7 @@ export type FeatureViewState =
export default class NoElementsInViewDetector {
public readonly hasFeatureInView: Store<FeatureViewState>
constructor(themeViewState: ThemeViewState) {
constructor(themeViewState: WithChangesState) {
const state = themeViewState
const minZoom = Math.min(
...themeViewState.theme.layers
@ -32,7 +32,6 @@ export default class NoElementsInViewDetector {
return "zoom-to-low"
}
let minzoomWithData = 9999
for (const [layerName, source] of themeViewState.perLayerFiltered) {
if (priviliged.has(layerName)) {
@ -45,7 +44,6 @@ export default class NoElementsInViewDetector {
}
const layer = themeViewState.theme.getLayer(layerName)
if (mapProperties.zoom.data < layer.minzoom) {
minzoomWithData = Math.min(layer.minzoom)
continue
}
if (!state.layerState.filteredLayers.get(layerName).isDisplayed.data) {

View file

@ -4,10 +4,12 @@
import SimpleMetaTagger from "../SimpleMetaTagger"
import { OsmTags } from "../../Models/OsmFeature"
import { Utils } from "../../Utils"
import ThemeViewState from "../../Models/ThemeViewState"
import { BBox } from "../BBox"
import { Feature } from "geojson"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { Changes } from "../Osm/Changes"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { WithChangesState } from "../../Models/ThemeViewState/WithChangesState"
export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set([
@ -18,9 +20,9 @@ export default class SelectedElementTagsUpdater {
"uid",
"id",
])
private readonly state: ThemeViewState
private readonly state: WithChangesState
constructor(state: ThemeViewState) {
constructor(state: WithChangesState) {
this.state = state
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
if (!isLoggedIn && !Utils.runningFromConsole) {
@ -32,7 +34,11 @@ export default class SelectedElementTagsUpdater {
})
}
public static applyUpdate(latestTags: OsmTags, id: string, state: SpecialVisualizationState) {
public static applyUpdate(latestTags: OsmTags, id: string, state: {
theme: ThemeConfig,
changes: Changes,
featureProperties: FeaturePropertiesStore
}) {
try {
const leftRightSensitive = state.theme.isLeftRightSensitive()

View file

@ -6,8 +6,11 @@ import { OsmId } from "../../../Models/OsmFeature"
import { GeoOperations } from "../../GeoOperations"
import { IndexedFeatureSource } from "../FeatureSource"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
import SelectedElementTagsUpdater from "../../Actors/SelectedElementTagsUpdater"
import FeaturePropertiesStore from "../Actors/FeaturePropertiesStore"
import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig"
import { Changes } from "../../Osm/Changes"
import { WithChangesState } from "../../../Models/ThemeViewState/WithChangesState"
/**
* Generates the favourites from the preferences and marks them as favourite
@ -22,7 +25,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
*/
public readonly allFavourites: Store<Feature[]>
constructor(state: SpecialVisualizationState) {
constructor(state: WithChangesState) {
const features: Store<Feature[]> = Stores.ListStabilized(
state.osmConnection.preferencesHandler.allPreferences.map((prefs) => {
const feats: Feature[] = []
@ -71,7 +74,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
this.allFavourites.addCallbackD((features) => {
for (const feature of features) {
this.updateFeature(feature, state.osmObjectDownloader, state)
this.updateFeature(feature, state)
}
return true
@ -80,11 +83,15 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
private async updateFeature(
feature: Feature,
osmObjectDownloader: OsmObjectDownloader,
state: SpecialVisualizationState
state: {
theme: ThemeConfig,
changes: Changes,
featureProperties: FeaturePropertiesStore,
osmObjectDownloader: OsmObjectDownloader,
}
) {
const id = feature.properties.id
const upstream = await osmObjectDownloader.DownloadObjectAsync(id)
const upstream = await state.osmObjectDownloader.DownloadObjectAsync(id)
if (upstream === "deleted") {
this.removeFavourite(feature)
return

View file

@ -1,6 +1,6 @@
import GeoJsonSource from "./GeoJsonSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource"
import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource"
import { Or } from "../../Tags/Or"
import FeatureSwitchState from "../../State/FeatureSwitchState"
import OverpassFeatureSource from "./OverpassFeatureSource"
@ -12,13 +12,15 @@ import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeature
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"
import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource"
import FeatureSourceMerger from "./FeatureSourceMerger"
import { Feature } from "geojson"
import { OsmFeature } from "../../../Models/OsmFeature"
/**
* This source will fetch the needed data from various sources for the given layout.
*
* Note that special layers (with `source=null` will be ignored)
*/
export default class ThemeSource extends FeatureSourceMerger {
export default class ThemeSource implements IndexedFeatureSource {
/**
* Indicates if a data source is loading something
*/
@ -26,6 +28,61 @@ export default class ThemeSource extends FeatureSourceMerger {
public static readonly fromCacheZoomLevel = 15
public features: UIEventSource<Feature[]> = new UIEventSource([])
public readonly featuresById: Store<Map<string, Feature>>
private readonly core: Store<ThemeSourceCore>
private readonly addedSources: FeatureSource[] = []
private readonly addedItems: OsmFeature[] = []
constructor(
layers: LayerConfig[],
featureSwitches: FeatureSwitchState,
mapProperties: { bounds: Store<BBox>; zoom: Store<number> },
backend: string,
isDisplayed: (id: string) => Store<boolean>,
mvtAvailableLayers: Store<Set<string>>,
fullNodeDatabaseSource?: FullNodeDatabaseSource
) {
const isLoading = new UIEventSource(true)
this.isLoading = isLoading
const features = this.features = new UIEventSource<Feature[]>([])
const featuresById = this.featuresById = new UIEventSource(new Map())
this.core = mvtAvailableLayers.mapD(mvtAvailableLayers => {
const core = new ThemeSourceCore(layers, featureSwitches, mapProperties, backend, isDisplayed, mvtAvailableLayers, isLoading, fullNodeDatabaseSource)
this.addedSources.forEach(src => core.addSource(src))
this.addedItems.forEach(item => core.addItem(item))
core.features.addCallbackAndRun(data => features.set(data))
core.featuresById.addCallbackAndRun(data => featuresById.set(data))
return core
})
}
public async downloadAll() {
return this.core.data.downloadAll()
}
public addSource(source: FeatureSource) {
this.core.data?.addSource(source)
this.addedSources.push(source)
}
public addItem(obj: OsmFeature) {
this.core.data?.addItem(obj)
this.addedItems.push(obj)
}
}
/**
* This source will fetch the needed data from various sources for the given layout.
*
* Note that special layers (with `source=null` will be ignored)
*/
class ThemeSourceCore extends FeatureSourceMerger {
/**
* This source is _only_ triggered when the data is downloaded for CSV export
* @private
@ -40,6 +97,7 @@ export default class ThemeSource extends FeatureSourceMerger {
backend: string,
isDisplayed: (id: string) => Store<boolean>,
mvtAvailableLayers: Set<string>,
isLoading: UIEventSource<boolean>,
fullNodeDatabaseSource?: FullNodeDatabaseSource
) {
const { bounds, zoom } = mapProperties
@ -58,7 +116,7 @@ export default class ThemeSource extends FeatureSourceMerger {
mapProperties,
{
isActive: isDisplayed(layer.id),
maxAge: layer.maxAgeOfCache,
maxAge: layer.maxAgeOfCache
}
)
fromCache.set(layer.id, src)
@ -66,13 +124,11 @@ export default class ThemeSource extends FeatureSourceMerger {
}
const mvtSources: UpdatableFeatureSource[] = osmLayers
.filter((f) => mvtAvailableLayers.has(f.id))
.map((l) => ThemeSource.setupMvtSource(l, mapProperties, isDisplayed(l.id)))
.map((l) => ThemeSourceCore.setupMvtSource(l, mapProperties, isDisplayed(l.id)))
const nonMvtSources: FeatureSource[] = []
const nonMvtLayers: LayerConfig[] = osmLayers.filter((l) => !mvtAvailableLayers.has(l.id))
const isLoading = new UIEventSource(false)
const osmApiSource = ThemeSource.setupOsmApiSource(
const osmApiSource = ThemeSourceCore.setupOsmApiSource(
osmLayers,
bounds,
zoom,
@ -89,7 +145,7 @@ export default class ThemeSource extends FeatureSourceMerger {
nonMvtLayers.map((l) => l.id),
" cannot be fetched from the cache server, defaulting to overpass/OSM-api"
)
overpassSource = ThemeSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
overpassSource = ThemeSourceCore.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
nonMvtSources.push(overpassSource)
}
@ -102,7 +158,7 @@ export default class ThemeSource extends FeatureSourceMerger {
osmApiSource?.isRunning?.addCallbackAndRun(() => setIsLoading())
const geojsonSources: UpdatableFeatureSource[] = geojsonlayers.map((l) =>
ThemeSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
ThemeSourceCore.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
)
const downloadAll = new OverpassFeatureSource(
@ -113,11 +169,11 @@ export default class ThemeSource extends FeatureSourceMerger {
overpassUrl: featureSwitches.overpassUrl,
overpassTimeout: featureSwitches.overpassTimeout,
overpassMaxZoom: new ImmutableStore(99),
widenFactor: 0,
widenFactor: 0
},
{
ignoreZoom: true,
isActive: new ImmutableStore(false),
isActive: new ImmutableStore(false)
}
)
@ -129,7 +185,6 @@ export default class ThemeSource extends FeatureSourceMerger {
downloadAll
)
this.isLoading = isLoading
this._downloadAll = downloadAll
this._mapBounds = mapProperties.bounds
}
@ -192,7 +247,7 @@ export default class ThemeSource extends FeatureSourceMerger {
backend,
isActive,
patchRelations: true,
fullNodeDatabase,
fullNodeDatabase
})
}
@ -224,11 +279,11 @@ export default class ThemeSource extends FeatureSourceMerger {
widenFactor: 1.5,
overpassUrl: featureSwitches.overpassUrl,
overpassTimeout: featureSwitches.overpassTimeout,
overpassMaxZoom: featureSwitches.overpassMaxZoom,
overpassMaxZoom: featureSwitches.overpassMaxZoom
},
{
padToTiles: zoom.map((zoom) => Math.min(15, zoom + 1)),
isActive,
isActive
}
)
}

View file

@ -46,7 +46,7 @@ export class Changes {
constructor(
state: {
featureSwitches: {
featureSwitches?: {
featureSwitchMorePrivacy?: Store<boolean>
featureSwitchIsTesting?: Store<boolean>
}
@ -56,8 +56,7 @@ export class Changes {
historicalUserLocations?: FeatureSource
allElements?: IndexedFeatureSource
},
leftRightSensitive: boolean = false,
reportError?: (string: string | Error, extramessage?: string) => void
leftRightSensitive: boolean = false
) {
this._leftRightSensitive = leftRightSensitive
// We keep track of all changes just as well
@ -73,7 +72,7 @@ export class Changes {
}
this.state = state
this.backend = state.osmConnection.Backend()
this._reportError = reportError
this._reportError = state.reportError
this._changesetHandler = new ChangesetHandler(
state.featureSwitches?.featureSwitchIsTesting ?? new ImmutableStore(false),
state.osmConnection,

View file

@ -1,8 +1,8 @@
import { Store, UIEventSource } from "../UIEventSource"
import GeocodingProvider, { GeocodingOptions, GeocodeResult } from "./GeocodingProvider"
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
import { OsmId } from "../../Models/OsmFeature"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { Utils } from "../../Utils"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
export default class OpenStreetMapIdSearch implements GeocodingProvider {
private static readonly regex =
@ -13,11 +13,11 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
w: "way",
r: "relation",
}
private readonly _osmObjectDownloader: OsmObjectDownloader
private readonly _state: SpecialVisualizationState
constructor(state: SpecialVisualizationState) {
this._state = state
constructor(osmObjectDownloader: OsmObjectDownloader) {
this._osmObjectDownloader = osmObjectDownloader
}
/**
@ -49,7 +49,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
private async getInfoAbout(id: OsmId): Promise<GeocodeResult> {
const [osm_type, osm_id] = id.split("/")
const obj = await this._state.osmObjectDownloader.DownloadObjectAsync(id)
const obj = await this._osmObjectDownloader.DownloadObjectAsync(id)
if (obj === "deleted") {
return {
display_name: id + " was deleted",
@ -74,13 +74,13 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
}
}
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
async search(query: string, _: GeocodingOptions): Promise<GeocodeResult[]> {
if (!isNaN(Number(query))) {
const n = Number(query)
return Utils.NoNullInplace(
await Promise.all([
this.getInfoAbout(`node/${n}`).catch((x) => undefined),
this.getInfoAbout(`way/${n}`).catch((x) => undefined),
this.getInfoAbout(`node/${n}`).catch(() => undefined),
this.getInfoAbout(`way/${n}`).catch(() => undefined),
this.getInfoAbout(`relation/${n}`).catch(() => undefined),
])
)

View file

@ -39,9 +39,9 @@ export default class SearchState {
new LocalElementSearch(state, 5),
new CoordinateSearch(),
new OpenLocationCodeSearch(),
new OpenStreetMapIdSearch(state),
new OpenStreetMapIdSearch(state.osmObjectDownloader),
new PhotonSearch(true, 2),
new PhotonSearch(),
new PhotonSearch()
// new NominatimGeocoding(),
]
@ -122,7 +122,7 @@ export default class SearchState {
const layersToShow = payload.map((fsr) => fsr.layer.id)
console.log("Layers to show are", layersToShow)
for (const [name, otherLayer] of state.layerState.filteredLayers) {
for (const otherLayer of state.layerState.filteredLayers.values()) {
const layer = otherLayer.layerDef
if (!layer.isNormal()) {
continue

View file

@ -17,7 +17,6 @@ import FeatureSwitchState from "./FeatureSwitchState"
import Constants from "../../Models/Constants"
import { QueryParameters } from "../Web/QueryParameters"
import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
import { MapProperties } from "../../Models/MapProperties"
import Showdown from "showdown"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeocodeResult } from "../Search/GeocodingProvider"
@ -53,7 +52,7 @@ export class OptionallySyncedHistory<T> {
))
this.syncPreference.addCallback((syncmode) => {
if (syncmode === "sync") {
let list = [...thisSession.data, ...synced.data].slice(0, maxHistory)
const list = [...thisSession.data, ...synced.data].slice(0, maxHistory)
if (this._isSame) {
for (let i = 0; i < list.length; i++) {
for (let j = i + 1; j < list.length; j++) {
@ -178,7 +177,6 @@ export default class UserRelatedState {
* Note: these are linked via OsmConnection.preferences which exports all preferences as UIEventSource
*/
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
private readonly _mapProperties: MapProperties
public readonly recentlyVisitedThemes: OptionallySyncedHistory<string>
public readonly recentlyVisitedSearch: OptionallySyncedHistory<GeocodeResult>
@ -187,10 +185,9 @@ export default class UserRelatedState {
osmConnection: OsmConnection,
layout?: ThemeConfig,
featureSwitches?: FeatureSwitchState,
mapProperties?: MapProperties
currentRasterLayer?: Store<{ properties: { id: string } }>
) {
this.osmConnection = osmConnection
this._mapProperties = mapProperties
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
this.osmConnection.getPreference("show-all-questions", "false")
@ -224,7 +221,7 @@ export default class UserRelatedState {
this.translationMode = this.initTranslationMode()
this.homeLocation = this.initHomeLocation()
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches, currentRasterLayer)
this.recentlyVisitedThemes = new OptionallySyncedHistory<string>(
"theme",
@ -405,7 +402,8 @@ export default class UserRelatedState {
* */
private initAmendedPrefs(
layout?: ThemeConfig,
featureSwitches?: FeatureSwitchState
featureSwitches?: FeatureSwitchState,
currentRasterLayer?: Store<{ properties: { id: string } }>
): UIEventSource<Record<string, string>> {
const amendedPrefs = new UIEventSource<Record<string, string>>({
_theme: layout?.id,
@ -541,7 +539,7 @@ export default class UserRelatedState {
if (tags[key] === null) {
continue
}
let pref = this.osmConnection.GetPreference(key, undefined, { prefix: "" })
const pref = this.osmConnection.GetPreference(key, undefined, { prefix: "" })
pref.set(tags[key])
}
@ -560,7 +558,7 @@ export default class UserRelatedState {
}
}
this._mapProperties?.rasterLayer?.addCallbackAndRun((l) => {
currentRasterLayer?.addCallbackAndRun((l) => {
amendedPrefs.data["__current_background"] = l?.properties?.id
amendedPrefs.ping()
})

View file

@ -1,10 +1,16 @@
import ThemeViewState from "../../Models/ThemeViewState"
import Hash from "./Hash"
import { MenuState } from "../../Models/MenuState"
import { AndroidPolyfill } from "./AndroidPolyfill"
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { Feature } from "geojson"
import { UIEventSource } from "../UIEventSource"
export default class ThemeViewStateHashActor {
private readonly _state: ThemeViewState
private readonly _state: {
indexedFeatures: IndexedFeatureSource,
selectedElement: UIEventSource<Feature>,
guistate: MenuState
}
private isUpdatingHash = false
public static readonly documentation = [
@ -17,7 +23,7 @@ export default class ThemeViewStateHashActor {
"",
"The possible hashes are:",
"",
MenuState.pageNames.map((tab) => "`" + tab + "`").join(","),
MenuState.pageNames.map((tab) => "`" + tab + "`").join(",")
]
/**
@ -29,7 +35,11 @@ export default class ThemeViewStateHashActor {
* As such, we use a change in the hash to close the appropriate windows
*
*/
constructor(state: ThemeViewState) {
constructor(state: {
indexedFeatures: IndexedFeatureSource,
selectedElement: UIEventSource<Feature>,
guistate: MenuState,
}) {
this._state = state
AndroidPolyfill.onBackButton(() => this.back(), {
returnToIndex: state.featureSwitches.featureSwitchBackToThemeOverview
@ -146,26 +156,8 @@ export default class ThemeViewStateHashActor {
* Returns 'true' if an action was taken
* @private
*/
private back() {
console.log("Received back!")
private back(): boolean {
const state = this._state
if (state.previewedImage.data) {
state.previewedImage.setData(undefined)
return true
}
if(state.searchState.showSearchDrawer.data){
state.searchState.showSearchDrawer.set(false)
return true
}
if (state.guistate.closeAll()) {
return true
}
if (state.selectedElement.data) {
state.selectedElement.setData(undefined)
return true
}
return false
return state.guistate.closeAll()
}
}