new color and icon for navigation

This commit is contained in:
Ward 2021-07-19 16:23:13 +02:00
parent d585ec9048
commit bd1b29e344
10 changed files with 870 additions and 815 deletions

View file

@ -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<string>,
layoutName: string,
layoutDefinition: string = ""
) {
if (layoutToUse === undefined) {
console.log("Incorrect layout");
new FixedUiElement(
`Error: incorrect layout <i>${layoutName}</i><br/><a href='https://${window.location.host}/'>Go back</a>`
)
.AttachTo("centermessage")
.onClick(() => {});
throw "Incorrect layout";
}
console.log(
"Using layout: ",
layoutToUse.id,
"LayoutFromBase64 is ",
layoutFromBase64
);
static InitAll(layoutToUse: LayoutConfig, layoutFromBase64: string, testing: UIEventSource<string>, layoutName: string,
layoutDefinition: string = "") {
State.state = new State(layoutToUse);
if (layoutToUse === undefined) {
console.log("Incorrect layout")
new FixedUiElement(`Error: incorrect layout <i>${layoutName}</i><br/><a href='https://${window.location.host}/'>Go back</a>`).AttachTo("centermessage").onClick(() => {
});
throw "Incorrect layout"
}
// 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<LayerConfig>();
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<string>): [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:<br/> " + e).AttachTo("centermessage");
throw e;
}
}
private static OnlyIf(featureSwitch: UIEventSource<boolean>, callback: () => void) {
featureSwitch.addCallbackAndRun(() => {
if (featureSwitch.data) {
callback();
}
});
}
private static InitWelcomeMessage() {
const isOpened = new UIEventSource<boolean>(false);
const fullOptions = new FullWelcomePaneWithTabs(isOpened);
// ?-Button on Desktop, opens panel with close-X.
const help = new MapControlButton(Svg.help_svg());
help.onClick(() => isOpened.setData(true))
new Toggle(
fullOptions
.SetClass("welcomeMessage"),
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<LayerConfig>();
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<boolean>((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<boolean>(false);
const addNewPoint = new ScrollableFullScreen(
() => Translations.t.general.add.title.Clone(),
() => new SimpleAddUI(newPointDialogIsShown),
"new",
newPointDialogIsShown)
addNewPoint.isShown.addCallback(isShown => {
if (!isShown) {
State.state.LastClickLocation.setData(undefined)
}
})
new StrayClickHandler(
State.state.LastClickLocation,
State.state.selectedElement,
State.state.filteredLayers,
State.state.leafletMap,
addNewPoint
);
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();
}
}
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<string>
): [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:<br/> " + e
).AttachTo("centermessage");
throw e;
}
}
private static OnlyIf(
featureSwitch: UIEventSource<boolean>,
callback: () => void
) {
featureSwitch.addCallbackAndRun(() => {
if (featureSwitch.data) {
callback();
}
});
}
private static InitWelcomeMessage() {
const isOpened = new UIEventSource<boolean>(false);
const fullOptions = new FullWelcomePaneWithTabs(isOpened);
// ?-Button on Desktop, opens panel with close-X.
const help = new MapControlButton(Svg.help_svg());
help.onClick(() => isOpened.setData(true));
new Toggle(fullOptions.SetClass("welcomeMessage"), 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<boolean>(
(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<boolean>(false);
const addNewPoint = new ScrollableFullScreen(
() => Translations.t.general.add.title.Clone(),
() => new SimpleAddUI(newPointDialogIsShown),
"new",
newPointDialogIsShown
);
addNewPoint.isShown.addCallback((isShown) => {
if (!isShown) {
State.state.LastClickLocation.setData(undefined);
}
});
new StrayClickHandler(
State.state.LastClickLocation,
State.state.selectedElement,
State.state.filteredLayers,
State.state.leafletMap,
addNewPoint
);
});
}
}

View file

@ -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<boolean>;
/**
* Wether or not the geolocation is active, aka the user requested the current location
* @private
*/
private readonly _isActive: UIEventSource<boolean>;
/**
* The callback over the permission API
* @private
*/
private readonly _permission: UIEventSource<string>;
/***
* The marker on the map, in order to update it
* @private
*/
private _marker: L.Marker;
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: UIEventSource<boolean>;
private readonly _currentGPSLocation: UIEventSource<{
latlng: any;
accuracy: number;
}>;
/**
* Kept in order to update the marker
* @private
*/
private readonly _leafletMap: UIEventSource<L.Map>;
/**
* 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<string>;
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
/**
* The callback over the permission API
* @private
*/
private readonly _permission: UIEventSource<string>;
/***
* The marker on the map, in order to update it
* @private
*/
private _marker: L.Marker;
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: UIEventSource<boolean>;
private readonly _currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>;
/**
* Kept in order to update the marker
* @private
*/
private readonly _leafletMap: UIEventSource<L.Map>;
/**
* 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<string>;
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
constructor(
currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
leafletMap: UIEventSource<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>
) {
const hasLocation = currentGPSLocation.map(
(location) => location !== undefined
);
const previousLocationGrant = LocalStorageSource.Get(
"geolocation-permissions"
);
const isActive = new UIEventSource<boolean>(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<string>("");
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<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>) {
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<boolean>(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<string>("")
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();
})
}
});
}
}
}
}

17
Svg.ts

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

@ -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": []
}
]
]

4
assets/svg/location.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.7304 11.1411H19.8992C19.7137 9.27763 18.8888 7.53597 17.5647 6.21181C16.2405 4.88765 14.4988 4.06268 12.6353 3.87719V0H11.1029V3.91554C9.2389 4.09962 7.49635 4.92404 6.17192 6.24847C4.84748 7.5729 4.02306 9.31545 3.83898 11.1794H0.0078125V12.7579H3.83898C4.02191 14.6215 4.84615 16.3639 6.17098 17.6874C7.49581 19.0108 9.23903 19.8331 11.1029 20.0141V23.8376H12.6353V20.0064C14.4986 19.8241 16.2411 19.0012 17.5656 17.678C18.8902 16.3548 19.7149 14.6134 19.8992 12.7502H23.7304V11.1717V11.1411ZM11.8614 18.4433C10.5736 18.4418 9.31515 18.0586 8.24511 17.342C7.17506 16.6254 6.34147 15.6076 5.84969 14.4174C5.35791 13.2272 5.23002 11.918 5.48219 10.6551C5.73436 9.39217 6.35529 8.23234 7.26646 7.32225C8.17762 6.41216 9.33805 5.7926 10.6012 5.54191C11.8644 5.29123 13.1736 5.42063 14.3633 5.91381C15.5529 6.40698 16.5696 7.24189 17.2849 8.31278C18.0002 9.38366 18.3821 10.6425 18.3821 11.9303C18.3851 12.7885 18.2187 13.639 17.8923 14.4327C17.566 15.2265 17.0861 15.948 16.4803 16.556C15.8745 17.1639 15.1547 17.6463 14.3621 17.9754C13.5694 18.3046 12.7197 18.474 11.8614 18.474V18.4433Z" fill="white"/>
<path d="M11.8613 7.61646C12.7145 7.61646 13.5485 7.86939 14.2579 8.3434C14.9673 8.81742 15.5203 9.49112 15.8468 10.2794C16.1733 11.0676 16.2587 11.9351 16.0923 12.7719C15.9258 13.6087 15.5149 14.3773 14.9116 14.9806C14.3083 15.5839 13.5397 15.9949 12.7029 16.1614C11.8661 16.3278 10.9986 16.2423 10.2104 15.9158C9.42212 15.5893 8.74842 15.0364 8.27441 14.327C7.80039 13.6176 7.54736 12.7835 7.54736 11.9303C7.54939 10.7868 8.0045 9.69074 8.81307 8.88216C9.62164 8.07359 10.7178 7.61848 11.8613 7.61646Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

3
assets/svg/min-zoom.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.346 13.5045H14.3761L14.0324 13.173C15.2355 11.7734 15.9598 9.95647 15.9598 7.97991C15.9598 3.57254 12.3873 0 7.97991 0C3.57254 0 0 3.57254 0 7.97991C0 12.3873 3.57254 15.9598 7.97991 15.9598C9.95647 15.9598 11.7734 15.2355 13.173 14.0324L13.5045 14.3761V15.346L19.6429 21.4721L21.4721 19.6429L15.346 13.5045ZM7.97991 13.5045C4.92299 13.5045 2.45536 11.0368 2.45536 7.97991C2.45536 4.92299 4.92299 2.45536 7.97991 2.45536C11.0368 2.45536 13.5045 4.92299 13.5045 7.97991C13.5045 11.0368 11.0368 13.5045 7.97991 13.5045ZM4.91071 7.36607H11.0491V8.59375H4.91071V7.36607Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 699 B

5
assets/svg/plus-zoom.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.0291 14.1876H15.0592L14.7155 13.8561C15.9186 12.4565 16.6429 10.6396 16.6429 8.66302C16.6429 4.25565 13.0704 0.683105 8.66302 0.683105C4.25565 0.683105 0.683105 4.25565 0.683105 8.66302C0.683105 13.0704 4.25565 16.6429 8.66302 16.6429C10.6396 16.6429 12.4565 15.9186 13.8561 14.7155L14.1876 15.0592V16.0291L20.326 22.1552L22.1552 20.326L16.0291 14.1876ZM8.66302 14.1876C5.6061 14.1876 3.13846 11.7199 3.13846 8.66302C3.13846 5.6061 5.6061 3.13846 8.66302 3.13846C11.7199 3.13846 14.1876 5.6061 14.1876 8.66302C14.1876 11.7199 11.7199 14.1876 8.66302 14.1876Z" fill="black"/>
<path d="M16.0291 14.1876H15.0592L14.7155 13.8561C15.9186 12.4565 16.6429 10.6396 16.6429 8.66302C16.6429 4.25565 13.0704 0.683105 8.66302 0.683105C4.25565 0.683105 0.683105 4.25565 0.683105 8.66302C0.683105 13.0704 4.25565 16.6429 8.66302 16.6429C10.6396 16.6429 12.4565 15.9186 13.8561 14.7155L14.1876 15.0592V16.0291L20.326 22.1552L22.1552 20.326L16.0291 14.1876ZM8.66302 14.1876C5.6061 14.1876 3.13846 11.7199 3.13846 8.66302C3.13846 5.6061 5.6061 3.13846 8.66302 3.13846C11.7199 3.13846 14.1876 5.6061 14.1876 8.66302C14.1876 11.7199 11.7199 14.1876 8.66302 14.1876Z" fill="white"/>
<path d="M11.7321 9.27679H9.27679V11.7321H8.04911V9.27679H5.59375V8.04911H8.04911V5.59375H9.27679V8.04911H11.7321V9.27679Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -48,7 +48,7 @@
:root {
--subtle-detail-color: #e5f5ff;
--subtle-detail-color: #007759;
--subtle-detail-color-contrast: black;
--subtle-detail-color-light-contrast: lightgrey;