forked from MapComplete/MapComplete
Huge refactoring of state and initial UI setup
This commit is contained in:
parent
4e43673de5
commit
eff6b5bfad
37 changed files with 5232 additions and 4907 deletions
93
Logic/State/ElementsState.ts
Normal file
93
Logic/State/ElementsState.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import FeatureSwitchState from "./FeatureSwitchState";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import {Changes} from "../Osm/Changes";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {BBox} from "../BBox";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import ChangeToElementsActor from "../Actors/ChangeToElementsActor";
|
||||
import PendingChangesUploader from "../Actors/PendingChangesUploader";
|
||||
import TitleHandler from "../Actors/TitleHandler";
|
||||
|
||||
/**
|
||||
* The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc
|
||||
*/
|
||||
export default class ElementsState extends FeatureSwitchState{
|
||||
|
||||
/**
|
||||
The mapping from id -> UIEventSource<properties>
|
||||
*/
|
||||
public allElements: ElementStorage = new ElementStorage();
|
||||
/**
|
||||
THe change handler
|
||||
*/
|
||||
public changes: Changes = new Changes();
|
||||
|
||||
/**
|
||||
The latest element that was selected
|
||||
*/
|
||||
public readonly selectedElement = new UIEventSource<any>(
|
||||
undefined,
|
||||
"Selected element"
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* The map location: currently centered lat, lon and zoom
|
||||
*/
|
||||
public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl");
|
||||
|
||||
/**
|
||||
* The current visible extent of the screen
|
||||
*/
|
||||
public readonly currentBounds = new UIEventSource<BBox>(undefined)
|
||||
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
super(layoutToUse);
|
||||
{
|
||||
// -- Location control initialization
|
||||
const zoom = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"z",
|
||||
"" + (layoutToUse?.startZoom ?? 1),
|
||||
"The initial/current zoom level"
|
||||
).syncWith(LocalStorageSource.Get("zoom"))
|
||||
);
|
||||
const lat = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"lat",
|
||||
"" + (layoutToUse?.startLat ?? 0),
|
||||
"The initial/current latitude"
|
||||
).syncWith(LocalStorageSource.Get("lat"))
|
||||
);
|
||||
const lon = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"lon",
|
||||
"" + (layoutToUse?.startLon ?? 0),
|
||||
"The initial/current longitude of the app"
|
||||
).syncWith(LocalStorageSource.Get("lon"))
|
||||
);
|
||||
|
||||
this.locationControl.setData({
|
||||
zoom: Utils.asFloat(zoom.data),
|
||||
lat: Utils.asFloat(lat.data),
|
||||
lon: Utils.asFloat(lon.data),
|
||||
})
|
||||
this.locationControl.addCallback((latlonz) => {
|
||||
// Sync th location controls
|
||||
zoom.setData(latlonz.zoom);
|
||||
lat.setData(latlonz.lat);
|
||||
lon.setData(latlonz.lon);
|
||||
});
|
||||
}
|
||||
|
||||
new ChangeToElementsActor(this.changes, this.allElements)
|
||||
new PendingChangesUploader(this.changes, this.selectedElement);
|
||||
new TitleHandler(this);
|
||||
|
||||
}
|
||||
}
|
172
Logic/State/FeaturePipelineState.ts
Normal file
172
Logic/State/FeaturePipelineState.ts
Normal file
|
@ -0,0 +1,172 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
|
||||
import State from "../../State";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer";
|
||||
import {TileHierarchyAggregator} from "../../UI/ShowDataLayer/TileHierarchyAggregator";
|
||||
import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import MapState from "./MapState";
|
||||
import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler";
|
||||
import Hash from "../Web/Hash";
|
||||
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import SimpleAddUI from "../../UI/BigComponents/SimpleAddUI";
|
||||
import StrayClickHandler from "../Actors/StrayClickHandler";
|
||||
|
||||
export default class FeaturePipelineState extends MapState {
|
||||
|
||||
/**
|
||||
* The piece of code which fetches data from various sources and shows it on the background map
|
||||
*/
|
||||
public readonly featurePipeline: FeaturePipeline;
|
||||
private readonly featureAggregator: TileHierarchyAggregator;
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
super(layoutToUse);
|
||||
|
||||
const clustering = layoutToUse.clustering
|
||||
this.featureAggregator = TileHierarchyAggregator.createHierarchy();
|
||||
const clusterCounter = this.featureAggregator
|
||||
const self = this;
|
||||
this.featurePipeline = new FeaturePipeline(
|
||||
source => {
|
||||
|
||||
clusterCounter.addTile(source)
|
||||
|
||||
// Do show features indicates if the 'showDataLayer' should be shown
|
||||
const doShowFeatures = source.features.map(
|
||||
f => {
|
||||
const z = State.state.locationControl.data.zoom
|
||||
|
||||
if (!source.layer.isDisplayed.data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bounds = State.state.currentBounds.data
|
||||
if (bounds === undefined) {
|
||||
// Map is not yet displayed
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!source.bbox.overlapsWith(bounds)) {
|
||||
// Not within range -> features are hidden
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (z < source.layer.layerDef.minzoom) {
|
||||
// Layer is always hidden for this zoom level
|
||||
return false;
|
||||
}
|
||||
|
||||
if (z > clustering.maxZoom) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (f.length > clustering.minNeededElements) {
|
||||
// This tile alone already has too much features
|
||||
return false
|
||||
}
|
||||
|
||||
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex);
|
||||
if (tileZ >= z) {
|
||||
|
||||
while (tileZ > z) {
|
||||
tileZ--
|
||||
tileX = Math.floor(tileX / 2)
|
||||
tileY = Math.floor(tileY / 2)
|
||||
}
|
||||
|
||||
if (clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))?.totalValue > clustering.minNeededElements) {
|
||||
// To much elements
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}, [State.state.currentBounds, source.layer.isDisplayed]
|
||||
)
|
||||
|
||||
new ShowDataLayer(
|
||||
{
|
||||
features: source,
|
||||
leafletMap: self.leafletMap,
|
||||
layerToShow: source.layer.layerDef,
|
||||
doShowLayer: doShowFeatures,
|
||||
allElements: self.allElements,
|
||||
selectedElement: self.selectedElement
|
||||
}
|
||||
);
|
||||
}, this
|
||||
);
|
||||
new SelectedFeatureHandler(Hash.hash, this)
|
||||
|
||||
this.AddClusteringToMap(this.leafletMap)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the cluster-tiles to the given map
|
||||
* @param leafletMap: a UIEventSource possible having a leaflet map
|
||||
* @constructor
|
||||
*/
|
||||
public AddClusteringToMap(leafletMap: UIEventSource<any>) {
|
||||
const clustering = this.layoutToUse.clustering
|
||||
new ShowDataLayer({
|
||||
features: this.featureAggregator.getCountsForZoom(clustering, this.locationControl, clustering.minNeededElements),
|
||||
leafletMap: leafletMap,
|
||||
layerToShow: ShowTileInfo.styling,
|
||||
enablePopups: false,
|
||||
})
|
||||
}
|
||||
|
||||
public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, leafletMap: UIEventSource<any>) {
|
||||
|
||||
const self = this
|
||||
function setup(){
|
||||
let presetCount = 0;
|
||||
for (const layer of self.layoutToUse.layers) {
|
||||
for (const preset of layer.presets) {
|
||||
presetCount++;
|
||||
}
|
||||
}
|
||||
if (presetCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPointDialogIsShown = new UIEventSource<boolean>(false);
|
||||
const addNewPoint = new ScrollableFullScreen(
|
||||
() => Translations.t.general.add.title.Clone(),
|
||||
() => new SimpleAddUI(newPointDialogIsShown, filterViewIsOpened, self),
|
||||
"new",
|
||||
newPointDialogIsShown
|
||||
);
|
||||
addNewPoint.isShown.addCallback((isShown) => {
|
||||
if (!isShown) {
|
||||
self.LastClickLocation.setData(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
new StrayClickHandler(
|
||||
self.LastClickLocation,
|
||||
self.selectedElement,
|
||||
self.filteredLayers,
|
||||
leafletMap,
|
||||
addNewPoint
|
||||
);
|
||||
}
|
||||
|
||||
this.featureSwitchAddNew.addCallbackAndRunD(addNewAllowed => {
|
||||
if (addNewAllowed) {
|
||||
setup()
|
||||
return true;
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
208
Logic/State/FeatureSwitchState.ts
Normal file
208
Logic/State/FeatureSwitchState.ts
Normal file
|
@ -0,0 +1,208 @@
|
|||
/**
|
||||
* The part of the global state which initializes the feature switches, based on default values and on the layoutToUse
|
||||
*/
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
import Constants from "../../Models/Constants";
|
||||
|
||||
export default class FeatureSwitchState {
|
||||
|
||||
/**
|
||||
* The layout that is being used in this run
|
||||
*/
|
||||
public readonly layoutToUse: LayoutConfig;
|
||||
|
||||
public readonly featureSwitchUserbadge: UIEventSource<boolean>;
|
||||
public readonly featureSwitchSearch: UIEventSource<boolean>;
|
||||
public readonly featureSwitchBackgroundSlection: UIEventSource<boolean>;
|
||||
public readonly featureSwitchAddNew: UIEventSource<boolean>;
|
||||
public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>;
|
||||
public readonly featureSwitchIframePopoutEnabled: UIEventSource<boolean>;
|
||||
public readonly featureSwitchMoreQuests: UIEventSource<boolean>;
|
||||
public readonly featureSwitchShareScreen: UIEventSource<boolean>;
|
||||
public readonly featureSwitchGeolocation: UIEventSource<boolean>;
|
||||
public readonly featureSwitchIsTesting: UIEventSource<boolean>;
|
||||
public readonly featureSwitchIsDebugging: UIEventSource<boolean>;
|
||||
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
|
||||
public readonly featureSwitchApiURL: UIEventSource<string>;
|
||||
public readonly featureSwitchFilter: UIEventSource<boolean>;
|
||||
public readonly featureSwitchEnableExport: UIEventSource<boolean>;
|
||||
public readonly featureSwitchFakeUser: UIEventSource<boolean>;
|
||||
public readonly featureSwitchExportAsPdf: UIEventSource<boolean>;
|
||||
public readonly overpassUrl: UIEventSource<string[]>;
|
||||
public readonly overpassTimeout: UIEventSource<number>;
|
||||
public readonly overpassMaxZoom: UIEventSource<number>;
|
||||
public readonly osmApiTileSize: UIEventSource<number>;
|
||||
public readonly backgroundLayerId: UIEventSource<string>;
|
||||
|
||||
protected constructor(layoutToUse: LayoutConfig) {
|
||||
this.layoutToUse = layoutToUse;
|
||||
|
||||
|
||||
// Helper function to initialize feature switches
|
||||
function featSw(
|
||||
key: string,
|
||||
deflt: (layout: LayoutConfig) => boolean,
|
||||
documentation: string
|
||||
): UIEventSource<boolean> {
|
||||
|
||||
const defaultValue = deflt(layoutToUse);
|
||||
const queryParam = QueryParameters.GetQueryParameter(
|
||||
key,
|
||||
"" + defaultValue,
|
||||
documentation
|
||||
);
|
||||
|
||||
// It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
|
||||
return queryParam.map((str) =>
|
||||
str === undefined ? defaultValue : str !== "false"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
this.featureSwitchUserbadge = featSw(
|
||||
"fs-userbadge",
|
||||
(layoutToUse) => layoutToUse?.enableUserBadge ?? true,
|
||||
"Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode."
|
||||
);
|
||||
this.featureSwitchSearch = featSw(
|
||||
"fs-search",
|
||||
(layoutToUse) => layoutToUse?.enableSearch ?? true,
|
||||
"Disables/Enables the search bar"
|
||||
);
|
||||
this.featureSwitchBackgroundSlection = featSw(
|
||||
"fs-background",
|
||||
(layoutToUse) => layoutToUse?.enableBackgroundLayerSelection ?? true,
|
||||
"Disables/Enables the background layer control"
|
||||
);
|
||||
|
||||
this.featureSwitchFilter = featSw(
|
||||
"fs-filter",
|
||||
(layoutToUse) => layoutToUse?.enableLayers ?? true,
|
||||
"Disables/Enables the filter"
|
||||
);
|
||||
this.featureSwitchAddNew = featSw(
|
||||
"fs-add-new",
|
||||
(layoutToUse) => layoutToUse?.enableAddNewPoints ?? true,
|
||||
"Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)"
|
||||
);
|
||||
this.featureSwitchWelcomeMessage = featSw(
|
||||
"fs-welcome-message",
|
||||
() => true,
|
||||
"Disables/enables the help menu or welcome message"
|
||||
);
|
||||
this.featureSwitchIframePopoutEnabled = featSw(
|
||||
"fs-iframe-popout",
|
||||
(layoutToUse) => layoutToUse?.enableIframePopout,
|
||||
"Disables/Enables the iframe-popout button. If in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch)"
|
||||
);
|
||||
this.featureSwitchMoreQuests = featSw(
|
||||
"fs-more-quests",
|
||||
(layoutToUse) => layoutToUse?.enableMoreQuests ?? true,
|
||||
"Disables/Enables the 'More Quests'-tab in the welcome message"
|
||||
);
|
||||
this.featureSwitchShareScreen = featSw(
|
||||
"fs-share-screen",
|
||||
(layoutToUse) => layoutToUse?.enableShareScreen ?? true,
|
||||
"Disables/Enables the 'Share-screen'-tab in the welcome message"
|
||||
);
|
||||
this.featureSwitchGeolocation = featSw(
|
||||
"fs-geolocation",
|
||||
(layoutToUse) => layoutToUse?.enableGeolocation ?? true,
|
||||
"Disables/Enables the geolocation button"
|
||||
);
|
||||
this.featureSwitchShowAllQuestions = featSw(
|
||||
"fs-all-questions",
|
||||
(layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false,
|
||||
"Always show all questions"
|
||||
);
|
||||
|
||||
this.featureSwitchEnableExport = featSw(
|
||||
"fs-export",
|
||||
(layoutToUse) => layoutToUse?.enableExportButton ?? false,
|
||||
"Enable the export as GeoJSON and CSV button"
|
||||
);
|
||||
this.featureSwitchExportAsPdf = featSw(
|
||||
"fs-pdf",
|
||||
(layoutToUse) => layoutToUse?.enablePdfDownload ?? false,
|
||||
"Enable the PDF download button"
|
||||
);
|
||||
|
||||
this.featureSwitchApiURL = QueryParameters.GetQueryParameter(
|
||||
"backend",
|
||||
"osm",
|
||||
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'"
|
||||
);
|
||||
|
||||
|
||||
let testingDefaultValue = false;
|
||||
if (this.featureSwitchApiURL.data !== "osm-test" &&
|
||||
(location.hostname === "localhost" || location.hostname === "127.0.0.1")) {
|
||||
testingDefaultValue = true
|
||||
}
|
||||
|
||||
|
||||
this.featureSwitchIsTesting = QueryParameters.GetQueryParameter(
|
||||
"test",
|
||||
""+testingDefaultValue,
|
||||
"If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org"
|
||||
).map(
|
||||
(str) => str === "true",
|
||||
[],
|
||||
(b) => "" + b
|
||||
);
|
||||
|
||||
this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter(
|
||||
"debug",
|
||||
"false",
|
||||
"If true, shows some extra debugging help such as all the available tags on every object"
|
||||
).map(
|
||||
(str) => str === "true",
|
||||
[],
|
||||
(b) => "" + b
|
||||
);
|
||||
|
||||
this.featureSwitchFakeUser = QueryParameters.GetQueryParameter("fake-user", "false",
|
||||
"If true, 'dryrun' mode is activated and a fake user account is loaded")
|
||||
.map(str => str === "true", [], b => "" + b);
|
||||
|
||||
|
||||
|
||||
|
||||
this.overpassUrl = QueryParameters.GetQueryParameter("overpassUrl",
|
||||
(layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","),
|
||||
"Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter"
|
||||
).map(param => param.split(","), [], urls => urls.join(","))
|
||||
|
||||
this.overpassTimeout = UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassTimeout",
|
||||
"" + layoutToUse?.overpassTimeout,
|
||||
"Set a different timeout (in seconds) for queries in overpass"))
|
||||
|
||||
|
||||
this.overpassMaxZoom =
|
||||
UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassMaxZoom",
|
||||
"" + layoutToUse?.overpassMaxZoom,
|
||||
" point to switch between OSM-api and overpass"))
|
||||
|
||||
this.osmApiTileSize =
|
||||
UIEventSource.asFloat(QueryParameters.GetQueryParameter("osmApiTileSize",
|
||||
"" + layoutToUse?.osmApiTileSize,
|
||||
"Tilesize when the OSM-API is used to fetch data within a BBOX"))
|
||||
|
||||
this.featureSwitchUserbadge.addCallbackAndRun(userbadge => {
|
||||
if (!userbadge) {
|
||||
this.featureSwitchAddNew.setData(false)
|
||||
}
|
||||
})
|
||||
|
||||
this.backgroundLayerId = QueryParameters.GetQueryParameter(
|
||||
"background",
|
||||
layoutToUse?.defaultBackgroundId ?? "osm",
|
||||
"The id of the background layer to start with"
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
272
Logic/State/MapState.ts
Normal file
272
Logic/State/MapState.ts
Normal file
|
@ -0,0 +1,272 @@
|
|||
import UserRelatedState from "./UserRelatedState";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import AvailableBaseLayers from "../Actors/AvailableBaseLayers";
|
||||
import BackgroundLayerResetter from "../Actors/BackgroundLayerResetter";
|
||||
import Attribution from "../../UI/BigComponents/Attribution";
|
||||
import Minimap, {MinimapObj} from "../../UI/Base/Minimap";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
import * as L from "leaflet";
|
||||
import Img from "../../UI/Base/Img";
|
||||
import Svg from "../../Svg";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
import * as personal from "../../assets/themes/personal/personal.json";
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
|
||||
|
||||
/**
|
||||
* Contains all the leaflet-map related state
|
||||
*/
|
||||
export default class MapState extends UserRelatedState {
|
||||
|
||||
/**
|
||||
The leaflet instance of the big basemap
|
||||
*/
|
||||
public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap");
|
||||
/**
|
||||
* A list of currently available background layers
|
||||
*/
|
||||
public availableBackgroundLayers: UIEventSource<BaseLayer[]>;
|
||||
|
||||
/**
|
||||
* The current background layer
|
||||
*/
|
||||
public backgroundLayer: UIEventSource<BaseLayer>;
|
||||
/**
|
||||
* Last location where a click was registered
|
||||
*/
|
||||
public readonly LastClickLocation: UIEventSource<{
|
||||
lat: number;
|
||||
lon: number;
|
||||
}> = new UIEventSource<{ lat: number; lon: number }>(undefined);
|
||||
|
||||
/**
|
||||
* The location as delivered by the GPS
|
||||
*/
|
||||
public currentGPSLocation: UIEventSource<{
|
||||
latlng: { lat: number; lng: number };
|
||||
accuracy: number;
|
||||
}> = new UIEventSource<{
|
||||
latlng: { lat: number; lng: number };
|
||||
accuracy: number;
|
||||
}>(undefined);
|
||||
|
||||
public readonly mainMapObject: BaseUIElement & MinimapObj;
|
||||
|
||||
|
||||
/**
|
||||
* WHich layers are enabled in the current theme
|
||||
*/
|
||||
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers");
|
||||
/**
|
||||
* Which overlays are shown
|
||||
*/
|
||||
public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]
|
||||
|
||||
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
super(layoutToUse);
|
||||
|
||||
this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl);
|
||||
|
||||
this.backgroundLayer = this.backgroundLayerId.map(
|
||||
(selectedId: string) => {
|
||||
if (selectedId === undefined) {
|
||||
return AvailableBaseLayers.osmCarto;
|
||||
}
|
||||
|
||||
const available = this.availableBackgroundLayers.data;
|
||||
for (const layer of available) {
|
||||
if (layer.id === selectedId) {
|
||||
return layer;
|
||||
}
|
||||
}
|
||||
return AvailableBaseLayers.osmCarto;
|
||||
},
|
||||
[this.availableBackgroundLayers],
|
||||
(layer) => layer.id
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Selects a different background layer if the background layer has no coverage at the current location
|
||||
*/
|
||||
new BackgroundLayerResetter(
|
||||
this.backgroundLayer,
|
||||
this.locationControl,
|
||||
this.availableBackgroundLayers,
|
||||
this.layoutToUse.defaultBackgroundId
|
||||
);
|
||||
|
||||
const attr = new Attribution(
|
||||
this.locationControl,
|
||||
this.osmConnection.userDetails,
|
||||
this.layoutToUse,
|
||||
this.currentBounds
|
||||
);
|
||||
|
||||
// Will write into this.leafletMap
|
||||
this.mainMapObject = Minimap.createMiniMap({
|
||||
background: this.backgroundLayer,
|
||||
location: this.locationControl,
|
||||
leafletMap: this.leafletMap,
|
||||
bounds: this.currentBounds,
|
||||
attribution: attr,
|
||||
lastClickLocation: this.LastClickLocation
|
||||
})
|
||||
|
||||
|
||||
this.overlayToggles = this.layoutToUse.tileLayerSources.filter(c => c.name !== undefined).map(c => ({
|
||||
config: c,
|
||||
isDisplayed: QueryParameters.GetQueryParameter("overlay-" + c.id, "" + c.defaultState, "Wether or not the overlay " + c.id + " is shown").map(str => str === "true", [], b => "" + b)
|
||||
}))
|
||||
this.filteredLayers = this.InitializeFilteredLayers()
|
||||
|
||||
|
||||
this.lockBounds()
|
||||
this.AddAllOverlaysToMap(this.leafletMap)
|
||||
this.addHomeMarker()
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private addHomeMarker() {
|
||||
const leafletMap = this.leafletMap
|
||||
const osmConnection = this.osmConnection
|
||||
|
||||
function addHomeMarker() {
|
||||
const userDetails = osmConnection.userDetails.data;
|
||||
if (userDetails === undefined) {
|
||||
return false;
|
||||
}
|
||||
const home = userDetails.home;
|
||||
if (home === undefined) {
|
||||
return userDetails.loggedIn; // If logged in, the home is not set and we unregister. If not logged in, we stay registered if a login still comes
|
||||
}
|
||||
const leaflet = leafletMap.data;
|
||||
if (leaflet === undefined) {
|
||||
return false;
|
||||
}
|
||||
const color = getComputedStyle(document.body).getPropertyValue(
|
||||
"--subtle-detail-color"
|
||||
);
|
||||
const icon = L.icon({
|
||||
iconUrl: Img.AsData(
|
||||
Svg.home_white_bg.replace(/#ffffff/g, color)
|
||||
),
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 15],
|
||||
});
|
||||
const marker = L.marker([home.lat, home.lon], {icon: icon});
|
||||
marker.addTo(leaflet);
|
||||
return true;
|
||||
}
|
||||
|
||||
osmConnection.userDetails.addCallbackAndRunD(_ => addHomeMarker());
|
||||
leafletMap.addCallbackAndRunD(_ => addHomeMarker())
|
||||
}
|
||||
|
||||
private lockBounds() {
|
||||
const layout = this.layoutToUse;
|
||||
if (layout.lockLocation) {
|
||||
if (layout.lockLocation === true) {
|
||||
const tile = Tiles.embedded_tile(
|
||||
layout.startLat,
|
||||
layout.startLon,
|
||||
layout.startZoom - 1
|
||||
);
|
||||
const bounds = Tiles.tile_bounds(tile.z, tile.x, tile.y);
|
||||
// We use the bounds to get a sense of distance for this zoom level
|
||||
const latDiff = bounds[0][0] - bounds[1][0];
|
||||
const lonDiff = bounds[0][1] - bounds[1][1];
|
||||
layout.lockLocation = [
|
||||
[layout.startLat - latDiff, layout.startLon - lonDiff],
|
||||
[layout.startLat + latDiff, layout.startLon + lonDiff],
|
||||
];
|
||||
}
|
||||
console.warn("Locking the bounds to ", layout.lockLocation);
|
||||
this.leafletMap.addCallbackAndRunD(map => {
|
||||
// @ts-ignore
|
||||
map.setMaxBounds(layout.lockLocation);
|
||||
map.setMinZoom(layout.startZoom);
|
||||
})
|
||||
}
|
||||
}
|
||||
private InitializeFilteredLayers() {
|
||||
// Initialize the filtered layers state
|
||||
|
||||
const layoutToUse = this.layoutToUse;
|
||||
const empty = []
|
||||
const flayers: FilteredLayer[] = [];
|
||||
for (const layer of layoutToUse.layers) {
|
||||
let isDisplayed: UIEventSource<boolean>
|
||||
if (layoutToUse.id === personal.id) {
|
||||
isDisplayed = this.osmConnection.GetPreference("personal-theme-layer-" + layer.id + "-enabled")
|
||||
.map(value => value === "yes", [], enabled => {
|
||||
return enabled ? "yes" : "";
|
||||
})
|
||||
isDisplayed.addCallbackAndRun(d => console.log("IsDisplayed for layer", layer.id, "is currently", d))
|
||||
} else {
|
||||
isDisplayed = QueryParameters.GetQueryParameter(
|
||||
"layer-" + layer.id,
|
||||
"true",
|
||||
"Wether or not layer " + layer.id + " is shown"
|
||||
).map<boolean>(
|
||||
(str) => str !== "false",
|
||||
[],
|
||||
(b) => b.toString()
|
||||
);
|
||||
}
|
||||
const flayer = {
|
||||
isDisplayed: isDisplayed,
|
||||
layerDef: layer,
|
||||
appliedFilters: new UIEventSource<{ filter: FilterConfig, selected: number }[]>([]),
|
||||
};
|
||||
|
||||
if (layer.filters.length > 0) {
|
||||
const filtersPerName = new Map<string, FilterConfig>()
|
||||
layer.filters.forEach(f => filtersPerName.set(f.id, f))
|
||||
const qp = QueryParameters.GetQueryParameter("filter-" + layer.id, "", "Filtering state for a layer")
|
||||
flayer.appliedFilters.map(filters => {
|
||||
filters = filters ?? []
|
||||
return filters.map(f => f.filter.id + "." + f.selected).join(",")
|
||||
}, [], textual => {
|
||||
if (textual.length === 0) {
|
||||
return empty
|
||||
}
|
||||
return textual.split(",").map(part => {
|
||||
const [filterId, selected] = part.split(".");
|
||||
return {filter: filtersPerName.get(filterId), selected: Number(selected)}
|
||||
}).filter(f => f.filter !== undefined && !isNaN(f.selected))
|
||||
}).syncWith(qp, true)
|
||||
}
|
||||
|
||||
flayers.push(flayer);
|
||||
}
|
||||
return new UIEventSource<FilteredLayer[]>(flayers);
|
||||
}
|
||||
|
||||
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>){
|
||||
const initialized =new Set()
|
||||
for (const overlayToggle of this.overlayToggles) {
|
||||
new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed)
|
||||
initialized.add(overlayToggle.config)
|
||||
}
|
||||
|
||||
for (const tileLayerSource of this.layoutToUse.tileLayerSources) {
|
||||
if (initialized.has(tileLayerSource)) {
|
||||
continue
|
||||
}
|
||||
new ShowOverlayLayer(tileLayerSource, leafletMap)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
107
Logic/State/UserRelatedState.ts
Normal file
107
Logic/State/UserRelatedState.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {OsmConnection} from "../Osm/OsmConnection";
|
||||
import {MangroveIdentity} from "../Web/MangroveReviews";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {QueryParameters} from "../Web/QueryParameters";
|
||||
import InstalledThemes from "../Actors/InstalledThemes";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import {Utils} from "../../Utils";
|
||||
import Locale from "../../UI/i18n/Locale";
|
||||
import ElementsState from "./ElementsState";
|
||||
import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater";
|
||||
|
||||
/**
|
||||
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
|
||||
* which layers they enabled, ...
|
||||
*/
|
||||
export default class UserRelatedState extends ElementsState {
|
||||
|
||||
|
||||
/**
|
||||
The user credentials
|
||||
*/
|
||||
public osmConnection: OsmConnection;
|
||||
/**
|
||||
* The key for mangrove
|
||||
*/
|
||||
public mangroveIdentity: MangroveIdentity;
|
||||
/**
|
||||
* Which layers are enabled in the personal theme
|
||||
*/
|
||||
public favouriteLayers: UIEventSource<string[]>;
|
||||
|
||||
/**
|
||||
* WHich other themes the user previously visited
|
||||
*/
|
||||
public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>;
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
super(layoutToUse);
|
||||
|
||||
this.osmConnection = new OsmConnection({
|
||||
changes: this.changes,
|
||||
dryRun: this.featureSwitchIsTesting.data,
|
||||
fakeUser: this.featureSwitchFakeUser.data,
|
||||
allElements: this.allElements,
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
"oauth_token",
|
||||
undefined,
|
||||
"Used to complete the login"
|
||||
),
|
||||
layoutName: layoutToUse?.id,
|
||||
osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data
|
||||
})
|
||||
|
||||
this.mangroveIdentity = new MangroveIdentity(
|
||||
this.osmConnection.GetLongPreference("identity", "mangrove")
|
||||
);
|
||||
|
||||
if (layoutToUse?.hideFromOverview) {
|
||||
this.osmConnection
|
||||
.GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled")
|
||||
.setData("true");
|
||||
}
|
||||
|
||||
this.installedThemes = new InstalledThemes(
|
||||
this.osmConnection
|
||||
).installedThemes;
|
||||
|
||||
// Important: the favourite layers are initialized _after_ the installed themes, as these might contain an installedTheme
|
||||
this.favouriteLayers = LocalStorageSource.Get("favouriteLayers")
|
||||
.syncWith(this.osmConnection.GetLongPreference("favouriteLayers"))
|
||||
.map(
|
||||
(str) => Utils.Dedup(str?.split(";")) ?? [],
|
||||
[],
|
||||
(layers) => Utils.Dedup(layers)?.join(";")
|
||||
);
|
||||
|
||||
|
||||
this.InitializeLanguage();
|
||||
new SelectedElementTagsUpdater(this)
|
||||
|
||||
}
|
||||
|
||||
private InitializeLanguage() {
|
||||
const layoutToUse = this.layoutToUse;
|
||||
Locale.language.syncWith(this.osmConnection.GetPreference("language"));
|
||||
Locale.language
|
||||
.addCallback((currentLanguage) => {
|
||||
if (layoutToUse === undefined) {
|
||||
return;
|
||||
}
|
||||
if (this.layoutToUse.language.indexOf(currentLanguage) < 0) {
|
||||
console.log(
|
||||
"Resetting language to",
|
||||
layoutToUse.language[0],
|
||||
"as",
|
||||
currentLanguage,
|
||||
" is unsupported"
|
||||
);
|
||||
// The current language is not supported -> switch to a supported one
|
||||
Locale.language.setData(layoutToUse.language[0]);
|
||||
}
|
||||
})
|
||||
.ping();
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue