import { Changes } from "../../Logic/Osm/Changes" import { NewGeometryFromChangesFeatureSource } from "../../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" import { WithLayoutSourceState } from "./WithLayoutSourceState" import ThemeConfig from "../ThemeConfig/ThemeConfig" import { Utils } from "../../Utils" import Constants from "../Constants" import { WritableFeatureSource } from "../../Logic/FeatureSource/FeatureSource" import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" import ChangeToElementsActor from "../../Logic/Actors/ChangeToElementsActor" import MetaTagging from "../../Logic/MetaTagging" import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore" import SaveFeatureSourceToLocalStorage from "../../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" import ThemeSource from "../../Logic/FeatureSource/Sources/ThemeSource" import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" import ChangeGeometryApplicator from "../../Logic/FeatureSource/Sources/ChangeGeometryApplicator" import { Store } from "../../Logic/UIEventSource" import { Map as MlMap } from "maplibre-gl" import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource" import ShowDataLayer from "../../UI/Map/ShowDataLayer" import SelectedElementTagsUpdater from "../../Logic/Actors/SelectedElementTagsUpdater" import NoElementsInViewDetector, { FeatureViewState } from "../../Logic/Actors/NoElementsInViewDetector" export class WithChangesState extends WithLayoutSourceState { readonly changes: Changes readonly newFeatures: WritableFeatureSource readonly osmObjectDownloader: OsmObjectDownloader readonly perLayer: ReadonlyMap readonly perLayerFiltered: ReadonlyMap readonly toCacheSavers: ReadonlyMap /** * Indicates if there is _some_ data in view, even if it is not shown due to the filters */ readonly hasDataInView: Store constructor(theme: ThemeConfig, mvtAvailableLayers: Store>) { super(theme, mvtAvailableLayers) this.changes = new Changes( { featureSwitches: this.featureSwitches, allElements: this.indexedFeatures, osmConnection: this.osmConnection, featureProperties: this.featureProperties, historicalUserLocations: this.historicalUserLocations, reportError: (err, msg) => this.reportError(err, msg) }, theme?.isLeftRightSensitive() ?? false ) this.newFeatures = new NewGeometryFromChangesFeatureSource( this.changes, this.indexedFeatures, this.featureProperties ) this.indexedFeatures.addSource(this.newFeatures) this.osmObjectDownloader = new OsmObjectDownloader( this.osmConnection.Backend(), this.changes ) const perLayer = new PerLayerFeatureSourceSplitter( Array.from(this.layerState.filteredLayers.values()).filter( (l) => l.layerDef?.source !== null ), new ChangeGeometryApplicator(this.indexedFeatures, this.changes), { constructStore: (features, layer) => new GeoIndexedStoreForLayer(features, layer), handleLeftovers: (features) => { console.warn( "Got ", features.length, "leftover features, such as", features[0].properties ) }, } ) this.perLayer = perLayer.perLayer this.perLayerFiltered = this.showNormalDataOn(this.map) this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView this.toCacheSavers = theme.enableCache ? this.initSaveToLocalStorage() : undefined ////// ACTORS //////// new ChangeToElementsActor(this.changes, this.featureProperties) new SelectedElementTagsUpdater(this) new MetaTagging({ theme: this.theme, selectedElement: this.selectedElement, featureProperties: this.featureProperties, indexedFeatures: this.indexedFeatures, osmObjectDownloader: this.osmObjectDownloader, perLayer: this.perLayer, }) } public async reportError(message: string | Error | XMLHttpRequest, extramessage: string = "") { if (Utils.runningFromConsole) { console.error("Got (in themeViewSTate.reportError):", message, extramessage) return } const isTesting = this.featureSwitchIsTesting?.data if ("" + message === "[object XMLHttpRequest]") { const req = message let body = "" try { body = req.responseText } catch (e) { // pass } message = "XMLHttpRequest with status code " + req.status + ", " + req.statusText + ", received: " + body } if (extramessage) { message += " (" + extramessage + ")" } const stacktrace: string = new Error().stack try { const err = { stacktrace, message: "" + message, theme: this.theme?.id, version: Constants.vNumber, language: this.userRelatedState.language.data, username: this.osmConnection.userDetails.data?.name, userid: this.osmConnection.userDetails.data?.uid, pendingChanges: this.changes.pendingChanges.data, previousChanges: this.changes.allChanges.data, changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings) } console.trace( isTesting ? ">>> _Not_ reporting error to report server as testmode is on" : ">>> Reporting error to", Constants.ErrorReportServer, message, err ) if (isTesting) { return } await fetch(Constants.ErrorReportServer, { method: "POST", body: JSON.stringify(err) }) } catch (e) { console.error("Could not upload an error report", e) } } public initSaveToLocalStorage() { const toLocalStorage = new Map() this.perLayer.forEach((fs, layerId) => { if (fs.layer.layerDef.source.geojsonSource !== undefined) { return // We don't cache external data layers } const storage = new SaveFeatureSourceToLocalStorage( this.osmConnection.Backend(), fs.layer.layerDef.id, ThemeSource.fromCacheZoomLevel, fs, this.featureProperties, fs.layer.layerDef.maxAgeOfCache ) toLocalStorage.set(layerId, storage) }) return toLocalStorage } public showNormalDataOn(map: Store): ReadonlyMap { const filteringFeatureSource = new Map() this.perLayer.forEach((fs, layerName) => { const doShowLayer = this.mapProperties.zoom.map( (z) => { if ( (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0) ) { return true } if (this.layerState.globalFilters.data.some((f) => f.forceShowOnMatch)) { return true } return false }, [fs.layer.isDisplayed, this.layerState.globalFilters] ) if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) { /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined) * * This means that we don't have to filter it, nor do we have to display it * * Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden. * However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer! * */ return } const filtered = new FilteringFeatureSource( fs.layer, fs, (id) => this.featureProperties.getStore(id), this.layerState.globalFilters, undefined, this.mapProperties.zoom, this.selectedElement ) filteringFeatureSource.set(layerName, filtered) new ShowDataLayer(map, { layer: fs.layer.layerDef, features: filtered, doShowLayer, metaTags: this.userRelatedState.preferencesAsTags, selectedElement: this.selectedElement, fetchStore: (id) => this.featureProperties.getStore(id), }) }) return filteringFeatureSource } }