diff --git a/Logic/Actors/AvailableBaseLayersImplementation.ts b/Logic/Actors/AvailableBaseLayersImplementation.ts index 44ea0ba8f..94f22e73c 100644 --- a/Logic/Actors/AvailableBaseLayersImplementation.ts +++ b/Logic/Actors/AvailableBaseLayersImplementation.ts @@ -64,6 +64,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL console.warn("Editor layer index: name not defined on ", props) continue } + const leafletLayer: () => TileLayer = () => AvailableBaseLayersImplementation.CreateBackgroundLayer( props.id, @@ -83,7 +84,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL min_zoom: props.min_zoom ?? 1, name: props.name, layer: leafletLayer, - feature: layer, + feature: layer.geometry !== null ? layer : null, isBest: props.best ?? false, category: props.category }); @@ -96,14 +97,14 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL X; // Import X to make sure the namespace is not optimized away function l(id: string, name: string): BaseLayer { try { - const layer: any = () => L.tileLayer.provider(id, undefined); + const layer: any = L.tileLayer.provider(id, undefined); return { feature: null, id: id, name: name, - layer: layer, - min_zoom: layer.minzoom, - max_zoom: layer.maxzoom, + layer: () => L.tileLayer.provider(id, undefined), + min_zoom: 1, + max_zoom: layer.options.maxZoom, category: "osmbasedmap", isBest: false } @@ -114,7 +115,6 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL } const layers = [ - l("CyclOSM", "CyclOSM - A bicycle oriented map"), l("Stamen.TonerLite", "Toner Lite (by Stamen)"), l("Stamen.TonerBackground", "Toner Background - no labels (by Stamen)"), l("Stamen.Watercolor", "Watercolor (by Stamen)"), @@ -193,37 +193,20 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL subdomains: domains }); } - + public AvailableLayersAt(location: UIEventSource): UIEventSource { - const source = location.map( + return UIEventSource.ListStabilized(location.map( (currentLocation) => { - if (currentLocation === undefined) { return this.layerOverview; } - - const currentLayers = source?.data; // A bit unorthodox - I know - const newLayers = this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); - - if (currentLayers === undefined) { - return newLayers; - } - if (newLayers.length !== currentLayers.length) { - return newLayers; - } - for (let i = 0; i < newLayers.length; i++) { - if (newLayers[i].name !== currentLayers[i].name) { - return newLayers; - } - } - - return currentLayers; - }); - return source; + return this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); + })); } public SelectBestLayerAccordingTo(location: UIEventSource, preferedCategory: UIEventSource): UIEventSource { - return this.AvailableLayersAt(location).map(available => { + return this.AvailableLayersAt(location) + .map(available => { // First float all 'best layers' to the top available.sort((a, b) => { if (a.isBest && b.isBest) { @@ -267,6 +250,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL }, [preferedCategory]) } + private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { const availableLayers = [this.osmCarto] const globalLayers = []; diff --git a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts b/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts index 283341a87..c1da5ab37 100644 --- a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts +++ b/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts @@ -11,27 +11,28 @@ import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; import {BBox} from "../../BBox"; import SimpleFeatureSource from "../Sources/SimpleFeatureSource"; import FilteredLayer from "../../../Models/FilteredLayer"; +import Loc from "../../../Models/Loc"; export default class SaveTileToLocalStorageActor { private readonly visitedTiles: UIEventSource> private readonly _layer: LayerConfig; - private readonly _flayer : FilteredLayer + private readonly _flayer: FilteredLayer private readonly initializeTime = new Date() constructor(layer: FilteredLayer) { this._flayer = layer this._layer = layer.layerDef - this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, - {defaultValue: new Map(), }) + this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, + {defaultValue: new Map(),}) this.visitedTiles.stabilized(100).addCallbackAndRunD(tiles => { for (const key of Array.from(tiles.keys())) { const tileFreshness = tiles.get(key) - const toOld = (this.initializeTime.getTime() - tileFreshness.getTime()) > 1000 * this._layer.maxAgeOfCache - if(toOld){ + const toOld = (this.initializeTime.getTime() - tileFreshness.getTime()) > 1000 * this._layer.maxAgeOfCache + if (toOld) { // Purge this tile this.SetIdb(key, undefined) - console.debug("Purging tile",this._layer.id,key) + console.debug("Purging tile", this._layer.id, key) tiles.delete(key) } } @@ -39,66 +40,70 @@ export default class SaveTileToLocalStorageActor { return true; }) } + - public LoadTilesFromDisk(currentBounds: UIEventSource, - registerFreshness: (tileId: number, freshness: Date) => void, - registerTile: ((src: FeatureSource & Tiled ) => void)){ + public LoadTilesFromDisk(currentBounds: UIEventSource, location: UIEventSource, + registerFreshness: (tileId: number, freshness: Date) => void, + registerTile: ((src: FeatureSource & Tiled) => void)) { const self = this; + const loadedTiles = new Set() this.visitedTiles.addCallbackD(tiles => { - if(tiles.size === 0){ + if (tiles.size === 0) { // We don't do anything yet as probably not yet loaded from disk // We'll unregister later on return; } - for (const key of Array.from(tiles.keys())) { - const tileFreshness = tiles.get(key) - if(tileFreshness > self.initializeTime){ - // This tile is loaded by another source - continue + currentBounds.addCallbackAndRunD(bbox => { + + if(self._layer.minzoomVisible > location.data.zoom){ + // Not enough zoom + return; } - registerFreshness(key, tileFreshness) - - const tileBbox = BBox.fromTileIndex(key) - currentBounds.addCallbackAndRunD(bbox => { - if(bbox.overlapsWith(tileBbox)){ - // The current tile should be loaded from disk - this.GetIdb(key).then((features:{feature: any, freshness: Date}[] ) => { - console.log("Loaded tile "+self._layer.id+"_"+key+" from disk") - const src = new SimpleFeatureSource(self._flayer, key, new UIEventSource<{feature: any; freshness: Date}[]>(features)) - registerTile(src) - }) - return true; // only load once: unregister + + // Iterate over all available keys in the local storage, check which are needed and fresh enough + for (const key of Array.from(tiles.keys())) { + const tileFreshness = tiles.get(key) + if (tileFreshness > self.initializeTime) { + // This tile is loaded by another source + continue } - }) - - } - + + registerFreshness(key, tileFreshness) + const tileBbox = BBox.fromTileIndex(key) + if (!bbox.overlapsWith(tileBbox)) { + continue; + } + if (loadedTiles.has(key)) { + // Already loaded earlier + continue + } + loadedTiles.add(key) + this.GetIdb(key).then((features: { feature: any, freshness: Date }[]) => { + console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk") + const src = new SimpleFeatureSource(self._flayer, key, new UIEventSource<{ feature: any; freshness: Date }[]>(features)) + registerTile(src) + }) + } + }) + return true; // Remove the callback - + }) } - private SetIdb(tileIndex, data){ - IdbLocalStorage.SetDirectly(this._layer.id+"_"+tileIndex, data) - } - - private GetIdb(tileIndex){ - return IdbLocalStorage.GetDirectly(this._layer.id+"_"+tileIndex) - } - - public addTile(tile: FeatureSource & Tiled){ + public addTile(tile: FeatureSource & Tiled) { const self = this tile.features.addCallbackAndRunD(features => { const now = new Date() if (features.length > 0) { - self.SetIdb(tile.tileIndex, features) + self.SetIdb(tile.tileIndex, features) } // We _still_ write the time to know that this tile is empty! this.MarkVisited(tile.tileIndex, now) }) } - + public poison(lon: number, lat: number) { for (let z = 0; z < 25; z++) { const {x, y} = Tiles.embedded_tile(lat, lon, z) @@ -110,6 +115,14 @@ export default class SaveTileToLocalStorageActor { public MarkVisited(tileId: number, freshness: Date) { this.visitedTiles.data.set(tileId, freshness) - this.visitedTiles.ping() + this.visitedTiles.ping() + } + + private SetIdb(tileIndex, data) { + IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data) + } + + private GetIdb(tileIndex) { + return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex) } } \ No newline at end of file diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 56ed5cfd5..2effce698 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -157,7 +157,7 @@ export default class FeaturePipeline { // We load the cached values and register them // Getting data from upstream happens a bit lower localTileSaver.LoadTilesFromDisk( - state.currentBounds, + state.currentBounds, state.locationControl, (tileIndex, freshness) => self.freshnesses.get(id).addTileLoad(tileIndex, freshness), (tile) => { new RegisteringAllFromFeatureSourceActor(tile) @@ -221,7 +221,13 @@ export default class FeaturePipeline { state.filteredLayers.data.forEach(flayer => { const layer = flayer.layerDef if (layer.maxAgeOfCache > 0) { - self.localStorageSavers.get(layer.id).MarkVisited(tileId, new Date()) + const saver = self.localStorageSavers.get(layer.id) + if(saver === undefined){ + console.warn("No local storage saver found for ", layer.id) + }else{ + + saver.MarkVisited(tileId, new Date()) + } } self.freshnesses.get(layer.id).addTileLoad(tileId, new Date()) }) diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index 92a2cd1f3..5a728cf22 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -3,7 +3,6 @@ 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"; @@ -84,35 +83,17 @@ export default class MapState extends UserRelatedState { 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 - ); - + let defaultLayer = AvailableBaseLayers.osmCarto + const available = this.availableBackgroundLayers.data; + for (const layer of available) { + if (this.backgroundLayerId.data === layer.id) { + defaultLayer = layer; + } + } + const self = this + this.backgroundLayer = new UIEventSource(defaultLayer) + this.backgroundLayer.addCallbackAndRunD(layer => self.backgroundLayerId.setData(layer.id)) + const attr = new Attribution( this.locationControl, this.osmConnection.userDetails, @@ -334,10 +315,7 @@ export default class MapState extends UserRelatedState { const filtersPerName = new Map() 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 => { + flayer.appliedFilters.map(filters => (filters ?? []).map(f => f.filter.id + "." + f.selected).join(","), [], textual => { if (textual.length === 0) { return empty } diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 1a0fd4c02..d4eaf6b51 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -13,7 +13,8 @@ export interface MinimapOptions { attribution?: BaseUIElement | boolean, onFullyLoaded?: (leaflet: L.Map) => void, leafletMap?: UIEventSource, - lastClickLocation?: UIEventSource<{ lat: number, lon: number }> + lastClickLocation?: UIEventSource<{ lat: number, lon: number }>, + addLayerControl?: boolean | false } export interface MinimapObj { diff --git a/UI/Base/MinimapImplementation.ts b/UI/Base/MinimapImplementation.ts index 3ade20f17..4d93c4fae 100644 --- a/UI/Base/MinimapImplementation.ts +++ b/UI/Base/MinimapImplementation.ts @@ -10,6 +10,7 @@ import Minimap, {MinimapObj, MinimapOptions} from "./Minimap"; import {BBox} from "../../Logic/BBox"; import 'leaflet-polylineoffset' import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; +import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"; export default class MinimapImplementation extends BaseUIElement implements MinimapObj { private static _nextId = 0; @@ -24,6 +25,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini private readonly _attribution: BaseUIElement | boolean; private readonly _lastClickLocation: UIEventSource<{ lat: number; lon: number }>; private readonly _bounds: UIEventSource | undefined; + private readonly _addLayerControl: boolean; private constructor(options: MinimapOptions) { super() @@ -38,6 +40,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini this._onFullyLoaded = options.onFullyLoaded this._attribution = options.attribution this._lastClickLocation = options.lastClickLocation; + this._addLayerControl = options.addLayerControl ?? false MinimapImplementation._nextId++ } @@ -131,6 +134,17 @@ export default class MinimapImplementation extends BaseUIElement implements Mini }); resizeObserver.observe(div); + + if (this._addLayerControl) { + const switcher = new BackgroundMapSwitch({ + locationControl: this._location, + backgroundLayer: this._background + }, + this._background + ).SetClass("top-0 right-0 z-above-map absolute") + wrapper.appendChild(switcher.ConstructElement()) + } + return wrapper; } diff --git a/UI/BigComponents/BackgroundMapSwitch.ts b/UI/BigComponents/BackgroundMapSwitch.ts index 64aba12a5..5af1c305c 100644 --- a/UI/BigComponents/BackgroundMapSwitch.ts +++ b/UI/BigComponents/BackgroundMapSwitch.ts @@ -1,24 +1,140 @@ import Combine from "../Base/Combine"; import {UIEventSource} from "../../Logic/UIEventSource"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import State from "../../State"; import Loc from "../../Models/Loc"; import Svg from "../../Svg"; -import {VariableUiElement} from "../Base/VariableUIElement"; import Toggle from "../Input/Toggle"; +import BaseLayer from "../../Models/BaseLayer"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import BaseUIElement from "../BaseUIElement"; +import {GeoOperations} from "../../Logic/GeoOperations"; class SingleLayerSelectionButton extends Toggle { - constructor(state: { - locationControl: UIEventSource - }, prefered: string) { - const layer = AvailableBaseLayers.SelectBestLayerAccordingTo(state.locationControl, new UIEventSource(prefered)) - const layerIsCorrectType = layer.map(bl => bl?.category === prefered) + + public readonly activate: () => void + + /** + * + * The SingeLayerSelectionButton also acts as an actor to keep the layers in check + * + * It works the following way: + * + * - It has a boolean state to indicate wether or not the button is active + * - It keeps track of the available layers + */ + constructor( + locationControl: UIEventSource, + options: { + currentBackground: UIEventSource, + preferredType: string, + preferredLayer?: BaseLayer, + notAvailable?: () => void + }) { + + + const prefered = options.preferredType + const previousLayer = new UIEventSource(options.preferredLayer) + + const unselected = SingleLayerSelectionButton.getIconFor(prefered) + .SetClass("rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-invisible") + + const selected = SingleLayerSelectionButton.getIconFor(prefered) + .SetClass("rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-attention-catch") + + + const available = AvailableBaseLayers + .SelectBestLayerAccordingTo(locationControl, new UIEventSource(options.preferredType)) + + let toggle: BaseUIElement = new Toggle( + selected, + unselected, + options.currentBackground.map(bg => bg.category === options.preferredType) + ) + super( - SingleLayerSelectionButton.getIconFor(prefered).SetClass("rounded-full p-3 h-10 w-10"), + toggle, undefined, - layerIsCorrectType + available.map(av => av.category === options.preferredType) ); + + /** + * Checks that the previous layer is still usable on the current location. + * If not, clears the 'previousLayer' + */ + function checkPreviousLayer() { + if (previousLayer.data === undefined) { + return + } + if (previousLayer.data.feature === null || previousLayer.data.feature === undefined) { + // Global layer + return + } + const loc = locationControl.data + if (!GeoOperations.inside([loc.lon, loc.lat], previousLayer.data.feature)) { + // The previous layer is out of bounds + previousLayer.setData(undefined) + } + } + + unselected.onClick(() => { + // Note: a check if 'available' has the correct type is not needed: + // Unselected will _not_ be visible if availableBaseLayer has a wrong type! + checkPreviousLayer() + + previousLayer.setData(previousLayer.data ?? available.data) + options.currentBackground.setData(previousLayer.data) + }) + + + available.addCallbackAndRunD(availableLayer => { + + if (previousLayer.data === undefined) { + // PreviousLayer is unset -> we definitively weren't using this category -> no need to switch + return; + } + if (options.currentBackground.data?.id !== previousLayer.data?.id) { + // The previously used layer doesn't match the current layer -> no need to switch + return; + } + + if (availableLayer.category === options.preferredType) { + // Allright, we can set this different layer + options.currentBackground.setData(availableLayer) + previousLayer.setData(availableLayer) + } else { + // Uh oh - no correct layer is available... We pass the torch! + if (options.notAvailable !== undefined) { + options.notAvailable() + } else { + // Fallback to OSM carto + options.currentBackground.setData(AvailableBaseLayers.osmCarto) + } + } + }) + + options.currentBackground.addCallbackAndRunD(background => { + if (background.category === options.preferredType) { + previousLayer.setData(background) + } + }) + + this.activate = () => { + checkPreviousLayer() + if (available.data.category !== options.preferredType) { + // This object can't help either - pass the torch! + if (options.notAvailable !== undefined) { + options.notAvailable() + } else { + // Fallback to OSM carto + options.currentBackground.setData(AvailableBaseLayers.osmCarto) + } + return; + } + + previousLayer.setData(previousLayer.data ?? available.data) + options.currentBackground.setData(previousLayer.data) + } + } private static getIconFor(type: string) { @@ -27,27 +143,54 @@ class SingleLayerSelectionButton extends Toggle { return Svg.generic_map_svg() case "photo": return Svg.satellite_svg() + case "osmbasedmap": + return Svg.osm_logo_svg() default: return Svg.generic_map_svg() } } } -export default class BackgroundMapSwitch extends VariableUiElement { +export default class BackgroundMapSwitch extends Combine { constructor( state: { - locationControl: UIEventSource + locationControl: UIEventSource, + backgroundLayer: UIEventSource }, - options?: { - allowedLayers?: UIEventSource - } + currentBackground: UIEventSource, + preferredCategory?: string ) { - options = options ?? {} - options.allowedLayers = options.allowedLayers ?? new UIEventSource(["photo", "map"]) + const allowedCategories = ["osmbasedmap", "photo", "map"] + const previousLayer = state.backgroundLayer.data + const buttons = [] + let activatePrevious: () => void = undefined + for (const category of allowedCategories) { + let preferredLayer = undefined + if (previousLayer.category === category) { + preferredLayer = previousLayer + } - super(options.allowedLayers.map(layers => new Combine(layers.map(prefered => new SingleLayerSelectionButton(state, prefered))))); + const button = new SingleLayerSelectionButton( + state.locationControl, + { + preferredType: category, + preferredLayer: preferredLayer, + currentBackground: currentBackground, + notAvailable: activatePrevious + }) + activatePrevious = button.activate + if (category === preferredCategory) { + button.activate() + } + buttons.push(button) + } + + // Selects the initial map + + super(buttons) + this.SetClass("flex") } } \ No newline at end of file diff --git a/UI/BigComponents/CopyrightPanel.ts b/UI/BigComponents/CopyrightPanel.ts index 8b92d77ae..d88b1a36f 100644 --- a/UI/BigComponents/CopyrightPanel.ts +++ b/UI/BigComponents/CopyrightPanel.ts @@ -1,7 +1,5 @@ import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; -import Attribution from "./Attribution"; -import State from "../../State"; import {UIEventSource} from "../../Logic/UIEventSource"; import {FixedUiElement} from "../Base/FixedUiElement"; import * as licenses from "../../assets/generated/license_info.json" @@ -22,6 +20,7 @@ import Toggle from "../Input/Toggle"; import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import Constants from "../../Models/Constants"; import PrivacyPolicy from "./PrivacyPolicy"; +import ContributorCount from "../../Logic/ContributorCount"; /** * The attribution panel shown on mobile @@ -36,7 +35,7 @@ export default class CopyrightPanel extends Combine { currentBounds: UIEventSource, locationControl: UIEventSource, osmConnection: OsmConnection - }, contributions: UIEventSource>) { + }) { const t = Translations.t.general.attribution const layoutToUse = state.layoutToUse @@ -103,6 +102,8 @@ export default class CopyrightPanel extends Combine { maintainer = Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.maintainer}) } + const contributions = new ContributorCount(state).Contributors + super([ Translations.t.general.attribution.attributionContent, new FixedUiElement("MapComplete "+Constants.vNumber).SetClass("font-bold"), @@ -144,8 +145,7 @@ export default class CopyrightPanel extends Combine { })), CopyrightPanel.CodeContributors(), new Title(t.iconAttribution.title, 3), - ...iconAttributions, - new PrivacyPolicy() + ...iconAttributions ].map(e => e?.SetClass("mt-4"))); this.SetClass("flex flex-col link-underline overflow-hidden") this.SetStyle("max-width: calc(100vw - 3em); width: 40rem; margin-left: 0.75rem; margin-right: 0.5rem") diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 38ad51ddb..957998b2e 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -17,6 +17,9 @@ import UserRelatedState from "../../Logic/State/UserRelatedState"; import Loc from "../../Models/Loc"; import BaseLayer from "../../Models/BaseLayer"; import FilteredLayer from "../../Models/FilteredLayer"; +import CopyrightPanel from "./CopyrightPanel"; +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; +import PrivacyPolicy from "./PrivacyPolicy"; export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { @@ -29,6 +32,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { featureSwitchShareScreen: UIEventSource, featureSwitchMoreQuests: UIEventSource, locationControl: UIEventSource, + featurePipeline: FeaturePipeline, backgroundLayer: UIEventSource, filteredLayers: UIEventSource } & UserRelatedState) { @@ -46,6 +50,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { osmConnection: OsmConnection, featureSwitchShareScreen: UIEventSource, featureSwitchMoreQuests: UIEventSource, + featurePipeline: FeaturePipeline, locationControl: UIEventSource, backgroundLayer: UIEventSource, filteredLayers: UIEventSource } & UserRelatedState, isShown: UIEventSource): @@ -55,16 +60,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [ {header: ``, content: welcome}, - { - header: Svg.osm_logo_img, - content: Translations.t.general.openStreetMapIntro.SetClass("link-underline") - }, - ] - if (state.featureSwitchShareScreen.data) { - tabs.push({header: Svg.share_img, content: new ShareScreen(state)}); - } if (state.featureSwitchMoreQuests.data) { tabs.push({ @@ -77,6 +74,31 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { }); } + + if (state.featureSwitchShareScreen.data) { + tabs.push({header: Svg.share_img, content: new ShareScreen(state)}); + } + + const copyright = { + header: Svg.copyright_svg(), + content: + new Combine( + [ + Translations.t.general.openStreetMapIntro.SetClass("link-underline"), + Translations.t.general.attribution.attributionTitle, + new CopyrightPanel(state) + + ] + ) + } + tabs.push(copyright) + + const privacy = { + header: Svg.eye_svg(), + content: new PrivacyPolicy() + } + tabs.push(privacy) + return tabs; } @@ -85,6 +107,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { osmConnection: OsmConnection, featureSwitchShareScreen: UIEventSource, featureSwitchMoreQuests: UIEventSource, + featurePipeline: FeaturePipeline, locationControl: UIEventSource, backgroundLayer: UIEventSource, filteredLayers: UIEventSource } & UserRelatedState, currentTab: UIEventSource, isShown: UIEventSource) { diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index b0fc84da2..ef367abaa 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -1,8 +1,6 @@ import Combine from "../Base/Combine"; import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import Translations from "../i18n/Translations"; -import CopyrightPanel from "./CopyrightPanel"; -import ContributorCount from "../../Logic/ContributorCount"; import Toggle from "../Input/Toggle"; import MapControlButton from "../MapControlButton"; import Svg from "../../Svg"; @@ -16,6 +14,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import FilteredLayer from "../../Models/FilteredLayer"; import BaseLayer from "../../Models/BaseLayer"; import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import BackgroundMapSwitch from "./BackgroundMapSwitch"; export default class LeftControls extends Combine { @@ -38,23 +37,6 @@ export default class LeftControls extends Combine { copyrightViewIsOpened: UIEventSource }) { - const toggledCopyright = new ScrollableFullScreen( - () => Translations.t.general.attribution.attributionTitle.Clone(), - () => - new CopyrightPanel( - state, - new ContributorCount(state).Contributors - ), - "copyright", - guiState.copyrightViewIsOpened - ); - - const copyrightButton = new Toggle( - toggledCopyright, - new MapControlButton(Svg.copyright_svg()) - .onClick(() => toggledCopyright.isShown.setData(true)), - toggledCopyright.isShown - ).SetClass("p-0.5"); const toggledDownload = new Toggle( new AllDownloads( @@ -93,10 +75,10 @@ export default class LeftControls extends Combine { state.featureSwitchFilter ); - super([filterButton, downloadButtonn, - copyrightButton]) + new BackgroundMapSwitch(state, state.backgroundLayer) + ]) this.SetClass("flex flex-col") diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index df7958de7..99c6f8dc7 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -150,7 +150,8 @@ export default class LocationInput extends InputElement implements MinimapO background: this.mapBackground, attribution: this.mapBackground !== State.state?.backgroundLayer, lastClickLocation: this.clickLocation, - bounds: this._bounds + bounds: this._bounds, + addLayerControl: true } ) this.leafletMap = this.map.leafletMap diff --git a/UI/Popup/MoveWizard.ts b/UI/Popup/MoveWizard.ts index b86a55937..212494023 100644 --- a/UI/Popup/MoveWizard.ts +++ b/UI/Popup/MoveWizard.ts @@ -18,6 +18,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import MoveConfig from "../../Models/ThemeConfig/MoveConfig"; import {ElementStorage} from "../../Logic/ElementStorage"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import BaseLayer from "../../Models/BaseLayer"; interface MoveReason { text: Translation | string, @@ -133,10 +134,12 @@ export default class MoveWizard extends Toggle { background = reason.background } + const preferredBackground = AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background)).data const locationInput = new LocationInput({ minZoom: reason.minZoom, centerLocation: loc, - mapBackground: AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background)) + mapBackground: new UIEventSource(preferredBackground) // We detach the layer + }) if (reason.lockBounds) { diff --git a/assets/layers/gps_location_history/gps_location_history.json b/assets/layers/gps_location_history/gps_location_history.json index 988016183..056ce5980 100644 --- a/assets/layers/gps_location_history/gps_location_history.json +++ b/assets/layers/gps_location_history/gps_location_history.json @@ -4,7 +4,8 @@ "minzoom": 0, "source": { "osmTags": "user:location=yes", - "maxCacheAge": 604800 + "#": "Cache is disabled here as these points are kept seperately", + "maxCacheAge": 0 }, "mapRendering": null } \ No newline at end of file diff --git a/assets/svg/copyright.svg b/assets/svg/copyright.svg index 0483bef25..bc10eaf78 100644 --- a/assets/svg/copyright.svg +++ b/assets/svg/copyright.svg @@ -1,4 +1,53 @@ - - - \ No newline at end of file + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/svg/eye.svg b/assets/svg/eye.svg new file mode 100644 index 000000000..92566e947 --- /dev/null +++ b/assets/svg/eye.svg @@ -0,0 +1,59 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index ae0b757cd..844bc24ce 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -487,6 +487,16 @@ "authors": [], "sources": [] }, + { + "path": "eye.svg", + "license": "CC-BY-SA 3.0 Unported", + "authors": [ + "Dave Gandy" + ], + "sources": [ + "https://en.wikipedia.org/wiki/File:Eye_open_font_awesome.svg" + ] + }, { "path": "filter.svg", "license": "CC0", diff --git a/assets/svg/osm-logo.svg b/assets/svg/osm-logo.svg index 246a82440..eab6bc5b9 100644 --- a/assets/svg/osm-logo.svg +++ b/assets/svg/osm-logo.svg @@ -4,7 +4,9 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns="http://www.w3.org/2000/svg" width="256" height="256" id="svg3038" version="1.1" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 256 256" + id="svg3038" version="1.1" inkscape:version="0.48.2 r9819" sodipodi:docname="Public-images-osm_logo.svg" inkscape:export-filename="/home/fred/bla.png" inkscape:export-xdpi="180" inkscape:export-ydpi="180" sodipodi:version="0.32" inkscape:output_extension="org.inkscape.output.svg.inkscape"> diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 72f9f9cfb..2a9b65a04 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -760,14 +760,14 @@ video { top: 0px; } -.left-0 { - left: 0px; -} - .right-0 { right: 0px; } +.left-0 { + left: 0px; +} + .isolate { isolation: isolate; } @@ -1920,6 +1920,10 @@ li::marker { border: 5px solid var(--catch-detail-color); } +.border-invisible { + border: 5px solid #00000000; +} + .border-attention { border-color: var(--catch-detail-color); } diff --git a/index.css b/index.css index 6efe3035a..90988d29e 100644 --- a/index.css +++ b/index.css @@ -208,6 +208,10 @@ li::marker { border: 5px solid var(--catch-detail-color); } +.border-invisible { + border: 5px solid #00000000; +} + .border-attention { border-color: var(--catch-detail-color); } diff --git a/langs/nl.json b/langs/nl.json index f914b98ba..cdba7145c 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -71,7 +71,7 @@ "emailOf": "Wat is het email-adres van {category}?", "emailIs": "Het email-adres van {category} is {email}" }, - "openStreetMapIntro": "

Een open kaart

Zou het niet fantastisch zijn als er een open kaart zou zijn die door iedereen aangepast én gebruikt kan worden? Een kaart waar iedereen zijn interesses aan zou kunnen toevoegen? Dan zouden er geen duizend-en-één verschillende kleine kaartjes, websites, ... meer nodig zijn

OpenStreetMap is deze open kaart. Je mag de kaartdata gratis gebruiken (mits bronvermelding en herpublicatie van aanpassingen). Daarenboven mag je de kaart ook gratis aanpassen als je een account maakt. Ook deze website is gebaseerd op OpenStreetMap. Als je hier een vraag beantwoord, gaat het antwoord daar ook naartoe

Tenslotte zijn er reeds vele gebruikers van OpenStreetMap. Denk maar Organic Maps, OsmAnd, verschillende gespecialiseerde routeplanners, de achtergrondkaarten op Facebook, Instagram,...<br/>Zelfs Apple Maps en Bing-Maps gebruiken OpenStreetMap in hun kaarten!

Kortom, als je hier een punt toevoegd of een vraag beantwoord, zal dat na een tijdje ook in al dié applicaties te zien zijn.

", + "openStreetMapIntro": "

Een open kaart

Zou het niet fantastisch zijn als er een open kaart zou zijn die door iedereen aangepast én gebruikt kan worden? Een kaart waar iedereen zijn interesses aan zou kunnen toevoegen? Dan zouden er geen duizend-en-één verschillende kleine kaartjes, websites, ... meer nodig zijn

OpenStreetMap is deze open kaart. Je mag de kaartdata gratis gebruiken (mits bronvermelding en herpublicatie van aanpassingen). Daarenboven mag je de kaart ook gratis aanpassen als je een account maakt. Ook deze website is gebaseerd op OpenStreetMap. Als je hier een vraag beantwoord, gaat het antwoord daar ook naartoe

Tenslotte zijn er reeds vele gebruikers van OpenStreetMap. Denk maar Organic Maps, OsmAnd, verschillende gespecialiseerde routeplanners, de achtergrondkaarten op Facebook, Instagram,...
;Zelfs Apple Maps en Bing-Maps gebruiken OpenStreetMap in hun kaarten!

Kortom, als je hier een punt toevoegd of een vraag beantwoord, zal dat na een tijdje ook in al dié applicaties te zien zijn.

", "attribution": { "attributionTitle": "Met dank aan", "attributionContent": "

Alle data is voorzien door OpenStreetMap, gratis en vrij te hergebruiken onder de Open DataBase Licentie.

", diff --git a/scripts/generateIncludedImages.ts b/scripts/generateIncludedImages.ts index f706e455e..ae1569719 100644 --- a/scripts/generateIncludedImages.ts +++ b/scripts/generateIncludedImages.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; -function genImages() { +function genImages(dryrun = false) { console.log("Generating images") const dir = fs.readdirSync("./assets/svg") @@ -17,7 +17,7 @@ function genImages() { throw "Non-svg file detected in the svg files: " + path; } - const svg = fs.readFileSync("./assets/svg/" + path, "utf-8") + let svg = fs.readFileSync("./assets/svg/" + path, "utf-8") .replace(/<\?xml.*?>/, "") .replace(/fill: ?none;/g, "fill: none !important;") // This is such a brittle hack... .replace(/\n/g, " ") @@ -26,11 +26,18 @@ function genImages() { .replace(/"/g, "\\\"") const name = path.substr(0, path.length - 4) .replace(/[ -]/g, "_"); + + if (dryrun) { + svg = "xxx" + } + module += ` public static ${name} = "${svg}"\n` module += ` public static ${name}_img = Img.AsImageElement(Svg.${name})\n` module += ` public static ${name}_svg() { return new Img(Svg.${name}, true);}\n` module += ` public static ${name}_ui() { return new FixedUiElement(Svg.${name}_img);}\n\n` - allNames.push(`"${path}": Svg.${name}`) + if (!dryrun) { + allNames.push(`"${path}": Svg.${name}`) + } } module += `public static All = {${allNames.join(",")}};` module += "}\n"; diff --git a/test.ts b/test.ts index 01a43f90f..7e9e298ad 100644 --- a/test.ts +++ b/test.ts @@ -3,12 +3,22 @@ import {UIEventSource} from "./Logic/UIEventSource"; import Loc from "./Models/Loc"; import AvailableBaseLayersImplementation from "./Logic/Actors/AvailableBaseLayersImplementation"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; -AvailableBaseLayers.implement(new AvailableBaseLayersImplementation()) +import BaseLayer from "./Models/BaseLayer"; +import {VariableUiElement} from "./UI/Base/VariableUIElement"; -new BackgroundMapSwitch({ +AvailableBaseLayers.implement(new AvailableBaseLayersImplementation()) +const state = { + currentBackground: new UIEventSource(AvailableBaseLayers.osmCarto), locationControl: new UIEventSource({ zoom: 19, - lat: 51.5, - lon: 4.1 + lat: 51.2, + lon: 3.2 }) -}).AttachTo("maindiv") \ No newline at end of file +} +const actualBackground = new UIEventSource(AvailableBaseLayers.osmCarto) +new BackgroundMapSwitch(state, + { + currentBackground: actualBackground + }).AttachTo("maindiv") + +new VariableUiElement(actualBackground.map(bg => bg.id)).AttachTo("extradiv") \ No newline at end of file