Merge develop
This commit is contained in:
commit
9b7052767a
49 changed files with 1744 additions and 1414 deletions
File diff suppressed because one or more lines are too long
|
@ -22,6 +22,7 @@ import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Js
|
||||||
import Script from "./Script"
|
import Script from "./Script"
|
||||||
import crypto from "crypto"
|
import crypto from "crypto"
|
||||||
import { RasterLayerProperties } from "../src/Models/RasterLayerProperties"
|
import { RasterLayerProperties } from "../src/Models/RasterLayerProperties"
|
||||||
|
|
||||||
const sharp = require("sharp")
|
const sharp = require("sharp")
|
||||||
|
|
||||||
class GenerateLayouts extends Script {
|
class GenerateLayouts extends Script {
|
||||||
|
@ -172,7 +173,7 @@ class GenerateLayouts extends Script {
|
||||||
const icons = []
|
const icons = []
|
||||||
|
|
||||||
const whiteIcons: string[] = []
|
const whiteIcons: string[] = []
|
||||||
let icon = layout.icon
|
const icon = layout.icon
|
||||||
if (icon.endsWith(".svg") || icon.startsWith("<svg") || icon.startsWith("<?xml")) {
|
if (icon.endsWith(".svg") || icon.startsWith("<svg") || icon.startsWith("<?xml")) {
|
||||||
// This is an svg. Lets create the needed pngs and do some checkes!
|
// This is an svg. Lets create the needed pngs and do some checkes!
|
||||||
|
|
||||||
|
@ -582,7 +583,7 @@ class GenerateLayouts extends Script {
|
||||||
const filename = "index_" + theme.id + ".ts"
|
const filename = "index_" + theme.id + ".ts"
|
||||||
|
|
||||||
const imports = [
|
const imports = [
|
||||||
`import layout from "./public/assets/generated/themes/${theme.id}.json"`,
|
`import theme from "./public/assets/generated/themes/${theme.id}.json"`,
|
||||||
`import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`,
|
`import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`,
|
||||||
]
|
]
|
||||||
for (const layerName of Constants.added_by_default) {
|
for (const layerName of Constants.added_by_default) {
|
||||||
|
@ -595,10 +596,10 @@ class GenerateLayouts extends Script {
|
||||||
const addLayers = []
|
const addLayers = []
|
||||||
|
|
||||||
for (const layerName of Constants.added_by_default) {
|
for (const layerName of Constants.added_by_default) {
|
||||||
addLayers.push(` layout.layers.push(<any> ${layerName})`)
|
addLayers.push(` theme.layers.push(<any> ${layerName})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
let codeTemplate = this.codeTemplate.replace(
|
const codeTemplate = this.codeTemplate.replace(
|
||||||
" // LAYOUT.ADD_LAYERS",
|
" // LAYOUT.ADD_LAYERS",
|
||||||
addLayers.join("\n")
|
addLayers.join("\n")
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import Script from "./Script"
|
import Script from "./Script"
|
||||||
import { appendFileSync, readFileSync, writeFile, writeFileSync } from "fs"
|
import { readFileSync, writeFileSync } from "fs"
|
||||||
import { ChangeDescription } from "../src/Logic/Osm/Actions/ChangeDescription"
|
import { ChangeDescription } from "../src/Logic/Osm/Actions/ChangeDescription"
|
||||||
import { Changes } from "../src/Logic/Osm/Changes"
|
import { Changes } from "../src/Logic/Osm/Changes"
|
||||||
import { OsmObject } from "../src/Logic/Osm/OsmObject"
|
import { OsmObject } from "../src/Logic/Osm/OsmObject"
|
||||||
import OsmObjectDownloader from "../src/Logic/Osm/OsmObjectDownloader"
|
import OsmObjectDownloader from "../src/Logic/Osm/OsmObjectDownloader"
|
||||||
import { OsmConnection } from "../src/Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../src/Logic/Osm/OsmConnection"
|
||||||
import { ImmutableStore } from "../src/Logic/UIEventSource"
|
|
||||||
import Constants from "../src/Models/Constants"
|
import Constants from "../src/Models/Constants"
|
||||||
|
|
||||||
type ErrorMessage = {
|
type ErrorMessage = {
|
||||||
|
@ -130,11 +129,10 @@ ${changeset}`
|
||||||
|
|
||||||
const changesObj = new Changes(
|
const changesObj = new Changes(
|
||||||
{
|
{
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection,
|
osmConnection,
|
||||||
|
reportError: (err) => console.error(err)
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
(err) => console.error(err)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const all: ErrorMessage[] = []
|
const all: ErrorMessage[] = []
|
||||||
|
|
|
@ -2,18 +2,17 @@ import { QueryParameters } from "../Web/QueryParameters"
|
||||||
import { BBox } from "../BBox"
|
import { BBox } from "../BBox"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import { GeoLocationState } from "../State/GeoLocationState"
|
import { GeoLocationState } from "../State/GeoLocationState"
|
||||||
import { UIEventSource } from "../UIEventSource"
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
import { Feature, LineString, Point } from "geojson"
|
import { Feature, LineString, Point } from "geojson"
|
||||||
import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSource"
|
import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSource"
|
||||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
import { OsmTags } from "../../Models/OsmFeature"
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
import StaticFeatureSource, {
|
import StaticFeatureSource, { WritableStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource"
|
||||||
WritableStaticFeatureSource,
|
|
||||||
} from "../FeatureSource/Sources/StaticFeatureSource"
|
|
||||||
import { MapProperties } from "../../Models/MapProperties"
|
import { MapProperties } from "../../Models/MapProperties"
|
||||||
import { Orientation } from "../../Sensors/Orientation"
|
import { Orientation } from "../../Sensors/Orientation"
|
||||||
;("use strict")
|
|
||||||
|
("use strict")
|
||||||
/**
|
/**
|
||||||
* The geolocation-handler takes a map-location and a geolocation state.
|
* 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
|
* 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<
|
public readonly mapHasMoved: UIEventSource<Date | undefined> = new UIEventSource<
|
||||||
Date | undefined
|
Date | undefined
|
||||||
>(undefined)
|
>(undefined)
|
||||||
private readonly selectedElement: UIEventSource<Feature>
|
private readonly selectedElement: Store<object>
|
||||||
private readonly mapProperties?: MapProperties
|
private readonly mapProperties: MapProperties
|
||||||
private readonly gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
private readonly gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
geolocationState: GeoLocationState,
|
geolocationState: GeoLocationState,
|
||||||
selectedElement: UIEventSource<Feature>,
|
selectedElement: Store<object>,
|
||||||
mapProperties?: MapProperties,
|
mapProperties: MapProperties,
|
||||||
gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
||||||
) {
|
) {
|
||||||
this.geolocationState = geolocationState
|
this.geolocationState = geolocationState
|
||||||
|
@ -62,7 +61,7 @@ export default class GeoLocationHandler {
|
||||||
this.gpsLocationHistoryRetentionTime = gpsLocationHistoryRetentionTime
|
this.gpsLocationHistoryRetentionTime = gpsLocationHistoryRetentionTime
|
||||||
// Did an interaction move the map?
|
// Did an interaction move the map?
|
||||||
const initTime = new Date()
|
const initTime = new Date()
|
||||||
mapLocation.addCallbackD(() => {
|
mapLocation?.addCallbackD(() => {
|
||||||
if (new Date().getTime() - initTime.getTime() < 250) {
|
if (new Date().getTime() - initTime.getTime() < 250) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -139,7 +138,7 @@ export default class GeoLocationHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mapLocation.setData({
|
mapLocation?.setData({
|
||||||
lon: newLocation.longitude,
|
lon: newLocation.longitude,
|
||||||
lat: newLocation.latitude,
|
lat: newLocation.latitude,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { BBox } from "../BBox"
|
import { BBox } from "../BBox"
|
||||||
import { Store } from "../UIEventSource"
|
import { Store } from "../UIEventSource"
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
|
import { WithChangesState } from "../../Models/ThemeViewState/WithChangesState"
|
||||||
|
|
||||||
export type FeatureViewState =
|
export type FeatureViewState =
|
||||||
| "no-data"
|
| "no-data"
|
||||||
|
@ -11,7 +11,7 @@ export type FeatureViewState =
|
||||||
export default class NoElementsInViewDetector {
|
export default class NoElementsInViewDetector {
|
||||||
public readonly hasFeatureInView: Store<FeatureViewState>
|
public readonly hasFeatureInView: Store<FeatureViewState>
|
||||||
|
|
||||||
constructor(themeViewState: ThemeViewState) {
|
constructor(themeViewState: WithChangesState) {
|
||||||
const state = themeViewState
|
const state = themeViewState
|
||||||
const minZoom = Math.min(
|
const minZoom = Math.min(
|
||||||
...themeViewState.theme.layers
|
...themeViewState.theme.layers
|
||||||
|
@ -32,7 +32,6 @@ export default class NoElementsInViewDetector {
|
||||||
return "zoom-to-low"
|
return "zoom-to-low"
|
||||||
}
|
}
|
||||||
|
|
||||||
let minzoomWithData = 9999
|
|
||||||
|
|
||||||
for (const [layerName, source] of themeViewState.perLayerFiltered) {
|
for (const [layerName, source] of themeViewState.perLayerFiltered) {
|
||||||
if (priviliged.has(layerName)) {
|
if (priviliged.has(layerName)) {
|
||||||
|
@ -45,7 +44,6 @@ export default class NoElementsInViewDetector {
|
||||||
}
|
}
|
||||||
const layer = themeViewState.theme.getLayer(layerName)
|
const layer = themeViewState.theme.getLayer(layerName)
|
||||||
if (mapProperties.zoom.data < layer.minzoom) {
|
if (mapProperties.zoom.data < layer.minzoom) {
|
||||||
minzoomWithData = Math.min(layer.minzoom)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!state.layerState.filteredLayers.get(layerName).isDisplayed.data) {
|
if (!state.layerState.filteredLayers.get(layerName).isDisplayed.data) {
|
||||||
|
|
|
@ -4,10 +4,12 @@
|
||||||
import SimpleMetaTagger from "../SimpleMetaTagger"
|
import SimpleMetaTagger from "../SimpleMetaTagger"
|
||||||
import { OsmTags } from "../../Models/OsmFeature"
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
|
||||||
import { BBox } from "../BBox"
|
import { BBox } from "../BBox"
|
||||||
import { Feature } from "geojson"
|
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 {
|
export default class SelectedElementTagsUpdater {
|
||||||
private static readonly metatags = new Set([
|
private static readonly metatags = new Set([
|
||||||
|
@ -18,9 +20,9 @@ export default class SelectedElementTagsUpdater {
|
||||||
"uid",
|
"uid",
|
||||||
"id",
|
"id",
|
||||||
])
|
])
|
||||||
private readonly state: ThemeViewState
|
private readonly state: WithChangesState
|
||||||
|
|
||||||
constructor(state: ThemeViewState) {
|
constructor(state: WithChangesState) {
|
||||||
this.state = state
|
this.state = state
|
||||||
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
|
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
|
||||||
if (!isLoggedIn && !Utils.runningFromConsole) {
|
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 {
|
try {
|
||||||
const leftRightSensitive = state.theme.isLeftRightSensitive()
|
const leftRightSensitive = state.theme.isLeftRightSensitive()
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,11 @@ import { OsmId } from "../../../Models/OsmFeature"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
import { IndexedFeatureSource } from "../FeatureSource"
|
import { IndexedFeatureSource } from "../FeatureSource"
|
||||||
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
|
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
|
||||||
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
|
|
||||||
import SelectedElementTagsUpdater from "../../Actors/SelectedElementTagsUpdater"
|
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
|
* 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[]>
|
public readonly allFavourites: Store<Feature[]>
|
||||||
|
|
||||||
constructor(state: SpecialVisualizationState) {
|
constructor(state: WithChangesState) {
|
||||||
const features: Store<Feature[]> = Stores.ListStabilized(
|
const features: Store<Feature[]> = Stores.ListStabilized(
|
||||||
state.osmConnection.preferencesHandler.allPreferences.map((prefs) => {
|
state.osmConnection.preferencesHandler.allPreferences.map((prefs) => {
|
||||||
const feats: Feature[] = []
|
const feats: Feature[] = []
|
||||||
|
@ -71,7 +74,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
|
||||||
|
|
||||||
this.allFavourites.addCallbackD((features) => {
|
this.allFavourites.addCallbackD((features) => {
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
this.updateFeature(feature, state.osmObjectDownloader, state)
|
this.updateFeature(feature, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -80,11 +83,15 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
|
||||||
|
|
||||||
private async updateFeature(
|
private async updateFeature(
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
osmObjectDownloader: OsmObjectDownloader,
|
state: {
|
||||||
state: SpecialVisualizationState
|
theme: ThemeConfig,
|
||||||
|
changes: Changes,
|
||||||
|
featureProperties: FeaturePropertiesStore,
|
||||||
|
osmObjectDownloader: OsmObjectDownloader,
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
const id = feature.properties.id
|
const id = feature.properties.id
|
||||||
const upstream = await osmObjectDownloader.DownloadObjectAsync(id)
|
const upstream = await state.osmObjectDownloader.DownloadObjectAsync(id)
|
||||||
if (upstream === "deleted") {
|
if (upstream === "deleted") {
|
||||||
this.removeFavourite(feature)
|
this.removeFavourite(feature)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import GeoJsonSource from "./GeoJsonSource"
|
import GeoJsonSource from "./GeoJsonSource"
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource"
|
import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource"
|
||||||
import { Or } from "../../Tags/Or"
|
import { Or } from "../../Tags/Or"
|
||||||
import FeatureSwitchState from "../../State/FeatureSwitchState"
|
import FeatureSwitchState from "../../State/FeatureSwitchState"
|
||||||
import OverpassFeatureSource from "./OverpassFeatureSource"
|
import OverpassFeatureSource from "./OverpassFeatureSource"
|
||||||
|
@ -12,13 +12,15 @@ import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeature
|
||||||
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"
|
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"
|
||||||
import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource"
|
import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource"
|
||||||
import FeatureSourceMerger from "./FeatureSourceMerger"
|
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.
|
* This source will fetch the needed data from various sources for the given layout.
|
||||||
*
|
*
|
||||||
* Note that special layers (with `source=null` will be ignored)
|
* 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
|
* Indicates if a data source is loading something
|
||||||
*/
|
*/
|
||||||
|
@ -26,6 +28,61 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
|
|
||||||
public static readonly fromCacheZoomLevel = 15
|
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
|
* This source is _only_ triggered when the data is downloaded for CSV export
|
||||||
* @private
|
* @private
|
||||||
|
@ -40,6 +97,7 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
backend: string,
|
backend: string,
|
||||||
isDisplayed: (id: string) => Store<boolean>,
|
isDisplayed: (id: string) => Store<boolean>,
|
||||||
mvtAvailableLayers: Set<string>,
|
mvtAvailableLayers: Set<string>,
|
||||||
|
isLoading: UIEventSource<boolean>,
|
||||||
fullNodeDatabaseSource?: FullNodeDatabaseSource
|
fullNodeDatabaseSource?: FullNodeDatabaseSource
|
||||||
) {
|
) {
|
||||||
const { bounds, zoom } = mapProperties
|
const { bounds, zoom } = mapProperties
|
||||||
|
@ -58,7 +116,7 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
mapProperties,
|
mapProperties,
|
||||||
{
|
{
|
||||||
isActive: isDisplayed(layer.id),
|
isActive: isDisplayed(layer.id),
|
||||||
maxAge: layer.maxAgeOfCache,
|
maxAge: layer.maxAgeOfCache
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
fromCache.set(layer.id, src)
|
fromCache.set(layer.id, src)
|
||||||
|
@ -66,13 +124,11 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
}
|
}
|
||||||
const mvtSources: UpdatableFeatureSource[] = osmLayers
|
const mvtSources: UpdatableFeatureSource[] = osmLayers
|
||||||
.filter((f) => mvtAvailableLayers.has(f.id))
|
.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 nonMvtSources: FeatureSource[] = []
|
||||||
const nonMvtLayers: LayerConfig[] = osmLayers.filter((l) => !mvtAvailableLayers.has(l.id))
|
const nonMvtLayers: LayerConfig[] = osmLayers.filter((l) => !mvtAvailableLayers.has(l.id))
|
||||||
|
|
||||||
const isLoading = new UIEventSource(false)
|
const osmApiSource = ThemeSourceCore.setupOsmApiSource(
|
||||||
|
|
||||||
const osmApiSource = ThemeSource.setupOsmApiSource(
|
|
||||||
osmLayers,
|
osmLayers,
|
||||||
bounds,
|
bounds,
|
||||||
zoom,
|
zoom,
|
||||||
|
@ -89,7 +145,7 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
nonMvtLayers.map((l) => l.id),
|
nonMvtLayers.map((l) => l.id),
|
||||||
" cannot be fetched from the cache server, defaulting to overpass/OSM-api"
|
" 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)
|
nonMvtSources.push(overpassSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +158,7 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
osmApiSource?.isRunning?.addCallbackAndRun(() => setIsLoading())
|
osmApiSource?.isRunning?.addCallbackAndRun(() => setIsLoading())
|
||||||
|
|
||||||
const geojsonSources: UpdatableFeatureSource[] = geojsonlayers.map((l) =>
|
const geojsonSources: UpdatableFeatureSource[] = geojsonlayers.map((l) =>
|
||||||
ThemeSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
|
ThemeSourceCore.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
|
||||||
)
|
)
|
||||||
|
|
||||||
const downloadAll = new OverpassFeatureSource(
|
const downloadAll = new OverpassFeatureSource(
|
||||||
|
@ -113,11 +169,11 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
overpassUrl: featureSwitches.overpassUrl,
|
overpassUrl: featureSwitches.overpassUrl,
|
||||||
overpassTimeout: featureSwitches.overpassTimeout,
|
overpassTimeout: featureSwitches.overpassTimeout,
|
||||||
overpassMaxZoom: new ImmutableStore(99),
|
overpassMaxZoom: new ImmutableStore(99),
|
||||||
widenFactor: 0,
|
widenFactor: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ignoreZoom: true,
|
ignoreZoom: true,
|
||||||
isActive: new ImmutableStore(false),
|
isActive: new ImmutableStore(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -129,7 +185,6 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
downloadAll
|
downloadAll
|
||||||
)
|
)
|
||||||
|
|
||||||
this.isLoading = isLoading
|
|
||||||
this._downloadAll = downloadAll
|
this._downloadAll = downloadAll
|
||||||
this._mapBounds = mapProperties.bounds
|
this._mapBounds = mapProperties.bounds
|
||||||
}
|
}
|
||||||
|
@ -192,7 +247,7 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
backend,
|
backend,
|
||||||
isActive,
|
isActive,
|
||||||
patchRelations: true,
|
patchRelations: true,
|
||||||
fullNodeDatabase,
|
fullNodeDatabase
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,11 +279,11 @@ export default class ThemeSource extends FeatureSourceMerger {
|
||||||
widenFactor: 1.5,
|
widenFactor: 1.5,
|
||||||
overpassUrl: featureSwitches.overpassUrl,
|
overpassUrl: featureSwitches.overpassUrl,
|
||||||
overpassTimeout: featureSwitches.overpassTimeout,
|
overpassTimeout: featureSwitches.overpassTimeout,
|
||||||
overpassMaxZoom: featureSwitches.overpassMaxZoom,
|
overpassMaxZoom: featureSwitches.overpassMaxZoom
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
padToTiles: zoom.map((zoom) => Math.min(15, zoom + 1)),
|
padToTiles: zoom.map((zoom) => Math.min(15, zoom + 1)),
|
||||||
isActive,
|
isActive
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class Changes {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state: {
|
state: {
|
||||||
featureSwitches: {
|
featureSwitches?: {
|
||||||
featureSwitchMorePrivacy?: Store<boolean>
|
featureSwitchMorePrivacy?: Store<boolean>
|
||||||
featureSwitchIsTesting?: Store<boolean>
|
featureSwitchIsTesting?: Store<boolean>
|
||||||
}
|
}
|
||||||
|
@ -56,8 +56,7 @@ export class Changes {
|
||||||
historicalUserLocations?: FeatureSource
|
historicalUserLocations?: FeatureSource
|
||||||
allElements?: IndexedFeatureSource
|
allElements?: IndexedFeatureSource
|
||||||
},
|
},
|
||||||
leftRightSensitive: boolean = false,
|
leftRightSensitive: boolean = false
|
||||||
reportError?: (string: string | Error, extramessage?: string) => void
|
|
||||||
) {
|
) {
|
||||||
this._leftRightSensitive = leftRightSensitive
|
this._leftRightSensitive = leftRightSensitive
|
||||||
// We keep track of all changes just as well
|
// We keep track of all changes just as well
|
||||||
|
@ -73,7 +72,7 @@ export class Changes {
|
||||||
}
|
}
|
||||||
this.state = state
|
this.state = state
|
||||||
this.backend = state.osmConnection.Backend()
|
this.backend = state.osmConnection.Backend()
|
||||||
this._reportError = reportError
|
this._reportError = state.reportError
|
||||||
this._changesetHandler = new ChangesetHandler(
|
this._changesetHandler = new ChangesetHandler(
|
||||||
state.featureSwitches?.featureSwitchIsTesting ?? new ImmutableStore(false),
|
state.featureSwitches?.featureSwitchIsTesting ?? new ImmutableStore(false),
|
||||||
state.osmConnection,
|
state.osmConnection,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Store, UIEventSource } from "../UIEventSource"
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
import GeocodingProvider, { GeocodingOptions, GeocodeResult } from "./GeocodingProvider"
|
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||||
import { OsmId } from "../../Models/OsmFeature"
|
import { OsmId } from "../../Models/OsmFeature"
|
||||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
|
||||||
|
|
||||||
export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
||||||
private static readonly regex =
|
private static readonly regex =
|
||||||
|
@ -13,11 +13,11 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
||||||
w: "way",
|
w: "way",
|
||||||
r: "relation",
|
r: "relation",
|
||||||
}
|
}
|
||||||
|
private readonly _osmObjectDownloader: OsmObjectDownloader
|
||||||
|
|
||||||
private readonly _state: SpecialVisualizationState
|
|
||||||
|
|
||||||
constructor(state: SpecialVisualizationState) {
|
constructor(osmObjectDownloader: OsmObjectDownloader) {
|
||||||
this._state = state
|
this._osmObjectDownloader = osmObjectDownloader
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,7 +49,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
||||||
|
|
||||||
private async getInfoAbout(id: OsmId): Promise<GeocodeResult> {
|
private async getInfoAbout(id: OsmId): Promise<GeocodeResult> {
|
||||||
const [osm_type, osm_id] = id.split("/")
|
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") {
|
if (obj === "deleted") {
|
||||||
return {
|
return {
|
||||||
display_name: id + " was deleted",
|
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))) {
|
if (!isNaN(Number(query))) {
|
||||||
const n = Number(query)
|
const n = Number(query)
|
||||||
return Utils.NoNullInplace(
|
return Utils.NoNullInplace(
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.getInfoAbout(`node/${n}`).catch((x) => undefined),
|
this.getInfoAbout(`node/${n}`).catch(() => undefined),
|
||||||
this.getInfoAbout(`way/${n}`).catch((x) => undefined),
|
this.getInfoAbout(`way/${n}`).catch(() => undefined),
|
||||||
this.getInfoAbout(`relation/${n}`).catch(() => undefined),
|
this.getInfoAbout(`relation/${n}`).catch(() => undefined),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,9 +39,9 @@ export default class SearchState {
|
||||||
new LocalElementSearch(state, 5),
|
new LocalElementSearch(state, 5),
|
||||||
new CoordinateSearch(),
|
new CoordinateSearch(),
|
||||||
new OpenLocationCodeSearch(),
|
new OpenLocationCodeSearch(),
|
||||||
new OpenStreetMapIdSearch(state),
|
new OpenStreetMapIdSearch(state.osmObjectDownloader),
|
||||||
new PhotonSearch(true, 2),
|
new PhotonSearch(true, 2),
|
||||||
new PhotonSearch(),
|
new PhotonSearch()
|
||||||
// new NominatimGeocoding(),
|
// new NominatimGeocoding(),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ export default class SearchState {
|
||||||
|
|
||||||
const layersToShow = payload.map((fsr) => fsr.layer.id)
|
const layersToShow = payload.map((fsr) => fsr.layer.id)
|
||||||
console.log("Layers to show are", layersToShow)
|
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
|
const layer = otherLayer.layerDef
|
||||||
if (!layer.isNormal()) {
|
if (!layer.isNormal()) {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -17,7 +17,6 @@ import FeatureSwitchState from "./FeatureSwitchState"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import { QueryParameters } from "../Web/QueryParameters"
|
import { QueryParameters } from "../Web/QueryParameters"
|
||||||
import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
|
import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
|
||||||
import { MapProperties } from "../../Models/MapProperties"
|
|
||||||
import Showdown from "showdown"
|
import Showdown from "showdown"
|
||||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||||
import { GeocodeResult } from "../Search/GeocodingProvider"
|
import { GeocodeResult } from "../Search/GeocodingProvider"
|
||||||
|
@ -53,7 +52,7 @@ export class OptionallySyncedHistory<T> {
|
||||||
))
|
))
|
||||||
this.syncPreference.addCallback((syncmode) => {
|
this.syncPreference.addCallback((syncmode) => {
|
||||||
if (syncmode === "sync") {
|
if (syncmode === "sync") {
|
||||||
let list = [...thisSession.data, ...synced.data].slice(0, maxHistory)
|
const list = [...thisSession.data, ...synced.data].slice(0, maxHistory)
|
||||||
if (this._isSame) {
|
if (this._isSame) {
|
||||||
for (let i = 0; i < list.length; i++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
for (let j = i + 1; j < list.length; j++) {
|
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
|
* Note: these are linked via OsmConnection.preferences which exports all preferences as UIEventSource
|
||||||
*/
|
*/
|
||||||
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
||||||
private readonly _mapProperties: MapProperties
|
|
||||||
|
|
||||||
public readonly recentlyVisitedThemes: OptionallySyncedHistory<string>
|
public readonly recentlyVisitedThemes: OptionallySyncedHistory<string>
|
||||||
public readonly recentlyVisitedSearch: OptionallySyncedHistory<GeocodeResult>
|
public readonly recentlyVisitedSearch: OptionallySyncedHistory<GeocodeResult>
|
||||||
|
@ -187,10 +185,9 @@ export default class UserRelatedState {
|
||||||
osmConnection: OsmConnection,
|
osmConnection: OsmConnection,
|
||||||
layout?: ThemeConfig,
|
layout?: ThemeConfig,
|
||||||
featureSwitches?: FeatureSwitchState,
|
featureSwitches?: FeatureSwitchState,
|
||||||
mapProperties?: MapProperties
|
currentRasterLayer?: Store<{ properties: { id: string } }>
|
||||||
) {
|
) {
|
||||||
this.osmConnection = osmConnection
|
this.osmConnection = osmConnection
|
||||||
this._mapProperties = mapProperties
|
|
||||||
|
|
||||||
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
|
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
|
||||||
this.osmConnection.getPreference("show-all-questions", "false")
|
this.osmConnection.getPreference("show-all-questions", "false")
|
||||||
|
@ -224,7 +221,7 @@ export default class UserRelatedState {
|
||||||
this.translationMode = this.initTranslationMode()
|
this.translationMode = this.initTranslationMode()
|
||||||
this.homeLocation = this.initHomeLocation()
|
this.homeLocation = this.initHomeLocation()
|
||||||
|
|
||||||
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
|
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches, currentRasterLayer)
|
||||||
|
|
||||||
this.recentlyVisitedThemes = new OptionallySyncedHistory<string>(
|
this.recentlyVisitedThemes = new OptionallySyncedHistory<string>(
|
||||||
"theme",
|
"theme",
|
||||||
|
@ -405,7 +402,8 @@ export default class UserRelatedState {
|
||||||
* */
|
* */
|
||||||
private initAmendedPrefs(
|
private initAmendedPrefs(
|
||||||
layout?: ThemeConfig,
|
layout?: ThemeConfig,
|
||||||
featureSwitches?: FeatureSwitchState
|
featureSwitches?: FeatureSwitchState,
|
||||||
|
currentRasterLayer?: Store<{ properties: { id: string } }>
|
||||||
): UIEventSource<Record<string, string>> {
|
): UIEventSource<Record<string, string>> {
|
||||||
const amendedPrefs = new UIEventSource<Record<string, string>>({
|
const amendedPrefs = new UIEventSource<Record<string, string>>({
|
||||||
_theme: layout?.id,
|
_theme: layout?.id,
|
||||||
|
@ -541,7 +539,7 @@ export default class UserRelatedState {
|
||||||
if (tags[key] === null) {
|
if (tags[key] === null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let pref = this.osmConnection.GetPreference(key, undefined, { prefix: "" })
|
const pref = this.osmConnection.GetPreference(key, undefined, { prefix: "" })
|
||||||
|
|
||||||
pref.set(tags[key])
|
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.data["__current_background"] = l?.properties?.id
|
||||||
amendedPrefs.ping()
|
amendedPrefs.ping()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
|
||||||
import Hash from "./Hash"
|
import Hash from "./Hash"
|
||||||
import { MenuState } from "../../Models/MenuState"
|
import { MenuState } from "../../Models/MenuState"
|
||||||
import { AndroidPolyfill } from "./AndroidPolyfill"
|
import { AndroidPolyfill } from "./AndroidPolyfill"
|
||||||
|
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
import { UIEventSource } from "../UIEventSource"
|
||||||
|
|
||||||
export default class ThemeViewStateHashActor {
|
export default class ThemeViewStateHashActor {
|
||||||
private readonly _state: ThemeViewState
|
private readonly _state: {
|
||||||
|
indexedFeatures: IndexedFeatureSource,
|
||||||
|
selectedElement: UIEventSource<Feature>,
|
||||||
|
guistate: MenuState
|
||||||
|
}
|
||||||
private isUpdatingHash = false
|
private isUpdatingHash = false
|
||||||
|
|
||||||
public static readonly documentation = [
|
public static readonly documentation = [
|
||||||
|
@ -17,7 +23,7 @@ export default class ThemeViewStateHashActor {
|
||||||
"",
|
"",
|
||||||
"The possible hashes are:",
|
"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
|
* 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
|
this._state = state
|
||||||
AndroidPolyfill.onBackButton(() => this.back(), {
|
AndroidPolyfill.onBackButton(() => this.back(), {
|
||||||
returnToIndex: state.featureSwitches.featureSwitchBackToThemeOverview
|
returnToIndex: state.featureSwitches.featureSwitchBackToThemeOverview
|
||||||
|
@ -146,26 +156,8 @@ export default class ThemeViewStateHashActor {
|
||||||
* Returns 'true' if an action was taken
|
* Returns 'true' if an action was taken
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private back() {
|
private back(): boolean {
|
||||||
console.log("Received back!")
|
|
||||||
const state = this._state
|
const state = this._state
|
||||||
if (state.previewedImage.data) {
|
return state.guistate.closeAll()
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ export type PageType = (typeof MenuState.pageNames)[number]
|
||||||
* Some convenience methods are provided for this as well
|
* Some convenience methods are provided for this as well
|
||||||
*/
|
*/
|
||||||
export class MenuState {
|
export class MenuState {
|
||||||
|
|
||||||
public static readonly pageNames = [
|
public static readonly pageNames = [
|
||||||
"copyright",
|
"copyright",
|
||||||
"copyright_icons",
|
"copyright_icons",
|
||||||
|
@ -27,17 +28,25 @@ export class MenuState {
|
||||||
"favourites",
|
"favourites",
|
||||||
"usersettings",
|
"usersettings",
|
||||||
"share",
|
"share",
|
||||||
"menu",
|
"menu"
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the 'providedImage' which is currently displayed on top of the UI
|
||||||
|
* This object merely acts as lock or as means to signal the need to close
|
||||||
|
*/
|
||||||
|
public static readonly previewedImage: UIEventSource<object> = new UIEventSource<object>(undefined)
|
||||||
|
|
||||||
public readonly pageStates: Record<PageType, UIEventSource<boolean>>
|
public readonly pageStates: Record<PageType, UIEventSource<boolean>>
|
||||||
|
|
||||||
public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>(
|
public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>(
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
|
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||||
|
private readonly _selectedElement: UIEventSource<any>
|
||||||
|
|
||||||
constructor(shouldShowWelcomeMessage: boolean, themeid: string) {
|
constructor(selectedElement: UIEventSource<any>) {
|
||||||
|
this._selectedElement = selectedElement
|
||||||
// Note: this class is _not_ responsible to update the Hash, @see ThemeViewStateHashActor for this
|
// Note: this class is _not_ responsible to update the Hash, @see ThemeViewStateHashActor for this
|
||||||
const states = {}
|
const states = {}
|
||||||
for (const pageName of MenuState.pageNames) {
|
for (const pageName of MenuState.pageNames) {
|
||||||
|
@ -56,7 +65,9 @@ export class MenuState {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public openMenuIfNeeded(shouldShowWelcomeMessage: boolean, themeid: string) {
|
||||||
const visitedBefore = LocalStorageSource.getParsed<boolean>(
|
const visitedBefore = LocalStorageSource.getParsed<boolean>(
|
||||||
themeid + "thememenuisopened",
|
themeid + "thememenuisopened",
|
||||||
false
|
false
|
||||||
|
@ -103,6 +114,12 @@ export class MenuState {
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSomethingOpen(): boolean {
|
public isSomethingOpen(): boolean {
|
||||||
|
if (MenuState.previewedImage.data !== undefined) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (this._selectedElement.data) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return Object.values(this.pageStates).some((t) => t.data)
|
return Object.values(this.pageStates).some((t) => t.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +128,18 @@ export class MenuState {
|
||||||
* Returns 'true' if at least one menu was opened
|
* Returns 'true' if at least one menu was opened
|
||||||
*/
|
*/
|
||||||
public closeAll(): boolean {
|
public closeAll(): boolean {
|
||||||
|
console.log("Closing all")
|
||||||
const ps = this.pageStates
|
const ps = this.pageStates
|
||||||
|
if (ps.menu.data) {
|
||||||
|
ps.menu.set(false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MenuState.previewedImage.data !== undefined) {
|
||||||
|
MenuState.previewedImage.setData(undefined)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
for (const key in ps) {
|
for (const key in ps) {
|
||||||
const toggle = ps[key]
|
const toggle = ps[key]
|
||||||
const wasOpen = toggle.data
|
const wasOpen = toggle.data
|
||||||
|
@ -120,12 +148,10 @@ export class MenuState {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this._selectedElement.data) {
|
||||||
|
this._selectedElement.setData(undefined)
|
||||||
|
|
||||||
if (ps.menu.data) {
|
|
||||||
ps.menu.set(false)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
292
src/Models/ThemeViewState/UserMapFeatureswitchState.ts
Normal file
292
src/Models/ThemeViewState/UserMapFeatureswitchState.ts
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import { Map as MlMap } from "maplibre-gl"
|
||||||
|
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
||||||
|
import InitialMapPositioning from "../../Logic/Actors/InitialMapPositioning"
|
||||||
|
import { MapLibreAdaptor } from "../../UI/Map/MapLibreAdaptor"
|
||||||
|
import { ExportableMap, MapProperties } from "../MapProperties"
|
||||||
|
import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
||||||
|
import { PreferredRasterLayerSelector } from "../../Logic/Actors/PreferredRasterLayerSelector"
|
||||||
|
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "../RasterLayers"
|
||||||
|
import BackgroundLayerResetter from "../../Logic/Actors/BackgroundLayerResetter"
|
||||||
|
import Hotkeys from "../../UI/Base/Hotkeys"
|
||||||
|
import Translations from "../../UI/i18n/Translations"
|
||||||
|
import { EliCategory } from "../RasterLayerProperties"
|
||||||
|
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||||
|
import { Feature, Point, Polygon } from "geojson"
|
||||||
|
import { FeatureSource, WritableFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||||
|
import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||||
|
import { WithUserRelatedState } from "./WithUserRelatedState"
|
||||||
|
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
|
||||||
|
import { GeolocationControlState } from "../../UI/BigComponents/GeolocationControl"
|
||||||
|
import ShowOverlayRasterLayer from "../../UI/Map/ShowOverlayRasterLayer"
|
||||||
|
import { BBox } from "../../Logic/BBox"
|
||||||
|
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first core of the state management; everything related to:
|
||||||
|
* - the OSM connection
|
||||||
|
* - setting up the basemap
|
||||||
|
* - the feature switches
|
||||||
|
* - the GPS-location
|
||||||
|
*
|
||||||
|
* Anything that handles editable elements is _not_ done on this level.
|
||||||
|
* Anything that handles the UI is not done on this level
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class UserMapFeatureswitchState extends WithUserRelatedState {
|
||||||
|
|
||||||
|
readonly map: UIEventSource<MlMap>
|
||||||
|
|
||||||
|
|
||||||
|
readonly mapProperties: MapLibreAdaptor & MapProperties & ExportableMap
|
||||||
|
readonly lastClickObject: LastClickFeatureSource
|
||||||
|
|
||||||
|
readonly geolocationState: GeoLocationState
|
||||||
|
readonly geolocation: GeoLocationHandler
|
||||||
|
readonly geolocationControl: GeolocationControlState
|
||||||
|
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
||||||
|
|
||||||
|
|
||||||
|
readonly availableLayers: { store: Store<RasterLayerPolygon[]> }
|
||||||
|
readonly currentView: FeatureSource<Feature<Polygon>>
|
||||||
|
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||||
|
|
||||||
|
|
||||||
|
constructor(theme: ThemeConfig, selectedElement: Store<object>) {
|
||||||
|
const rasterLayer: UIEventSource<RasterLayerPolygon> = new UIEventSource<RasterLayerPolygon>(undefined)
|
||||||
|
super(theme, rasterLayer)
|
||||||
|
this.geolocationState = new GeoLocationState()
|
||||||
|
const initial = new InitialMapPositioning(theme, this.geolocationState, this.osmConnection)
|
||||||
|
this.map = new UIEventSource<MlMap>(undefined)
|
||||||
|
this.mapProperties = new MapLibreAdaptor(this.map, { rasterLayer, ...initial }, { correctClick: 20 })
|
||||||
|
|
||||||
|
this.geolocation = new GeoLocationHandler(
|
||||||
|
this.geolocationState,
|
||||||
|
selectedElement,
|
||||||
|
this.mapProperties,
|
||||||
|
this.userRelatedState.gpsLocationHistoryRetentionTime
|
||||||
|
)
|
||||||
|
this.geolocationControl = new GeolocationControlState(this.geolocation, this.mapProperties)
|
||||||
|
this.historicalUserLocations = this.geolocation.historicalUserLocations
|
||||||
|
|
||||||
|
|
||||||
|
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
|
||||||
|
this.mapProperties.allowRotating.setData(fixated !== "yes")
|
||||||
|
})
|
||||||
|
|
||||||
|
this.availableLayers = AvailableRasterLayers.layersAvailableAt(
|
||||||
|
this.mapProperties.location,
|
||||||
|
this.osmConnection.isLoggedIn
|
||||||
|
)
|
||||||
|
|
||||||
|
this.userRelatedState.markLayoutAsVisited(this.theme)
|
||||||
|
|
||||||
|
this.lastClickObject = new LastClickFeatureSource(
|
||||||
|
this.theme,
|
||||||
|
this.mapProperties.lastClickLocation,
|
||||||
|
this.userRelatedState.addNewFeatureMode
|
||||||
|
)
|
||||||
|
{
|
||||||
|
let currentViewIndex = 0
|
||||||
|
const empty = []
|
||||||
|
this.currentView = new StaticFeatureSource(
|
||||||
|
this.mapProperties.bounds.map((bbox) => {
|
||||||
|
if (!bbox) {
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
currentViewIndex++
|
||||||
|
return <Feature[]>[
|
||||||
|
bbox.asGeoJson({
|
||||||
|
zoom: this.mapProperties.zoom.data,
|
||||||
|
...this.mapProperties.location.data,
|
||||||
|
id: "current_view_" + currentViewIndex
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.theme.layers.some((l) => l._needsFullNodeDatabase)) {
|
||||||
|
this.fullNodeDatabase = new FullNodeDatabaseSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////// Actors ///////////////
|
||||||
|
|
||||||
|
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers)
|
||||||
|
|
||||||
|
|
||||||
|
this.userRelatedState.showScale.addCallbackAndRun((showScale) => {
|
||||||
|
this.mapProperties.showScale.set(showScale)
|
||||||
|
})
|
||||||
|
new PreferredRasterLayerSelector(
|
||||||
|
this.mapProperties.rasterLayer,
|
||||||
|
this.availableLayers,
|
||||||
|
this.featureSwitches.backgroundLayerId,
|
||||||
|
this.userRelatedState.preferredBackgroundLayer
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
this.initHotkeys()
|
||||||
|
this.drawOverlayLayers()
|
||||||
|
this.drawLock()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the map is locked to a certain area _and_ we are in test mode, draw this on the map
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private drawLock() {
|
||||||
|
if (!this.theme?.lockLocation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const bbox = new BBox(<[[number, number], [number, number]]>this.theme.lockLocation)
|
||||||
|
this.mapProperties.maxbounds.setData(bbox)
|
||||||
|
ShowDataLayer.showRange(
|
||||||
|
this.map,
|
||||||
|
new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
|
||||||
|
this.featureSwitches.featureSwitchIsTesting
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawOverlayLayers() {
|
||||||
|
for (const rasterInfo of this.theme.tileLayerSources) {
|
||||||
|
const state = this.overlayLayerStates.get(rasterInfo.id)
|
||||||
|
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* By focussing on the map, the keyboard panning and zoom with '+' and '+' works */
|
||||||
|
public focusOnMap() {
|
||||||
|
if (this.map.data) {
|
||||||
|
this.map.data.getCanvas().focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.map.addCallbackAndRunD((map) => {
|
||||||
|
map.on("load", () => {
|
||||||
|
map.getCanvas().focus()
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private initHotkeys() {
|
||||||
|
const docs = Translations.t.hotkeyDocumentation
|
||||||
|
|
||||||
|
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
|
||||||
|
if (!enable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLayerCategory = (category: EliCategory, skipLayers: number = 0) => {
|
||||||
|
const timeOfCall = new Date()
|
||||||
|
this.availableLayers.store.addCallbackAndRunD((available) => {
|
||||||
|
const now = new Date()
|
||||||
|
const timeDiff = (now.getTime() - timeOfCall.getTime()) / 1000
|
||||||
|
if (timeDiff > 3) {
|
||||||
|
return true // unregister
|
||||||
|
}
|
||||||
|
const current = this.mapProperties.rasterLayer
|
||||||
|
const best = RasterLayerUtils.SelectBestLayerAccordingTo(
|
||||||
|
available,
|
||||||
|
category,
|
||||||
|
current.data,
|
||||||
|
skipLayers
|
||||||
|
)
|
||||||
|
if (!best) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("Best layer for category", category, "is", best?.properties?.id)
|
||||||
|
current.setData(best)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{ nomod: "O" },
|
||||||
|
docs.selectOsmbasedmap,
|
||||||
|
() => setLayerCategory("osmbasedmap")
|
||||||
|
)
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{ nomod: "M" },
|
||||||
|
docs.selectMap,
|
||||||
|
() => setLayerCategory("map")
|
||||||
|
)
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{ nomod: "P" },
|
||||||
|
docs.selectAerial,
|
||||||
|
() => setLayerCategory("photo")
|
||||||
|
)
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{ shift: "O" },
|
||||||
|
docs.selectOsmbasedmap,
|
||||||
|
() => setLayerCategory("osmbasedmap", 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{ shift: "M" },
|
||||||
|
docs.selectMap,
|
||||||
|
() => setLayerCategory("map", 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{ shift: "P" },
|
||||||
|
docs.selectAerial,
|
||||||
|
() => setLayerCategory("photo", 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{ nomod: "L" },
|
||||||
|
Translations.t.hotkeyDocumentation.geolocate,
|
||||||
|
() => {
|
||||||
|
this.geolocationControl.handleClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{
|
||||||
|
shift: "T"
|
||||||
|
},
|
||||||
|
docs.translationMode,
|
||||||
|
() => {
|
||||||
|
const tm = this.userRelatedState.translationMode
|
||||||
|
if (tm.data === "false") {
|
||||||
|
tm.setData("true")
|
||||||
|
} else {
|
||||||
|
tm.setData("false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the current GPS-location marker on the given map.
|
||||||
|
* This is used to show the location on _other_ maps, e.g. on the map to add a new feature.
|
||||||
|
*
|
||||||
|
* This is _NOT_ to be used on the main map!
|
||||||
|
*/
|
||||||
|
public showCurrentLocationOn(map: Store<MlMap>) {
|
||||||
|
const id = "gps_location"
|
||||||
|
const layer = this.theme.getLayer(id)
|
||||||
|
if (layer === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (map === this.map) {
|
||||||
|
throw "Invalid use of showCurrentLocationOn"
|
||||||
|
}
|
||||||
|
const features = this.geolocation.currentUserLocation
|
||||||
|
return new ShowDataLayer(map, {
|
||||||
|
features,
|
||||||
|
layer,
|
||||||
|
metaTags: this.userRelatedState.preferencesAsTags
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
236
src/Models/ThemeViewState/WithChangesState.ts
Normal file
236
src/Models/ThemeViewState/WithChangesState.ts
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
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<string, GeoIndexedStoreForLayer>
|
||||||
|
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>
|
||||||
|
readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage>
|
||||||
|
/**
|
||||||
|
* Indicates if there is _some_ data in view, even if it is not shown due to the filters
|
||||||
|
*/
|
||||||
|
readonly hasDataInView: Store<FeatureViewState>
|
||||||
|
|
||||||
|
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
|
||||||
|
super(theme, mvtAvailableLayers)
|
||||||
|
this.changes = new Changes(
|
||||||
|
{
|
||||||
|
featureSwitches: this.featureSwitches,
|
||||||
|
allElements: this.indexedFeatures,
|
||||||
|
osmConnection: this.osmConnection,
|
||||||
|
featureProperties: this.featureProperties,
|
||||||
|
historicalUserLocations: this.historicalUserLocations,
|
||||||
|
reportError: this.reportError
|
||||||
|
},
|
||||||
|
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
|
||||||
|
console.log(
|
||||||
|
isTesting
|
||||||
|
? ">>> _Not_ reporting error to report server as testmode is on"
|
||||||
|
: ">>> Reporting error to",
|
||||||
|
Constants.ErrorReportServer,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
if (isTesting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("" + message === "[object XMLHttpRequest]") {
|
||||||
|
const req = <XMLHttpRequest>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 {
|
||||||
|
await fetch(Constants.ErrorReportServer, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not upload an error report")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public initSaveToLocalStorage() {
|
||||||
|
const toLocalStorage = new Map<string, SaveFeatureSourceToLocalStorage>()
|
||||||
|
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<MlMap>): ReadonlyMap<string, FilteringFeatureSource> {
|
||||||
|
const filteringFeatureSource = new Map<string, FilteringFeatureSource>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
74
src/Models/ThemeViewState/WithGuiState.ts
Normal file
74
src/Models/ThemeViewState/WithGuiState.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
|
import { MenuState } from "../MenuState"
|
||||||
|
import Hotkeys from "../../UI/Base/Hotkeys"
|
||||||
|
import Translations from "../../UI/i18n/Translations"
|
||||||
|
import { WithSpecialLayers } from "./WithSpecialLayers"
|
||||||
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does all things related to:
|
||||||
|
* - The UI state
|
||||||
|
*/
|
||||||
|
export class WithGuiState extends WithSpecialLayers {
|
||||||
|
readonly guistate: MenuState
|
||||||
|
|
||||||
|
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
|
||||||
|
super(theme, mvtAvailableLayers)
|
||||||
|
this.guistate = new MenuState(this.selectedElement)
|
||||||
|
this.guistate.openMenuIfNeeded(
|
||||||
|
this.featureSwitches.featureSwitchWelcomeMessage.data,
|
||||||
|
theme.id
|
||||||
|
)
|
||||||
|
|
||||||
|
Object.values(this.guistate.pageStates).forEach((toggle) => {
|
||||||
|
toggle.addCallbackD((isOpened) => {
|
||||||
|
// When a panel is closed: focus on the map again
|
||||||
|
if (!isOpened) {
|
||||||
|
if (!this.guistate.isSomethingOpen()) {
|
||||||
|
this.focusOnMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.initHotkeysGui()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private initHotkeysGui() {
|
||||||
|
const docs = Translations.t.hotkeyDocumentation
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey({ nomod: "f" }, docs.selectFavourites, () => {
|
||||||
|
this.guistate.pageStates.favourites.set(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{
|
||||||
|
nomod: "b"
|
||||||
|
},
|
||||||
|
docs.openLayersPanel,
|
||||||
|
() => {
|
||||||
|
if (this.featureSwitches.featureSwitchBackgroundSelection.data) {
|
||||||
|
this.guistate.pageStates.background.setData(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{
|
||||||
|
nomod: "s"
|
||||||
|
},
|
||||||
|
Translations.t.hotkeyDocumentation.openFilterPanel,
|
||||||
|
() => {
|
||||||
|
if (this.featureSwitches.featureSwitchFilter.data) {
|
||||||
|
this.guistate.openFilterView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectCurrentView() {
|
||||||
|
this.guistate.closeAll()
|
||||||
|
this.selectedElement.setData(this.currentView.features?.data?.[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
67
src/Models/ThemeViewState/WithImageState.ts
Normal file
67
src/Models/ThemeViewState/WithImageState.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { ImageUploadManager } from "../../Logic/ImageProviders/ImageUploadManager"
|
||||||
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
||||||
|
import { CombinedFetcher } from "../../Logic/Web/NearbyImagesSearch"
|
||||||
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
|
import { PanoramaxUploader } from "../../Logic/ImageProviders/Panoramax"
|
||||||
|
import Constants from "../Constants"
|
||||||
|
import Hash from "../../Logic/Web/Hash"
|
||||||
|
import ThemeViewStateHashActor from "../../Logic/Web/ThemeViewStateHashActor"
|
||||||
|
import PendingChangesUploader from "../../Logic/Actors/PendingChangesUploader"
|
||||||
|
import { WithGuiState } from "./WithGuiState"
|
||||||
|
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||||
|
|
||||||
|
export class WithImageState extends WithGuiState implements SpecialVisualizationState {
|
||||||
|
|
||||||
|
readonly imageUploadManager: ImageUploadManager
|
||||||
|
readonly previewedImage = new UIEventSource<ProvidedImage>(undefined)
|
||||||
|
readonly nearbyImageSearcher: CombinedFetcher
|
||||||
|
|
||||||
|
|
||||||
|
constructor(layout: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
|
||||||
|
super(layout, mvtAvailableLayers)
|
||||||
|
this.imageUploadManager = new ImageUploadManager(
|
||||||
|
layout,
|
||||||
|
new PanoramaxUploader(
|
||||||
|
Constants.panoramax.url,
|
||||||
|
Constants.panoramax.token,
|
||||||
|
this.featureSwitchIsTesting.map((t) =>
|
||||||
|
t ? Constants.panoramax.testsequence : Constants.panoramax.sequence
|
||||||
|
)
|
||||||
|
),
|
||||||
|
this.featureProperties,
|
||||||
|
this.osmConnection,
|
||||||
|
this.changes,
|
||||||
|
this.geolocation.geolocationState.currentGPSLocation,
|
||||||
|
this.indexedFeatures,
|
||||||
|
this.reportError
|
||||||
|
)
|
||||||
|
const longAgo = new Date()
|
||||||
|
longAgo.setTime(new Date().getTime() - 5 * 365 * 24 * 60 * 60 * 1000)
|
||||||
|
this.nearbyImageSearcher = new CombinedFetcher(50, longAgo, this.indexedFeatures)
|
||||||
|
|
||||||
|
|
||||||
|
this.initActors()
|
||||||
|
Hash.hash.addCallbackAndRunD((hash) => {
|
||||||
|
if (hash === "current_view" || hash.match(/current_view_[0-9]+/)) {
|
||||||
|
this.selectCurrentView()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup various services for which no reference are needed
|
||||||
|
*/
|
||||||
|
private initActors() {
|
||||||
|
|
||||||
|
new ThemeViewStateHashActor({
|
||||||
|
selectedElement: this.selectedElement,
|
||||||
|
indexedFeatures: this.indexedFeatures,
|
||||||
|
guistate: this.guistate
|
||||||
|
})
|
||||||
|
new PendingChangesUploader(this.changes, this.selectedElement, this.imageUploadManager)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
129
src/Models/ThemeViewState/WithLayoutSourceState.ts
Normal file
129
src/Models/ThemeViewState/WithLayoutSourceState.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import { WithSelectedElementState } from "./WithSelectedElementState"
|
||||||
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
|
import ThemeSource from "../../Logic/FeatureSource/Sources/ThemeSource"
|
||||||
|
import BBoxFeatureSource from "../../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||||
|
import FeaturePropertiesStore from "../../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
||||||
|
import LayerState from "../../Logic/State/LayerState"
|
||||||
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
|
import { FeatureSource, IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||||
|
import { Tag } from "../../Logic/Tags/Tag"
|
||||||
|
|
||||||
|
export class WithLayoutSourceState extends WithSelectedElementState {
|
||||||
|
|
||||||
|
readonly layerState: LayerState
|
||||||
|
readonly dataIsLoading: Store<boolean>
|
||||||
|
|
||||||
|
readonly featureProperties: FeaturePropertiesStore
|
||||||
|
readonly indexedFeatures: IndexedFeatureSource & ThemeSource
|
||||||
|
readonly featuresInView: FeatureSource
|
||||||
|
/**
|
||||||
|
* All 'level'-tags that are available with the current features
|
||||||
|
*/
|
||||||
|
readonly floors: Store<string[]>
|
||||||
|
|
||||||
|
|
||||||
|
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
|
||||||
|
super(theme)
|
||||||
|
/* Set up the layout source
|
||||||
|
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generates a stream of new and changed features too
|
||||||
|
*/
|
||||||
|
this.layerState = new LayerState(
|
||||||
|
this.osmConnection,
|
||||||
|
theme.layers,
|
||||||
|
theme.id,
|
||||||
|
this.featureSwitches.featureSwitchLayerDefault
|
||||||
|
)
|
||||||
|
|
||||||
|
const layoutSource = new ThemeSource(
|
||||||
|
theme.layers,
|
||||||
|
this.featureSwitches,
|
||||||
|
this.mapProperties,
|
||||||
|
this.osmConnection.Backend(),
|
||||||
|
(id) => this.layerState.filteredLayers.get(id).isDisplayed,
|
||||||
|
mvtAvailableLayers,
|
||||||
|
this.fullNodeDatabase
|
||||||
|
)
|
||||||
|
|
||||||
|
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
|
||||||
|
this.dataIsLoading = layoutSource.isLoading
|
||||||
|
this.indexedFeatures = layoutSource
|
||||||
|
this.featureProperties = new FeaturePropertiesStore(layoutSource)
|
||||||
|
|
||||||
|
this.floors = WithLayoutSourceState.initFloors(this.featuresInView)
|
||||||
|
|
||||||
|
this.initFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special bypass: if "favourites" is set, we still show items marked as 'favourite' even though the main layer is disabled
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private initFilters() {
|
||||||
|
this.layerState.filteredLayers
|
||||||
|
.get("favourite")
|
||||||
|
?.isDisplayed?.addCallbackAndRunD((favouritesShown) => {
|
||||||
|
const oldGlobal = this.layerState.globalFilters.data
|
||||||
|
const key = "show-favourite"
|
||||||
|
if (favouritesShown) {
|
||||||
|
this.layerState.globalFilters.set([
|
||||||
|
...oldGlobal,
|
||||||
|
{
|
||||||
|
forceShowOnMatch: true,
|
||||||
|
id: key,
|
||||||
|
osmTags: new Tag("_favourite", "yes"),
|
||||||
|
state: 0,
|
||||||
|
onNewPoint: undefined
|
||||||
|
}
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
this.layerState.globalFilters.set(oldGlobal.filter((gl) => gl.id !== key))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static initFloors(features: FeatureSource): Store<string[]> {
|
||||||
|
return features.features.stabilized(500).map((features) => {
|
||||||
|
if (!features) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const floors = new Set<string>()
|
||||||
|
for (const feature of features) {
|
||||||
|
const level = feature.properties["_level"]
|
||||||
|
if (level) {
|
||||||
|
const levels = level.split(";")
|
||||||
|
for (const l of levels) {
|
||||||
|
floors.add(l)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
floors.add("0") // '0' is the default and is thus _always_ present
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sorted = Array.from(floors)
|
||||||
|
// Sort alphabetically first, to deal with floor "A", "B" and "C"
|
||||||
|
sorted.sort()
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
// We use the laxer 'parseInt' to deal with floor '1A'
|
||||||
|
const na = parseInt(a)
|
||||||
|
const nb = parseInt(b)
|
||||||
|
if (isNaN(na) || isNaN(nb)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return na - nb
|
||||||
|
})
|
||||||
|
sorted.reverse(/* new list, no side-effects */)
|
||||||
|
return sorted
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public openNewDialog() {
|
||||||
|
this.selectedElement.setData(undefined)
|
||||||
|
|
||||||
|
const { lon, lat } = this.mapProperties.location.data
|
||||||
|
const feature = this.lastClickObject.createFeature(lon, lat)
|
||||||
|
this.featureProperties.trackFeature(feature)
|
||||||
|
this.selectedElement.setData(feature)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
65
src/Models/ThemeViewState/WithSearchState.ts
Normal file
65
src/Models/ThemeViewState/WithSearchState.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
|
import SearchState from "../../Logic/State/SearchState"
|
||||||
|
import Hotkeys from "../../UI/Base/Hotkeys"
|
||||||
|
import Translations from "../../UI/i18n/Translations"
|
||||||
|
import Zoomcontrol from "../../UI/Zoomcontrol"
|
||||||
|
import { WithVisualFeedbackState } from "./WithVisualFeedbackState"
|
||||||
|
import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions"
|
||||||
|
import LayerConfig from "../ThemeConfig/LayerConfig"
|
||||||
|
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||||
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
|
|
||||||
|
export class WithSearchState extends WithVisualFeedbackState {
|
||||||
|
public readonly searchState: SearchState
|
||||||
|
|
||||||
|
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
|
||||||
|
super(theme, mvtAvailableLayers)
|
||||||
|
this.searchState = new SearchState(this)
|
||||||
|
this.initHotkeysSearch()
|
||||||
|
|
||||||
|
{
|
||||||
|
// Register the search layer on the map
|
||||||
|
|
||||||
|
const source = this.searchState.locationResults
|
||||||
|
const flayer = this.layerState.filteredLayers.get("search")
|
||||||
|
this.featureProperties.trackFeatureSource(source)
|
||||||
|
const options: ShowDataLayerOptions & { layer: LayerConfig } = {
|
||||||
|
features: source,
|
||||||
|
doShowLayer: flayer.isDisplayed,
|
||||||
|
layer: flayer.layerDef,
|
||||||
|
metaTags: this.userRelatedState.preferencesAsTags,
|
||||||
|
onClick: (feature) => {
|
||||||
|
this.searchState.clickedOnMap(feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new ShowDataLayer(this.map, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initHotkeysSearch() {
|
||||||
|
const docs = Translations.t.hotkeyDocumentation
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{ ctrl: "F" },
|
||||||
|
docs.selectSearch,
|
||||||
|
() => {
|
||||||
|
this.searchState.feedback.set(undefined)
|
||||||
|
this.searchState.searchIsFocused.set(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey({ nomod: "Escape", onUp: true }, docs.closeSidebar, () => {
|
||||||
|
|
||||||
|
if (this.guistate.closeAll()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.searchState.showSearchDrawer.data) {
|
||||||
|
this.searchState.showSearchDrawer.set(false)
|
||||||
|
}
|
||||||
|
Zoomcontrol.resetzoom()
|
||||||
|
this.focusOnMap()
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
78
src/Models/ThemeViewState/WithSelectedElementState.ts
Normal file
78
src/Models/ThemeViewState/WithSelectedElementState.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { UserMapFeatureswitchState } from "./UserMapFeatureswitchState"
|
||||||
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
import Zoomcontrol from "../../UI/Zoomcontrol"
|
||||||
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
|
import { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state interactions with a selected element, but is blind to loading elements
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* No GUI stuff
|
||||||
|
*/
|
||||||
|
export class WithSelectedElementState extends UserMapFeatureswitchState {
|
||||||
|
|
||||||
|
|
||||||
|
readonly selectedElement: UIEventSource<Feature>
|
||||||
|
|
||||||
|
constructor(theme: ThemeConfig) {
|
||||||
|
const selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
|
||||||
|
super(theme, selectedElement)
|
||||||
|
this.selectedElement = selectedElement
|
||||||
|
this.selectedElement.addCallback((selected) => {
|
||||||
|
if (selected === undefined) {
|
||||||
|
Zoomcontrol.resetzoom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mapProperties.lastClickLocation.addCallbackD((lastClick) => {
|
||||||
|
if (lastClick.mode !== "left") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setSelectedElement(lastClick.nearestFeature)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Add the selected element to the recently visited history
|
||||||
|
this.selectedElement.addCallbackD((selected) => {
|
||||||
|
const [osm_type, osm_id] = selected.properties.id.split("/")
|
||||||
|
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
||||||
|
const layer = this.theme.getMatchingLayer(selected.properties)
|
||||||
|
|
||||||
|
const nameOptions = [
|
||||||
|
selected?.properties?.name,
|
||||||
|
selected?.properties?.alt_name,
|
||||||
|
selected?.properties?.local_name,
|
||||||
|
layer?.title.GetRenderValue(selected?.properties ?? {}).txt,
|
||||||
|
selected.properties.display_name,
|
||||||
|
selected.properties.id
|
||||||
|
]
|
||||||
|
const r = <GeocodeResult>{
|
||||||
|
feature: selected,
|
||||||
|
display_name: nameOptions.find((opt) => opt !== undefined),
|
||||||
|
osm_id,
|
||||||
|
osm_type,
|
||||||
|
lon,
|
||||||
|
lat
|
||||||
|
}
|
||||||
|
this.userRelatedState.recentlyVisitedSearch.add(r)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setSelectedElement(feature: Feature) {
|
||||||
|
const current = this.selectedElement.data
|
||||||
|
if (
|
||||||
|
current?.properties?.id !== undefined &&
|
||||||
|
current.properties.id === feature?.properties?.id
|
||||||
|
) {
|
||||||
|
console.log("Not setting selected, same id", current, feature)
|
||||||
|
return // already set
|
||||||
|
}
|
||||||
|
this.selectedElement.setData(feature)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
234
src/Models/ThemeViewState/WithSpecialLayers.ts
Normal file
234
src/Models/ThemeViewState/WithSpecialLayers.ts
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
|
import { WithChangesState } from "./WithChangesState"
|
||||||
|
import FavouritesFeatureSource from "../../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||||
|
import Constants from "../Constants"
|
||||||
|
import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||||
|
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
import { BBox } from "../../Logic/BBox"
|
||||||
|
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||||
|
import MetaTagging from "../../Logic/MetaTagging"
|
||||||
|
import FilteredLayer from "../FilteredLayer"
|
||||||
|
import LayerConfig from "../ThemeConfig/LayerConfig"
|
||||||
|
import { LayerConfigJson } from "../ThemeConfig/Json/LayerConfigJson"
|
||||||
|
import last_click_layerconfig from "../../assets/generated/layers/last_click.json"
|
||||||
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
|
import summaryLayer from "../../assets/generated/layers/summary.json"
|
||||||
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import NearbyFeatureSource from "../../Logic/FeatureSource/Sources/NearbyFeatureSource"
|
||||||
|
import {
|
||||||
|
SummaryTileSource,
|
||||||
|
SummaryTileSourceRewriter
|
||||||
|
} from "../../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
|
||||||
|
import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions"
|
||||||
|
|
||||||
|
export class WithSpecialLayers extends WithChangesState {
|
||||||
|
|
||||||
|
readonly favourites: FavouritesFeatureSource
|
||||||
|
/**
|
||||||
|
* When hovering (in the popup) an image, the location of the image will be revealed on the main map.
|
||||||
|
* This store contains those images that should be shown, probably only the currently hovered image
|
||||||
|
*/
|
||||||
|
readonly geocodedImages: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
||||||
|
/**
|
||||||
|
* Contains a few (<10) >features that are near the center of the map.
|
||||||
|
*/
|
||||||
|
readonly closestFeatures: NearbyFeatureSource
|
||||||
|
|
||||||
|
readonly featureSummary: SummaryTileSourceRewriter
|
||||||
|
/**
|
||||||
|
* When using arrow keys to move, the accessibility mode is activated, which has a small rectangle set.
|
||||||
|
* This is the 'viewport' which 'closestFeatures' uses to filter wilt
|
||||||
|
*/
|
||||||
|
readonly visualFeedbackViewportBounds: UIEventSource<BBox> = new UIEventSource<BBox>(undefined)
|
||||||
|
|
||||||
|
|
||||||
|
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
|
||||||
|
super(theme, mvtAvailableLayers)
|
||||||
|
|
||||||
|
this.favourites = new FavouritesFeatureSource(this)
|
||||||
|
|
||||||
|
this.closestFeatures = new NearbyFeatureSource(
|
||||||
|
this.mapProperties.location,
|
||||||
|
this.perLayerFiltered,
|
||||||
|
{
|
||||||
|
currentZoom: this.mapProperties.zoom,
|
||||||
|
layerState: this.layerState,
|
||||||
|
bounds: this.visualFeedbackViewportBounds.map(
|
||||||
|
(bounds) => bounds ?? this.mapProperties.bounds?.data,
|
||||||
|
[this.mapProperties.bounds]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.closestFeatures.registerSource(this.favourites, "favourite")
|
||||||
|
|
||||||
|
this.featureSummary = this.setupSummaryLayer()
|
||||||
|
this.initActorsSpecialLayers()
|
||||||
|
this.drawSelectedElement()
|
||||||
|
this.drawSpecialLayers()
|
||||||
|
this.drawLastClick()
|
||||||
|
// Note: the lock-range is handled by UserMapFeatureSwitchState
|
||||||
|
{
|
||||||
|
// Activate metatagging for the 'current_view' layer
|
||||||
|
const currentViewLayer = this.layerState.filteredLayers.get("current_view")?.layerDef
|
||||||
|
if (currentViewLayer?.tagRenderings?.length > 0) {
|
||||||
|
const params = MetaTagging.createExtraFuncParams(this)
|
||||||
|
this.currentView.features.addCallbackAndRunD((features) => {
|
||||||
|
MetaTagging.addMetatags(
|
||||||
|
features,
|
||||||
|
params,
|
||||||
|
currentViewLayer,
|
||||||
|
this.theme,
|
||||||
|
this.osmObjectDownloader,
|
||||||
|
this.featureProperties
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private setupSummaryLayer(): SummaryTileSourceRewriter | undefined {
|
||||||
|
/**
|
||||||
|
* MaxZoom for the summary layer
|
||||||
|
*/
|
||||||
|
const normalLayers = this.theme.layers.filter((l) => l.isNormal())
|
||||||
|
|
||||||
|
const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom))
|
||||||
|
|
||||||
|
const layers = this.theme.layers.filter(
|
||||||
|
(l) =>
|
||||||
|
(<string[]>(<unknown>Constants.priviliged_layers)).indexOf(l.id) < 0 &&
|
||||||
|
l.source.geojsonSource === undefined &&
|
||||||
|
l.doCount
|
||||||
|
)
|
||||||
|
if (!Constants.SummaryServer || layers.length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const summaryTileSource = new SummaryTileSource(
|
||||||
|
Constants.SummaryServer,
|
||||||
|
layers.map((l) => l.id),
|
||||||
|
this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)),
|
||||||
|
this.mapProperties,
|
||||||
|
{
|
||||||
|
isActive: this.mapProperties.zoom.map((z) => z < maxzoom)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const source = new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers)
|
||||||
|
|
||||||
|
new ShowDataLayer(this.map, {
|
||||||
|
features: source,
|
||||||
|
layer: new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer"),
|
||||||
|
// doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom),
|
||||||
|
selectedElement: this.selectedElement
|
||||||
|
})
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
protected registerSpecialLayer(flayer: FilteredLayer, source: FeatureSource) {
|
||||||
|
if (!source?.features) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.featureProperties.trackFeatureSource(source)
|
||||||
|
const options: ShowDataLayerOptions & { layer: LayerConfig } = {
|
||||||
|
features: source,
|
||||||
|
doShowLayer: flayer.isDisplayed,
|
||||||
|
layer: flayer.layerDef,
|
||||||
|
metaTags: this.userRelatedState.preferencesAsTags,
|
||||||
|
selectedElement: this.selectedElement
|
||||||
|
}
|
||||||
|
new ShowDataLayer(this.map, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawLastClick() {
|
||||||
|
const source = this.lastClickObject
|
||||||
|
const lastClickLayerConfig = new LayerConfig(
|
||||||
|
<LayerConfigJson>last_click_layerconfig,
|
||||||
|
"last_click"
|
||||||
|
)
|
||||||
|
const lastClickFiltered =
|
||||||
|
lastClickLayerConfig.isShown === undefined
|
||||||
|
? source
|
||||||
|
: source.features.mapD((fs) =>
|
||||||
|
fs.filter((f) => {
|
||||||
|
const matches = lastClickLayerConfig.isShown.matchesProperties(
|
||||||
|
f.properties
|
||||||
|
)
|
||||||
|
console.debug("LastClick ", f, "matches", matches)
|
||||||
|
return matches
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// show last click = new point/note marker
|
||||||
|
const features = new StaticFeatureSource(lastClickFiltered)
|
||||||
|
this.featureProperties.trackFeatureSource(features)
|
||||||
|
new ShowDataLayer(this.map, {
|
||||||
|
features,
|
||||||
|
layer: lastClickLayerConfig,
|
||||||
|
onClick: (feature) => {
|
||||||
|
if (this.mapProperties.zoom.data >= Constants.minZoomLevelToAddNewPoint) {
|
||||||
|
this.selectedElement.setData(feature)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.map.data.flyTo({
|
||||||
|
zoom: Constants.minZoomLevelToAddNewPoint,
|
||||||
|
center: GeoOperations.centerpointCoordinates(feature)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawSelectedElement() {
|
||||||
|
const src = new StaticFeatureSource(
|
||||||
|
this.selectedElement.map((f) => (f === undefined ? [] : [f]))
|
||||||
|
)
|
||||||
|
ShowDataLayer.showMultipleLayers(this.map, src, this.theme.layers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawSpecialLayers() {
|
||||||
|
|
||||||
|
type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
|
||||||
|
type LayersToAdd = "current_view" | Exclude<AddedByDefaultTypes,
|
||||||
|
"search" // Handled by WithSearchState
|
||||||
|
| "last_click" // handled by this.drawLastClick()
|
||||||
|
| "summary" // handled by setupSummaryLayer
|
||||||
|
| "range" // handled by UserMapFeatureSwitchState
|
||||||
|
| "selected_element" // handled by this.drawSelectedElement
|
||||||
|
>
|
||||||
|
const empty = []
|
||||||
|
/**
|
||||||
|
* A listing which maps the layerId onto the featureSource
|
||||||
|
*/
|
||||||
|
const specialLayers: Record<LayersToAdd, FeatureSource> = {
|
||||||
|
home_location: this.userRelatedState.homeLocation,
|
||||||
|
gps_location: this.geolocation.currentUserLocation,
|
||||||
|
gps_location_history: this.geolocation.historicalUserLocations,
|
||||||
|
gps_track: this.geolocation.historicalUserLocationsTrack,
|
||||||
|
current_view: this.currentView,
|
||||||
|
favourite: this.favourites,
|
||||||
|
geocoded_image: new StaticFeatureSource(this.geocodedImages)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// enumerate all 'normal' layers and match them with the appropriate 'special' layer - if applicable
|
||||||
|
this.layerState.filteredLayers.forEach((flayer) => {
|
||||||
|
this.registerSpecialLayer(flayer, specialLayers[flayer.layerDef.id])
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private initActorsSpecialLayers() {
|
||||||
|
this.selectedElement.addCallback((selected) => {
|
||||||
|
if (selected === undefined) {
|
||||||
|
this.focusOnMap()
|
||||||
|
this.geocodedImages.set([])
|
||||||
|
} else {
|
||||||
|
this.lastClickObject.clear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
107
src/Models/ThemeViewState/WithUserRelatedState.ts
Normal file
107
src/Models/ThemeViewState/WithUserRelatedState.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
|
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||||
|
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||||
|
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
||||||
|
import FeatureSwitchState from "../../Logic/State/FeatureSwitchState"
|
||||||
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import LayerConfig from "../ThemeConfig/LayerConfig"
|
||||||
|
import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
||||||
|
import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
|
||||||
|
|
||||||
|
export class WithUserRelatedState {
|
||||||
|
readonly theme: ThemeConfig
|
||||||
|
|
||||||
|
readonly featureSwitches: FeatureSwitchState
|
||||||
|
readonly featureSwitchIsTesting: Store<boolean>
|
||||||
|
readonly featureSwitchUserbadge: Store<boolean>
|
||||||
|
|
||||||
|
readonly osmConnection: OsmConnection
|
||||||
|
readonly userRelatedState: UserRelatedState
|
||||||
|
readonly overlayLayerStates: ReadonlyMap<
|
||||||
|
string,
|
||||||
|
{ readonly isDisplayed: UIEventSource<boolean> }
|
||||||
|
>
|
||||||
|
|
||||||
|
constructor(theme: ThemeConfig, rasterLayer: Store<{ properties: { id: string } }>) {
|
||||||
|
{
|
||||||
|
// Some weird setups
|
||||||
|
Utils.initDomPurify()
|
||||||
|
if (!Utils.runningFromConsole && theme.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) {
|
||||||
|
Utils.LoadCustomCss(theme.customCss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.theme = theme
|
||||||
|
this.featureSwitches = new FeatureSwitchState(theme)
|
||||||
|
this.osmConnection = new OsmConnection({
|
||||||
|
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
||||||
|
fakeUser: this.featureSwitches.featureSwitchFakeUser.data,
|
||||||
|
oauth_token: QueryParameters.GetQueryParameter(
|
||||||
|
"oauth_token",
|
||||||
|
undefined,
|
||||||
|
"Used to complete the login"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
||||||
|
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
|
||||||
|
|
||||||
|
this.userRelatedState = new UserRelatedState(
|
||||||
|
this.osmConnection,
|
||||||
|
theme,
|
||||||
|
this.featureSwitches,
|
||||||
|
rasterLayer
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!this.theme.official) {
|
||||||
|
// Add custom themes to the "visited custom themes"
|
||||||
|
const th = this.theme
|
||||||
|
this.userRelatedState.addUnofficialTheme({
|
||||||
|
id: th.id,
|
||||||
|
icon: th.icon,
|
||||||
|
title: th.title.translations,
|
||||||
|
shortDescription: th.shortDescription.translations,
|
||||||
|
layers: th.layers.filter((l) => l.isNormal()).map((l) => l.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayLayerStates = new Map<string, { isDisplayed: UIEventSource<boolean> }>()
|
||||||
|
this.overlayLayerStates = overlayLayerStates
|
||||||
|
for (const rasterInfo of this.theme.tileLayerSources) {
|
||||||
|
const isDisplayed = QueryParameters.GetBooleanQueryParameter(
|
||||||
|
"overlay-" + rasterInfo.id,
|
||||||
|
rasterInfo.defaultState ?? true,
|
||||||
|
"Whether or not overlay layer " + rasterInfo.id + " is shown"
|
||||||
|
)
|
||||||
|
const state = { isDisplayed }
|
||||||
|
overlayLayerStates.set(rasterInfo.id, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the theme
|
||||||
|
*/
|
||||||
|
public getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined {
|
||||||
|
const id = properties.id
|
||||||
|
|
||||||
|
if (id.startsWith("summary_")) {
|
||||||
|
// We don't select 'summary'-objects
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id === "settings") {
|
||||||
|
return UserRelatedState.usersettingsConfig
|
||||||
|
}
|
||||||
|
if (id.startsWith(LastClickFeatureSource.newPointElementId)) {
|
||||||
|
return this.theme.layers.find((l) => l.id === "last_click")
|
||||||
|
}
|
||||||
|
if (id.startsWith("search_result")) {
|
||||||
|
return GeocodingUtils.searchLayer
|
||||||
|
}
|
||||||
|
if (id === "location_track") {
|
||||||
|
return this.theme.layers.find((l) => l.id === "gps_track")
|
||||||
|
}
|
||||||
|
return this.theme.getMatchingLayer(properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
108
src/Models/ThemeViewState/WithVisualFeedbackState.ts
Normal file
108
src/Models/ThemeViewState/WithVisualFeedbackState.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import Hotkeys from "../../UI/Base/Hotkeys"
|
||||||
|
import Translations from "../../UI/i18n/Translations"
|
||||||
|
import ThemeViewState from "../ThemeViewState"
|
||||||
|
|
||||||
|
export class WithVisualFeedbackState extends ThemeViewState {
|
||||||
|
/**
|
||||||
|
* If true, the user interface will toggle some extra aids for people using screenreaders and keyboard navigation
|
||||||
|
* Triggered by navigating the map with arrows or by pressing 'space' or 'enter'
|
||||||
|
*/
|
||||||
|
public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||||
|
|
||||||
|
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
|
||||||
|
super(theme, mvtAvailableLayers)
|
||||||
|
this.initHotkeysVisualFeedback()
|
||||||
|
|
||||||
|
///// ACTORS /////
|
||||||
|
|
||||||
|
this.userRelatedState.a11y.addCallbackAndRunD((a11y) => {
|
||||||
|
if (a11y === "always") {
|
||||||
|
this.visualFeedback.setData(true)
|
||||||
|
} else if (a11y === "never") {
|
||||||
|
this.visualFeedback.setData(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.mapProperties.onKeyNavigationEvent((keyEvent) => {
|
||||||
|
if (this.userRelatedState.a11y.data === "never") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (["north", "east", "south", "west"].indexOf(keyEvent.key) >= 0) {
|
||||||
|
this.visualFeedback.setData(true)
|
||||||
|
return true // Our job is done, unregister
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the feature that is 'i' closest to the map center
|
||||||
|
*/
|
||||||
|
private selectClosestAtCenter(i: number = 0) {
|
||||||
|
console.log("Selecting closest", i)
|
||||||
|
if (this.userRelatedState.a11y.data !== "never") {
|
||||||
|
this.visualFeedback.setData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toSelect = this.closestFeatures.features?.data?.[i]
|
||||||
|
if (!toSelect) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
const toSelect = this.closestFeatures.features?.data?.[i]
|
||||||
|
if (!toSelect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setSelectedElement(toSelect)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setSelectedElement(toSelect)
|
||||||
|
}
|
||||||
|
|
||||||
|
private initHotkeysVisualFeedback() {
|
||||||
|
const docs = Translations.t.hotkeyDocumentation
|
||||||
|
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{
|
||||||
|
nomod: " ",
|
||||||
|
onUp: true
|
||||||
|
},
|
||||||
|
docs.selectItem,
|
||||||
|
() => {
|
||||||
|
if (this.selectedElement.data !== undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.guistate.isSomethingOpen()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
document.activeElement.tagName === "button" ||
|
||||||
|
document.activeElement.tagName === "input"
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selectClosestAtCenter(0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for (let i = 1; i < 9; i++) {
|
||||||
|
let doc = docs.selectItemI.Subs({ i })
|
||||||
|
if (i === 1) {
|
||||||
|
doc = docs.selectItem
|
||||||
|
} else if (i === 2) {
|
||||||
|
doc = docs.selectItem2
|
||||||
|
} else if (i === 3) {
|
||||||
|
doc = docs.selectItem3
|
||||||
|
}
|
||||||
|
Hotkeys.RegisterHotkey(
|
||||||
|
{
|
||||||
|
nomod: "" + i,
|
||||||
|
onUp: true
|
||||||
|
},
|
||||||
|
doc,
|
||||||
|
() => this.selectClosestAtCenter(i - 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@
|
||||||
export let fullscreen: boolean = false
|
export let fullscreen: boolean = false
|
||||||
export let bodyPadding = "p-4 md:p-5 "
|
export let bodyPadding = "p-4 md:p-5 "
|
||||||
export let shown: UIEventSource<boolean>
|
export let shown: UIEventSource<boolean>
|
||||||
export let dismissable = true
|
export let dismissable = false
|
||||||
/**
|
/**
|
||||||
* Default: 50
|
* Default: 50
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
export let presetProperties: Tag[] = []
|
export let presetProperties: Tag[] = []
|
||||||
let presetPropertiesUnpacked = TagUtils.KVtoProperties(presetProperties)
|
let presetPropertiesUnpacked = TagUtils.KVtoProperties(presetProperties)
|
||||||
|
|
||||||
export let snappedTo: UIEventSource<WayId | undefined>
|
export let snappedTo: UIEventSource<WayId | undefined> = new UIEventSource<WayId | undefined>(undefined)
|
||||||
|
|
||||||
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||||
export let mapProperties: Partial<MapProperties> & { location } = {
|
export let mapProperties: Partial<MapProperties> & { location } = {
|
||||||
|
|
|
@ -141,7 +141,6 @@
|
||||||
{state}
|
{state}
|
||||||
imgClass="h-32 shrink-0"
|
imgClass="h-32 shrink-0"
|
||||||
image={{ url: image, id: image }}
|
image={{ url: image, id: image }}
|
||||||
previewedImage={state.previewedImage}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
31
src/UI/CustomThemeError.svelte
Normal file
31
src/UI/CustomThemeError.svelte
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
|
||||||
|
import { Utils } from "../Utils"
|
||||||
|
|
||||||
|
export let customDefinition: string
|
||||||
|
export let stack: string[]
|
||||||
|
|
||||||
|
function offerDefinitionForDownload() {
|
||||||
|
Utils.offerContentsAsDownloadableFile(
|
||||||
|
customDefinition,
|
||||||
|
"mapcomplete-theme.json",
|
||||||
|
{ mimetype: "application/json" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full h-full m-8 justify-center items-center">
|
||||||
|
|
||||||
|
<h1>Something went wrong</h1>
|
||||||
|
<div class="alert">{stack[0]}</div>
|
||||||
|
{#each stack.slice(1) as stck}
|
||||||
|
<div>{stck}</div>
|
||||||
|
{/each}
|
||||||
|
{#if customDefinition}
|
||||||
|
<button on:click={() => offerDefinitionForDownload()}>
|
||||||
|
<ArrowDownTray class="w-16 h-16" />
|
||||||
|
Download the theme definition file
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
|
@ -1,13 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
|
||||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||||
import type { Feature } from "geojson"
|
import type { Feature } from "geojson"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
|
|
||||||
export let feature: Feature
|
export let feature: Feature
|
||||||
let properties: Record<string, string> = feature.properties
|
let properties: Record<string, string> = feature.properties
|
||||||
export let state: SpecialVisualizationState
|
export let state: ThemeViewState
|
||||||
let tags =
|
let tags =
|
||||||
state.featureProperties.getStore(properties.id) ??
|
state.featureProperties.getStore(properties.id) ??
|
||||||
new UIEventSource<Record<string, string>>(properties)
|
new UIEventSource<Record<string, string>>(properties)
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import DotMenu from "../Base/DotMenu.svelte"
|
import DotMenu from "../Base/DotMenu.svelte"
|
||||||
import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte"
|
import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte"
|
||||||
|
import { MenuState } from "../../Models/MenuState"
|
||||||
|
|
||||||
export let image: Partial<ProvidedImage>
|
export let image: Partial<ProvidedImage>
|
||||||
let fallbackImage: string = undefined
|
let fallbackImage: string = undefined
|
||||||
|
@ -29,7 +30,7 @@
|
||||||
export let imgClass: string = undefined
|
export let imgClass: string = undefined
|
||||||
export let state: SpecialVisualizationState = undefined
|
export let state: SpecialVisualizationState = undefined
|
||||||
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
|
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
|
||||||
export let previewedImage: UIEventSource<Partial<ProvidedImage>> = undefined
|
let previewedImage: UIEventSource<Partial<ProvidedImage>> = MenuState.previewedImage
|
||||||
export let canZoom = previewedImage !== undefined
|
export let canZoom = previewedImage !== undefined
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let showBigPreview = new UIEventSource(false)
|
let showBigPreview = new UIEventSource(false)
|
||||||
|
@ -70,12 +71,11 @@
|
||||||
coordinates: [image.lon, image.lat],
|
coordinates: [image.lon, image.lat],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
console.log(f)
|
|
||||||
state?.geocodedImages.set([f])
|
state?.geocodedImages.set([f])
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Popup shown={showBigPreview} bodyPadding="p-0" dismissable={true}>
|
<Popup shown={showBigPreview} bodyPadding="p-0" dismissable={false}>
|
||||||
<div slot="close" />
|
<div slot="close" />
|
||||||
<div style="height: 80vh">
|
<div style="height: 80vh">
|
||||||
<ImageOperations {image}>
|
<ImageOperations {image}>
|
||||||
|
@ -87,7 +87,6 @@
|
||||||
<CloseButton
|
<CloseButton
|
||||||
class="normal-background"
|
class="normal-background"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
console.log("Closing")
|
|
||||||
previewedImage?.set(undefined)
|
previewedImage?.set(undefined)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -122,7 +121,6 @@
|
||||||
class={imgClass ?? ""}
|
class={imgClass ?? ""}
|
||||||
class:cursor-zoom-in={canZoom}
|
class:cursor-zoom-in={canZoom}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
console.log("Setting", image.url)
|
|
||||||
previewedImage?.set(image)
|
previewedImage?.set(image)
|
||||||
}}
|
}}
|
||||||
on:error={() => {
|
on:error={() => {
|
||||||
|
|
|
@ -149,7 +149,6 @@
|
||||||
imgClass="carousel-max-height"
|
imgClass="carousel-max-height"
|
||||||
{image}
|
{image}
|
||||||
{state}
|
{state}
|
||||||
previewedImage={state?.previewedImage}
|
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="dot-menu-actions">
|
<svelte:fragment slot="dot-menu-actions">
|
||||||
<button on:click={() => ImageProvider.offerImageAsDownload(image)}>
|
<button on:click={() => ImageProvider.offerImageAsDownload(image)}>
|
||||||
|
|
|
@ -87,7 +87,6 @@
|
||||||
{state}
|
{state}
|
||||||
image={providedImage}
|
image={providedImage}
|
||||||
imgClass="max-h-64 w-auto sm:h-32 md:h-64"
|
imgClass="max-h-64 w-auto sm:h-32 md:h-64"
|
||||||
previewedImage={state.previewedImage}
|
|
||||||
attributionFormat="minimal"
|
attributionFormat="minimal"
|
||||||
>
|
>
|
||||||
<!--
|
<!--
|
||||||
|
|
|
@ -16,7 +16,6 @@ import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFea
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
||||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||||
import { featureEach } from "@turf/turf"
|
|
||||||
|
|
||||||
class PointRenderingLayer {
|
class PointRenderingLayer {
|
||||||
private readonly _config: PointRenderingConfig
|
private readonly _config: PointRenderingConfig
|
||||||
|
@ -542,7 +541,7 @@ export default class ShowDataLayer {
|
||||||
mlmap: UIEventSource<MlMap>,
|
mlmap: UIEventSource<MlMap>,
|
||||||
features: FeatureSource,
|
features: FeatureSource,
|
||||||
layers: LayerConfig[],
|
layers: LayerConfig[],
|
||||||
options?: Partial<ShowDataLayerOptions>
|
options?: Partial<Omit<ShowDataLayerOptions, "features" | "layer">>
|
||||||
) {
|
) {
|
||||||
const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> =
|
const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> =
|
||||||
new PerLayerFeatureSourceSplitter(
|
new PerLayerFeatureSourceSplitter(
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
* This component ties together all the steps that are needed to create a new point.
|
* This component ties together all the steps that are needed to create a new point.
|
||||||
* There are many subcomponents which help with that
|
* There are many subcomponents which help with that
|
||||||
*/
|
*/
|
||||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
|
||||||
import PresetList from "./PresetList.svelte"
|
import PresetList from "./PresetList.svelte"
|
||||||
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"
|
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
|
@ -37,9 +36,10 @@
|
||||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||||
import BaseUIElement from "../../BaseUIElement"
|
import BaseUIElement from "../../BaseUIElement"
|
||||||
import TitledPanel from "../../Base/TitledPanel.svelte"
|
import TitledPanel from "../../Base/TitledPanel.svelte"
|
||||||
|
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||||
|
|
||||||
export let coordinate: { lon: number; lat: number }
|
export let coordinate: { lon: number; lat: number }
|
||||||
export let state: SpecialVisualizationState
|
export let state: ThemeViewState
|
||||||
|
|
||||||
let selectedPreset: {
|
let selectedPreset: {
|
||||||
preset: PresetConfig
|
preset: PresetConfig
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
|
|
||||||
const isLoading = state.dataIsLoading
|
const isLoading = state.dataIsLoading
|
||||||
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
|
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
|
||||||
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined)
|
let snappedToObject: UIEventSource<WayId> = new UIEventSource<string>(undefined)
|
||||||
|
|
||||||
// Small helper variable: if the map is tapped, we should let the 'Next'-button grab some attention as users have to click _that_ to continue, not the map
|
// Small helper variable: if the map is tapped, we should let the 'Next'-button grab some attention as users have to click _that_ to continue, not the map
|
||||||
let preciseInputIsTapped = false
|
let preciseInputIsTapped = false
|
||||||
|
@ -153,7 +153,7 @@
|
||||||
|
|
||||||
function confirmSync() {
|
function confirmSync() {
|
||||||
confirm()
|
confirm()
|
||||||
.then((_) => console.debug("New point successfully handled"))
|
.then(() => console.debug("New point successfully handled"))
|
||||||
.catch((e) => console.error("Handling the new point went wrong due to", e))
|
.catch((e) => console.error("Handling the new point went wrong due to", e))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -20,9 +20,11 @@
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||||
import Confirm from "../../../assets/svg/Confirm.svelte"
|
import Confirm from "../../../assets/svg/Confirm.svelte"
|
||||||
import Layers from "../../../assets/svg/Layers.svelte"
|
import Layers from "../../../assets/svg/Layers.svelte"
|
||||||
|
import { WithGuiState } from "../../../Models/ThemeViewState/WithGuiState"
|
||||||
|
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||||
|
|
||||||
export let importFlow: ImportFlow<ImportFlowArguments>
|
export let importFlow: ImportFlow<ImportFlowArguments>
|
||||||
let state = importFlow.state
|
let state: ThemeViewState = importFlow.state
|
||||||
|
|
||||||
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
|
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { SpecialVisualizationState } from "../../SpecialVisualization"
|
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../../Logic/UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
import { Tag } from "../../../Logic/Tags/Tag"
|
import { Tag } from "../../../Logic/Tags/Tag"
|
||||||
|
@ -10,6 +9,7 @@ import FilteredLayer from "../../../Models/FilteredLayer"
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJson"
|
import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||||
import conflation_json from "../../../../assets/layers/conflation/conflation.json"
|
import conflation_json from "../../../../assets/layers/conflation/conflation.json"
|
||||||
|
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||||
|
|
||||||
export interface ImportFlowArguments {
|
export interface ImportFlowArguments {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
|
@ -123,7 +123,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
argsRaw: string[]
|
argsRaw: string[]
|
||||||
): string[] {
|
): string[] {
|
||||||
const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec)
|
const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec)
|
||||||
const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw)
|
const argsParsed: PointImportFlowArguments = Utils.ParseVisArgs <PointImportFlowArguments>(argSpec, argsRaw)
|
||||||
const snapOntoLayers = argsParsed.snap_onto_layers?.split(";")?.map((l) => l.trim()) ?? []
|
const snapOntoLayers = argsParsed.snap_onto_layers?.split(";")?.map((l) => l.trim()) ?? []
|
||||||
deps.push(...snapOntoLayers)
|
deps.push(...snapOntoLayers)
|
||||||
return deps
|
return deps
|
||||||
|
@ -136,14 +136,14 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
* This class works together closely with ImportFlow.svelte
|
* This class works together closely with ImportFlow.svelte
|
||||||
*/
|
*/
|
||||||
export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
||||||
public readonly state: SpecialVisualizationState
|
public readonly state: ThemeViewState
|
||||||
public readonly args: ArgT
|
public readonly args: ArgT
|
||||||
public readonly targetLayer: FilteredLayer[]
|
public readonly targetLayer: FilteredLayer[]
|
||||||
public readonly tagsToApply: Store<Tag[]>
|
public readonly tagsToApply: Store<Tag[]>
|
||||||
protected readonly _originalFeatureTags: UIEventSource<Record<string, string>>
|
protected readonly _originalFeatureTags: UIEventSource<Record<string, string>>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state: SpecialVisualizationState,
|
state: ThemeViewState,
|
||||||
args: ArgT,
|
args: ArgT,
|
||||||
tagsToApply: Store<Tag[]>,
|
tagsToApply: Store<Tag[]>,
|
||||||
originalTags: UIEventSource<Record<string, string>>
|
originalTags: UIEventSource<Record<string, string>>
|
||||||
|
@ -153,7 +153,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
||||||
this.tagsToApply = tagsToApply
|
this.tagsToApply = tagsToApply
|
||||||
this._originalFeatureTags = originalTags
|
this._originalFeatureTags = originalTags
|
||||||
this.targetLayer = args.targetLayer.split(" ").map((tl) => {
|
this.targetLayer = args.targetLayer.split(" ").map((tl) => {
|
||||||
let found = state.layerState.filteredLayers.get(tl)
|
const found = state.layerState.filteredLayers.get(tl)
|
||||||
if (!found) {
|
if (!found) {
|
||||||
throw "Layer " + tl + " not found"
|
throw "Layer " + tl + " not found"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import ImportFlow, { ImportFlowArguments } from "./ImportFlow"
|
import ImportFlow, { ImportFlowArguments } from "./ImportFlow"
|
||||||
import { SpecialVisualizationState } from "../../SpecialVisualization"
|
|
||||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
import { OsmObject, OsmWay } from "../../../Logic/Osm/OsmObject"
|
import { OsmObject, OsmWay } from "../../../Logic/Osm/OsmObject"
|
||||||
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
|
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
|
||||||
|
@ -7,6 +6,7 @@ import { Feature, Point } from "geojson"
|
||||||
import Maproulette from "../../../Logic/Maproulette"
|
import Maproulette from "../../../Logic/Maproulette"
|
||||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||||
import { Tag } from "../../../Logic/Tags/Tag"
|
import { Tag } from "../../../Logic/Tags/Tag"
|
||||||
|
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||||
|
|
||||||
export interface PointImportFlowArguments extends ImportFlowArguments {
|
export interface PointImportFlowArguments extends ImportFlowArguments {
|
||||||
max_snap_distance?: string
|
max_snap_distance?: string
|
||||||
|
@ -21,7 +21,7 @@ export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
||||||
public readonly startCoordinate: [number, number]
|
public readonly startCoordinate: [number, number]
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state: SpecialVisualizationState,
|
state: ThemeViewState,
|
||||||
originalFeature: Feature<Point>,
|
originalFeature: Feature<Point>,
|
||||||
args: PointImportFlowArguments,
|
args: PointImportFlowArguments,
|
||||||
tagsToApply: Store<Tag[]>,
|
tagsToApply: Store<Tag[]>,
|
||||||
|
|
|
@ -20,9 +20,10 @@
|
||||||
import NextButton from "../../Base/NextButton.svelte"
|
import NextButton from "../../Base/NextButton.svelte"
|
||||||
import Note from "../../../assets/svg/Note.svelte"
|
import Note from "../../../assets/svg/Note.svelte"
|
||||||
import TitledPanel from "../../Base/TitledPanel.svelte"
|
import TitledPanel from "../../Base/TitledPanel.svelte"
|
||||||
|
import ThemeViewState from "../../../Models/ThemeViewState"
|
||||||
|
|
||||||
export let coordinate: UIEventSource<{ lon: number; lat: number }>
|
export let coordinate: UIEventSource<{ lon: number; lat: number }>
|
||||||
export let state: SpecialVisualizationState
|
export let state: ThemeViewState
|
||||||
|
|
||||||
let comment: UIEventSource<string> = LocalStorageSource.get("note-text")
|
let comment: UIEventSource<string> = LocalStorageSource.get("note-text")
|
||||||
let created = false
|
let created = false
|
||||||
|
@ -33,7 +34,6 @@
|
||||||
let isDisplayed = notelayer?.isDisplayed
|
let isDisplayed = notelayer?.isDisplayed
|
||||||
|
|
||||||
let submitted = false
|
let submitted = false
|
||||||
let textEntered = false
|
|
||||||
|
|
||||||
function enableNoteLayer() {
|
function enableNoteLayer() {
|
||||||
state.guistate.closeAll()
|
state.guistate.closeAll()
|
||||||
|
|
|
@ -87,7 +87,6 @@
|
||||||
{state}
|
{state}
|
||||||
{image}
|
{image}
|
||||||
imgClass="max-h-64 w-auto sm:h-32 md:h-64"
|
imgClass="max-h-64 w-auto sm:h-32 md:h-64"
|
||||||
previewedImage={state.previewedImage}
|
|
||||||
attributionFormat="minimal"
|
attributionFormat="minimal"
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
|
|
||||||
export let state: SpecialVisualizationState = undefined
|
export let state: ThemeViewState = undefined
|
||||||
export let review: Review & {
|
export let review: Review & {
|
||||||
kid: string
|
kid: string
|
||||||
signature: string
|
signature: string
|
||||||
|
|
|
@ -13,8 +13,9 @@
|
||||||
import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
|
import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||||
import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
|
import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
|
||||||
|
import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState"
|
||||||
|
|
||||||
export let state: SpecialVisualizationState
|
export let state: WithSearchState
|
||||||
|
|
||||||
let searchTerm = state.searchState.searchTerm
|
let searchTerm = state.searchState.searchTerm
|
||||||
let results = state.searchState.suggestions
|
let results = state.searchState.suggestions
|
||||||
|
|
64
src/UI/SingleThemeGui.svelte
Normal file
64
src/UI/SingleThemeGui.svelte
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<script lang="ts">/**
|
||||||
|
* Wrapper around 'ThemeViewGui', which is responsible for some boiler plate things, such as:
|
||||||
|
*
|
||||||
|
* - Checking for WebGL support
|
||||||
|
* - Loading the available layers
|
||||||
|
* - ...
|
||||||
|
*/
|
||||||
|
import ThemeViewGUI from "./ThemeViewGUI.svelte"
|
||||||
|
import Constants from "../Models/Constants.js"
|
||||||
|
import { Utils } from "../Utils.js"
|
||||||
|
import { UIEventSource } from "../Logic/UIEventSource"
|
||||||
|
import { WithSearchState } from "../Models/ThemeViewState/WithSearchState"
|
||||||
|
import { ThemeConfigJson } from "../Models/ThemeConfig/Json/ThemeConfigJson"
|
||||||
|
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
|
||||||
|
|
||||||
|
function webgl_support() {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement("canvas")
|
||||||
|
if (
|
||||||
|
!!window.WebGLRenderingContext &&
|
||||||
|
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getAvailableLayers(): Promise<Set<string>> {
|
||||||
|
if (!Constants.SummaryServer) {
|
||||||
|
return new Set<string>()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const host = new URL(Constants.SummaryServer).host
|
||||||
|
const status: { layers: string[] } = await Promise.any([
|
||||||
|
Utils.downloadJson<{ layers }>("https://" + host + "/summary/status.json"),
|
||||||
|
Utils.waitFor(2500, { layers: [] })
|
||||||
|
])
|
||||||
|
return new Set<string>(status.layers)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not get MVT available layers due to", e)
|
||||||
|
return new Set<string>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export let theme: ThemeConfig
|
||||||
|
|
||||||
|
let webgl_supported = webgl_support()
|
||||||
|
|
||||||
|
let availableLayers = UIEventSource.FromPromise(getAvailableLayers())
|
||||||
|
const state = new WithSearchState(theme, availableLayers)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !webgl_supported}
|
||||||
|
<div class="alert w-full h-full justify-center items-center m-8">WebGL is not supported or not enabled. This is
|
||||||
|
essential
|
||||||
|
for MapComplete to function, please enable this.
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ThemeViewGUI {state} />
|
||||||
|
{/if}
|
|
@ -1,11 +1,7 @@
|
||||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||||
import BaseUIElement from "./BaseUIElement"
|
import BaseUIElement from "./BaseUIElement"
|
||||||
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
|
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
|
||||||
import {
|
import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
||||||
FeatureSource,
|
|
||||||
IndexedFeatureSource,
|
|
||||||
WritableFeatureSource,
|
|
||||||
} from "../Logic/FeatureSource/FeatureSource"
|
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
import { Changes } from "../Logic/Osm/Changes"
|
import { Changes } from "../Logic/Osm/Changes"
|
||||||
import { ExportableMap, MapProperties } from "../Models/MapProperties"
|
import { ExportableMap, MapProperties } from "../Models/MapProperties"
|
||||||
|
@ -15,17 +11,14 @@ import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/Fu
|
||||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||||
import { MenuState } from "../Models/MenuState"
|
|
||||||
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
||||||
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
||||||
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||||
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
|
||||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||||
import ThemeSource from "../Logic/FeatureSource/Sources/ThemeSource"
|
import ThemeSource from "../Logic/FeatureSource/Sources/ThemeSource"
|
||||||
import { Map as MlMap } from "maplibre-gl"
|
import { Map as MlMap } from "maplibre-gl"
|
||||||
import ShowDataLayer from "./Map/ShowDataLayer"
|
import ShowDataLayer from "./Map/ShowDataLayer"
|
||||||
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
|
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
|
||||||
import SearchState from "../Logic/State/SearchState"
|
|
||||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||||
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
||||||
|
|
||||||
|
@ -33,7 +26,6 @@ import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropert
|
||||||
* The state needed to render a special Visualisation.
|
* The state needed to render a special Visualisation.
|
||||||
*/
|
*/
|
||||||
export interface SpecialVisualizationState {
|
export interface SpecialVisualizationState {
|
||||||
readonly guistate: MenuState
|
|
||||||
readonly theme: ThemeConfig
|
readonly theme: ThemeConfig
|
||||||
readonly featureSwitches: FeatureSwitchState
|
readonly featureSwitches: FeatureSwitchState
|
||||||
|
|
||||||
|
@ -78,11 +70,9 @@ export interface SpecialVisualizationState {
|
||||||
|
|
||||||
readonly imageUploadManager: ImageUploadManager
|
readonly imageUploadManager: ImageUploadManager
|
||||||
|
|
||||||
readonly previewedImage: UIEventSource<ProvidedImage>
|
|
||||||
readonly nearbyImageSearcher: CombinedFetcher
|
readonly nearbyImageSearcher: CombinedFetcher
|
||||||
readonly geolocation: GeoLocationHandler
|
readonly geolocation: GeoLocationHandler
|
||||||
readonly geocodedImages: UIEventSource<Feature[]>
|
readonly geocodedImages: UIEventSource<Feature[]>
|
||||||
readonly searchState: SearchState
|
|
||||||
|
|
||||||
getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined
|
getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,10 @@
|
||||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||||
import MapControlButton from "./Base/MapControlButton.svelte"
|
import MapControlButton from "./Base/MapControlButton.svelte"
|
||||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
|
||||||
import If from "./Base/If.svelte"
|
import If from "./Base/If.svelte"
|
||||||
import type { Feature } from "geojson"
|
import type { Feature } from "geojson"
|
||||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
|
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||||
import ThemeViewState from "../Models/ThemeViewState"
|
|
||||||
import type { MapProperties } from "../Models/MapProperties"
|
import type { MapProperties } from "../Models/MapProperties"
|
||||||
import Translations from "./i18n/Translations"
|
import Translations from "./i18n/Translations"
|
||||||
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid"
|
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
|
@ -48,8 +46,15 @@
|
||||||
import { Drawer } from "flowbite-svelte"
|
import { Drawer } from "flowbite-svelte"
|
||||||
import { linear } from "svelte/easing"
|
import { linear } from "svelte/easing"
|
||||||
import DefaultIcon from "./Map/DefaultIcon.svelte"
|
import DefaultIcon from "./Map/DefaultIcon.svelte"
|
||||||
|
import Loading from "./Base/Loading.svelte"
|
||||||
|
import { WithSearchState } from "../Models/ThemeViewState/WithSearchState"
|
||||||
|
import TitleHandler from "../Logic/Actors/TitleHandler"
|
||||||
|
|
||||||
export let state: ThemeViewState
|
export let state: WithSearchState
|
||||||
|
new TitleHandler(state.selectedElement, state)
|
||||||
|
|
||||||
|
console.log("The state is", state)
|
||||||
|
state.focusOnMap()
|
||||||
|
|
||||||
let theme = state.theme
|
let theme = state.theme
|
||||||
let maplibremap: UIEventSource<MlMap> = state.map
|
let maplibremap: UIEventSource<MlMap> = state.map
|
||||||
|
@ -421,7 +426,9 @@
|
||||||
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
||||||
<div class="alert w-fit">Faking a user (Testmode)</div>
|
<div class="alert w-fit">Faking a user (Testmode)</div>
|
||||||
</If>
|
</If>
|
||||||
{#if $apiState !== "online" && $apiState !== "unknown"}
|
{#if $apiState === "unknown"}
|
||||||
|
<Loading />
|
||||||
|
{:else if $apiState !== "online"}
|
||||||
<div class="alert w-fit">API is {$apiState}</div>
|
<div class="alert w-fit">API is {$apiState}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1125,9 +1125,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
element.click()
|
element.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async waitFor(timeMillis: number): Promise<void> {
|
public static async waitFor(timeMillis: number): Promise<void>;
|
||||||
|
public static async waitFor<T>(timeMillis: number, t: T): Promise<T>;
|
||||||
|
|
||||||
|
public static async waitFor<T = void>(timeMillis: number, t: T): Promise<T> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
window.setTimeout(resolve, timeMillis)
|
window.setTimeout(() => {
|
||||||
|
resolve(t)
|
||||||
|
}, timeMillis)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
87
src/index.ts
87
src/index.ts
|
@ -1,66 +1,16 @@
|
||||||
import DetermineTheme from "./Logic/DetermineTheme"
|
import DetermineTheme from "./Logic/DetermineTheme"
|
||||||
import ThemeViewState from "./Models/ThemeViewState"
|
import SingleThemeGui from "./UI/SingleThemeGui.svelte"
|
||||||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
import CustomThemeError from "./UI/CustomThemeError.svelte"
|
||||||
import ThemeViewGUI from "./UI/ThemeViewGUI.svelte"
|
|
||||||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
|
||||||
import Combine from "./UI/Base/Combine"
|
|
||||||
import { SubtleButton } from "./UI/Base/SubtleButton"
|
|
||||||
import { Utils } from "./Utils"
|
|
||||||
import Constants from "./Models/Constants"
|
|
||||||
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
|
|
||||||
import { AndroidPolyfill } from "./Logic/Web/AndroidPolyfill"
|
|
||||||
|
|
||||||
function webgl_support() {
|
|
||||||
try {
|
|
||||||
const canvas = document.createElement("canvas")
|
|
||||||
return (
|
|
||||||
!!window.WebGLRenderingContext &&
|
|
||||||
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function timeout(timeMS: number): Promise<{ layers: string[] }> {
|
|
||||||
await Utils.waitFor(timeMS)
|
|
||||||
return { layers: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAvailableLayers(): Promise<Set<string>> {
|
|
||||||
if (!Constants.SummaryServer) {
|
|
||||||
return new Set<string>()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const host = new URL(Constants.SummaryServer).host
|
|
||||||
const status: { layers: string[] } = await Promise.any([
|
|
||||||
Utils.downloadJson<{ layers }>("https://" + host + "/summary/status.json"),
|
|
||||||
timeout(2500),
|
|
||||||
])
|
|
||||||
return new Set<string>(status.layers)
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Could not get MVT available layers due to", e)
|
|
||||||
return new Set<string>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const target = document.getElementById("maindiv")
|
||||||
|
const childs = Array.from(target.children)
|
||||||
try {
|
try {
|
||||||
if (!webgl_support()) {
|
const theme = await DetermineTheme.getTheme()
|
||||||
throw "WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this."
|
new SingleThemeGui({
|
||||||
}
|
|
||||||
new AndroidPolyfill().init().then(() => console.log("Android polyfill setup completed"))
|
|
||||||
const [theme, availableLayers] = await Promise.all([
|
|
||||||
DetermineTheme.getTheme(),
|
|
||||||
await getAvailableLayers(),
|
|
||||||
])
|
|
||||||
console.log("The available layers on server are", Array.from(availableLayers))
|
|
||||||
const state = new ThemeViewState(theme, availableLayers)
|
|
||||||
const target = document.getElementById("maindiv")
|
|
||||||
const childs = Array.from(target.children)
|
|
||||||
new ThemeViewGUI({
|
|
||||||
target,
|
target,
|
||||||
props: { state },
|
props: { theme }
|
||||||
})
|
})
|
||||||
childs.forEach((ch) => target.removeChild(ch))
|
childs.forEach((ch) => target.removeChild(ch))
|
||||||
Array.from(document.getElementsByClassName("delete-on-load")).forEach((el) => {
|
Array.from(document.getElementsByClassName("delete-on-load")).forEach((el) => {
|
||||||
|
@ -69,22 +19,15 @@ async function main() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error while initializing: ", err, err.stack)
|
console.error("Error while initializing: ", err, err.stack)
|
||||||
const customDefinition = DetermineTheme.getCustomDefinition()
|
const customDefinition = DetermineTheme.getCustomDefinition()
|
||||||
new Combine([
|
|
||||||
new FixedUiElement(err.toString().split("\n").join("<br/>")).SetClass("block alert"),
|
|
||||||
|
|
||||||
customDefinition?.length > 0
|
new CustomThemeError({
|
||||||
? new SubtleButton(
|
target,
|
||||||
new SvelteUIElement(ArrowDownTray),
|
props: {
|
||||||
"Download the raw file"
|
stack: err.toString().split("\n"),
|
||||||
).onClick(() =>
|
customDefinition
|
||||||
Utils.offerContentsAsDownloadableFile(
|
}
|
||||||
DetermineTheme.getCustomDefinition(),
|
})
|
||||||
"mapcomplete-theme.json",
|
|
||||||
{ mimetype: "application/json" }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
]).AttachTo("maindiv")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,67 +1,20 @@
|
||||||
import ThemeViewState from "./src/Models/ThemeViewState"
|
import MetaTagging from "./src/Logic/MetaTagging"
|
||||||
import ThemeViewGUI from "./src/UI/ThemeViewGUI.svelte"
|
import SingleThemeGui from "./UI/SingleThemeGui.svelte"
|
||||||
import ThemeConfig from "./src/Models/ThemeConfig/ThemeConfig";
|
|
||||||
import MetaTagging from "./src/Logic/MetaTagging";
|
|
||||||
import { FixedUiElement } from "./src/UI/Base/FixedUiElement";
|
|
||||||
import { Utils } from "./src/Utils"
|
|
||||||
import Constants from "./src/Models/Constants"
|
|
||||||
import { AndroidPolyfill } from "./src/Logic/Web/AndroidPolyfill"
|
|
||||||
|
|
||||||
function webgl_support() {
|
|
||||||
try {
|
|
||||||
var canvas = document.createElement("canvas")
|
|
||||||
return (
|
|
||||||
!!window.WebGLRenderingContext &&
|
|
||||||
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function timeout(timeMS: number): Promise<{ layers: string[] }> {
|
|
||||||
await Utils.waitFor(timeMS)
|
|
||||||
return { layers: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function getAvailableLayers(): Promise<Set<string>> {
|
|
||||||
if(!Constants.SummaryServer){
|
|
||||||
return new Set<string>()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const host = new URL(Constants.SummaryServer).host
|
|
||||||
const status = await Promise.any([
|
|
||||||
Utils.downloadJson("https://" + host + "/summary/status.json"),
|
|
||||||
timeout(0)
|
|
||||||
])
|
|
||||||
return new Set<string>(status.layers)
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Could not get MVT available layers due to", e)
|
|
||||||
return new Set<string>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (!webgl_support()) {
|
|
||||||
new FixedUiElement("WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this.").SetClass("block alert").AttachTo("maindiv")
|
|
||||||
}else{
|
|
||||||
new AndroidPolyfill().init().then(() => console.log("Android polyfill setup completed"))
|
|
||||||
const availableLayers = await getAvailableLayers()
|
|
||||||
MetaTagging.setThemeMetatagging(new ThemeMetaTagging())
|
MetaTagging.setThemeMetatagging(new ThemeMetaTagging())
|
||||||
// LAYOUT.ADD_LAYERS
|
// LAYOUT.ADD_LAYERS
|
||||||
// LAYOUT.ADD_CONFIG
|
// LAYOUT.ADD_CONFIG
|
||||||
const state = new ThemeViewState(new ThemeConfig(<any> layout), availableLayers)
|
|
||||||
const target = document.getElementById("maindiv")
|
const target = document.getElementById("maindiv")
|
||||||
const childs = Array.from(target.children)
|
const childs = Array.from(target.children)
|
||||||
new ThemeViewGUI({
|
new SingleThemeGui({
|
||||||
target,
|
target,
|
||||||
props: { state },
|
props: { theme },
|
||||||
})
|
})
|
||||||
childs.forEach(ch => target.removeChild(ch))
|
childs.forEach(ch => target.removeChild(ch))
|
||||||
Array.from(document.getElementsByClassName("delete-on-load")).forEach(el => {
|
Array.from(document.getElementsByClassName("delete-on-load")).forEach(el => {
|
||||||
el.parentElement.removeChild(el)
|
el.parentElement.removeChild(el)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Feature, Geometry } from "geojson"
|
||||||
import { expect, it } from "vitest"
|
import { expect, it } from "vitest"
|
||||||
import ThemeViewState from "../../../src/Models/ThemeViewState"
|
import ThemeViewState from "../../../src/Models/ThemeViewState"
|
||||||
import ScriptUtils from "../../../scripts/ScriptUtils"
|
import ScriptUtils from "../../../scripts/ScriptUtils"
|
||||||
|
import { ImmutableStore } from "../../../src/Logic/UIEventSource"
|
||||||
|
|
||||||
const latestTags = {
|
const latestTags = {
|
||||||
amenity: "public_bookcase",
|
amenity: "public_bookcase",
|
||||||
|
@ -46,7 +47,8 @@ Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/node/556
|
||||||
Utils.injectJsonDownloadForTests("./assets/data/editor-layer-index.json", '{"features": [] }')
|
Utils.injectJsonDownloadForTests("./assets/data/editor-layer-index.json", '{"features": [] }')
|
||||||
|
|
||||||
it("should download the latest version", async () => {
|
it("should download the latest version", async () => {
|
||||||
const state = new ThemeViewState(new ThemeConfig(<any>bookcaseJson, true), new Set<string>())
|
const state = new ThemeViewState(new ThemeConfig(<any>bookcaseJson, true),
|
||||||
|
new ImmutableStore<Set<string>>(new Set()))
|
||||||
const feature: Feature<Geometry, OsmTags> = {
|
const feature: Feature<Geometry, OsmTags> = {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
id: "node/5568693115",
|
id: "node/5568693115",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue