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
|
@ -1,567 +0,0 @@
|
|||
import {FixedUiElement} from "./UI/Base/FixedUiElement";
|
||||
import Toggle from "./UI/Input/Toggle";
|
||||
import State from "./State";
|
||||
import {UIEventSource} from "./Logic/UIEventSource";
|
||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
||||
import StrayClickHandler from "./Logic/Actors/StrayClickHandler";
|
||||
import SimpleAddUI from "./UI/BigComponents/SimpleAddUI";
|
||||
import CenterMessageBox from "./UI/CenterMessageBox";
|
||||
import UserBadge from "./UI/BigComponents/UserBadge";
|
||||
import SearchAndGo from "./UI/BigComponents/SearchAndGo";
|
||||
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
|
||||
import {Utils} from "./Utils";
|
||||
import Svg from "./Svg";
|
||||
import Link from "./UI/Base/Link";
|
||||
import * as personal from "./assets/themes/personal/personal.json";
|
||||
import * as L from "leaflet";
|
||||
import Img from "./UI/Base/Img";
|
||||
import Attribution from "./UI/BigComponents/Attribution";
|
||||
import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter";
|
||||
import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
|
||||
import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer";
|
||||
import Hash from "./Logic/Web/Hash";
|
||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||
import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen";
|
||||
import Translations from "./UI/i18n/Translations";
|
||||
import MapControlButton from "./UI/MapControlButton";
|
||||
import LZString from "lz-string";
|
||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||
import LeftControls from "./UI/BigComponents/LeftControls";
|
||||
import RightControls from "./UI/BigComponents/RightControls";
|
||||
import {LayoutConfigJson} from "./Models/ThemeConfig/Json/LayoutConfigJson";
|
||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
|
||||
import Minimap from "./UI/Base/Minimap";
|
||||
import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler";
|
||||
import Combine from "./UI/Base/Combine";
|
||||
import {SubtleButton} from "./UI/Base/SubtleButton";
|
||||
import ShowTileInfo from "./UI/ShowDataLayer/ShowTileInfo";
|
||||
import {Tiles} from "./Models/TileRange";
|
||||
import {TileHierarchyAggregator} from "./UI/ShowDataLayer/TileHierarchyAggregator";
|
||||
import FilterConfig from "./Models/ThemeConfig/FilterConfig";
|
||||
import FilteredLayer from "./Models/FilteredLayer";
|
||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||
import ShowOverlayLayer from "./UI/ShowDataLayer/ShowOverlayLayer";
|
||||
|
||||
export class InitUiElements {
|
||||
static InitAll(
|
||||
layoutToUse: LayoutConfig,
|
||||
layoutFromBase64: string,
|
||||
testing: UIEventSource<string>,
|
||||
layoutName: string,
|
||||
layoutDefinition: string = ""
|
||||
) {
|
||||
if (layoutToUse === undefined) {
|
||||
console.log("Incorrect layout");
|
||||
new FixedUiElement(
|
||||
`Error: incorrect layout <i>${layoutName}</i><br/><a href='https://${window.location.host}/'>Go back</a>`
|
||||
)
|
||||
.AttachTo("centermessage")
|
||||
.onClick(() => {
|
||||
});
|
||||
throw "Incorrect layout";
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Using layout: ",
|
||||
layoutToUse.id,
|
||||
"LayoutFromBase64 is ",
|
||||
layoutFromBase64
|
||||
);
|
||||
|
||||
if(layoutToUse.id === personal.id){
|
||||
layoutToUse.layers = AllKnownLayouts.AllPublicLayers()
|
||||
for (const layer of layoutToUse.layers) {
|
||||
layer.minzoomVisible = Math.max(layer.minzoomVisible, layer.minzoom)
|
||||
layer.minzoom = Math.max(16, layer.minzoom)
|
||||
}
|
||||
}
|
||||
|
||||
State.state = new State(layoutToUse);
|
||||
|
||||
if(layoutToUse.id === personal.id) {
|
||||
// Disable overpass all together
|
||||
State.state.overpassMaxZoom.setData(0)
|
||||
|
||||
}
|
||||
|
||||
// This 'leaks' the global state via the window object, useful for debugging
|
||||
// @ts-ignore
|
||||
window.mapcomplete_state = State.state;
|
||||
|
||||
if (layoutToUse.hideFromOverview) {
|
||||
State.state.osmConnection
|
||||
.GetPreference("hidden-theme-" + layoutToUse.id + "-enabled")
|
||||
.setData("true");
|
||||
}
|
||||
|
||||
if (layoutFromBase64 !== "false") {
|
||||
State.state.layoutDefinition = layoutDefinition;
|
||||
console.log(
|
||||
"Layout definition:",
|
||||
Utils.EllipsesAfter(State.state.layoutDefinition, 100)
|
||||
);
|
||||
if (testing.data !== "true") {
|
||||
State.state.osmConnection.OnLoggedIn(() => {
|
||||
State.state.osmConnection
|
||||
.GetLongPreference("installed-theme-" + layoutToUse.id)
|
||||
.setData(State.state.layoutDefinition);
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
"NOT saving custom layout to OSM as we are tesing -> probably in an iFrame"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutToUse.customCss !== undefined) {
|
||||
Utils.LoadCustomCss(layoutToUse.customCss);
|
||||
}
|
||||
|
||||
InitUiElements.InitBaseMap();
|
||||
|
||||
InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => {
|
||||
new UserBadge().AttachTo("userbadge");
|
||||
});
|
||||
|
||||
InitUiElements.OnlyIf(State.state.featureSwitchSearch, () => {
|
||||
new SearchAndGo().AttachTo("searchbox");
|
||||
});
|
||||
|
||||
InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => {
|
||||
InitUiElements.InitWelcomeMessage();
|
||||
});
|
||||
|
||||
if (State.state.featureSwitchIframe.data) {
|
||||
const currentLocation = State.state.locationControl;
|
||||
const url = `${window.location.origin}${
|
||||
window.location.pathname
|
||||
}?z=${currentLocation.data.zoom ?? 0}&lat=${
|
||||
currentLocation.data.lat ?? 0
|
||||
}&lon=${currentLocation.data.lon ?? 0}`;
|
||||
new MapControlButton(
|
||||
new Link(Svg.pop_out_img, url, true).SetClass(
|
||||
"block w-full h-full p-1.5"
|
||||
)
|
||||
).AttachTo("messagesbox");
|
||||
}
|
||||
|
||||
function addHomeMarker() {
|
||||
const userDetails = State.state.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 = State.state.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;
|
||||
}
|
||||
|
||||
State.state.osmConnection.userDetails
|
||||
.addCallbackAndRunD(_ => addHomeMarker());
|
||||
State.state.leafletMap.addCallbackAndRunD(_ => addHomeMarker())
|
||||
|
||||
InitUiElements.setupAllLayerElements();
|
||||
State.state.locationControl.ping();
|
||||
|
||||
new SelectedFeatureHandler(Hash.hash, State.state)
|
||||
|
||||
// Reset the loading message once things are loaded
|
||||
new CenterMessageBox().AttachTo("centermessage");
|
||||
document
|
||||
.getElementById("centermessage")
|
||||
.classList.add("pointer-events-none");
|
||||
}
|
||||
|
||||
static LoadLayoutFromHash(
|
||||
userLayoutParam: UIEventSource<string>
|
||||
): [LayoutConfig, string] {
|
||||
let hash = location.hash.substr(1);
|
||||
try {
|
||||
const layoutFromBase64 = userLayoutParam.data;
|
||||
// layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
|
||||
|
||||
const dedicatedHashFromLocalStorage = LocalStorageSource.Get(
|
||||
"user-layout-" + layoutFromBase64.replace(" ", "_")
|
||||
);
|
||||
if (dedicatedHashFromLocalStorage.data?.length < 10) {
|
||||
dedicatedHashFromLocalStorage.setData(undefined);
|
||||
}
|
||||
|
||||
const hashFromLocalStorage = LocalStorageSource.Get(
|
||||
"last-loaded-user-layout"
|
||||
);
|
||||
if (hash.length < 10) {
|
||||
hash =
|
||||
dedicatedHashFromLocalStorage.data ??
|
||||
hashFromLocalStorage.data;
|
||||
} else {
|
||||
console.log("Saving hash to local storage");
|
||||
hashFromLocalStorage.setData(hash);
|
||||
dedicatedHashFromLocalStorage.setData(hash);
|
||||
}
|
||||
|
||||
let json: {};
|
||||
try {
|
||||
json = JSON.parse(atob(hash));
|
||||
} catch (e) {
|
||||
// We try to decode with lz-string
|
||||
json = JSON.parse(
|
||||
Utils.UnMinify(LZString.decompressFromBase64(hash))
|
||||
) as LayoutConfigJson;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const layoutToUse = new LayoutConfig(json, false);
|
||||
userLayoutParam.setData(layoutToUse.id);
|
||||
return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];
|
||||
} catch (e) {
|
||||
|
||||
if (hash === undefined || hash.length < 10) {
|
||||
e = "Did you effectively add a theme? It seems no data could be found."
|
||||
}
|
||||
|
||||
new Combine([
|
||||
"Error: could not parse the custom layout:",
|
||||
new FixedUiElement("" + e).SetClass("alert"),
|
||||
new SubtleButton("./assets/svg/mapcomplete_logo.svg",
|
||||
"Go back to the theme overview",
|
||||
{url: window.location.protocol + "//" + window.location.hostname + "/index.html", newTab: false})
|
||||
|
||||
])
|
||||
.SetClass("flex flex-col")
|
||||
.AttachTo("centermessage");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static OnlyIf(
|
||||
featureSwitch: UIEventSource<boolean>,
|
||||
callback: () => void
|
||||
) {
|
||||
featureSwitch.addCallbackAndRun(() => {
|
||||
if (featureSwitch.data) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static InitWelcomeMessage() {
|
||||
const isOpened = new UIEventSource<boolean>(false);
|
||||
const fullOptions = new FullWelcomePaneWithTabs(isOpened);
|
||||
|
||||
// ?-Button on Desktop, opens panel with close-X.
|
||||
const help = new MapControlButton(Svg.help_svg());
|
||||
help.onClick(() => isOpened.setData(true));
|
||||
new Toggle(
|
||||
fullOptions.SetClass("welcomeMessage pointer-events-auto"),
|
||||
help.SetClass("pointer-events-auto"),
|
||||
isOpened
|
||||
)
|
||||
.AttachTo("messagesbox");
|
||||
const openedTime = new Date().getTime();
|
||||
State.state.locationControl.addCallback(() => {
|
||||
if (new Date().getTime() - openedTime < 15 * 1000) {
|
||||
// Don't autoclose the first 15 secs when the map is moving
|
||||
return;
|
||||
}
|
||||
isOpened.setData(false);
|
||||
});
|
||||
|
||||
State.state.selectedElement.addCallbackAndRunD((_) => {
|
||||
isOpened.setData(false);
|
||||
});
|
||||
isOpened.setData(
|
||||
Hash.hash.data === undefined ||
|
||||
Hash.hash.data === "" ||
|
||||
Hash.hash.data == "welcome"
|
||||
);
|
||||
}
|
||||
|
||||
private static InitBaseMap() {
|
||||
State.state.availableBackgroundLayers =
|
||||
AvailableBaseLayers.AvailableLayersAt(State.state.locationControl);
|
||||
State.state.backgroundLayer = State.state.backgroundLayerId.map(
|
||||
(selectedId: string) => {
|
||||
if (selectedId === undefined) {
|
||||
return AvailableBaseLayers.osmCarto;
|
||||
}
|
||||
|
||||
const available = State.state.availableBackgroundLayers.data;
|
||||
for (const layer of available) {
|
||||
if (layer.id === selectedId) {
|
||||
return layer;
|
||||
}
|
||||
}
|
||||
return AvailableBaseLayers.osmCarto;
|
||||
},
|
||||
[State.state.availableBackgroundLayers],
|
||||
(layer) => layer.id
|
||||
);
|
||||
|
||||
new BackgroundLayerResetter(
|
||||
State.state.backgroundLayer,
|
||||
State.state.locationControl,
|
||||
State.state.availableBackgroundLayers,
|
||||
State.state.layoutToUse.defaultBackgroundId
|
||||
);
|
||||
|
||||
const attr = new Attribution(
|
||||
State.state.locationControl,
|
||||
State.state.osmConnection.userDetails,
|
||||
State.state.layoutToUse,
|
||||
State.state.currentBounds
|
||||
);
|
||||
|
||||
Minimap.createMiniMap({
|
||||
background: State.state.backgroundLayer,
|
||||
location: State.state.locationControl,
|
||||
leafletMap: State.state.leafletMap,
|
||||
bounds: State.state.currentBounds,
|
||||
attribution: attr,
|
||||
lastClickLocation: State.state.LastClickLocation
|
||||
}).SetClass("w-full h-full")
|
||||
.AttachTo("leafletDiv")
|
||||
|
||||
const layout = State.state.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);
|
||||
State.state.leafletMap.addCallbackAndRunD(map => {
|
||||
// @ts-ignore
|
||||
map.setMaxBounds(layout.lockLocation);
|
||||
map.setMinZoom(layout.startZoom);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private static InitLayers(): void {
|
||||
const state = State.state;
|
||||
const empty = []
|
||||
|
||||
const flayers: FilteredLayer[] = [];
|
||||
|
||||
for (const layer of state.layoutToUse.layers) {
|
||||
let defaultShown = "true"
|
||||
if(state.layoutToUse.id === personal.id){
|
||||
defaultShown = "false"
|
||||
}
|
||||
|
||||
let isDisplayed: UIEventSource<boolean>
|
||||
if(state.layoutToUse.id === personal.id){
|
||||
isDisplayed = State.state.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,
|
||||
defaultShown,
|
||||
"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);
|
||||
}
|
||||
state.filteredLayers = new UIEventSource<FilteredLayer[]>(flayers);
|
||||
|
||||
|
||||
|
||||
const clustering = State.state.layoutToUse.clustering
|
||||
const clusterCounter = TileHierarchyAggregator.createHierarchy()
|
||||
new ShowDataLayer({
|
||||
features: clusterCounter.getCountsForZoom(clustering, State.state.locationControl, State.state.layoutToUse.clustering.minNeededElements),
|
||||
leafletMap: State.state.leafletMap,
|
||||
layerToShow: ShowTileInfo.styling,
|
||||
enablePopups: false
|
||||
})
|
||||
|
||||
State.state.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: State.state.leafletMap,
|
||||
layerToShow: source.layer.layerDef,
|
||||
doShowLayer: doShowFeatures
|
||||
}
|
||||
);
|
||||
}, state
|
||||
);
|
||||
|
||||
|
||||
const initialized =new Set()
|
||||
for (const overlayToggle of State.state.overlayToggles) {
|
||||
new ShowOverlayLayer(overlayToggle.config, state.leafletMap, overlayToggle.isDisplayed)
|
||||
initialized.add(overlayToggle.config)
|
||||
}
|
||||
|
||||
for (const tileLayerSource of state.layoutToUse.tileLayerSources) {
|
||||
if (initialized.has(tileLayerSource)) {
|
||||
continue
|
||||
}
|
||||
new ShowOverlayLayer(tileLayerSource, state.leafletMap)
|
||||
}
|
||||
}
|
||||
|
||||
private static setupAllLayerElements() {
|
||||
// ------------- Setup the layers -------------------------------
|
||||
|
||||
InitUiElements.InitLayers();
|
||||
|
||||
new LeftControls(State.state).AttachTo("bottom-left");
|
||||
new RightControls().AttachTo("bottom-right");
|
||||
|
||||
// ------------------ Setup various other UI elements ------------
|
||||
|
||||
InitUiElements.OnlyIf(State.state.featureSwitchAddNew, () => {
|
||||
let presetCount = 0;
|
||||
for (const layer of State.state.filteredLayers.data) {
|
||||
for (const preset of layer.layerDef.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),
|
||||
"new",
|
||||
newPointDialogIsShown
|
||||
);
|
||||
addNewPoint.isShown.addCallback((isShown) => {
|
||||
if (!isShown) {
|
||||
State.state.LastClickLocation.setData(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
new StrayClickHandler(
|
||||
State.state.LastClickLocation,
|
||||
State.state.selectedElement,
|
||||
State.state.filteredLayers,
|
||||
State.state.leafletMap,
|
||||
addNewPoint
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
168
Logic/DetermineLayout.ts
Normal file
168
Logic/DetermineLayout.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
import {QueryParameters} from "./Web/QueryParameters";
|
||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
|
||||
import {FixedUiElement} from "../UI/Base/FixedUiElement";
|
||||
import {Utils} from "../Utils";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import {SubtleButton} from "../UI/Base/SubtleButton";
|
||||
import BaseUIElement from "../UI/BaseUIElement";
|
||||
import {UIEventSource} from "./UIEventSource";
|
||||
import {LocalStorageSource} from "./Web/LocalStorageSource";
|
||||
import LZString from "lz-string";
|
||||
import * as personal from "../assets/themes/personal/personal.json";
|
||||
|
||||
export default class DetermineLayout {
|
||||
|
||||
/**
|
||||
* Gets the correct layout for this website
|
||||
*/
|
||||
public static async GetLayout(): Promise<[LayoutConfig, string]> {
|
||||
|
||||
|
||||
const loadCustomThemeParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme")
|
||||
const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data);
|
||||
|
||||
if (layoutFromBase64.startsWith("http")) {
|
||||
// The userLayout is actually an url
|
||||
const layout = await DetermineLayout.LoadRemoteTheme(layoutFromBase64)
|
||||
return [layout, undefined]
|
||||
}
|
||||
|
||||
if (layoutFromBase64 !== "false") {
|
||||
// We have to load something from the hash (or from disk)
|
||||
let loaded = DetermineLayout.LoadLayoutFromHash(loadCustomThemeParam);
|
||||
if (loaded === null) {
|
||||
return [null, undefined]
|
||||
}
|
||||
return loaded
|
||||
}
|
||||
|
||||
let layoutId: string = undefined
|
||||
if (location.href.indexOf("buurtnatuur.be") >= 0) {
|
||||
layoutId = "buurtnatuur"
|
||||
}
|
||||
|
||||
|
||||
const path = window.location.pathname.split("/").slice(-1)[0];
|
||||
if (path !== "index.html" && path !== "") {
|
||||
layoutId = path;
|
||||
if (path.endsWith(".html")) {
|
||||
layoutId = path.substr(0, path.length - 5);
|
||||
}
|
||||
console.log("Using layout", layoutId);
|
||||
}
|
||||
layoutId = QueryParameters.GetQueryParameter("layout", layoutId, "The layout to load into MapComplete").data;
|
||||
const layoutToUse: LayoutConfig = AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase());
|
||||
|
||||
if (layoutToUse?.id === personal.id) {
|
||||
layoutToUse.layers = AllKnownLayouts.AllPublicLayers()
|
||||
for (const layer of layoutToUse.layers) {
|
||||
layer.minzoomVisible = Math.max(layer.minzoomVisible, layer.minzoom)
|
||||
layer.minzoom = Math.max(16, layer.minzoom)
|
||||
}
|
||||
}
|
||||
|
||||
return [layoutToUse, undefined]
|
||||
}
|
||||
|
||||
private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
|
||||
console.log("Downloading map theme from ", link);
|
||||
|
||||
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`)
|
||||
.AttachTo("centermessage");
|
||||
|
||||
try {
|
||||
|
||||
const data = await Utils.downloadJson(link)
|
||||
try {
|
||||
let parsed = data;
|
||||
if (typeof parsed == "string") {
|
||||
parsed = JSON.parse(parsed);
|
||||
}
|
||||
// Overwrite the id to the url
|
||||
parsed.id = link;
|
||||
return new LayoutConfig(parsed, false).patchImages(link, data);
|
||||
} catch (e) {
|
||||
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
DetermineLayout.ShowErrorOnCustomTheme(
|
||||
`<a href="${link}">${link}</a> is invalid - probably not found or invalid JSON:`,
|
||||
new FixedUiElement(e)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static LoadLayoutFromHash(
|
||||
userLayoutParam: UIEventSource<string>
|
||||
): [LayoutConfig, string] | null {
|
||||
let hash = location.hash.substr(1);
|
||||
try {
|
||||
// layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
|
||||
const dedicatedHashFromLocalStorage = LocalStorageSource.Get(
|
||||
"user-layout-" + userLayoutParam.data.replace(" ", "_")
|
||||
);
|
||||
if (dedicatedHashFromLocalStorage.data?.length < 10) {
|
||||
dedicatedHashFromLocalStorage.setData(undefined);
|
||||
}
|
||||
|
||||
const hashFromLocalStorage = LocalStorageSource.Get(
|
||||
"last-loaded-user-layout"
|
||||
);
|
||||
if (hash.length < 10) {
|
||||
hash =
|
||||
dedicatedHashFromLocalStorage.data ??
|
||||
hashFromLocalStorage.data;
|
||||
} else {
|
||||
console.log("Saving hash to local storage");
|
||||
hashFromLocalStorage.setData(hash);
|
||||
dedicatedHashFromLocalStorage.setData(hash);
|
||||
}
|
||||
|
||||
let json: any;
|
||||
try {
|
||||
json = JSON.parse(atob(hash));
|
||||
} catch (e) {
|
||||
// We try to decode with lz-string
|
||||
try {
|
||||
json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash)))
|
||||
} catch (e) {
|
||||
DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON"))
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const layoutToUse = new LayoutConfig(json, false);
|
||||
userLayoutParam.setData(layoutToUse.id);
|
||||
return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))];
|
||||
} catch (e) {
|
||||
if (hash === undefined || hash.length < 10) {
|
||||
DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data"))
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ShowErrorOnCustomTheme(
|
||||
intro: string = "Error: could not parse the custom layout:",
|
||||
error: BaseUIElement) {
|
||||
new Combine([
|
||||
intro,
|
||||
error.SetClass("alert"),
|
||||
new SubtleButton("./assets/svg/mapcomplete_logo.svg",
|
||||
"Go back to the theme overview",
|
||||
{url: window.location.protocol + "//" + window.location.hostname + "/index.html", newTab: false})
|
||||
|
||||
])
|
||||
.SetClass("flex flex-col clickable")
|
||||
.AttachTo("centermessage");
|
||||
}
|
||||
|
||||
}
|
|
@ -73,7 +73,7 @@ export default class FeaturePipeline {
|
|||
readonly overpassTimeout: UIEventSource<number>;
|
||||
readonly overpassMaxZoom: UIEventSource<number>;
|
||||
readonly osmConnection: OsmConnection
|
||||
readonly currentBounds: UIEventSource<BBox>
|
||||
readonly currentBounds: UIEventSource<BBox>,
|
||||
}) {
|
||||
this.state = state;
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
|
||||
import State from "../../../State";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import Loc from "../../../Models/Loc";
|
||||
import TileHierarchy from "./TileHierarchy";
|
||||
|
@ -25,7 +22,6 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
|
|||
leafletMap: any
|
||||
}
|
||||
) {
|
||||
state = State.state
|
||||
const self = this;
|
||||
|
||||
this.loadedTiles = new Map<number,FeatureSourceForLayer & Tiled>()
|
||||
|
|
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();
|
||||
}
|
||||
|
||||
}
|
|
@ -318,6 +318,22 @@ export class UIEventSource<T> {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
return source.map(
|
||||
(str) => {
|
||||
let parsed = parseFloat(str);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
},
|
||||
[],
|
||||
(fl) => {
|
||||
if (fl === undefined || isNaN(fl)) {
|
||||
return undefined;
|
||||
}
|
||||
return ("" + fl).substr(0, 8);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class UIEventSourceTools {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
import {LayerConfigJson} from "./LayerConfigJson";
|
||||
import TilesourceConfig from "../TilesourceConfig";
|
||||
import TilesourceConfigJson from "./TilesourceConfigJson";
|
||||
|
||||
/**
|
||||
|
@ -16,7 +15,7 @@ import TilesourceConfigJson from "./TilesourceConfigJson";
|
|||
* General remark: a type (string | any) indicates either a fixed or a translatable string.
|
||||
*/
|
||||
export interface LayoutConfigJson {
|
||||
|
||||
|
||||
/**
|
||||
* The id of this layout.
|
||||
*
|
||||
|
@ -106,7 +105,20 @@ export interface LayoutConfigJson {
|
|||
* IF widenfactor is 1, this feature is disabled. A recommended value is between 1 and 3
|
||||
*/
|
||||
widenFactor?: number;
|
||||
/**
|
||||
* At low zoom levels, overpass is used to query features.
|
||||
* At high zoom level, the OSM api is used to fetch one or more BBOX aligning with a slippy tile.
|
||||
* The overpassMaxZoom controls the flipoverpoint: if the zoom is this or lower, overpass is used.
|
||||
*/
|
||||
overpassMaxZoom?: 17 | number
|
||||
|
||||
/**
|
||||
* When the OSM-api is used to fetch features, it does so in a tiled fashion.
|
||||
* These tiles are using a ceratin zoom level, that can be controlled here
|
||||
* Default: overpassMaxZoom + 1
|
||||
*/
|
||||
osmApiTileSize: number
|
||||
|
||||
/**
|
||||
* A tagrendering depicts how to show some tags or how to show a question for it.
|
||||
*
|
||||
|
@ -269,6 +281,7 @@ export interface LayoutConfigJson {
|
|||
enableShowAllQuestions?: boolean;
|
||||
enableDownload?: boolean;
|
||||
enablePdfDownload?: boolean;
|
||||
enableIframePopout?: true | boolean;
|
||||
|
||||
/**
|
||||
* Set one or more overpass URLs to use for this theme..
|
||||
|
|
|
@ -46,6 +46,7 @@ export default class LayoutConfig {
|
|||
public readonly enableShowAllQuestions: boolean;
|
||||
public readonly enableExportButton: boolean;
|
||||
public readonly enablePdfDownload: boolean;
|
||||
public readonly enableIframePopout: boolean;
|
||||
|
||||
public readonly customCss?: string;
|
||||
/*
|
||||
|
@ -54,8 +55,10 @@ export default class LayoutConfig {
|
|||
public readonly cacheTimeout?: number;
|
||||
public readonly overpassUrl: string[];
|
||||
public readonly overpassTimeout: number;
|
||||
public readonly overpassMaxZoom: number
|
||||
public readonly osmApiTileSize: number
|
||||
public readonly official: boolean;
|
||||
|
||||
|
||||
constructor(json: LayoutConfigJson, official = true, context?: string) {
|
||||
this.official = official;
|
||||
this.id = json.id;
|
||||
|
@ -171,6 +174,7 @@ export default class LayoutConfig {
|
|||
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false;
|
||||
this.enableExportButton = json.enableDownload ?? false;
|
||||
this.enablePdfDownload = json.enablePdfDownload ?? false;
|
||||
this.enableIframePopout = json.enableIframePopout ?? true
|
||||
this.customCss = json.customCss;
|
||||
this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60)
|
||||
this.overpassUrl = Constants.defaultOverpassUrls
|
||||
|
@ -182,6 +186,8 @@ export default class LayoutConfig {
|
|||
}
|
||||
}
|
||||
this.overpassTimeout = json.overpassTimeout ?? 30
|
||||
this.overpassMaxZoom = json.overpassMaxZoom ?? 17
|
||||
this.osmApiTileSize = json.osmApiTileSize ?? this.overpassMaxZoom + 1
|
||||
|
||||
}
|
||||
|
||||
|
|
441
State.ts
441
State.ts
|
@ -1,448 +1,19 @@
|
|||
import {Utils} from "./Utils";
|
||||
import {ElementStorage} from "./Logic/ElementStorage";
|
||||
import {Changes} from "./Logic/Osm/Changes";
|
||||
import {OsmConnection} from "./Logic/Osm/OsmConnection";
|
||||
import Locale from "./UI/i18n/Locale";
|
||||
import {UIEventSource} from "./Logic/UIEventSource";
|
||||
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
|
||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
||||
import {MangroveIdentity} from "./Logic/Web/MangroveReviews";
|
||||
import InstalledThemes from "./Logic/Actors/InstalledThemes";
|
||||
import BaseLayer from "./Models/BaseLayer";
|
||||
import Loc from "./Models/Loc";
|
||||
import Constants from "./Models/Constants";
|
||||
import TitleHandler from "./Logic/Actors/TitleHandler";
|
||||
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
|
||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||
import FilteredLayer from "./Models/FilteredLayer";
|
||||
import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor";
|
||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
|
||||
import {BBox} from "./Logic/BBox";
|
||||
import SelectedElementTagsUpdater from "./Logic/Actors/SelectedElementTagsUpdater";
|
||||
import TilesourceConfig from "./Models/ThemeConfig/TilesourceConfig";
|
||||
import FeaturePipelineState from "./Logic/State/FeaturePipelineState";
|
||||
|
||||
/**
|
||||
* Contains the global state: a bunch of UI-event sources
|
||||
*/
|
||||
|
||||
export default class State {
|
||||
// The singleton of the global state
|
||||
public static state: State;
|
||||
|
||||
public readonly layoutToUse : LayoutConfig;
|
||||
|
||||
/**
|
||||
The mapping from id -> UIEventSource<properties>
|
||||
export default class State extends FeaturePipelineState {
|
||||
/* The singleton of the global state
|
||||
*/
|
||||
public allElements: ElementStorage = new ElementStorage();
|
||||
/**
|
||||
THe change handler
|
||||
*/
|
||||
public changes: Changes = new Changes();
|
||||
/**
|
||||
The leaflet instance of the big basemap
|
||||
*/
|
||||
public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap");
|
||||
/**
|
||||
* Background layer id
|
||||
*/
|
||||
public availableBackgroundLayers: UIEventSource<BaseLayer[]>;
|
||||
/**
|
||||
The user credentials
|
||||
*/
|
||||
public osmConnection: OsmConnection;
|
||||
|
||||
public mangroveIdentity: MangroveIdentity;
|
||||
|
||||
public favouriteLayers: UIEventSource<string[]>;
|
||||
|
||||
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers");
|
||||
|
||||
public overlayToggles : { config: TilesourceConfig, isDisplayed: UIEventSource<boolean>}[]
|
||||
|
||||
/**
|
||||
The latest element that was selected
|
||||
*/
|
||||
public readonly selectedElement = new UIEventSource<any>(
|
||||
undefined,
|
||||
"Selected element"
|
||||
);
|
||||
|
||||
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 featureSwitchIframe: 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> = new UIEventSource<number>(17, "overpass-max-zoom: point to switch between OSM-api and overpass");
|
||||
|
||||
public featurePipeline: FeaturePipeline;
|
||||
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
public backgroundLayer;
|
||||
public readonly backgroundLayerId: UIEventSource<string>;
|
||||
|
||||
/* 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 layoutDefinition: string;
|
||||
public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>;
|
||||
|
||||
public downloadControlIsOpened: UIEventSource<boolean> =
|
||||
QueryParameters.GetQueryParameter(
|
||||
"download-control-toggle",
|
||||
"false",
|
||||
"Whether or not the download panel is shown"
|
||||
).map<boolean>(
|
||||
(str) => str !== "false",
|
||||
[],
|
||||
(b) => "" + b
|
||||
);
|
||||
|
||||
public filterIsOpened: UIEventSource<boolean> =
|
||||
QueryParameters.GetQueryParameter(
|
||||
"filter-toggle",
|
||||
"false",
|
||||
"Whether or not the filter view is shown"
|
||||
).map<boolean>(
|
||||
(str) => str !== "false",
|
||||
[],
|
||||
(b) => "" + b
|
||||
);
|
||||
|
||||
public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter(
|
||||
"tab",
|
||||
"0",
|
||||
`The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`
|
||||
).map<number>(
|
||||
(str) => (isNaN(Number(str)) ? 0 : Number(str)),
|
||||
[],
|
||||
(n) => "" + n
|
||||
);
|
||||
public static state: FeaturePipelineState;
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
const self = this;
|
||||
this.layoutToUse = layoutToUse;
|
||||
super(layoutToUse)
|
||||
}
|
||||
|
||||
// -- Location control initialization
|
||||
{
|
||||
const zoom = State.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"z",
|
||||
"" + (layoutToUse?.startZoom ?? 1),
|
||||
"The initial/current zoom level"
|
||||
).syncWith(LocalStorageSource.Get("zoom"))
|
||||
);
|
||||
const lat = State.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"lat",
|
||||
"" + (layoutToUse?.startLat ?? 0),
|
||||
"The initial/current latitude"
|
||||
).syncWith(LocalStorageSource.Get("lat"))
|
||||
);
|
||||
const lon = State.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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Helper function to initialize feature switches
|
||||
function featSw(
|
||||
key: string,
|
||||
deflt: (layout: LayoutConfig) => boolean,
|
||||
documentation: string
|
||||
): UIEventSource<boolean> {
|
||||
|
||||
const defaultValue = deflt(self.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"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// Feature switch initialization - not as a function as the UIEventSources are readonly
|
||||
{
|
||||
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.featureSwitchIframe = featSw(
|
||||
"fs-iframe",
|
||||
() => false,
|
||||
"Disables/Enables the iframe-popup"
|
||||
);
|
||||
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.featureSwitchIsTesting = QueryParameters.GetQueryParameter(
|
||||
"test",
|
||||
"false",
|
||||
"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.featureSwitchApiURL = QueryParameters.GetQueryParameter(
|
||||
"backend",
|
||||
"osm",
|
||||
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'"
|
||||
);
|
||||
|
||||
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 = QueryParameters.GetQueryParameter("overpassTimeout",
|
||||
"" + layoutToUse?.overpassTimeout,
|
||||
"Set a different timeout (in seconds) for queries in overpass")
|
||||
.map(str => Number(str), [], n => "" + n)
|
||||
|
||||
this.featureSwitchUserbadge.addCallbackAndRun(userbadge => {
|
||||
if (!userbadge) {
|
||||
this.featureSwitchAddNew.setData(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
{
|
||||
// Some other feature switches
|
||||
const customCssQP = QueryParameters.GetQueryParameter(
|
||||
"custom-css",
|
||||
"",
|
||||
"If specified, the custom css from the given link will be loaded additionaly"
|
||||
);
|
||||
if (customCssQP.data !== undefined && customCssQP.data !== "") {
|
||||
Utils.LoadCustomCss(customCssQP.data);
|
||||
}
|
||||
|
||||
this.backgroundLayerId = QueryParameters.GetQueryParameter(
|
||||
"background",
|
||||
layoutToUse?.defaultBackgroundId ?? "osm",
|
||||
"The id of the background layer to start with"
|
||||
);
|
||||
}
|
||||
|
||||
if (Utils.runningFromConsole) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
new ChangeToElementsActor(this.changes, this.allElements)
|
||||
|
||||
new PendingChangesUploader(this.changes, this.selectedElement);
|
||||
|
||||
new SelectedElementTagsUpdater(this)
|
||||
|
||||
this.mangroveIdentity = new MangroveIdentity(
|
||||
this.osmConnection.GetLongPreference("identity", "mangrove")
|
||||
);
|
||||
|
||||
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(";")
|
||||
);
|
||||
|
||||
Locale.language.syncWith(this.osmConnection.GetPreference("language"));
|
||||
|
||||
Locale.language
|
||||
.addCallback((currentLanguage) => {
|
||||
const layoutToUse = self.layoutToUse;
|
||||
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();
|
||||
|
||||
new TitleHandler(this);
|
||||
|
||||
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)
|
||||
}))
|
||||
}
|
||||
|
||||
private static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
return source.map(
|
||||
(str) => {
|
||||
let parsed = parseFloat(str);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
},
|
||||
[],
|
||||
(fl) => {
|
||||
if (fl === undefined || isNaN(fl)) {
|
||||
return undefined;
|
||||
}
|
||||
return ("" + fl).substr(0, 8);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
20
UI/AllThemesGui.ts
Normal file
20
UI/AllThemesGui.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import State from "../State";
|
||||
import Combine from "./Base/Combine";
|
||||
import MoreScreen from "./BigComponents/MoreScreen";
|
||||
import Translations from "./i18n/Translations";
|
||||
import Constants from "../Models/Constants";
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||
|
||||
export default class AllThemesGui {
|
||||
constructor() {
|
||||
new FixedUiElement("").AttachTo("centermessage")
|
||||
const state = new UserRelatedState(undefined);
|
||||
new Combine([new MoreScreen(state, true),
|
||||
Translations.t.general.aboutMapcomplete.SetClass("link-underline"),
|
||||
new FixedUiElement("v" + Constants.vNumber)
|
||||
]).SetClass("block m-5 lg:w-3/4 lg:ml-40")
|
||||
.SetStyle("pointer-events: all;")
|
||||
.AttachTo("topleft-tools");
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import State from "../../State";
|
||||
import ThemeIntroductionPanel from "./ThemeIntroductionPanel";
|
||||
import Svg from "../../Svg";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
@ -8,31 +7,46 @@ import Constants from "../../Models/Constants";
|
|||
import Combine from "../Base/Combine";
|
||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Utils} from "../../Utils";
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState";
|
||||
|
||||
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
||||
|
||||
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
const layoutToUse = State.state.layoutToUse;
|
||||
constructor(isShown: UIEventSource<boolean>,
|
||||
currentTab: UIEventSource<number>,
|
||||
state: {
|
||||
layoutToUse: LayoutConfig,
|
||||
osmConnection: OsmConnection,
|
||||
featureSwitchShareScreen: UIEventSource<boolean>,
|
||||
featureSwitchMoreQuests: UIEventSource<boolean>
|
||||
} & UserRelatedState) {
|
||||
const layoutToUse = state.layoutToUse;
|
||||
super(
|
||||
() => layoutToUse.title.Clone(),
|
||||
() => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails, isShown),
|
||||
"welcome", isShown
|
||||
() => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, isShown),
|
||||
undefined, isShown
|
||||
)
|
||||
}
|
||||
|
||||
private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource<boolean>): { header: string | BaseUIElement; content: BaseUIElement }[] {
|
||||
private static ConstructBaseTabs(state: {
|
||||
layoutToUse: LayoutConfig,
|
||||
osmConnection: OsmConnection,
|
||||
featureSwitchShareScreen: UIEventSource<boolean>,
|
||||
featureSwitchMoreQuests: UIEventSource<boolean>
|
||||
} & UserRelatedState,
|
||||
isShown: UIEventSource<boolean>):
|
||||
{ header: string | BaseUIElement; content: BaseUIElement }[] {
|
||||
|
||||
let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown);
|
||||
|
||||
|
||||
const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [
|
||||
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
|
||||
{header: `<img src='${state.layoutToUse.icon}'>`, content: welcome},
|
||||
{
|
||||
header: Svg.osm_logo_img,
|
||||
content: Translations.t.general.openStreetMapIntro.Clone().SetClass("link-underline")
|
||||
|
@ -40,31 +54,36 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
|
||||
]
|
||||
|
||||
if (State.state.featureSwitchShareScreen.data) {
|
||||
if (state.featureSwitchShareScreen.data) {
|
||||
tabs.push({header: Svg.share_img, content: new ShareScreen()});
|
||||
}
|
||||
|
||||
if (State.state.featureSwitchMoreQuests.data) {
|
||||
if (state.featureSwitchMoreQuests.data) {
|
||||
|
||||
tabs.push({
|
||||
header: Svg.add_img,
|
||||
content: new MoreScreen()
|
||||
content: new MoreScreen(state)
|
||||
});
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource<UserDetails>, isShown: UIEventSource<boolean>) {
|
||||
private static GenerateContents(state: {
|
||||
layoutToUse: LayoutConfig,
|
||||
osmConnection: OsmConnection,
|
||||
featureSwitchShareScreen: UIEventSource<boolean>,
|
||||
featureSwitchMoreQuests: UIEventSource<boolean>
|
||||
} & UserRelatedState, currentTab: UIEventSource<number>, isShown: UIEventSource<boolean>) {
|
||||
|
||||
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)
|
||||
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)]
|
||||
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)
|
||||
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)]
|
||||
|
||||
const now = new Date()
|
||||
const lastWeek = new Date(now.getDate() - 7 * 24 * 60 * 60 * 1000)
|
||||
const date = lastWeek.getFullYear()+"-"+Utils.TwoDigits(lastWeek.getMonth()+1)+"-"+Utils.TwoDigits(lastWeek.getDate())
|
||||
const date = lastWeek.getFullYear() + "-" + Utils.TwoDigits(lastWeek.getMonth() + 1) + "-" + Utils.TwoDigits(lastWeek.getDate())
|
||||
const osmcha_link = `https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%22${date}%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D`
|
||||
|
||||
|
||||
tabsWithAboutMc.push({
|
||||
header: Svg.help,
|
||||
content: new Combine([Translations.t.general.aboutMapcomplete.Clone()
|
||||
|
@ -75,11 +94,11 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
|||
|
||||
tabs.forEach(c => c.content.SetClass("p-4"))
|
||||
tabsWithAboutMc.forEach(c => c.content.SetClass("p-4"))
|
||||
|
||||
|
||||
return new Toggle(
|
||||
new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab),
|
||||
new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab),
|
||||
userDetails.map((userdetails: UserDetails) =>
|
||||
new TabbedComponent(tabsWithAboutMc, currentTab),
|
||||
new TabbedComponent(tabs, currentTab),
|
||||
state.osmConnection.userDetails.map((userdetails: UserDetails) =>
|
||||
userdetails.loggedIn &&
|
||||
userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock)
|
||||
)
|
||||
|
|
|
@ -2,7 +2,6 @@ import Combine from "../Base/Combine";
|
|||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||
import Translations from "../i18n/Translations";
|
||||
import AttributionPanel from "./AttributionPanel";
|
||||
import State from "../../State";
|
||||
import ContributorCount from "../../Logic/ContributorCount";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import MapControlButton from "../MapControlButton";
|
||||
|
@ -13,16 +12,33 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
|
||||
export default class LeftControls extends Combine {
|
||||
|
||||
constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc>, overlayToggles: any}) {
|
||||
constructor(state: {
|
||||
layoutToUse: LayoutConfig,
|
||||
featurePipeline: FeaturePipeline,
|
||||
currentBounds: UIEventSource<BBox>,
|
||||
locationControl: UIEventSource<Loc>,
|
||||
overlayToggles: any,
|
||||
featureSwitchEnableExport: UIEventSource<boolean>,
|
||||
featureSwitchExportAsPdf: UIEventSource<boolean>,
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>,
|
||||
featureSwitchFilter: UIEventSource<boolean>,
|
||||
selectedElement: UIEventSource<any>
|
||||
},
|
||||
guiState: {
|
||||
downloadControlIsOpened: UIEventSource<boolean>,
|
||||
filterViewIsOpened: UIEventSource<boolean>,
|
||||
}) {
|
||||
|
||||
const toggledCopyright = new ScrollableFullScreen(
|
||||
() => Translations.t.general.attribution.attributionTitle.Clone(),
|
||||
() =>
|
||||
new AttributionPanel(
|
||||
State.state.layoutToUse,
|
||||
state.layoutToUse,
|
||||
new ContributorCount(state).Contributors
|
||||
),
|
||||
undefined
|
||||
|
@ -38,50 +54,50 @@ export default class LeftControls extends Combine {
|
|||
|
||||
const toggledDownload = new Toggle(
|
||||
new AllDownloads(
|
||||
State.state.downloadControlIsOpened
|
||||
guiState.downloadControlIsOpened
|
||||
).SetClass("block p-1 rounded-full"),
|
||||
new MapControlButton(Svg.download_svg())
|
||||
.onClick(() => State.state.downloadControlIsOpened.setData(true)),
|
||||
State.state.downloadControlIsOpened
|
||||
.onClick(() => guiState.downloadControlIsOpened.setData(true)),
|
||||
guiState.downloadControlIsOpened
|
||||
)
|
||||
|
||||
const downloadButtonn = new Toggle(
|
||||
toggledDownload,
|
||||
undefined,
|
||||
State.state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || State.state.featureSwitchExportAsPdf.data,
|
||||
[State.state.featureSwitchExportAsPdf])
|
||||
state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || state.featureSwitchExportAsPdf.data,
|
||||
[state.featureSwitchExportAsPdf])
|
||||
);
|
||||
|
||||
const toggledFilter = new Toggle(
|
||||
new ScrollableFullScreen(
|
||||
() => Translations.t.general.layerSelection.title.Clone(),
|
||||
() =>
|
||||
new FilterView(State.state.filteredLayers, state.overlayToggles).SetClass(
|
||||
new FilterView(state.filteredLayers, state.overlayToggles).SetClass(
|
||||
"block p-1 rounded-full"
|
||||
),
|
||||
undefined,
|
||||
State.state.filterIsOpened
|
||||
guiState.filterViewIsOpened
|
||||
),
|
||||
new MapControlButton(Svg.filter_svg())
|
||||
.onClick(() => State.state.filterIsOpened.setData(true)),
|
||||
State.state.filterIsOpened
|
||||
.onClick(() => guiState.filterViewIsOpened.setData(true)),
|
||||
guiState.filterViewIsOpened
|
||||
)
|
||||
|
||||
const filterButton = new Toggle(
|
||||
toggledFilter,
|
||||
undefined,
|
||||
State.state.featureSwitchFilter
|
||||
state.featureSwitchFilter
|
||||
);
|
||||
|
||||
|
||||
State.state.locationControl.addCallback(() => {
|
||||
state.locationControl.addCallback(() => {
|
||||
// Close the layer selection when the map is moved
|
||||
toggledDownload.isEnabled.setData(false);
|
||||
copyrightButton.isEnabled.setData(false);
|
||||
toggledFilter.isEnabled.setData(false);
|
||||
});
|
||||
|
||||
State.state.selectedElement.addCallbackAndRunD((_) => {
|
||||
state.selectedElement.addCallbackAndRunD((_) => {
|
||||
toggledDownload.isEnabled.setData(false);
|
||||
copyrightButton.isEnabled.setData(false);
|
||||
toggledFilter.isEnabled.setData(false);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
@ -11,15 +10,21 @@ import LanguagePicker from "../LanguagePicker";
|
|||
import IndexText from "./IndexText";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {Utils} from "../../Utils";
|
||||
import Title from "../Base/Title";
|
||||
|
||||
export default class MoreScreen extends Combine {
|
||||
|
||||
|
||||
constructor(onMainScreen: boolean = false) {
|
||||
super(MoreScreen.Init(onMainScreen, State.state));
|
||||
}
|
||||
|
||||
private static Init(onMainScreen: boolean, state: State): BaseUIElement [] {
|
||||
constructor(state: UserRelatedState & {
|
||||
locationControl?: UIEventSource<Loc>,
|
||||
layoutToUse?: LayoutConfig
|
||||
}, onMainScreen: boolean = false) {
|
||||
const tr = Translations.t.general.morescreen;
|
||||
let intro: BaseUIElement = tr.intro.Clone();
|
||||
let themeButtonStyle = ""
|
||||
|
@ -35,30 +40,59 @@ export default class MoreScreen extends Combine {
|
|||
themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
|
||||
}
|
||||
|
||||
return [
|
||||
super([
|
||||
intro,
|
||||
MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle),
|
||||
MoreScreen.createUnofficialThemeList(themeButtonStyle)?.SetClass(themeListStyle),
|
||||
MoreScreen.createPreviouslyVistedHiddenList(state, themeButtonStyle).SetClass(themeListStyle),
|
||||
MoreScreen.createUnofficialThemeList(themeButtonStyle, state)?.SetClass(themeListStyle),
|
||||
tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10")
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
private static createUnofficialThemeList(buttonClass: string): BaseUIElement {
|
||||
return new VariableUiElement(State.state.installedThemes.map(customThemes => {
|
||||
|
||||
private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState): BaseUIElement {
|
||||
return new VariableUiElement(state.installedThemes.map(customThemes => {
|
||||
const els: BaseUIElement[] = []
|
||||
if (customThemes.length > 0) {
|
||||
els.push(Translations.t.general.customThemeIntro.Clone())
|
||||
|
||||
const customThemesElement = new Combine(
|
||||
customThemes.map(theme => MoreScreen.createLinkButton(theme.layout, theme.definition)?.SetClass(buttonClass))
|
||||
customThemes.map(theme => MoreScreen.createLinkButton(state, theme.layout, theme.definition)?.SetClass(buttonClass))
|
||||
)
|
||||
els.push(customThemesElement)
|
||||
}
|
||||
return els;
|
||||
}));
|
||||
}
|
||||
|
||||
private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string){
|
||||
const t= Translations.t.general.morescreen
|
||||
return new Toggle(
|
||||
new Combine([
|
||||
new Title(t.previouslyHiddenTitle.Clone()),
|
||||
t.hiddenExplanation,
|
||||
|
||||
|
||||
new VariableUiElement(
|
||||
state.osmConnection.preferencesHandler.preferences.map(allPreferences => {
|
||||
const knownThemes = Utils.NoNull( Object.keys(allPreferences).filter(key => key.startsWith("hidden-theme-"))
|
||||
.map(key => key.substr("hidden-theme-".length, key.length - "-enabled".length))
|
||||
.map(theme => AllKnownLayouts.allKnownLayouts.get(theme) ))
|
||||
return new Combine(knownThemes.map(layout =>
|
||||
MoreScreen.createLinkButton(state, layout ).SetClass(buttonClass)
|
||||
))
|
||||
|
||||
})
|
||||
|
||||
)]).SetClass("flex flex-col"),
|
||||
undefined,
|
||||
state.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
|
||||
private static createOfficialThemesList(state: State, buttonClass: string): BaseUIElement {
|
||||
}
|
||||
|
||||
private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string): BaseUIElement {
|
||||
let officialThemes = AllKnownLayouts.layoutsList
|
||||
|
||||
let buttons = officialThemes.map((layout) => {
|
||||
|
@ -66,10 +100,10 @@ export default class MoreScreen extends Combine {
|
|||
console.trace("Layout is undefined")
|
||||
return undefined
|
||||
}
|
||||
const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass);
|
||||
const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass);
|
||||
if (layout.id === personal.id) {
|
||||
return new VariableUiElement(
|
||||
State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
|
||||
state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
|
||||
.map(csCount => {
|
||||
if (csCount < Constants.userJourney.personalLayoutUnlock) {
|
||||
return undefined
|
||||
|
@ -91,7 +125,7 @@ export default class MoreScreen extends Combine {
|
|||
/*
|
||||
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
|
||||
* */
|
||||
private static createCustomGeneratorButton(state: State): VariableUiElement {
|
||||
private static createCustomGeneratorButton(state: { osmConnection: OsmConnection }): VariableUiElement {
|
||||
const tr = Translations.t.general.morescreen;
|
||||
return new VariableUiElement(
|
||||
state.osmConnection.userDetails.map(userDetails => {
|
||||
|
@ -111,13 +145,22 @@ export default class MoreScreen extends Combine {
|
|||
|
||||
/**
|
||||
* Creates a button linking to the given theme
|
||||
* @param layout
|
||||
* @param customThemeDefinition
|
||||
* @private
|
||||
*/
|
||||
private static createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined): BaseUIElement {
|
||||
if (layout === undefined) {
|
||||
return undefined;
|
||||
private static createLinkButton(
|
||||
state: {
|
||||
locationControl?: UIEventSource<Loc>,
|
||||
layoutToUse?: LayoutConfig
|
||||
}, layout: LayoutConfig, customThemeDefinition: string = undefined
|
||||
):
|
||||
BaseUIElement {
|
||||
if (layout
|
||||
|
||||
===
|
||||
undefined
|
||||
) {
|
||||
return
|
||||
undefined;
|
||||
}
|
||||
if (layout.id === undefined) {
|
||||
console.error("ID is undefined for layout", layout);
|
||||
|
@ -126,11 +169,11 @@ export default class MoreScreen extends Combine {
|
|||
if (layout.hideFromOverview) {
|
||||
return undefined;
|
||||
}
|
||||
if (layout.id === State.state.layoutToUse?.id) {
|
||||
if (layout.id === state?.layoutToUse?.id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const currentLocation = State.state.locationControl;
|
||||
const currentLocation = state?.locationControl;
|
||||
|
||||
let path = window.location.pathname;
|
||||
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
|
||||
|
@ -151,7 +194,7 @@ export default class MoreScreen extends Combine {
|
|||
linkSuffix = `#${customThemeDefinition}`
|
||||
}
|
||||
|
||||
const linkText = currentLocation.map(currentLocation => {
|
||||
const linkText = currentLocation?.map(currentLocation => {
|
||||
const params = [
|
||||
["z", currentLocation?.zoom],
|
||||
["lat", currentLocation?.lat],
|
||||
|
@ -160,7 +203,7 @@ export default class MoreScreen extends Combine {
|
|||
.map(part => part[0] + "=" + part[1])
|
||||
.join("&")
|
||||
return `${linkPrefix}${params}${linkSuffix}`;
|
||||
})
|
||||
}) ?? new UIEventSource<string>(`${linkPrefix}${linkSuffix}`)
|
||||
|
||||
|
||||
let description = Translations.WT(layout.shortDescription).Clone();
|
||||
|
|
|
@ -2,38 +2,38 @@ import Combine from "../Base/Combine";
|
|||
import Toggle from "../Input/Toggle";
|
||||
import MapControlButton from "../MapControlButton";
|
||||
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler";
|
||||
import State from "../../State";
|
||||
import Svg from "../../Svg";
|
||||
import MapState from "../../Logic/State/MapState";
|
||||
|
||||
export default class RightControls extends Combine {
|
||||
|
||||
constructor() {
|
||||
constructor(state:MapState) {
|
||||
const geolocationButton = new Toggle(
|
||||
new MapControlButton(
|
||||
new GeoLocationHandler(
|
||||
State.state.currentGPSLocation,
|
||||
State.state.leafletMap,
|
||||
State.state.layoutToUse
|
||||
state.currentGPSLocation,
|
||||
state.leafletMap,
|
||||
state.layoutToUse
|
||||
), {
|
||||
dontStyle: true
|
||||
}
|
||||
),
|
||||
undefined,
|
||||
State.state.featureSwitchGeolocation
|
||||
state.featureSwitchGeolocation
|
||||
);
|
||||
|
||||
const plus = new MapControlButton(
|
||||
Svg.plus_svg()
|
||||
).onClick(() => {
|
||||
State.state.locationControl.data.zoom++;
|
||||
State.state.locationControl.ping();
|
||||
state.locationControl.data.zoom++;
|
||||
state.locationControl.ping();
|
||||
});
|
||||
|
||||
const min = new MapControlButton(
|
||||
Svg.min_svg()
|
||||
).onClick(() => {
|
||||
State.state.locationControl.data.zoom--;
|
||||
State.state.locationControl.ping();
|
||||
state.locationControl.data.zoom--;
|
||||
state.locationControl.ping();
|
||||
});
|
||||
|
||||
super([plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1")))
|
||||
|
|
|
@ -2,7 +2,6 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import {Translation} from "../i18n/Translation";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import {Geocoding} from "../../Logic/Osm/Geocoding";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
@ -10,7 +9,10 @@ import Hash from "../../Logic/Web/Hash";
|
|||
import Combine from "../Base/Combine";
|
||||
|
||||
export default class SearchAndGo extends Combine {
|
||||
constructor() {
|
||||
constructor(state: {
|
||||
leafletMap: UIEventSource<any>,
|
||||
selectedElement: UIEventSource<any>
|
||||
}) {
|
||||
const goButton = Svg.search_ui().SetClass(
|
||||
"w-8 h-8 full-rounded border-black float-right"
|
||||
);
|
||||
|
@ -64,9 +66,9 @@ export default class SearchAndGo extends Combine {
|
|||
[bb[0], bb[2]],
|
||||
[bb[1], bb[3]],
|
||||
];
|
||||
State.state.selectedElement.setData(undefined);
|
||||
state.selectedElement.setData(undefined);
|
||||
Hash.hash.setData(poi.osm_type + "/" + poi.osm_id);
|
||||
State.state.leafletMap.data.fitBounds(bounds);
|
||||
state.leafletMap.data.fitBounds(bounds);
|
||||
placeholder.setData(Translations.t.general.search.search);
|
||||
},
|
||||
() => {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Svg from "../../Svg";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Constants from "../../Models/Constants";
|
||||
|
@ -12,7 +11,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||
import LocationInput from "../Input/LocationInput";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||
|
@ -20,6 +19,11 @@ import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
|
|||
import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {BBox} from "../../Logic/BBox";
|
||||
import Loc from "../../Models/Loc";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Changes} from "../../Logic/Osm/Changes";
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
||||
import {ElementStorage} from "../../Logic/ElementStorage";
|
||||
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
|
@ -38,9 +42,22 @@ interface PresetInfo extends PresetConfig {
|
|||
|
||||
export default class SimpleAddUI extends Toggle {
|
||||
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
constructor(isShown: UIEventSource<boolean>,
|
||||
filterViewIsOpened: UIEventSource<boolean>,
|
||||
state: {
|
||||
layoutToUse: LayoutConfig,
|
||||
osmConnection: OsmConnection,
|
||||
changes: Changes,
|
||||
allElements: ElementStorage,
|
||||
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
||||
featurePipeline: FeaturePipeline,
|
||||
selectedElement: UIEventSource<any>,
|
||||
locationControl: UIEventSource<Loc>,
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>,
|
||||
featureSwitchFilter: UIEventSource<boolean>,
|
||||
}) {
|
||||
const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone())
|
||||
.onClick(() => State.state.osmConnection.AttemptLogin());
|
||||
.onClick(() => state.osmConnection.AttemptLogin());
|
||||
const readYourMessages = new Combine([
|
||||
Translations.t.general.readYourMessages.Clone().SetClass("alert"),
|
||||
new SubtleButton(Svg.envelope_ui(),
|
||||
|
@ -50,20 +67,21 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
|
||||
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
|
||||
State.state.LastClickLocation.addCallback( _ => selectedPreset.setData(undefined))
|
||||
|
||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
||||
state.LastClickLocation.addCallback(_ => selectedPreset.setData(undefined))
|
||||
|
||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state)
|
||||
|
||||
|
||||
async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) {
|
||||
async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) {
|
||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
|
||||
theme: State.state?.layoutToUse?.id ?? "unkown",
|
||||
theme: state.layoutToUse?.id ?? "unkown",
|
||||
changeType: "create",
|
||||
snapOnto: snapOntoWay})
|
||||
await State.state.changes.applyAction(newElementAction)
|
||||
snapOnto: snapOntoWay
|
||||
})
|
||||
await state.changes.applyAction(newElementAction)
|
||||
selectedPreset.setData(undefined)
|
||||
isShown.setData(false)
|
||||
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
|
||||
state.selectedElement.setData(state.allElements.ContainingFeatures.get(
|
||||
newElementAction.newElementId
|
||||
))
|
||||
}
|
||||
|
@ -73,7 +91,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
if (preset === undefined) {
|
||||
return presetsOverview
|
||||
}
|
||||
return SimpleAddUI.CreateConfirmButton(preset,
|
||||
return SimpleAddUI.CreateConfirmButton(state, filterViewIsOpened, preset,
|
||||
(tags, location, snapOntoWayId?: string) => {
|
||||
if (snapOntoWayId === undefined) {
|
||||
createNewPoint(tags, location, undefined)
|
||||
|
@ -97,18 +115,18 @@ export default class SimpleAddUI extends Toggle {
|
|||
new Toggle(
|
||||
addUi,
|
||||
Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
|
||||
State.state.featurePipeline.somethingLoaded
|
||||
state.featurePipeline.somethingLoaded
|
||||
),
|
||||
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
|
||||
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
|
||||
state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
|
||||
),
|
||||
readYourMessages,
|
||||
State.state.osmConnection.userDetails.map((userdetails: UserDetails) =>
|
||||
state.osmConnection.userDetails.map((userdetails: UserDetails) =>
|
||||
userdetails.csCount >= Constants.userJourney.addNewPointWithUnreadMessagesUnlock ||
|
||||
userdetails.unreadMessages == 0)
|
||||
),
|
||||
loginButton,
|
||||
State.state.osmConnection.isLoggedIn
|
||||
state.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
|
||||
|
@ -116,11 +134,18 @@ export default class SimpleAddUI extends Toggle {
|
|||
}
|
||||
|
||||
|
||||
private static CreateConfirmButton(preset: PresetInfo,
|
||||
private static CreateConfirmButton(
|
||||
state: {
|
||||
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
||||
osmConnection: OsmConnection,
|
||||
featurePipeline: FeaturePipeline
|
||||
},
|
||||
filterViewIsOpened: UIEventSource<boolean>,
|
||||
preset: PresetInfo,
|
||||
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
|
||||
cancel: () => void): BaseUIElement {
|
||||
|
||||
let location = State.state.LastClickLocation;
|
||||
let location = state.LastClickLocation;
|
||||
let preciseInput: LocationInput = undefined
|
||||
if (preset.preciseInput !== undefined) {
|
||||
// We uncouple the event source
|
||||
|
@ -143,7 +168,6 @@ export default class SimpleAddUI extends Toggle {
|
|||
}
|
||||
|
||||
|
||||
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
preciseInput = new LocationInput({
|
||||
mapBackground: backgroundLayer,
|
||||
|
@ -160,24 +184,24 @@ export default class SimpleAddUI extends Toggle {
|
|||
if (preset.preciseInput.snapToLayers) {
|
||||
// We have to snap to certain layers.
|
||||
// Lets fetch them
|
||||
|
||||
let loadedBbox : BBox= undefined
|
||||
|
||||
let loadedBbox: BBox = undefined
|
||||
mapBounds?.addCallbackAndRunD(bbox => {
|
||||
if(loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)){
|
||||
if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) {
|
||||
// All is already there
|
||||
// return;
|
||||
}
|
||||
|
||||
bbox = bbox.pad(2);
|
||||
loadedBbox = bbox;
|
||||
const allFeatures: {feature: any}[] = []
|
||||
const allFeatures: { feature: any }[] = []
|
||||
preset.preciseInput.snapToLayers.forEach(layerId => {
|
||||
State.state.featurePipeline.GetFeaturesWithin(layerId, bbox).forEach(feats => allFeatures.push(...feats.map(f => ({feature :f}))))
|
||||
state.featurePipeline.GetFeaturesWithin(layerId, bbox).forEach(feats => allFeatures.push(...feats.map(f => ({feature: f}))))
|
||||
})
|
||||
snapToFeatures.setData(allFeatures)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -205,7 +229,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
Translations.t.general.add.openLayerControl
|
||||
])
|
||||
)
|
||||
.onClick(() => State.state.filterIsOpened.setData(true))
|
||||
.onClick(() => filterViewIsOpened.setData(true))
|
||||
|
||||
|
||||
const openLayerOrConfirm = new Toggle(
|
||||
|
@ -234,36 +258,35 @@ export default class SimpleAddUI extends Toggle {
|
|||
openLayerOrConfirm,
|
||||
disableFilter,
|
||||
preset.layerToAddTo.appliedFilters.map(filters => {
|
||||
if(filters === undefined || filters.length === 0){
|
||||
if (filters === undefined || filters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
for (const filter of filters) {
|
||||
if(filter.selected === 0 && filter.filter.options.length === 1){
|
||||
if (filter.selected === 0 && filter.filter.options.length === 1) {
|
||||
return false;
|
||||
}
|
||||
if(filter.selected !== undefined){
|
||||
if (filter.selected !== undefined) {
|
||||
const tags = filter.filter.options[filter.selected].osmTags
|
||||
if(tags !== undefined && tags["and"]?.length !== 0){
|
||||
if (tags !== undefined && tags["and"]?.length !== 0) {
|
||||
// This actually doesn't filter anything at all
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset);
|
||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection);
|
||||
|
||||
const cancelButton = new SubtleButton(Svg.close_ui(),
|
||||
Translations.t.general.cancel
|
||||
).onClick(cancel)
|
||||
|
||||
return new Combine([
|
||||
// Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
|
||||
State.state.osmConnection.userDetails.data.dryRun ?
|
||||
state.osmConnection.userDetails.data.dryRun ?
|
||||
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
|
||||
disableFiltersOrConfirm,
|
||||
cancelButton,
|
||||
|
@ -274,24 +297,29 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
}
|
||||
|
||||
private static CreateTagInfoFor(preset: PresetInfo, optionallyLinkToWiki = true) {
|
||||
const csCount = State.state.osmConnection.userDetails.data.csCount;
|
||||
private static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) {
|
||||
const csCount = osmConnection.userDetails.data.csCount;
|
||||
return new Toggle(
|
||||
Translations.t.general.add.presetInfo.Subs({
|
||||
tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"),
|
||||
}).SetStyle("word-break: break-all"),
|
||||
|
||||
undefined,
|
||||
State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt)
|
||||
osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt)
|
||||
);
|
||||
}
|
||||
|
||||
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
|
||||
const presetButtons = SimpleAddUI.CreatePresetButtons(selectedPreset)
|
||||
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>,
|
||||
state: {
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>,
|
||||
featureSwitchFilter: UIEventSource<boolean>,
|
||||
osmConnection: OsmConnection
|
||||
}): BaseUIElement {
|
||||
const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset)
|
||||
let intro: BaseUIElement = Translations.t.general.add.intro;
|
||||
|
||||
let testMode: BaseUIElement = undefined;
|
||||
if (State.state.osmConnection?.userDetails?.data?.dryRun) {
|
||||
if (state.osmConnection?.userDetails?.data?.dryRun) {
|
||||
testMode = Translations.t.general.testing.Clone().SetClass("alert")
|
||||
}
|
||||
|
||||
|
@ -299,9 +327,9 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
}
|
||||
|
||||
private static CreatePresetSelectButton(preset: PresetInfo) {
|
||||
private static CreatePresetSelectButton(preset: PresetInfo, osmConnection: OsmConnection) {
|
||||
|
||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, false);
|
||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, osmConnection ,false);
|
||||
return new SubtleButton(
|
||||
preset.icon(),
|
||||
new Combine([
|
||||
|
@ -316,11 +344,17 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
/*
|
||||
* Generates the list with all the buttons.*/
|
||||
private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
|
||||
private static CreatePresetButtons(
|
||||
state: {
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>,
|
||||
featureSwitchFilter: UIEventSource<boolean>,
|
||||
osmConnection: OsmConnection
|
||||
},
|
||||
selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
|
||||
const allButtons = [];
|
||||
for (const layer of State.state.filteredLayers.data) {
|
||||
for (const layer of state.filteredLayers.data) {
|
||||
|
||||
if (layer.isDisplayed.data === false && !State.state.featureSwitchFilter.data) {
|
||||
if (layer.isDisplayed.data === false && !state.featureSwitchFilter.data) {
|
||||
// The layer is not displayed and we cannot enable the layer control -> we skip
|
||||
continue;
|
||||
}
|
||||
|
@ -346,7 +380,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
preciseInput: preset.preciseInput
|
||||
}
|
||||
|
||||
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
|
||||
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo, state.osmConnection);
|
||||
button.onClick(() => {
|
||||
selectedPreset.setData(presetInfo)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Svg from "../../Svg";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import LanguagePicker from "../LanguagePicker";
|
||||
|
@ -8,24 +7,25 @@ import Translations from "../i18n/Translations";
|
|||
import Link from "../Base/Link";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import Img from "../Base/Img";
|
||||
import MapState from "../../Logic/State/MapState";
|
||||
|
||||
export default class UserBadge extends Toggle {
|
||||
|
||||
constructor() {
|
||||
constructor(state: MapState) {
|
||||
|
||||
|
||||
const userDetails = State.state.osmConnection.userDetails;
|
||||
const userDetails = state.osmConnection.userDetails;
|
||||
|
||||
const loginButton = Translations.t.general.loginWithOpenStreetMap
|
||||
.Clone()
|
||||
.SetClass("userbadge-login inline-flex justify-center items-center w-full h-full text-lg font-bold min-w-[20em]")
|
||||
.onClick(() => State.state.osmConnection.AttemptLogin());
|
||||
.onClick(() => state.osmConnection.AttemptLogin());
|
||||
|
||||
|
||||
const logout =
|
||||
Svg.logout_svg()
|
||||
.onClick(() => {
|
||||
State.state.osmConnection.LogOut();
|
||||
state.osmConnection.LogOut();
|
||||
});
|
||||
|
||||
|
||||
|
@ -39,15 +39,15 @@ export default class UserBadge extends Toggle {
|
|||
return " ";
|
||||
})
|
||||
).onClick(() => {
|
||||
const home = State.state.osmConnection.userDetails.data?.home;
|
||||
const home = state.osmConnection.userDetails.data?.home;
|
||||
if (home === undefined) {
|
||||
return;
|
||||
}
|
||||
State.state.leafletMap.data.setView([home.lat, home.lon], 16);
|
||||
state.leafletMap.data.setView([home.lat, home.lon], 16);
|
||||
});
|
||||
|
||||
const linkStyle = "flex items-baseline"
|
||||
const languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.language) ?? new FixedUiElement(""))
|
||||
const languagePicker = (LanguagePicker.CreateLanguagePicker(state.layoutToUse.language) ?? new FixedUiElement(""))
|
||||
.SetStyle("width:min-content;");
|
||||
|
||||
let messageSpan =
|
||||
|
@ -129,7 +129,7 @@ export default class UserBadge extends Toggle {
|
|||
super(
|
||||
userBadge,
|
||||
loginButton,
|
||||
State.state.osmConnection.isLoggedIn
|
||||
state.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import Translations from "./i18n/Translations";
|
||||
import State from "../State";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
|
||||
|
||||
export default class CenterMessageBox extends VariableUiElement {
|
||||
|
||||
constructor() {
|
||||
const state = State.state;
|
||||
const updater = State.state.featurePipeline;
|
||||
constructor(state: FeaturePipelineState) {
|
||||
const updater = state.featurePipeline;
|
||||
const t = Translations.t.centerMessage;
|
||||
const message = updater.runningQuery.map(
|
||||
isRunning => {
|
||||
|
|
161
UI/DefaultGUI.ts
Normal file
161
UI/DefaultGUI.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
|
||||
import State from "../State";
|
||||
import {Utils} from "../Utils";
|
||||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs";
|
||||
import MapControlButton from "./MapControlButton";
|
||||
import Svg from "../Svg";
|
||||
import Toggle from "./Input/Toggle";
|
||||
import Hash from "../Logic/Web/Hash";
|
||||
import {QueryParameters} from "../Logic/Web/QueryParameters";
|
||||
import Constants from "../Models/Constants";
|
||||
import UserBadge from "./BigComponents/UserBadge";
|
||||
import SearchAndGo from "./BigComponents/SearchAndGo";
|
||||
import Link from "./Base/Link";
|
||||
import BaseUIElement from "./BaseUIElement";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import LeftControls from "./BigComponents/LeftControls";
|
||||
import RightControls from "./BigComponents/RightControls";
|
||||
import CenterMessageBox from "./CenterMessageBox";
|
||||
|
||||
export class DefaultGuiState {
|
||||
public readonly welcomeMessageIsOpened;
|
||||
public readonly downloadControlIsOpened: UIEventSource<boolean>;
|
||||
public readonly filterViewIsOpened: UIEventSource<boolean>;
|
||||
public readonly welcomeMessageOpenedTab
|
||||
|
||||
constructor() {
|
||||
this.filterViewIsOpened = QueryParameters.GetQueryParameter(
|
||||
"filter-toggle",
|
||||
"false",
|
||||
"Whether or not the filter view is shown"
|
||||
).map<boolean>(
|
||||
(str) => str !== "false",
|
||||
[],
|
||||
(b) => "" + b
|
||||
);
|
||||
this.welcomeMessageIsOpened = new UIEventSource<boolean>(Hash.hash.data === undefined ||
|
||||
Hash.hash.data === "" ||
|
||||
Hash.hash.data == "welcome");
|
||||
|
||||
this.welcomeMessageOpenedTab = QueryParameters.GetQueryParameter(
|
||||
"tab",
|
||||
"0",
|
||||
`The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`
|
||||
).map<number>(
|
||||
(str) => (isNaN(Number(str)) ? 0 : Number(str)),
|
||||
[],
|
||||
(n) => "" + n
|
||||
);
|
||||
this.downloadControlIsOpened =
|
||||
QueryParameters.GetQueryParameter(
|
||||
"download-control-toggle",
|
||||
"false",
|
||||
"Whether or not the download panel is shown"
|
||||
).map<boolean>(
|
||||
(str) => str !== "false",
|
||||
[],
|
||||
(b) => "" + b
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The default MapComplete GUI initializor
|
||||
*
|
||||
* Adds a welcome pane, contorl buttons, ... etc to index.html
|
||||
*/
|
||||
export default class DefaultGUI {
|
||||
private readonly _guiState: DefaultGuiState;
|
||||
private readonly state: FeaturePipelineState;
|
||||
|
||||
|
||||
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
|
||||
this.state = state;
|
||||
this._guiState = guiState;
|
||||
const self = this;
|
||||
|
||||
if (state.layoutToUse.customCss !== undefined) {
|
||||
Utils.LoadCustomCss(state.layoutToUse.customCss);
|
||||
}
|
||||
|
||||
// Attach the map
|
||||
state.mainMapObject.SetClass("w-full h-full")
|
||||
.AttachTo("leafletDiv")
|
||||
|
||||
state.setupClickDialogOnMap(
|
||||
guiState.filterViewIsOpened,
|
||||
state.leafletMap
|
||||
)
|
||||
|
||||
this.InitWelcomeMessage();
|
||||
|
||||
Toggle.If(state.featureSwitchUserbadge,
|
||||
() => new UserBadge(state)
|
||||
).AttachTo("userbadge")
|
||||
|
||||
Toggle.If(state.featureSwitchSearch,
|
||||
() => new SearchAndGo(state))
|
||||
.AttachTo("searchbox");
|
||||
|
||||
|
||||
let iframePopout: () => BaseUIElement = undefined;
|
||||
|
||||
if (window !== window.top) {
|
||||
// MapComplete is running in an iframe
|
||||
iframePopout = () => new VariableUiElement(state.locationControl.map(loc => {
|
||||
const url = `${window.location.origin}${window.location.pathname}?z=${loc.zoom ?? 0}&lat=${loc.lat ?? 0}&lon=${loc.lon ?? 0}`;
|
||||
const link = new Link(Svg.pop_out_img, url, true).SetClass("block w-full h-full p-1.5")
|
||||
return new MapControlButton(link)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
new Toggle(self.InitWelcomeMessage(),
|
||||
Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout),
|
||||
state.featureSwitchWelcomeMessage
|
||||
).AttachTo("messagesbox");
|
||||
|
||||
new LeftControls(state, guiState).AttachTo("bottom-left");
|
||||
new RightControls(state).AttachTo("bottom-right");
|
||||
State.state.locationControl.ping();
|
||||
new CenterMessageBox(state).AttachTo("centermessage");
|
||||
document
|
||||
.getElementById("centermessage")
|
||||
.classList.add("pointer-events-none");
|
||||
|
||||
}
|
||||
|
||||
private InitWelcomeMessage() {
|
||||
const isOpened = this._guiState.welcomeMessageIsOpened
|
||||
const fullOptions = new FullWelcomePaneWithTabs(isOpened, this._guiState.welcomeMessageOpenedTab, this.state);
|
||||
|
||||
// ?-Button on Desktop, opens panel with close-X.
|
||||
const help = new MapControlButton(Svg.help_svg());
|
||||
help.onClick(() => isOpened.setData(true));
|
||||
|
||||
|
||||
const openedTime = new Date().getTime();
|
||||
this.state.locationControl.addCallback(() => {
|
||||
if (new Date().getTime() - openedTime < 15 * 1000) {
|
||||
// Don't autoclose the first 15 secs when the map is moving
|
||||
return;
|
||||
}
|
||||
isOpened.setData(false);
|
||||
});
|
||||
|
||||
this.state.selectedElement.addCallbackAndRunD((_) => {
|
||||
isOpened.setData(false);
|
||||
});
|
||||
|
||||
return new Toggle(
|
||||
fullOptions.SetClass("welcomeMessage pointer-events-auto"),
|
||||
help.SetClass("pointer-events-auto"),
|
||||
isOpened
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -104,19 +104,7 @@ export default class ExportPDF {
|
|||
|
||||
})
|
||||
|
||||
const initialized =new Set()
|
||||
for (const overlayToggle of State.state.overlayToggles) {
|
||||
new ShowOverlayLayer(overlayToggle.config, minimap.leafletMap, overlayToggle.isDisplayed)
|
||||
initialized.add(overlayToggle.config)
|
||||
}
|
||||
|
||||
for (const tileLayerSource of State.state.layoutToUse.tileLayerSources) {
|
||||
if (initialized.has(tileLayerSource)) {
|
||||
continue
|
||||
}
|
||||
new ShowOverlayLayer(tileLayerSource, minimap.leafletMap)
|
||||
}
|
||||
|
||||
State.state.AddAllOverlaysToMap(minimap.leafletMap)
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
|
|
|
@ -176,7 +176,8 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
|
|||
enablePopups: false,
|
||||
zoomToFeatures: false,
|
||||
leafletMap: this.map.leafletMap,
|
||||
layers: State.state.filteredLayers
|
||||
layers: State.state.filteredLayers,
|
||||
allElements: State.state.allElements
|
||||
}
|
||||
)
|
||||
// Show the central point
|
||||
|
@ -191,7 +192,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
|
|||
enablePopups: false,
|
||||
zoomToFeatures: false,
|
||||
leafletMap: this.map.leafletMap,
|
||||
layerToShow: this._matching_layer
|
||||
layerToShow: this._matching_layer,
|
||||
allElements: State.state.allElements,
|
||||
selectedElement: State.state.selectedElement
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Lazy from "../Base/Lazy";
|
||||
|
||||
/**
|
||||
* The 'Toggle' is a UIElement showing either one of two elements, depending on the state.
|
||||
|
@ -24,4 +25,16 @@ export default class Toggle extends VariableUiElement {
|
|||
})
|
||||
return this;
|
||||
}
|
||||
|
||||
public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement {
|
||||
if(constructor === undefined){
|
||||
return undefined
|
||||
}
|
||||
return new Toggle(
|
||||
new Lazy(constructor),
|
||||
undefined,
|
||||
condition
|
||||
)
|
||||
|
||||
}
|
||||
}
|
|
@ -68,7 +68,7 @@ export default class SplitRoadWizard extends Toggle {
|
|||
leafletMap: miniMap.leafletMap,
|
||||
zoomToFeatures: false,
|
||||
enablePopups: false,
|
||||
layerToShow: SplitRoadWizard.splitLayerStyling
|
||||
layerToShow: SplitRoadWizard.splitLayerStyling,
|
||||
})
|
||||
|
||||
new ShowDataMultiLayer({
|
||||
|
@ -76,7 +76,8 @@ export default class SplitRoadWizard extends Toggle {
|
|||
layers: State.state.filteredLayers,
|
||||
leafletMap: miniMap.leafletMap,
|
||||
enablePopups: false,
|
||||
zoomToFeatures: true
|
||||
zoomToFeatures: true,
|
||||
allElements: State.state.allElements,
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||
import State from "../../State";
|
||||
import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import {ElementStorage} from "../../Logic/ElementStorage";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
|
||||
export default class ShowDataLayer {
|
||||
|
||||
|
@ -14,7 +14,8 @@ export default class ShowDataLayer {
|
|||
private readonly _enablePopups: boolean;
|
||||
private readonly _features: UIEventSource<{ feature: any }[]>
|
||||
private readonly _layerToShow: LayerConfig;
|
||||
|
||||
private readonly _selectedElement: UIEventSource<any>
|
||||
private readonly allElements : ElementStorage
|
||||
// Used to generate a fresh ID when needed
|
||||
private _cleanCount = 0;
|
||||
private geoLayer = undefined;
|
||||
|
@ -43,6 +44,8 @@ export default class ShowDataLayer {
|
|||
const features = options.features.features.map(featFreshes => featFreshes.map(ff => ff.feature));
|
||||
this._features = features;
|
||||
this._layerToShow = options.layerToShow;
|
||||
this._selectedElement = options.selectedElement
|
||||
this.allElements = options.allElements;
|
||||
const self = this;
|
||||
|
||||
options.leafletMap.addCallbackAndRunD(_ => {
|
||||
|
@ -71,7 +74,7 @@ export default class ShowDataLayer {
|
|||
})
|
||||
|
||||
|
||||
State.state.selectedElement.addCallbackAndRunD(selected => {
|
||||
this._selectedElement?.addCallbackAndRunD(selected => {
|
||||
if (self._leafletMap.data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
@ -162,7 +165,7 @@ export default class ShowDataLayer {
|
|||
|
||||
|
||||
private createStyleFor(feature) {
|
||||
const tagsSource = State.state.allElements.addOrGetElement(feature);
|
||||
const tagsSource = this.allElements?.addOrGetElement(feature) ?? new UIEventSource<any>(feature.properties.id);
|
||||
// Every object is tied to exactly one layer
|
||||
const layer = this._layerToShow
|
||||
return layer?.GenerateLeafletStyle(tagsSource, true);
|
||||
|
@ -178,10 +181,7 @@ export default class ShowDataLayer {
|
|||
return;
|
||||
}
|
||||
|
||||
let tagSource = State.state.allElements.getEventSourceById(feature.properties.id)
|
||||
if (tagSource === undefined) {
|
||||
tagSource = new UIEventSource<any>(feature.properties)
|
||||
}
|
||||
let tagSource = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties)
|
||||
const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)
|
||||
const style = layer.GenerateLeafletStyle(tagSource, clickable);
|
||||
const baseElement = style.icon.html;
|
||||
|
@ -230,20 +230,20 @@ export default class ShowDataLayer {
|
|||
popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>`)
|
||||
leafletLayer.on("popupopen", () => {
|
||||
if (infobox === undefined) {
|
||||
const tags = State.state.allElements.getEventSourceById(feature.properties.id);
|
||||
const tags = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties);
|
||||
infobox = new FeatureInfoBox(tags, layer);
|
||||
|
||||
infobox.isShown.addCallback(isShown => {
|
||||
if (!isShown) {
|
||||
State.state.selectedElement.setData(undefined);
|
||||
this._selectedElement?.setData(undefined);
|
||||
leafletLayer.closePopup()
|
||||
}
|
||||
});
|
||||
}
|
||||
infobox.AttachTo(id)
|
||||
infobox.Activate();
|
||||
if (State.state?.selectedElement?.data?.properties?.id !== feature.properties.id) {
|
||||
State.state.selectedElement.setData(feature)
|
||||
if (this._selectedElement?.data?.properties?.id !== feature.properties.id) {
|
||||
this._selectedElement?.setData(feature)
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -254,6 +254,7 @@ export default class ShowDataLayer {
|
|||
feature: feature,
|
||||
leafletlayer: leafletLayer
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {ElementStorage} from "../../Logic/ElementStorage";
|
||||
|
||||
export interface ShowDataLayerOptions {
|
||||
features: FeatureSource,
|
||||
selectedElement?: UIEventSource<any>,
|
||||
allElements?: ElementStorage,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
enablePopups?: true | boolean,
|
||||
zoomToFeatures?: false | boolean,
|
||||
|
|
|
@ -208,7 +208,8 @@ export default class SpecialVisualizations {
|
|||
enablePopups: false,
|
||||
zoomToFeatures: true,
|
||||
layers: State.state.filteredLayers,
|
||||
features: new StaticFeatureSource(featuresToShow, true)
|
||||
features: new StaticFeatureSource(featuresToShow, true),
|
||||
allElements: State.state.allElements
|
||||
}
|
||||
)
|
||||
|
||||
|
|
13
Utils.ts
13
Utils.ts
|
@ -463,5 +463,18 @@ export class Utils {
|
|||
}
|
||||
return hours+":"+Utils.TwoDigits(minutes)+":"+Utils.TwoDigits(seconds)
|
||||
}
|
||||
|
||||
public static DisableLongPresses(){
|
||||
// Remove all context event listeners on mobile to prevent long presses
|
||||
window.addEventListener('contextmenu', (e) => { // Not compatible with IE < 9
|
||||
|
||||
if (e.target["nodeName"] === "INPUT") {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, false);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -263,5 +263,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"enableIframePopout": false
|
||||
}
|
|
@ -45,6 +45,7 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 1.2,
|
||||
"overpassMaxZoom": 0,
|
||||
"layers": [],
|
||||
"roamingRenderings": []
|
||||
}
|
176
index.ts
176
index.ts
|
@ -1,27 +1,24 @@
|
|||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||
import {FixedUiElement} from "./UI/Base/FixedUiElement";
|
||||
import {InitUiElements} from "./InitUiElements";
|
||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
||||
import {UIEventSource} from "./Logic/UIEventSource";
|
||||
import * as $ from "jquery";
|
||||
import MoreScreen from "./UI/BigComponents/MoreScreen";
|
||||
import State from "./State";
|
||||
import Combine from "./UI/Base/Combine";
|
||||
import Translations from "./UI/i18n/Translations";
|
||||
import ValidatedTextField from "./UI/Input/ValidatedTextField";
|
||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
|
||||
import Constants from "./Models/Constants";
|
||||
import MinimapImplementation from "./UI/Base/MinimapImplementation";
|
||||
import CountryCoder from "latlon2country/index";
|
||||
import SimpleMetaTagger from "./Logic/SimpleMetaTagger";
|
||||
import {Utils} from "./Utils";
|
||||
import AllThemesGui from "./UI/AllThemesGui";
|
||||
import DetermineLayout from "./Logic/DetermineLayout";
|
||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
|
||||
import DefaultGUI, {DefaultGuiState} from "./UI/DefaultGUI";
|
||||
import State from "./State";
|
||||
|
||||
MinimapImplementation.initialize()
|
||||
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
|
||||
ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
|
||||
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
|
||||
Utils.DisableLongPresses()
|
||||
|
||||
let defaultLayout = ""
|
||||
// --------------------- Special actions based on the parameters -----------------
|
||||
// @ts-ignore
|
||||
if (location.href.startsWith("http://buurtnatuur.be")) {
|
||||
|
@ -30,60 +27,65 @@ if (location.href.startsWith("http://buurtnatuur.be")) {
|
|||
}
|
||||
|
||||
|
||||
if (location.href.indexOf("buurtnatuur.be") >= 0) {
|
||||
defaultLayout = "buurtnatuur"
|
||||
}
|
||||
class Init {
|
||||
|
||||
|
||||
let testing: UIEventSource<string>;
|
||||
if (QueryParameters.GetQueryParameter("backend", undefined).data !== "osm-test" &&
|
||||
(location.hostname === "localhost" || location.hostname === "127.0.0.1")) {
|
||||
testing = QueryParameters.GetQueryParameter("test", "true");
|
||||
// Set to true if testing and changes should NOT be saved
|
||||
testing.setData(testing.data ?? "true")
|
||||
// If you have a testfile somewhere, enable this to spoof overpass
|
||||
// This should be hosted independantly, e.g. with `cd assets; webfsd -p 8080` + a CORS plugin to disable cors rules
|
||||
// Overpass.testUrl = "http://127.0.0.1:8080/streetwidths.geojson";
|
||||
} else {
|
||||
testing = QueryParameters.GetQueryParameter("test", "false");
|
||||
}
|
||||
public static Init(layoutToUse: LayoutConfig, encoded: string) {
|
||||
|
||||
if(layoutToUse === null){
|
||||
// Something went wrong, error message is already on screen
|
||||
return;
|
||||
}
|
||||
|
||||
if (layoutToUse === undefined) {
|
||||
// No layout found
|
||||
new AllThemesGui()
|
||||
return;
|
||||
}
|
||||
|
||||
// Workaround/legacy to keep the old paramters working as I renamed some of them
|
||||
if (layoutToUse?.id === "cyclofix") {
|
||||
const legacy = QueryParameters.GetQueryParameter("layer-bike_shops", "true", "Legacy - keep De Fietsambassade working");
|
||||
const correct = QueryParameters.GetQueryParameter("layer-bike_shop", "true", "Legacy - keep De Fietsambassade working")
|
||||
if (legacy.data !== "true") {
|
||||
correct.setData(legacy.data)
|
||||
}
|
||||
console.log("layer-bike_shop toggles: legacy:", legacy.data, "new:", correct.data)
|
||||
|
||||
const legacyCafe = QueryParameters.GetQueryParameter("layer-bike_cafes", "true", "Legacy - keep De Fietsambassade working")
|
||||
const correctCafe = QueryParameters.GetQueryParameter("layer-bike_cafe", "true", "Legacy - keep De Fietsambassade working")
|
||||
if (legacyCafe.data !== "true") {
|
||||
correctCafe.setData(legacy.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----------------- SELECT THE RIGHT Theme -----------------
|
||||
const guiState = new DefaultGuiState()
|
||||
State.state = new State(layoutToUse);
|
||||
// This 'leaks' the global state via the window object, useful for debugging
|
||||
// @ts-ignore
|
||||
window.mapcomplete_state = State.state;
|
||||
new DefaultGUI(State.state, guiState)
|
||||
|
||||
|
||||
if (encoded !== undefined && encoded.length > 10) {
|
||||
// We save the layout to the user settings and local storage
|
||||
State.state.osmConnection.OnLoggedIn(() => {
|
||||
State.state.osmConnection
|
||||
.GetLongPreference("installed-theme-" + layoutToUse.id)
|
||||
.setData(encoded);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
const path = window.location.pathname.split("/").slice(-1)[0];
|
||||
if (path !== "index.html" && path !== "") {
|
||||
defaultLayout = path;
|
||||
if (path.endsWith(".html")) {
|
||||
defaultLayout = path.substr(0, path.length - 5);
|
||||
}
|
||||
console.log("Using layout", defaultLayout);
|
||||
}
|
||||
defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout, "The layout to load into MapComplete").data;
|
||||
let layoutToUse: LayoutConfig = AllKnownLayouts.allKnownLayouts.get(defaultLayout.toLowerCase());
|
||||
|
||||
const userLayoutParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme");
|
||||
|
||||
// Workaround/legacy to keep the old paramters working as I renamed some of them
|
||||
if (layoutToUse?.id === "cyclofix") {
|
||||
const legacy = QueryParameters.GetQueryParameter("layer-bike_shops", "true", "Legacy - keep De Fietsambassade working");
|
||||
const correct = QueryParameters.GetQueryParameter("layer-bike_shop", "true", "Legacy - keep De Fietsambassade working")
|
||||
if (legacy.data !== "true") {
|
||||
correct.setData(legacy.data)
|
||||
}
|
||||
console.log("layer-bike_shop toggles: legacy:", legacy.data, "new:", correct.data)
|
||||
|
||||
const legacyCafe = QueryParameters.GetQueryParameter("layer-bike_cafes", "true", "Legacy - keep De Fietsambassade working")
|
||||
const correctCafe = QueryParameters.GetQueryParameter("layer-bike_cafe", "true", "Legacy - keep De Fietsambassade working")
|
||||
if (legacyCafe.data !== "true") {
|
||||
correctCafe.setData(legacy.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const layoutFromBase64 = decodeURIComponent(userLayoutParam.data);
|
||||
|
||||
document.getElementById("decoration-desktop").remove();
|
||||
new Combine(["Initializing... <br/>",
|
||||
new FixedUiElement("<a>If this message persist, something went wrong - click here to try again</a>")
|
||||
.SetClass("link-underline small")
|
||||
|
@ -93,71 +95,13 @@ new Combine(["Initializing... <br/>",
|
|||
|
||||
})])
|
||||
.AttachTo("centermessage"); // Add an initialization and reset button if something goes wrong
|
||||
document.getElementById("decoration-desktop").remove();
|
||||
|
||||
|
||||
if (layoutFromBase64.startsWith("http")) {
|
||||
const link = layoutFromBase64;
|
||||
console.log("Downloading map theme from ", link);
|
||||
new FixedUiElement(`Downloading the theme from the <a href="${link}">link</a>...`)
|
||||
.AttachTo("centermessage");
|
||||
DetermineLayout.GetLayout().then(value => {
|
||||
console.log("Got ", value)
|
||||
Init.Init(value[0], value[1])
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
$.ajax({
|
||||
url: link,
|
||||
success: (data) => {
|
||||
|
||||
try {
|
||||
console.log("Received ", data)
|
||||
let parsed = data;
|
||||
if (typeof parsed == "string") {
|
||||
parsed = JSON.parse(data);
|
||||
} else {
|
||||
data = JSON.stringify(parsed) // De wereld op zijn kop
|
||||
}
|
||||
// Overwrite the id to the wiki:value
|
||||
parsed.id = link;
|
||||
const layout = new LayoutConfig(parsed, false).patchImages(link, data);
|
||||
InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(data));
|
||||
} catch (e) {
|
||||
new FixedUiElement(`<a href="${link}">${link}</a> is invalid:<br/>${e}<br/> <a href='https://${window.location.host}/'>Go back</a>`)
|
||||
.SetClass("clickable")
|
||||
.AttachTo("centermessage");
|
||||
console.error("Could not parse the text", data)
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
}).fail((_, textstatus, error) => {
|
||||
console.error("Could not download the wiki theme:", textstatus, error)
|
||||
new FixedUiElement(`<a href="${link}">${link}</a> is invalid:<br/>Could not download - wrong URL?<br/>` +
|
||||
error +
|
||||
"<a href='https://${window.location.host}/'>Go back</a>")
|
||||
.SetClass("clickable")
|
||||
.AttachTo("centermessage");
|
||||
});
|
||||
|
||||
} else if (layoutFromBase64 !== "false") {
|
||||
let [layoutToUse, encoded] = InitUiElements.LoadLayoutFromHash(userLayoutParam);
|
||||
InitUiElements.InitAll(layoutToUse, layoutFromBase64, testing, defaultLayout, encoded);
|
||||
} else if (layoutToUse !== undefined) {
|
||||
// This is the default case: a builtin theme
|
||||
InitUiElements.InitAll(layoutToUse, layoutFromBase64, testing, defaultLayout);
|
||||
} else {
|
||||
// We fall through: no theme loaded: just show an overview of layouts
|
||||
new FixedUiElement("").AttachTo("centermessage")
|
||||
State.state = new State(undefined);
|
||||
new Combine([new MoreScreen(true),
|
||||
Translations.t.general.aboutMapcomplete.SetClass("link-underline"),
|
||||
new FixedUiElement("v" + Constants.vNumber)
|
||||
]).SetClass("block m-5 lg:w-3/4 lg:ml-40")
|
||||
.SetStyle("pointer-events: all;")
|
||||
.AttachTo("topleft-tools");
|
||||
}
|
||||
// Remove all context event listeners on mobile to prevent long presses
|
||||
window.addEventListener('contextmenu', (e) => { // Not compatible with IE < 9
|
||||
|
||||
if (e.target["nodeName"] === "INPUT") {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}, false);
|
||||
|
|
|
@ -124,7 +124,9 @@
|
|||
"intro": "<h3>More thematic maps?</h3>Do you enjoy collecting geodata? <br/>There are more themes available.",
|
||||
"requestATheme": "If you want a custom-built theme, request it in the issue tracker",
|
||||
"streetcomplete": "Another, similar application is <a href='https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete' class='underline hover:text-blue-800' class='underline hover:text-blue-800' target='_blank'>StreetComplete</a>.",
|
||||
"createYourOwnTheme": "Create your own MapComplete theme from scratch"
|
||||
"createYourOwnTheme": "Create your own MapComplete theme from scratch",
|
||||
"previouslyHiddenTitle": "Previously visited hidden themes",
|
||||
"hiddenExplanation": "These themes are only visible if you know the link..."
|
||||
},
|
||||
"sharescreen": {
|
||||
"intro": "<h3>Share this map</h3> Share this map by copying the link below and sending it to friends and family:",
|
||||
|
|
|
@ -1271,6 +1271,11 @@
|
|||
}
|
||||
},
|
||||
"shortDescription": "Help to build an open dataset of UK addresses",
|
||||
"tileLayerSources": {
|
||||
"0": {
|
||||
"name": "Property boundaries by osmuk.org"
|
||||
}
|
||||
},
|
||||
"title": "UK Addresses"
|
||||
},
|
||||
"waste_basket": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue