From bd1b29e3448076a56de4270310e75bf708ad0620 Mon Sep 17 00:00:00 2001 From: Ward Date: Mon, 19 Jul 2021 16:23:13 +0200 Subject: [PATCH] new color and icon for navigation --- InitUiElements.ts | 915 ++++++++++++++++------------- Logic/Actors/GeoLocationHandler.ts | 453 +++++++------- Svg.ts | 17 +- UI/Base/CenterFlexedElement.ts | 32 + UI/MapControlButton.ts | 16 +- assets/svg/license_info.json | 238 +++----- assets/svg/location.svg | 4 + assets/svg/min-zoom.svg | 3 + assets/svg/plus-zoom.svg | 5 + index.css | 2 +- 10 files changed, 870 insertions(+), 815 deletions(-) create mode 100644 UI/Base/CenterFlexedElement.ts create mode 100644 assets/svg/location.svg create mode 100644 assets/svg/min-zoom.svg create mode 100644 assets/svg/plus-zoom.svg diff --git a/InitUiElements.ts b/InitUiElements.ts index cdc00c41d..fb1382ed1 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -1,21 +1,22 @@ -import {FixedUiElement} from "./UI/Base/FixedUiElement"; +import { CenterFlexedElement } from "./UI/Base/CenterFlexedElement"; +import { FixedUiElement } from "./UI/Base/FixedUiElement"; import Toggle from "./UI/Input/Toggle"; -import {Basemap} from "./UI/BigComponents/Basemap"; +import { Basemap } from "./UI/BigComponents/Basemap"; import State from "./State"; import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource"; -import {UIEventSource} from "./Logic/UIEventSource"; -import {QueryParameters} from "./Logic/Web/QueryParameters"; +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 GeoLocationHandler from "./Logic/Actors/GeoLocationHandler"; -import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; -import {Utils} from "./Utils"; +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/personalLayout/personalLayout.json" +import * as personal from "./assets/themes/personalLayout/personalLayout.json"; import LayoutConfig from "./Customizations/JSON/LayoutConfig"; import * as L from "leaflet"; import Img from "./UI/Base/Img"; @@ -33,7 +34,7 @@ import MapControlButton from "./UI/MapControlButton"; import Combine from "./UI/Base/Combine"; import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; import LZString from "lz-string"; -import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; +import { LayoutConfigJson } from "./Customizations/JSON/LayoutConfigJson"; import AttributionPanel from "./UI/BigComponents/AttributionPanel"; import ContributorCount from "./Logic/ContributorCount"; import FeatureSource from "./Logic/FeatureSource/FeatureSource"; @@ -42,442 +43,508 @@ import LayerConfig from "./Customizations/JSON/LayerConfig"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; export class InitUiElements { + static InitAll( + layoutToUse: LayoutConfig, + layoutFromBase64: string, + testing: UIEventSource, + layoutName: string, + layoutDefinition: string = "" + ) { + if (layoutToUse === undefined) { + console.log("Incorrect layout"); + new FixedUiElement( + `Error: incorrect layout ${layoutName}
Go back` + ) + .AttachTo("centermessage") + .onClick(() => {}); + throw "Incorrect layout"; + } + console.log( + "Using layout: ", + layoutToUse.id, + "LayoutFromBase64 is ", + layoutFromBase64 + ); - static InitAll(layoutToUse: LayoutConfig, layoutFromBase64: string, testing: UIEventSource, layoutName: string, - layoutDefinition: string = "") { + State.state = new State(layoutToUse); - if (layoutToUse === undefined) { - console.log("Incorrect layout") - new FixedUiElement(`Error: incorrect layout ${layoutName}
Go back`).AttachTo("centermessage").onClick(() => { - }); - throw "Incorrect layout" - } + // This 'leaks' the global state via the window object, useful for debugging + // @ts-ignore + window.mapcomplete_state = State.state; - console.log("Using layout: ", layoutToUse.id, "LayoutFromBase64 is ", layoutFromBase64); + if (layoutToUse.hideFromOverview) { + State.state.osmConnection + .GetPreference("hidden-theme-" + layoutToUse.id + "-enabled") + .setData("true"); + } - State.state = new State(layoutToUse); - - // 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") - } - } - - - function updateFavs() { - // This is purely for the personal theme to load the layers there - const favs = State.state.favouriteLayers.data ?? []; - - const neededLayers = new Set(); - - console.log("Favourites are: ", favs) - layoutToUse.layers.splice(0, layoutToUse.layers.length); - let somethingChanged = false; - for (const fav of favs) { - - if (AllKnownLayers.sharedLayers.has(fav)) { - const layer = AllKnownLayers.sharedLayers.get(fav) - if (!neededLayers.has(layer)) { - neededLayers.add(layer) - somethingChanged = true; - } - } - - - for (const layouts of State.state.installedThemes.data) { - for (const layer of layouts.layout.layers) { - if (typeof layer === "string") { - continue; - } - if (layer.id === fav) { - if (!neededLayers.has(layer)) { - neededLayers.add(layer) - somethingChanged = true; - } - } - } - } - } - if (somethingChanged) { - console.log("layoutToUse.layers:", layoutToUse.layers) - State.state.layoutToUse.data.layers = Array.from(neededLayers); - State.state.layoutToUse.ping(); - State.state.layerUpdater?.ForceRefresh(); - } - - } - - - if (layoutToUse.customCss !== undefined) { - Utils.LoadCustomCss(layoutToUse.customCss); - } - - InitUiElements.InitBaseMap(); - - InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => { - new UserBadge().AttachTo('userbadge'); + 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); }); - - InitUiElements.OnlyIf((State.state.featureSwitchSearch), () => { - new SearchAndGo().AttachTo("searchbox"); - }); - - - InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => { - InitUiElements.InitWelcomeMessage() - }); - - if ((window != window.top && !State.state.featureSwitchWelcomeMessage.data) || 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"); - } - - State.state.osmConnection.userDetails.map((userDetails: UserDetails) => userDetails?.home) - .addCallbackAndRunD(home => { - 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(State.state.leafletMap.data) - }); - - const geolocationButton = new Toggle( - new MapControlButton( - new GeoLocationHandler( - State.state.currentGPSLocation, - State.state.leafletMap, - State.state.layoutToUse - )), - undefined, - State.state.featureSwitchGeolocation); - - const plus = new MapControlButton( - Svg.plus_ui() - ).onClick(() => { - State.state.locationControl.data.zoom++; - State.state.locationControl.ping(); - }) - - const min = new MapControlButton( - Svg.min_ui() - ).onClick(() => { - State.state.locationControl.data.zoom--; - State.state.locationControl.ping(); - }) - - new Combine([plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1"))) - .SetClass("flex flex-col") - .AttachTo("bottom-right"); - - if (layoutToUse.id === personal.id) { - updateFavs(); - } - InitUiElements.setupAllLayerElements(); - - if (layoutToUse.id === personal.id) { - State.state.favouriteLayers.addCallback(updateFavs); - State.state.installedThemes.addCallback(updateFavs); - } else { - State.state.locationControl.ping(); - } - - // Reset the loading message once things are loaded - new CenterMessageBox().AttachTo("centermessage"); - document.getElementById("centermessage").classList.add("pointer-events-none") - - - } - - static LoadLayoutFromHash(userLayoutParam: UIEventSource): [LayoutConfig, string] { - try { - let hash = location.hash.substr(1); - 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) { - - new FixedUiElement("Error: could not parse the custom layout:
" + e).AttachTo("centermessage"); - throw e; - } - } - - private static OnlyIf(featureSwitch: UIEventSource, callback: () => void) { - featureSwitch.addCallbackAndRun(() => { - if (featureSwitch.data) { - callback(); - } - }); - } - - private static InitWelcomeMessage() { - - const isOpened = new UIEventSource(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"), - help - , 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 InitLayerSelection(featureSource: FeatureSource) { - - const copyrightNotice = - new ScrollableFullScreen( - () => Translations.t.general.attribution.attributionTitle.Clone(), - () => new AttributionPanel(State.state.layoutToUse, new ContributorCount(featureSource).Contributors), - "copyright" - ) - - ; - const copyrightButton = new Toggle( - copyrightNotice, - new MapControlButton(Svg.osm_copyright_svg()), - copyrightNotice.isShown - ).ToggleOnClick() - .SetClass("p-0.5") - - const layerControlPanel = new LayerControlPanel( - State.state.layerControlIsOpened) - .SetClass("block p-1 rounded-full"); - const layerControlButton = new Toggle( - layerControlPanel, - new MapControlButton(Svg.layers_svg()), - State.state.layerControlIsOpened - ).ToggleOnClick() - - const layerControl = new Toggle( - layerControlButton, - "", - State.state.featureSwitchLayers - ) - - new Combine([copyrightButton, layerControl]) - .AttachTo("bottom-left"); - - - State.state.locationControl - .addCallback(() => { - // Close the layer selection when the map is moved - layerControlButton.isEnabled.setData(false); - copyrightButton.isEnabled.setData(false); - }); - - State.state.selectedElement.addCallbackAndRunD(_ => { - layerControlButton.isEnabled.setData(false); - copyrightButton.isEnabled.setData(false); - }) - - } - - private static InitBaseMap() { - - State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers; - - 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 LayerResetter( - State.state.backgroundLayer, State.state.locationControl, - State.state.availableBackgroundLayers, State.state.layoutToUse.map((layout: LayoutConfig) => layout.defaultBackgroundId)); - - - const attr = new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, - State.state.leafletMap); - - const bm = new Basemap("leafletDiv", - State.state.locationControl, - State.state.backgroundLayer, - State.state.LastClickLocation, - attr + } else { + console.warn( + "NOT saving custom layout to OSM as we are tesing -> probably in an iFrame" ); - State.state.leafletMap.setData(bm.map); - const layout = State.state.layoutToUse.data - if (layout.lockLocation) { + } + } - if (layout.lockLocation === true) { - const tile = Utils.embedded_tile(layout.startLat, layout.startLon, layout.startZoom - 1) - const bounds = Utils.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) - bm.map.setMaxBounds(layout.lockLocation); - bm.map.setMinZoom(layout.startZoom) + function updateFavs() { + // This is purely for the personal theme to load the layers there + const favs = State.state.favouriteLayers.data ?? []; + + const neededLayers = new Set(); + + console.log("Favourites are: ", favs); + layoutToUse.layers.splice(0, layoutToUse.layers.length); + let somethingChanged = false; + for (const fav of favs) { + if (AllKnownLayers.sharedLayers.has(fav)) { + const layer = AllKnownLayers.sharedLayers.get(fav); + if (!neededLayers.has(layer)) { + neededLayers.add(layer); + somethingChanged = true; + } } + for (const layouts of State.state.installedThemes.data) { + for (const layer of layouts.layout.layers) { + if (typeof layer === "string") { + continue; + } + if (layer.id === fav) { + if (!neededLayers.has(layer)) { + neededLayers.add(layer); + somethingChanged = true; + } + } + } + } + } + if (somethingChanged) { + console.log("layoutToUse.layers:", layoutToUse.layers); + State.state.layoutToUse.data.layers = Array.from(neededLayers); + State.state.layoutToUse.ping(); + State.state.layerUpdater?.ForceRefresh(); + } } - private static InitLayers(): FeatureSource { - - - const state = State.state; - state.filteredLayers = - state.layoutToUse.map(layoutToUse => { - const flayers = []; - - - for (const layer of layoutToUse.layers) { - const isDisplayed = QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wether or not layer " + layer.id + " is shown") - .map((str) => str !== "false", [], (b) => b.toString()); - const flayer = { - isDisplayed: isDisplayed, - layerDef: layer - } - flayers.push(flayer); - } - return flayers; - }); - - const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); - State.state.layerUpdater = updater; - - - const source = new FeaturePipeline(state.filteredLayers, - updater, - state.osmApiFeatureSource, - state.layoutToUse, - state.changes, - state.locationControl, - state.selectedElement); - - new ShowDataLayer(source.features, State.state.leafletMap, State.state.layoutToUse); - - const selectedFeatureHandler = new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source, State.state.osmApiFeatureSource); - selectedFeatureHandler.zoomToSelectedFeature(State.state.locationControl); - return source; + if (layoutToUse.customCss !== undefined) { + Utils.LoadCustomCss(layoutToUse.customCss); } - private static setupAllLayerElements() { + InitUiElements.InitBaseMap(); - // ------------- Setup the layers ------------------------------- + InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => { + new UserBadge().AttachTo("userbadge"); + }); - const source = InitUiElements.InitLayers(); - InitUiElements.InitLayerSelection(source); + InitUiElements.OnlyIf(State.state.featureSwitchSearch, () => { + new SearchAndGo().AttachTo("searchbox"); + }); + InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => { + InitUiElements.InitWelcomeMessage(); + }); - // ------------------ Setup various other UI elements ------------ + if ( + (window != window.top && !State.state.featureSwitchWelcomeMessage.data) || + 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"); + } - - 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(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 - ); + State.state.osmConnection.userDetails + .map((userDetails: UserDetails) => userDetails?.home) + .addCallbackAndRunD((home) => { + 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(State.state.leafletMap.data); + }); + const geolocationButton = new Toggle( + new MapControlButton( + new GeoLocationHandler( + State.state.currentGPSLocation, + State.state.leafletMap, + State.state.layoutToUse + ) + ), + undefined, + State.state.featureSwitchGeolocation + ); + const plus = new MapControlButton( + new CenterFlexedElement( + Img.AsImageElement(Svg.plus_zoom, "", "width:1.5rem;height:1.5rem") + ) + ).onClick(() => { + State.state.locationControl.data.zoom++; + State.state.locationControl.ping(); + }); + + const min = new MapControlButton( + new CenterFlexedElement( + Img.AsImageElement(Svg.min_zoom, "", "width:1.5rem;height:1.5rem") + ) + ).onClick(() => { + State.state.locationControl.data.zoom--; + State.state.locationControl.ping(); + }); + + new Combine( + [plus, min, geolocationButton].map((el) => el.SetClass("m-0.5 md:m-1")) + ) + .SetClass("flex flex-col") + .AttachTo("bottom-right"); + + if (layoutToUse.id === personal.id) { + updateFavs(); } -} \ No newline at end of file + InitUiElements.setupAllLayerElements(); + + if (layoutToUse.id === personal.id) { + State.state.favouriteLayers.addCallback(updateFavs); + State.state.installedThemes.addCallback(updateFavs); + } else { + State.state.locationControl.ping(); + } + + // Reset the loading message once things are loaded + new CenterMessageBox().AttachTo("centermessage"); + document + .getElementById("centermessage") + .classList.add("pointer-events-none"); + } + + static LoadLayoutFromHash( + userLayoutParam: UIEventSource + ): [LayoutConfig, string] { + try { + let hash = location.hash.substr(1); + 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) { + new FixedUiElement( + "Error: could not parse the custom layout:
" + e + ).AttachTo("centermessage"); + throw e; + } + } + + private static OnlyIf( + featureSwitch: UIEventSource, + callback: () => void + ) { + featureSwitch.addCallbackAndRun(() => { + if (featureSwitch.data) { + callback(); + } + }); + } + + private static InitWelcomeMessage() { + const isOpened = new UIEventSource(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"), help, 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 InitLayerSelection(featureSource: FeatureSource) { + const copyrightNotice = new ScrollableFullScreen( + () => Translations.t.general.attribution.attributionTitle.Clone(), + () => + new AttributionPanel( + State.state.layoutToUse, + new ContributorCount(featureSource).Contributors + ), + "copyright" + ); + + const copyrightButton = new Toggle( + copyrightNotice, + new MapControlButton(Svg.osm_copyright_svg()), + copyrightNotice.isShown + ) + .ToggleOnClick() + .SetClass("p-0.5"); + + const layerControlPanel = new LayerControlPanel( + State.state.layerControlIsOpened + ).SetClass("block p-1 rounded-full"); + const layerControlButton = new Toggle( + layerControlPanel, + new MapControlButton(Svg.layers_svg()), + State.state.layerControlIsOpened + ).ToggleOnClick(); + + const layerControl = new Toggle( + layerControlButton, + "", + State.state.featureSwitchLayers + ); + + new Combine([copyrightButton, layerControl]).AttachTo("bottom-left"); + + State.state.locationControl.addCallback(() => { + // Close the layer selection when the map is moved + layerControlButton.isEnabled.setData(false); + copyrightButton.isEnabled.setData(false); + }); + + State.state.selectedElement.addCallbackAndRunD((_) => { + layerControlButton.isEnabled.setData(false); + copyrightButton.isEnabled.setData(false); + }); + } + + private static InitBaseMap() { + State.state.availableBackgroundLayers = new AvailableBaseLayers( + State.state.locationControl + ).availableEditorLayers; + + 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 LayerResetter( + State.state.backgroundLayer, + State.state.locationControl, + State.state.availableBackgroundLayers, + State.state.layoutToUse.map( + (layout: LayoutConfig) => layout.defaultBackgroundId + ) + ); + + const attr = new Attribution( + State.state.locationControl, + State.state.osmConnection.userDetails, + State.state.layoutToUse, + State.state.leafletMap + ); + + const bm = new Basemap( + "leafletDiv", + State.state.locationControl, + State.state.backgroundLayer, + State.state.LastClickLocation, + attr + ); + State.state.leafletMap.setData(bm.map); + const layout = State.state.layoutToUse.data; + if (layout.lockLocation) { + if (layout.lockLocation === true) { + const tile = Utils.embedded_tile( + layout.startLat, + layout.startLon, + layout.startZoom - 1 + ); + const bounds = Utils.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); + bm.map.setMaxBounds(layout.lockLocation); + bm.map.setMinZoom(layout.startZoom); + } + } + + private static InitLayers(): FeatureSource { + const state = State.state; + state.filteredLayers = state.layoutToUse.map((layoutToUse) => { + const flayers = []; + + for (const layer of layoutToUse.layers) { + const isDisplayed = QueryParameters.GetQueryParameter( + "layer-" + layer.id, + "true", + "Wether or not layer " + layer.id + " is shown" + ).map( + (str) => str !== "false", + [], + (b) => b.toString() + ); + const flayer = { + isDisplayed: isDisplayed, + layerDef: layer, + }; + flayers.push(flayer); + } + return flayers; + }); + + const updater = new LoadFromOverpass( + state.locationControl, + state.layoutToUse, + state.leafletMap + ); + State.state.layerUpdater = updater; + + const source = new FeaturePipeline( + state.filteredLayers, + updater, + state.osmApiFeatureSource, + state.layoutToUse, + state.changes, + state.locationControl, + state.selectedElement + ); + + new ShowDataLayer( + source.features, + State.state.leafletMap, + State.state.layoutToUse + ); + + const selectedFeatureHandler = new SelectedFeatureHandler( + Hash.hash, + State.state.selectedElement, + source, + State.state.osmApiFeatureSource + ); + selectedFeatureHandler.zoomToSelectedFeature(State.state.locationControl); + return source; + } + + private static setupAllLayerElements() { + // ------------- Setup the layers ------------------------------- + + const source = InitUiElements.InitLayers(); + InitUiElements.InitLayerSelection(source); + + // ------------------ 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(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 + ); + }); + } +} diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index e86c1baa7..fb011dd59 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -1,244 +1,265 @@ import * as L from "leaflet"; -import {UIEventSource} from "../UIEventSource"; -import {Utils} from "../../Utils"; +import { UIEventSource } from "../UIEventSource"; +import { Utils } from "../../Utils"; import Svg from "../../Svg"; import Img from "../../UI/Base/Img"; -import {LocalStorageSource} from "../Web/LocalStorageSource"; +import { LocalStorageSource } from "../Web/LocalStorageSource"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; -import {VariableUiElement} from "../../UI/Base/VariableUIElement"; +import { VariableUiElement } from "../../UI/Base/VariableUIElement"; +import { CenterFlexedElement } from "../../UI/Base/CenterFlexedElement"; export default class GeoLocationHandler extends VariableUiElement { + /** + * Wether or not the geolocation is active, aka the user requested the current location + * @private + */ + private readonly _isActive: UIEventSource; - /** - * Wether or not the geolocation is active, aka the user requested the current location - * @private - */ - private readonly _isActive: UIEventSource; + /** + * The callback over the permission API + * @private + */ + private readonly _permission: UIEventSource; + /*** + * The marker on the map, in order to update it + * @private + */ + private _marker: L.Marker; + /** + * Literally: _currentGPSLocation.data != undefined + * @private + */ + private readonly _hasLocation: UIEventSource; + private readonly _currentGPSLocation: UIEventSource<{ + latlng: any; + accuracy: number; + }>; + /** + * Kept in order to update the marker + * @private + */ + private readonly _leafletMap: UIEventSource; + /** + * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs + * @private + */ + private _lastUserRequest: Date; + /** + * A small flag on localstorage. If the user previously granted the geolocation, it will be set. + * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. + * + * Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately. + * If the user denies the geolocation this time, we unset this flag + * @private + */ + private readonly _previousLocationGrant: UIEventSource; + private readonly _layoutToUse: UIEventSource; - /** - * The callback over the permission API - * @private - */ - private readonly _permission: UIEventSource; - /*** - * The marker on the map, in order to update it - * @private - */ - private _marker: L.Marker; - /** - * Literally: _currentGPSLocation.data != undefined - * @private - */ - private readonly _hasLocation: UIEventSource; - private readonly _currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>; - /** - * Kept in order to update the marker - * @private - */ - private readonly _leafletMap: UIEventSource; - /** - * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs - * @private - */ - private _lastUserRequest: Date; - /** - * A small flag on localstorage. If the user previously granted the geolocation, it will be set. - * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. - * - * Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately. - * If the user denies the geolocation this time, we unset this flag - * @private - */ - private readonly _previousLocationGrant: UIEventSource; - private readonly _layoutToUse: UIEventSource; + constructor( + currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, + leafletMap: UIEventSource, + layoutToUse: UIEventSource + ) { + const hasLocation = currentGPSLocation.map( + (location) => location !== undefined + ); + const previousLocationGrant = LocalStorageSource.Get( + "geolocation-permissions" + ); + const isActive = new UIEventSource(false); + super( + hasLocation.map( + (hasLocation) => { + if (hasLocation) { + return new CenterFlexedElement( + Img.AsImageElement(Svg.location, "", "width:1.5rem;height:1.5rem") + ); // crosshair_blue_ui() + } + if (isActive.data) { + return new CenterFlexedElement( + Img.AsImageElement(Svg.location, "", "width:1.5rem;height:1.5rem") + ); // crosshair_blue_center_ui + } + return new CenterFlexedElement( + Img.AsImageElement(Svg.location, "", "width:1.5rem;height:1.5rem") + ); //crosshair_ui + }, + [isActive] + ) + ); + this._isActive = isActive; + this._permission = new UIEventSource(""); + this._previousLocationGrant = previousLocationGrant; + this._currentGPSLocation = currentGPSLocation; + this._leafletMap = leafletMap; + this._layoutToUse = layoutToUse; + this._hasLocation = hasLocation; + const self = this; - constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, - leafletMap: UIEventSource, - layoutToUse: UIEventSource) { + const currentPointer = this._isActive.map( + (isActive) => { + if (isActive && !self._hasLocation.data) { + return "cursor-wait"; + } + return "cursor-pointer"; + }, + [this._hasLocation] + ); + currentPointer.addCallbackAndRun((pointerClass) => { + self.SetClass(pointerClass); + }); - const hasLocation = currentGPSLocation.map((location) => location !== undefined); - const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions") - const isActive = new UIEventSource(false); + this.onClick(() => self.init(true)); + this.init(false); + } - super( - hasLocation.map(hasLocation => { + private init(askPermission: boolean) { + const self = this; + const map = this._leafletMap.data; - if (hasLocation) { - return Svg.crosshair_blue_ui() - } - if (isActive.data) { - return Svg.crosshair_blue_center_ui(); - } - return Svg.crosshair_ui(); - }, [isActive]) + this._currentGPSLocation.addCallback((location) => { + self._previousLocationGrant.setData("granted"); + + const timeSinceRequest = + (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; + if (timeSinceRequest < 30) { + self.MoveToCurrentLoction(16); + } + + let color = "#1111cc"; + try { + color = getComputedStyle(document.body).getPropertyValue( + "--catch-detail-color" ); - this._isActive = isActive; - this._permission = new UIEventSource("") - this._previousLocationGrant = previousLocationGrant; - this._currentGPSLocation = currentGPSLocation; - this._leafletMap = leafletMap; - this._layoutToUse = layoutToUse; - this._hasLocation = hasLocation; - const self = this; + } catch (e) { + console.error(e); + } + const icon = L.icon({ + iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)), + iconSize: [40, 40], // size of the icon + iconAnchor: [20, 20], // point of the icon which will correspond to marker's location + }); - const currentPointer = this._isActive.map(isActive => { - if (isActive && !self._hasLocation.data) { - return "cursor-wait" - } - return "cursor-pointer" - }, [this._hasLocation]) - currentPointer.addCallbackAndRun(pointerClass => { - self.SetClass(pointerClass); - }) + const newMarker = L.marker(location.latlng, { icon: icon }); + newMarker.addTo(map); + if (self._marker !== undefined) { + map.removeLayer(self._marker); + } + self._marker = newMarker; + }); - this.onClick(() => self.init(true)) - this.init(false) - - } - - private init(askPermission: boolean) { - - const self = this; - const map = this._leafletMap.data; - - this._currentGPSLocation.addCallback((location) => { - self._previousLocationGrant.setData("granted"); - - const timeSinceRequest = (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; - if (timeSinceRequest < 30) { - self.MoveToCurrentLoction(16) - } - - let color = "#1111cc"; - try { - color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color") - } catch (e) { - console.error(e) - } - const icon = L.icon( - { - iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)), - iconSize: [40, 40], // size of the icon - iconAnchor: [20, 20], // point of the icon which will correspond to marker's location - }) - - const newMarker = L.marker(location.latlng, {icon: icon}); - newMarker.addTo(map); - - if (self._marker !== undefined) { - map.removeLayer(self._marker); - } - self._marker = newMarker; - }); - - try { - - navigator?.permissions?.query({name: 'geolocation'}) - ?.then(function (status) { - console.log("Geolocation is already", status) - if (status.state === "granted") { - self.StartGeolocating(false); - } - self._permission.setData(status.state); - status.onchange = function () { - self._permission.setData(status.state); - } - }); - - } catch (e) { - console.error(e) - } - if (askPermission) { - self.StartGeolocating(true); - } else if (this._previousLocationGrant.data === "granted") { - this._previousLocationGrant.setData(""); + try { + navigator?.permissions + ?.query({ name: "geolocation" }) + ?.then(function (status) { + console.log("Geolocation is already", status); + if (status.state === "granted") { self.StartGeolocating(false); - } + } + self._permission.setData(status.state); + status.onchange = function () { + self._permission.setData(status.state); + }; + }); + } catch (e) { + console.error(e); + } + if (askPermission) { + self.StartGeolocating(true); + } else if (this._previousLocationGrant.data === "granted") { + this._previousLocationGrant.setData(""); + self.StartGeolocating(false); + } + } + private locate() { + const self = this; + const map: any = this._leafletMap.data; + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + function (position) { + self._currentGPSLocation.setData({ + latlng: [position.coords.latitude, position.coords.longitude], + accuracy: position.coords.accuracy, + }); + }, + function () { + console.warn("Could not get location with navigator.geolocation"); + } + ); + return; + } else { + map.findAccuratePosition({ + maxWait: 10000, // defaults to 10000 + desiredAccuracy: 50, // defaults to 20 + }); + } + } + + private MoveToCurrentLoction(targetZoom = 16) { + const location = this._currentGPSLocation.data; + this._lastUserRequest = undefined; + + if ( + this._currentGPSLocation.data.latlng[0] === 0 && + this._currentGPSLocation.data.latlng[1] === 0 + ) { + console.debug("Not moving to GPS-location: it is null island"); + return; } - private locate() { - const self = this; - const map: any = this._leafletMap.data; + // We check that the GPS location is not out of bounds + const b = this._layoutToUse.data.lockLocation; + let inRange = true; + if (b) { + if (b !== true) { + // B is an array with our locklocation + inRange = + b[0][0] <= location.latlng[0] && + location.latlng[0] <= b[1][0] && + b[0][1] <= location.latlng[1] && + location.latlng[1] <= b[1][1]; + } + } + if (!inRange) { + console.log( + "Not zooming to GPS location: out of bounds", + b, + location.latlng + ); + } else { + this._leafletMap.data.setView(location.latlng, targetZoom); + } + } - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(function (position) { - self._currentGPSLocation.setData({ - latlng: [position.coords.latitude, position.coords.longitude], - accuracy: position.coords.accuracy - }); - }, function () { - console.warn("Could not get location with navigator.geolocation") - }); - return; - } else { - map.findAccuratePosition({ - maxWait: 10000, // defaults to 10000 - desiredAccuracy: 50 // defaults to 20 - }); - } + private StartGeolocating(zoomToGPS = true) { + const self = this; + console.log("Starting geolocation"); + + this._lastUserRequest = zoomToGPS ? new Date() : new Date(0); + if (self._permission.data === "denied") { + self._previousLocationGrant.setData(""); + return ""; + } + if (this._currentGPSLocation.data !== undefined) { + this.MoveToCurrentLoction(16); } - private MoveToCurrentLoction(targetZoom = 16) { - const location = this._currentGPSLocation.data; - this._lastUserRequest = undefined; + console.log("Searching location using GPS"); + this.locate(); - - if (this._currentGPSLocation.data.latlng[0] === 0 && this._currentGPSLocation.data.latlng[1] === 0) { - console.debug("Not moving to GPS-location: it is null island") - return; + if (!self._isActive.data) { + self._isActive.setData(true); + Utils.DoEvery(60000, () => { + if (document.visibilityState !== "visible") { + console.log("Not starting gps: document not visible"); + return; } - - // We check that the GPS location is not out of bounds - const b = this._layoutToUse.data.lockLocation - let inRange = true; - if (b) { - if (b !== true) { - // B is an array with our locklocation - inRange = b[0][0] <= location.latlng[0] && location.latlng[0] <= b[1][0] && - b[0][1] <= location.latlng[1] && location.latlng[1] <= b[1][1]; - } - } - if (!inRange) { - console.log("Not zooming to GPS location: out of bounds", b, location.latlng) - } else { - this._leafletMap.data.setView( - location.latlng, targetZoom - ); - } - } - - private StartGeolocating(zoomToGPS = true) { - const self = this; - console.log("Starting geolocation") - - this._lastUserRequest = zoomToGPS ? new Date() : new Date(0); - if (self._permission.data === "denied") { - self._previousLocationGrant.setData(""); - return ""; - } - if (this._currentGPSLocation.data !== undefined) { - this.MoveToCurrentLoction(16) - } - - - console.log("Searching location using GPS") this.locate(); - - - if (!self._isActive.data) { - self._isActive.setData(true); - Utils.DoEvery(60000, () => { - - if (document.visibilityState !== "visible") { - console.log("Not starting gps: document not visible") - return; - } - this.locate(); - }) - } + }); } - -} \ No newline at end of file + } +} diff --git a/Svg.ts b/Svg.ts index 9a5c94b8f..0ebc3f924 100644 --- a/Svg.ts +++ b/Svg.ts @@ -174,6 +174,11 @@ export default class Svg { public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} + public static location = " " + public static location_img = Img.AsImageElement(Svg.location) + public static location_svg() { return new Img(Svg.location, true);} + public static location_ui() { return new FixedUiElement(Svg.location_img);} + public static logo = " image/svg+xml " public static logo_img = Img.AsImageElement(Svg.logo) public static logo_svg() { return new Img(Svg.logo, true);} @@ -199,6 +204,11 @@ export default class Svg { public static mapillary_black_svg() { return new Img(Svg.mapillary_black, true);} public static mapillary_black_ui() { return new FixedUiElement(Svg.mapillary_black_img);} + public static min_zoom = " " + public static min_zoom_img = Img.AsImageElement(Svg.min_zoom) + public static min_zoom_svg() { return new Img(Svg.min_zoom, true);} + public static min_zoom_ui() { return new FixedUiElement(Svg.min_zoom_img);} + public static min = " image/svg+xml " public static min_img = Img.AsImageElement(Svg.min) public static min_svg() { return new Img(Svg.min, true);} @@ -244,6 +254,11 @@ export default class Svg { public static pin_svg() { return new Img(Svg.pin, true);} public static pin_ui() { return new FixedUiElement(Svg.pin_img);} + public static plus_zoom = " " + public static plus_zoom_img = Img.AsImageElement(Svg.plus_zoom) + public static plus_zoom_svg() { return new Img(Svg.plus_zoom, true);} + public static plus_zoom_ui() { return new FixedUiElement(Svg.plus_zoom_img);} + public static plus = " image/svg+xml " public static plus_img = Img.AsImageElement(Svg.plus) public static plus_svg() { return new Img(Svg.plus, true);} @@ -334,4 +349,4 @@ export default class Svg { public static wikipedia_svg() { return new Img(Svg.wikipedia, true);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} +public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"location.svg": Svg.location,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min-zoom.svg": Svg.min_zoom,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus-zoom.svg": Svg.plus_zoom,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/Base/CenterFlexedElement.ts b/UI/Base/CenterFlexedElement.ts new file mode 100644 index 000000000..5052c99e3 --- /dev/null +++ b/UI/Base/CenterFlexedElement.ts @@ -0,0 +1,32 @@ +import BaseUIElement from "../BaseUIElement"; + +export class CenterFlexedElement extends BaseUIElement { + private _html: string; + + constructor(html: string) { + super(); + this._html = html ?? ""; + } + + InnerRender(): string { + return this._html; + } + + protected InnerConstructElement(): HTMLElement { + const e = document.createElement("div"); + e.innerHTML = this._html; + e.style.display = "flex"; + e.style.height = "100%"; + e.style.width = "100%"; + e.style.flexDirection = "column"; + e.style.flexWrap = "nowrap"; + e.style.alignContent = "center"; + e.style.justifyContent = "center"; + e.style.alignItems = "center"; + return e; + } + + AsMarkdown(): string { + return this._html; + } +} diff --git a/UI/MapControlButton.ts b/UI/MapControlButton.ts index af1cef9a8..2ec07fcc5 100644 --- a/UI/MapControlButton.ts +++ b/UI/MapControlButton.ts @@ -5,11 +5,11 @@ import Combine from "./Base/Combine"; * A button floating above the map, in a uniform style */ export default class MapControlButton extends Combine { - - constructor(contents: BaseUIElement) { - super([contents]); - this.SetClass("relative block rounded-full w-10 h-10 p-1 pointer-events-auto z-above-map subtle-background") - this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);"); - } - -} \ No newline at end of file + constructor(contents: BaseUIElement) { + super([contents]); + this.SetClass( + "relative block rounded-full w-10 h-10 p-1 pointer-events-auto z-above-map subtle-background" + ); + this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);"); + } +} diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index 1ef8f94c8..82c1793ba 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -1,50 +1,36 @@ [ { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "direction_masked.svg", "license": "CC0", "sources": [] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "direction_outline.svg", "license": "CC0", "sources": [] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "direction_stroke.svg", "license": "CC0", "sources": [] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "SocialImageForeground.svg", "license": "CC-BY-SA", - "sources": [ - "https://mapcomplete.osm.be" - ] + "sources": ["https://mapcomplete.osm.be"] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "add.svg", "license": "CC0", "sources": [] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "addSmall.svg", "license": "CC0", "sources": [] @@ -56,33 +42,25 @@ "sources": [] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "arrow-left-smooth.svg", "license": "CC0", "sources": [] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "arrow-right-smooth.svg", "license": "CC0", "sources": [] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "back.svg", "license": "CC0", "sources": [] }, { - "authors": [ - "Github" - ], + "authors": ["Github"], "path": "bug.svg", "license": "MIT", "sources": [ @@ -93,35 +71,26 @@ { "path": "camera-plus.svg", "license": "CC-BY-SA 3.0", - "authors": [ - "Dave Gandy", - "Pieter Vander Vennet" - ], + "authors": ["Dave Gandy", "Pieter Vander Vennet"], "sources": [ "https://fontawesome.com/", "https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg" ] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "checkmark.svg", "license": "CC0", "sources": [] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "circle.svg", "license": "CC0", "sources": [] }, { - "authors": [ - "Pieter Vander Vennet" - ], + "authors": ["Pieter Vander Vennet"], "path": "clock.svg", "license": "CC0", "sources": [] @@ -163,9 +132,7 @@ "sources": [] }, { - "authors": [ - "Dave Gandy" - ], + "authors": ["Dave Gandy"], "path": "delete_icon.svg", "license": "CC-BY-SA", "sources": [ @@ -197,9 +164,7 @@ "sources": [] }, { - "authors": [ - "The Tango Desktop Project" - ], + "authors": ["The Tango Desktop Project"], "path": "floppy.svg", "license": "CC0", "sources": [ @@ -220,29 +185,19 @@ "sources": [] }, { - "authors": [ - "Timothy Miller" - ], + "authors": ["Timothy Miller"], "path": "home.svg", "license": "CC-BY-SA 3.0", - "sources": [ - "https://commons.wikimedia.org/wiki/File:Home-icon.svg" - ] + "sources": ["https://commons.wikimedia.org/wiki/File:Home-icon.svg"] }, { - "authors": [ - "Timothy Miller" - ], + "authors": ["Timothy Miller"], "path": "home_white_bg.svg", "license": "CC-BY-SA 3.0", - "sources": [ - "https://commons.wikimedia.org/wiki/File:Home-icon.svg" - ] + "sources": ["https://commons.wikimedia.org/wiki/File:Home-icon.svg"] }, { - "authors": [ - "JOSM Team" - ], + "authors": ["JOSM Team"], "path": "josm_logo.svg", "license": "CC0", "sources": [ @@ -265,9 +220,7 @@ { "path": "Ornament-Horiz-0.svg", "license": "CC-BY", - "authors": [ - "Nightwolfdezines" - ], + "authors": ["Nightwolfdezines"], "sources": [ "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" ] @@ -275,9 +228,7 @@ { "path": "Ornament-Horiz-1.svg", "license": "CC-BY", - "authors": [ - "Nightwolfdezines" - ], + "authors": ["Nightwolfdezines"], "sources": [ "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" ] @@ -285,9 +236,7 @@ { "path": "Ornament-Horiz-2.svg", "license": "CC-BY", - "authors": [ - "Nightwolfdezines" - ], + "authors": ["Nightwolfdezines"], "sources": [ "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" ] @@ -295,9 +244,7 @@ { "path": "Ornament-Horiz-3.svg", "license": "CC-BY", - "authors": [ - "Nightwolfdezines" - ], + "authors": ["Nightwolfdezines"], "sources": [ "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" ] @@ -305,9 +252,7 @@ { "path": "Ornament-Horiz-4.svg", "license": "CC-BY", - "authors": [ - "Nightwolfdezines" - ], + "authors": ["Nightwolfdezines"], "sources": [ "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" ] @@ -315,9 +260,7 @@ { "path": "Ornament-Horiz-5.svg", "license": "CC-BY", - "authors": [ - "Nightwolfdezines" - ], + "authors": ["Nightwolfdezines"], "sources": [ "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" ] @@ -325,9 +268,7 @@ { "path": "Ornament-Horiz-6.svg", "license": "CC-BY", - "authors": [ - "Nightwolfdezines" - ], + "authors": ["Nightwolfdezines"], "sources": [ "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" ] @@ -369,25 +310,16 @@ "sources": [] }, { - "authors": [ - "Pieter Vander Vennet", - " OSM" - ], + "authors": ["Pieter Vander Vennet", " OSM"], "path": "mapcomplete_logo.svg", "license": "Logo; CC-BY-SA", - "sources": [ - "https://mapcomplete.osm.be" - ] + "sources": ["https://mapcomplete.osm.be"] }, { - "authors": [ - "Mapillary" - ], + "authors": ["Mapillary"], "path": "mapillary.svg", "license": "Logo; All rights reserved", - "sources": [ - "https://mapillary.com/" - ] + "sources": ["https://mapillary.com/"] }, { "authors": [], @@ -411,32 +343,22 @@ "authors": [], "path": "osm-copyright.svg", "license": "logo; all rights reserved", - "sources": [ - "https://www.OpenStreetMap.org" - ] + "sources": ["https://www.OpenStreetMap.org"] }, { - "authors": [ - "OpenStreetMap U.S. Chapter" - ], + "authors": ["OpenStreetMap U.S. Chapter"], "path": "osm-logo-us.svg", "license": "Logo", - "sources": [ - "https://www.openstreetmap.us/" - ] + "sources": ["https://www.openstreetmap.us/"] }, { "authors": [], "path": "osm-logo.svg", "license": "logo; all rights reserved", - "sources": [ - "https://www.OpenStreetMap.org" - ] + "sources": ["https://www.OpenStreetMap.org"] }, { - "authors": [ - "GitHub Octicons" - ], + "authors": ["GitHub Octicons"], "path": "pencil.svg", "license": "MIT", "sources": [ @@ -445,14 +367,10 @@ ] }, { - "authors": [ - "@ tyskrat" - ], + "authors": ["@ tyskrat"], "path": "phone.svg", "license": "CC-BY 3.0", - "sources": [ - "https://www.onlinewebfonts.com/icon/1059" - ] + "sources": ["https://www.onlinewebfonts.com/icon/1059"] }, { "authors": [], @@ -467,14 +385,10 @@ "sources": [] }, { - "authors": [ - "@fatih" - ], + "authors": ["@fatih"], "path": "pop-out.svg", "license": "CC-BY 3.0", - "sources": [ - "https://www.onlinewebfonts.com/icon/2151" - ] + "sources": ["https://www.onlinewebfonts.com/icon/2151"] }, { "authors": [], @@ -489,9 +403,7 @@ "sources": [] }, { - "authors": [ - "OOjs UI Team and other contributors" - ], + "authors": ["OOjs UI Team and other contributors"], "path": "search.svg", "license": "MIT", "sources": [ @@ -518,19 +430,13 @@ "sources": [] }, { - "authors": [ - "@felpgrc" - ], + "authors": ["@felpgrc"], "path": "statistics.svg", "license": "CC-BY 3.0", - "sources": [ - "https://www.onlinewebfonts.com/icon/197818" - ] + "sources": ["https://www.onlinewebfonts.com/icon/197818"] }, { - "authors": [ - "MGalloway (WMF)" - ], + "authors": ["MGalloway (WMF)"], "path": "translate.svg", "license": "CC-BY-SA 3.0", "sources": [ @@ -544,43 +450,45 @@ "sources": [] }, { - "authors": [ - "Wikidata" - ], + "authors": ["Wikidata"], "path": "wikidata.svg", "license": "Logo; All rights reserved", - "sources": [ - "https://www.wikidata.org" - ] + "sources": ["https://www.wikidata.org"] }, { - "authors": [ - "Wikimedia" - ], + "authors": ["Wikimedia"], "path": "wikimedia-commons-white.svg", "license": "Logo; All rights reserved", - "sources": [ - "https://commons.wikimedia.org" - ] + "sources": ["https://commons.wikimedia.org"] }, { - "authors": [ - "Wikipedia" - ], + "authors": ["Wikipedia"], "path": "wikipedia.svg", "license": "Logo; All rights reserved", - "sources": [ - "https://www.wikipedia.org/" - ] + "sources": ["https://www.wikipedia.org/"] }, { - "authors": [ - "Mapillary" - ], + "authors": ["Mapillary"], "path": "mapillary_black.svg", "license": "Logo; All rights reserved", - "sources": [ - "https://www.mapillary.com/" - ] + "sources": ["https://www.mapillary.com/"] + }, + { + "authors": ["Hannah Declerck"], + "path": "location.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": ["Hannah Declerck"], + "path": "min-zoom.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": ["Hannah Declerck"], + "path": "plus-zoom.svg", + "license": "CC0", + "sources": [] } -] \ No newline at end of file +] diff --git a/assets/svg/location.svg b/assets/svg/location.svg new file mode 100644 index 000000000..3c4d168d6 --- /dev/null +++ b/assets/svg/location.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/svg/min-zoom.svg b/assets/svg/min-zoom.svg new file mode 100644 index 000000000..f617af197 --- /dev/null +++ b/assets/svg/min-zoom.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/plus-zoom.svg b/assets/svg/plus-zoom.svg new file mode 100644 index 000000000..fc9ff3c80 --- /dev/null +++ b/assets/svg/plus-zoom.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/index.css b/index.css index 47178a06f..5ddc0efe6 100644 --- a/index.css +++ b/index.css @@ -48,7 +48,7 @@ :root { - --subtle-detail-color: #e5f5ff; + --subtle-detail-color: #007759; --subtle-detail-color-contrast: black; --subtle-detail-color-light-contrast: lightgrey;