Add buttons to quickly swap background layers (also in the locationInput), move copyright into home panel, split privacy policy to seperate welcome message tab

This commit is contained in:
pietervdvn 2021-11-21 02:44:35 +01:00
parent 1d0fbe701c
commit 37c0129a6d
22 changed files with 477 additions and 183 deletions

View file

@ -65,6 +65,7 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
continue
}
const leafletLayer: () => TileLayer = () => AvailableBaseLayersImplementation.CreateBackgroundLayer(
props.id,
props.name,
@ -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)"),
@ -195,35 +195,18 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
}
public AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
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<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
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 = [];

View file

@ -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<Map<number, Date>>
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<number, Date>(), })
{defaultValue: new Map<number, Date>(),})
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){
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)
}
}
@ -40,53 +41,57 @@ export default class SaveTileToLocalStorageActor {
})
}
public LoadTilesFromDisk(currentBounds: UIEventSource<BBox>,
public LoadTilesFromDisk(currentBounds: UIEventSource<BBox>, location: UIEventSource<Loc>,
registerFreshness: (tileId: number, freshness: Date) => void,
registerTile: ((src: FeatureSource & Tiled ) => void)){
registerTile: ((src: FeatureSource & Tiled) => void)) {
const self = this;
const loadedTiles = new Set<number>()
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;
}
currentBounds.addCallbackAndRunD(bbox => {
if(self._layer.minzoomVisible > location.data.zoom){
// Not enough zoom
return;
}
// 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){
if (tileFreshness > self.initializeTime) {
// This tile is loaded by another source
continue
}
registerFreshness(key, tileFreshness)
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))
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; // only load once: unregister
}
})
}
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()
@ -112,4 +117,12 @@ export default class SaveTileToLocalStorageActor {
this.visitedTiles.data.set(tileId, freshness)
this.visitedTiles.ping()
}
private SetIdb(tileIndex, data) {
IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data)
}
private GetIdb(tileIndex) {
return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex)
}
}

View file

@ -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())
})

View file

@ -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,34 +83,16 @@ 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;
}
let defaultLayer = AvailableBaseLayers.osmCarto
const available = this.availableBackgroundLayers.data;
for (const layer of available) {
if (layer.id === selectedId) {
return layer;
if (this.backgroundLayerId.data === layer.id) {
defaultLayer = layer;
}
}
return AvailableBaseLayers.osmCarto;
},
[this.availableBackgroundLayers],
(layer) => layer.id
);
/*
* Selects a different background layer if the background layer has no coverage at the current location
*/
new BackgroundLayerResetter(
this.backgroundLayer,
this.locationControl,
this.availableBackgroundLayers,
this.layoutToUse.defaultBackgroundId
);
const self = this
this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer)
this.backgroundLayer.addCallbackAndRunD(layer => self.backgroundLayerId.setData(layer.id))
const attr = new Attribution(
this.locationControl,
@ -334,10 +315,7 @@ export default class MapState extends UserRelatedState {
const filtersPerName = new Map<string, FilterConfig>()
layer.filters.forEach(f => filtersPerName.set(f.id, f))
const qp = QueryParameters.GetQueryParameter("filter-" + layer.id, "", "Filtering state for a layer")
flayer.appliedFilters.map(filters => {
filters = filters ?? []
return filters.map(f => f.filter.id + "." + f.selected).join(",")
}, [], textual => {
flayer.appliedFilters.map(filters => (filters ?? []).map(f => f.filter.id + "." + f.selected).join(","), [], textual => {
if (textual.length === 0) {
return empty
}

View file

@ -13,7 +13,8 @@ export interface MinimapOptions {
attribution?: BaseUIElement | boolean,
onFullyLoaded?: (leaflet: L.Map) => void,
leafletMap?: UIEventSource<any>,
lastClickLocation?: UIEventSource<{ lat: number, lon: number }>
lastClickLocation?: UIEventSource<{ lat: number, lon: number }>,
addLayerControl?: boolean | false
}
export interface MinimapObj {

View file

@ -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<BBox> | 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;
}

View file

@ -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<Loc>
}, 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<Loc>,
options: {
currentBackground: UIEventSource<BaseLayer>,
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<string | string[]>(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<Loc>
locationControl: UIEventSource<Loc>,
backgroundLayer: UIEventSource<BaseLayer>
},
options?: {
allowedLayers?: UIEventSource<string[]>
}
currentBackground: UIEventSource<BaseLayer>,
preferredCategory?: string
) {
options = options ?? {}
options.allowedLayers = options.allowedLayers ?? new UIEventSource<string[]>(["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")
}
}

View file

@ -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<BBox>,
locationControl: UIEventSource<Loc>,
osmConnection: OsmConnection
}, contributions: UIEventSource<Map<string, number>>) {
}) {
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")

View file

@ -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<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>,
locationControl: UIEventSource<Loc>,
featurePipeline: FeaturePipeline,
backgroundLayer: UIEventSource<BaseLayer>,
filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState) {
@ -46,6 +50,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>,
featurePipeline: FeaturePipeline,
locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState,
isShown: UIEventSource<boolean>):
@ -55,16 +60,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [
{header: `<img src='${state.layoutToUse.icon}'>`, 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<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>,
featurePipeline: FeaturePipeline,
locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState, currentTab: UIEventSource<number>, isShown: UIEventSource<boolean>) {

View file

@ -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<boolean>
}) {
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")

View file

@ -150,7 +150,8 @@ export default class LocationInput extends InputElement<Loc> 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

View file

@ -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<BaseLayer>(preferredBackground) // We detach the layer
})
if (reason.lockBounds) {

View file

@ -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
}

View file

@ -1,4 +1,53 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.08 8.86C8.13 8.53 8.24 8.24 8.38 7.99C8.52 7.74 8.72 7.53 8.97 7.37C9.21 7.22 9.51 7.15 9.88 7.14C10.11 7.15 10.32 7.19 10.51 7.27C10.71 7.36 10.89 7.48 11.03 7.63C11.17 7.78 11.28 7.96 11.37 8.16C11.46 8.36 11.5 8.58 11.51 8.8H13.3C13.28 8.33 13.19 7.9 13.02 7.51C12.85 7.12 12.62 6.78 12.32 6.5C12.02 6.22 11.66 6 11.24 5.84C10.82 5.68 10.36 5.61 9.85 5.61C9.2 5.61 8.63 5.72 8.15 5.95C7.67 6.18 7.27 6.48 6.95 6.87C6.63 7.26 6.39 7.71 6.24 8.23C6.09 8.75 6 9.29 6 9.87V10.14C6 10.72 6.08 11.26 6.23 11.78C6.38 12.3 6.62 12.75 6.94 13.13C7.26 13.51 7.66 13.82 8.14 14.04C8.62 14.26 9.19 14.38 9.84 14.38C10.31 14.38 10.75 14.3 11.16 14.15C11.57 14 11.93 13.79 12.24 13.52C12.55 13.25 12.8 12.94 12.98 12.58C13.16 12.22 13.27 11.84 13.28 11.43H11.49C11.48 11.64 11.43 11.83 11.34 12.01C11.25 12.19 11.13 12.34 10.98 12.47C10.83 12.6 10.66 12.7 10.46 12.77C10.27 12.84 10.07 12.86 9.86 12.87C9.5 12.86 9.2 12.79 8.97 12.64C8.72 12.48 8.52 12.27 8.38 12.02C8.24 11.77 8.13 11.47 8.08 11.14C8.03 10.81 8 10.47 8 10.14V9.87C8 9.52 8.03 9.19 8.08 8.86V8.86ZM10 0C4.48 0 0 4.48 0 10C0 15.52 4.48 20 10 20C15.52 20 20 15.52 20 10C20 4.48 15.52 0 10 0ZM10 18C5.59 18 2 14.41 2 10C2 5.59 5.59 2 10 2C14.41 2 18 5.59 18 10C18 14.41 14.41 18 10 18Z"
fill="white"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="20"
height="20"
viewBox="0 0 20 20"
version="1.1"
id="svg4"
sodipodi:docname="copyright.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
style="fill:none">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview6"
showgrid="false"
inkscape:zoom="16.68772"
inkscape:cx="7.5715453"
inkscape:cy="7.3459724"
inkscape:current-layer="svg4" />
<path
d="M 8.4941968,9.1059293 C 8.5334104,8.8471194 8.6196804,8.6196804 8.7294785,8.4236123 8.8392767,8.2275441 8.9961312,8.0628469 9.1921993,7.9373633 9.3804247,7.8197224 9.6157065,7.7648234 9.9058877,7.7569806 c 0.1803823,0.00784 0.3450793,0.039214 0.4940913,0.1019555 0.156854,0.070584 0.298024,0.1646972 0.407822,0.2823381 0.109798,0.1176408 0.196068,0.2588099 0.266652,0.4156644 0.07058,0.1568545 0.101956,0.3293944 0.109798,0.5019344 h 1.403848 C 12.572414,8.6902649 12.501829,8.3530277 12.368503,8.0471615 12.235177,7.7412952 12.054794,7.4746425 11.819512,7.2550462 11.58423,7.0354499 11.301892,6.86291 10.972498,6.7374264 10.643103,6.6119428 10.282338,6.5570437 9.8823595,6.5570437 c -0.5097775,0 -0.9568128,0.08627 -1.3332636,0.2666526 C 8.1726451,7.004079 7.8589361,7.2393608 7.6079689,7.5452271 7.3570017,7.8510933 7.1687762,8.204016 7.0511354,8.6118377 6.9334945,9.0196594 6.86291,9.4431665 6.86291,9.8980443 v 0.2117537 c 0,0.454878 0.062742,0.878385 0.1803826,1.286207 0.1176409,0.407822 0.3058663,0.760744 0.5568335,1.058768 0.2509672,0.298024 0.5646762,0.541148 0.941127,0.713688 0.3764508,0.17254 0.8234862,0.266653 1.3332637,0.266653 0.3686072,0 0.7136872,-0.06274 1.0352392,-0.180383 0.321552,-0.117641 0.60389,-0.282338 0.847014,-0.494092 0.243125,-0.211753 0.439193,-0.454878 0.580362,-0.737216 0.141169,-0.282338 0.227439,-0.580362 0.235282,-0.901913 h -1.403848 c -0.0078,0.164697 -0.04706,0.313709 -0.117641,0.454878 -0.07058,0.141169 -0.164697,0.25881 -0.282338,0.360765 -0.117641,0.101956 -0.250967,0.180383 -0.407822,0.235282 -0.149011,0.0549 -0.305866,0.07058 -0.4705628,0.07843 C 9.6078637,12.243019 9.372582,12.18812 9.1921993,12.070479 8.9961312,11.944996 8.8392767,11.780299 8.7294785,11.58423 8.6196804,11.388162 8.5334104,11.152881 8.4941968,10.894071 8.4549832,10.635261 8.431455,10.368608 8.431455,10.109798 V 9.8980443 c 0,-0.2744951 0.023528,-0.533305 0.062742,-0.792115 z M 10,2.1572749 c -4.3291842,0 -7.8427251,3.5135409 -7.8427251,7.8427248 0,4.3291843 3.5135409,7.8427253 7.8427251,7.8427253 4.329184,0 7.842725,-3.513541 7.842725,-7.8427253 C 17.842725,5.6708158 14.329184,2.1572749 10,2.1572749 Z M 10,16.27418 c -3.4586418,0 -6.2741801,-2.815538 -6.2741801,-6.2741803 0,-3.4586415 2.8155383,-6.2741798 6.2741801,-6.2741798 3.458642,0 6.27418,2.8155383 6.27418,6.2741798 0,3.4586423 -2.815538,6.2741803 -6.27418,6.2741803 z"
id="path2"
style="fill:#000000;fill-opacity:1;stroke-width:0.78427249"
inkscape:connector-curvature="0" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

59
assets/svg/eye.svg Normal file
View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 1850 1850"
id="svg2"
version="1.1"
inkscape:version="0.48.3.1 r9886"
width="100%"
height="100%"
sodipodi:docname="eye_open_font_awesome.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview8"
showgrid="false"
inkscape:zoom="0.13169643"
inkscape:cx="896"
inkscape:cy="896"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<g
transform="matrix(1,0,0,-1,30.372881,1259.8983)"
id="g4">
<path
d="m 1664,576 q -152,236 -381,353 61,-104 61,-225 0,-185 -131.5,-316.5 Q 1081,256 896,256 711,256 579.5,387.5 448,519 448,704 448,825 509,929 280,812 128,576 261,371 461.5,249.5 662,128 896,128 1130,128 1330.5,249.5 1531,371 1664,576 z M 944,960 q 0,20 -14,34 -14,14 -34,14 -125,0 -214.5,-89.5 Q 592,829 592,704 q 0,-20 14,-34 14,-14 34,-14 20,0 34,14 14,14 14,34 0,86 61,147 61,61 147,61 20,0 34,14 14,14 14,34 z m 848,-384 q 0,-34 -20,-69 Q 1632,277 1395.5,138.5 1159,0 896,0 633,0 396.5,139 160,278 20,507 0,542 0,576 q 0,34 20,69 140,229 376.5,368 236.5,139 499.5,139 263,0 499.5,-139 236.5,-139 376.5,-368 20,-35 20,-69 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:currentColor" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -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",

View file

@ -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">

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

View file

@ -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);
}

View file

@ -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);
}

View file

@ -71,7 +71,7 @@
"emailOf": "Wat is het email-adres van {category}?",
"emailIs": "Het email-adres van {category} is <a href=\"mailto:{email}\" target=\"_blank\">{email}</a>"
},
"openStreetMapIntro": "<h3>Een open kaart</h3><p>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</p><p><b><a href=\"https://OpenStreetMap.org\" target=\"_blank\">OpenStreetMap</a></b> is deze open kaart. Je mag de kaartdata gratis gebruiken (mits <a href=\"https://osm.org/copyright\" target=\"_blank\">bronvermelding en herpublicatie van aanpassingen</a>). 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</p><p>Tenslotte zijn er reeds vele gebruikers van OpenStreetMap. Denk maar <a href=\"https://organicmaps.app//\" target=\"_blank\">Organic Maps</a>, <a href=\"https://osmAnd.net\" target=\"_blank\">OsmAnd</a>, verschillende gespecialiseerde routeplanners, de achtergrondkaarten op Facebook, Instagram,...&lt;br/&gt;Zelfs Apple Maps en Bing-Maps gebruiken OpenStreetMap in hun kaarten!</p><p></p><p>Kortom, als je hier een punt toevoegd of een vraag beantwoord, zal dat na een tijdje ook in al dié applicaties te zien zijn.</p>",
"openStreetMapIntro": "<h3>Een open kaart</h3><p>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</p><p><b><a href=\"https://OpenStreetMap.org\" target=\"_blank\">OpenStreetMap</a></b> is deze open kaart. Je mag de kaartdata gratis gebruiken (mits <a href=\"https://osm.org/copyright\" target=\"_blank\">bronvermelding en herpublicatie van aanpassingen</a>). 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</p><p>Tenslotte zijn er reeds vele gebruikers van OpenStreetMap. Denk maar <a href=\"https://organicmaps.app//\" target=\"_blank\">Organic Maps</a>, <a href=\"https://osmAnd.net\" target=\"_blank\">OsmAnd</a>, verschillende gespecialiseerde routeplanners, de achtergrondkaarten op Facebook, Instagram,...<br/>;Zelfs Apple Maps en Bing-Maps gebruiken OpenStreetMap in hun kaarten!</p><p></p><p>Kortom, als je hier een punt toevoegd of een vraag beantwoord, zal dat na een tijdje ook in al dié applicaties te zien zijn.</p>",
"attribution": {
"attributionTitle": "Met dank aan",
"attributionContent": "<p>Alle data is voorzien door <a href='https://osm.org' target='_blank'>OpenStreetMap</a>, gratis en vrij te hergebruiken onder <a href='https://osm.org/copyright' target='_blank'>de Open DataBase Licentie</a>.</p>",

View file

@ -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,12 +26,19 @@ 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`
if (!dryrun) {
allNames.push(`"${path}": Svg.${name}`)
}
}
module += `public static All = {${allNames.join(",")}};`
module += "}\n";
fs.writeFileSync("Svg.ts", module);

20
test.ts
View file

@ -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<BaseLayer>(AvailableBaseLayers.osmCarto),
locationControl: new UIEventSource<Loc>({
zoom: 19,
lat: 51.5,
lon: 4.1
lat: 51.2,
lon: 3.2
})
}).AttachTo("maindiv")
}
const actualBackground = new UIEventSource(AvailableBaseLayers.osmCarto)
new BackgroundMapSwitch(state,
{
currentBackground: actualBackground
}).AttachTo("maindiv")
new VariableUiElement(actualBackground.map(bg => bg.id)).AttachTo("extradiv")