new color and icon for navigation
This commit is contained in:
parent
d585ec9048
commit
bd1b29e344
10 changed files with 870 additions and 815 deletions
|
@ -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 Toggle from "./UI/Input/Toggle";
|
||||||
import {Basemap} from "./UI/BigComponents/Basemap";
|
import { Basemap } from "./UI/BigComponents/Basemap";
|
||||||
import State from "./State";
|
import State from "./State";
|
||||||
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
||||||
import {UIEventSource} from "./Logic/UIEventSource";
|
import { UIEventSource } from "./Logic/UIEventSource";
|
||||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
import { QueryParameters } from "./Logic/Web/QueryParameters";
|
||||||
import StrayClickHandler from "./Logic/Actors/StrayClickHandler";
|
import StrayClickHandler from "./Logic/Actors/StrayClickHandler";
|
||||||
import SimpleAddUI from "./UI/BigComponents/SimpleAddUI";
|
import SimpleAddUI from "./UI/BigComponents/SimpleAddUI";
|
||||||
import CenterMessageBox from "./UI/CenterMessageBox";
|
import CenterMessageBox from "./UI/CenterMessageBox";
|
||||||
import UserBadge from "./UI/BigComponents/UserBadge";
|
import UserBadge from "./UI/BigComponents/UserBadge";
|
||||||
import SearchAndGo from "./UI/BigComponents/SearchAndGo";
|
import SearchAndGo from "./UI/BigComponents/SearchAndGo";
|
||||||
import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler";
|
import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler";
|
||||||
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
|
import { LocalStorageSource } from "./Logic/Web/LocalStorageSource";
|
||||||
import {Utils} from "./Utils";
|
import { Utils } from "./Utils";
|
||||||
import Svg from "./Svg";
|
import Svg from "./Svg";
|
||||||
import Link from "./UI/Base/Link";
|
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 LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
import Img from "./UI/Base/Img";
|
import Img from "./UI/Base/Img";
|
||||||
|
@ -33,7 +34,7 @@ import MapControlButton from "./UI/MapControlButton";
|
||||||
import Combine from "./UI/Base/Combine";
|
import Combine from "./UI/Base/Combine";
|
||||||
import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler";
|
import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler";
|
||||||
import LZString from "lz-string";
|
import LZString from "lz-string";
|
||||||
import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
|
import { LayoutConfigJson } from "./Customizations/JSON/LayoutConfigJson";
|
||||||
import AttributionPanel from "./UI/BigComponents/AttributionPanel";
|
import AttributionPanel from "./UI/BigComponents/AttributionPanel";
|
||||||
import ContributorCount from "./Logic/ContributorCount";
|
import ContributorCount from "./Logic/ContributorCount";
|
||||||
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
|
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
|
||||||
|
@ -42,442 +43,508 @@ import LayerConfig from "./Customizations/JSON/LayerConfig";
|
||||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||||
|
|
||||||
export class InitUiElements {
|
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,
|
State.state = new State(layoutToUse);
|
||||||
layoutDefinition: string = "") {
|
|
||||||
|
|
||||||
if (layoutToUse === undefined) {
|
// This 'leaks' the global state via the window object, useful for debugging
|
||||||
console.log("Incorrect layout")
|
// @ts-ignore
|
||||||
new FixedUiElement(`Error: incorrect layout <i>${layoutName}</i><br/><a href='https://${window.location.host}/'>Go back</a>`).AttachTo("centermessage").onClick(() => {
|
window.mapcomplete_state = State.state;
|
||||||
});
|
|
||||||
throw "Incorrect layout"
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
if (layoutFromBase64 !== "false") {
|
||||||
|
State.state.layoutDefinition = layoutDefinition;
|
||||||
// This 'leaks' the global state via the window object, useful for debugging
|
console.log(
|
||||||
// @ts-ignore
|
"Layout definition:",
|
||||||
window.mapcomplete_state = State.state;
|
Utils.EllipsesAfter(State.state.layoutDefinition, 100)
|
||||||
|
);
|
||||||
if (layoutToUse.hideFromOverview) {
|
if (testing.data !== "true") {
|
||||||
State.state.osmConnection.GetPreference("hidden-theme-" + layoutToUse.id + "-enabled").setData("true");
|
State.state.osmConnection.OnLoggedIn(() => {
|
||||||
}
|
State.state.osmConnection
|
||||||
|
.GetLongPreference("installed-theme-" + layoutToUse.id)
|
||||||
if (layoutFromBase64 !== "false") {
|
.setData(State.state.layoutDefinition);
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
InitUiElements.OnlyIf((State.state.featureSwitchSearch), () => {
|
console.warn(
|
||||||
new SearchAndGo().AttachTo("searchbox");
|
"NOT saving custom layout to OSM as we are tesing -> probably in an iFrame"
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
);
|
||||||
State.state.leafletMap.setData(bm.map);
|
}
|
||||||
const layout = State.state.layoutToUse.data
|
}
|
||||||
if (layout.lockLocation) {
|
|
||||||
|
|
||||||
if (layout.lockLocation === true) {
|
function updateFavs() {
|
||||||
const tile = Utils.embedded_tile(layout.startLat, layout.startLon, layout.startZoom - 1)
|
// This is purely for the personal theme to load the layers there
|
||||||
const bounds = Utils.tile_bounds(tile.z, tile.x, tile.y)
|
const favs = State.state.favouriteLayers.data ?? [];
|
||||||
// We use the bounds to get a sense of distance for this zoom level
|
|
||||||
const latDiff = bounds[0][0] - bounds[1][0]
|
const neededLayers = new Set<LayerConfig>();
|
||||||
const lonDiff = bounds[0][1] - bounds[1][1]
|
|
||||||
layout.lockLocation = [[layout.startLat - latDiff, layout.startLon - lonDiff],
|
console.log("Favourites are: ", favs);
|
||||||
[layout.startLat + latDiff, layout.startLon + lonDiff],
|
layoutToUse.layers.splice(0, layoutToUse.layers.length);
|
||||||
];
|
let somethingChanged = false;
|
||||||
}
|
for (const fav of favs) {
|
||||||
console.warn("Locking the bounds to ", layout.lockLocation)
|
if (AllKnownLayers.sharedLayers.has(fav)) {
|
||||||
bm.map.setMaxBounds(layout.lockLocation);
|
const layer = AllKnownLayers.sharedLayers.get(fav);
|
||||||
bm.map.setMinZoom(layout.startZoom)
|
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 {
|
if (layoutToUse.customCss !== undefined) {
|
||||||
|
Utils.LoadCustomCss(layoutToUse.customCss);
|
||||||
|
|
||||||
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() {
|
InitUiElements.InitBaseMap();
|
||||||
|
|
||||||
// ------------- Setup the layers -------------------------------
|
InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => {
|
||||||
|
new UserBadge().AttachTo("userbadge");
|
||||||
|
});
|
||||||
|
|
||||||
const source = InitUiElements.InitLayers();
|
InitUiElements.OnlyIf(State.state.featureSwitchSearch, () => {
|
||||||
InitUiElements.InitLayerSelection(source);
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
State.state.osmConnection.userDetails
|
||||||
InitUiElements.OnlyIf(State.state.featureSwitchAddNew, () => {
|
.map((userDetails: UserDetails) => userDetails?.home)
|
||||||
|
.addCallbackAndRunD((home) => {
|
||||||
let presetCount = 0;
|
const color = getComputedStyle(document.body).getPropertyValue(
|
||||||
for (const layer of State.state.filteredLayers.data) {
|
"--subtle-detail-color"
|
||||||
for (const preset of layer.layerDef.presets) {
|
);
|
||||||
presetCount++;
|
const icon = L.icon({
|
||||||
}
|
iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)),
|
||||||
}
|
iconSize: [30, 30],
|
||||||
if (presetCount == 0) {
|
iconAnchor: [15, 15],
|
||||||
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
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,244 +1,265 @@
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
import {UIEventSource} from "../UIEventSource";
|
import { UIEventSource } from "../UIEventSource";
|
||||||
import {Utils} from "../../Utils";
|
import { Utils } from "../../Utils";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import Img from "../../UI/Base/Img";
|
import Img from "../../UI/Base/Img";
|
||||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
import { LocalStorageSource } from "../Web/LocalStorageSource";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
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 {
|
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
|
* The callback over the permission API
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private readonly _isActive: UIEventSource<boolean>;
|
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(
|
||||||
* The callback over the permission API
|
currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
|
||||||
* @private
|
leafletMap: UIEventSource<L.Map>,
|
||||||
*/
|
layoutToUse: UIEventSource<LayoutConfig>
|
||||||
private readonly _permission: UIEventSource<string>;
|
) {
|
||||||
/***
|
const hasLocation = currentGPSLocation.map(
|
||||||
* The marker on the map, in order to update it
|
(location) => location !== undefined
|
||||||
* @private
|
);
|
||||||
*/
|
const previousLocationGrant = LocalStorageSource.Get(
|
||||||
private _marker: L.Marker;
|
"geolocation-permissions"
|
||||||
/**
|
);
|
||||||
* Literally: _currentGPSLocation.data != undefined
|
const isActive = new UIEventSource<boolean>(false);
|
||||||
* @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>;
|
|
||||||
|
|
||||||
|
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 }>,
|
const currentPointer = this._isActive.map(
|
||||||
leafletMap: UIEventSource<L.Map>,
|
(isActive) => {
|
||||||
layoutToUse: UIEventSource<LayoutConfig>) {
|
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);
|
this.onClick(() => self.init(true));
|
||||||
const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions")
|
this.init(false);
|
||||||
const isActive = new UIEventSource<boolean>(false);
|
}
|
||||||
|
|
||||||
super(
|
private init(askPermission: boolean) {
|
||||||
hasLocation.map(hasLocation => {
|
const self = this;
|
||||||
|
const map = this._leafletMap.data;
|
||||||
|
|
||||||
if (hasLocation) {
|
this._currentGPSLocation.addCallback((location) => {
|
||||||
return Svg.crosshair_blue_ui()
|
self._previousLocationGrant.setData("granted");
|
||||||
}
|
|
||||||
if (isActive.data) {
|
const timeSinceRequest =
|
||||||
return Svg.crosshair_blue_center_ui();
|
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
|
||||||
}
|
if (timeSinceRequest < 30) {
|
||||||
return Svg.crosshair_ui();
|
self.MoveToCurrentLoction(16);
|
||||||
}, [isActive])
|
}
|
||||||
|
|
||||||
|
let color = "#1111cc";
|
||||||
|
try {
|
||||||
|
color = getComputedStyle(document.body).getPropertyValue(
|
||||||
|
"--catch-detail-color"
|
||||||
);
|
);
|
||||||
this._isActive = isActive;
|
} catch (e) {
|
||||||
this._permission = new UIEventSource<string>("")
|
console.error(e);
|
||||||
this._previousLocationGrant = previousLocationGrant;
|
}
|
||||||
this._currentGPSLocation = currentGPSLocation;
|
const icon = L.icon({
|
||||||
this._leafletMap = leafletMap;
|
iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)),
|
||||||
this._layoutToUse = layoutToUse;
|
iconSize: [40, 40], // size of the icon
|
||||||
this._hasLocation = hasLocation;
|
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
|
||||||
const self = this;
|
});
|
||||||
|
|
||||||
const currentPointer = this._isActive.map(isActive => {
|
const newMarker = L.marker(location.latlng, { icon: icon });
|
||||||
if (isActive && !self._hasLocation.data) {
|
newMarker.addTo(map);
|
||||||
return "cursor-wait"
|
|
||||||
}
|
|
||||||
return "cursor-pointer"
|
|
||||||
}, [this._hasLocation])
|
|
||||||
currentPointer.addCallbackAndRun(pointerClass => {
|
|
||||||
self.SetClass(pointerClass);
|
|
||||||
})
|
|
||||||
|
|
||||||
|
if (self._marker !== undefined) {
|
||||||
|
map.removeLayer(self._marker);
|
||||||
|
}
|
||||||
|
self._marker = newMarker;
|
||||||
|
});
|
||||||
|
|
||||||
this.onClick(() => self.init(true))
|
try {
|
||||||
this.init(false)
|
navigator?.permissions
|
||||||
|
?.query({ name: "geolocation" })
|
||||||
}
|
?.then(function (status) {
|
||||||
|
console.log("Geolocation is already", status);
|
||||||
private init(askPermission: boolean) {
|
if (status.state === "granted") {
|
||||||
|
|
||||||
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("");
|
|
||||||
self.StartGeolocating(false);
|
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() {
|
// We check that the GPS location is not out of bounds
|
||||||
const self = this;
|
const b = this._layoutToUse.data.lockLocation;
|
||||||
const map: any = this._leafletMap.data;
|
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) {
|
private StartGeolocating(zoomToGPS = true) {
|
||||||
navigator.geolocation.getCurrentPosition(function (position) {
|
const self = this;
|
||||||
self._currentGPSLocation.setData({
|
console.log("Starting geolocation");
|
||||||
latlng: [position.coords.latitude, position.coords.longitude],
|
|
||||||
accuracy: position.coords.accuracy
|
this._lastUserRequest = zoomToGPS ? new Date() : new Date(0);
|
||||||
});
|
if (self._permission.data === "denied") {
|
||||||
}, function () {
|
self._previousLocationGrant.setData("");
|
||||||
console.warn("Could not get location with navigator.geolocation")
|
return "";
|
||||||
});
|
}
|
||||||
return;
|
if (this._currentGPSLocation.data !== undefined) {
|
||||||
} else {
|
this.MoveToCurrentLoction(16);
|
||||||
map.findAccuratePosition({
|
|
||||||
maxWait: 10000, // defaults to 10000
|
|
||||||
desiredAccuracy: 50 // defaults to 20
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MoveToCurrentLoction(targetZoom = 16) {
|
console.log("Searching location using GPS");
|
||||||
const location = this._currentGPSLocation.data;
|
this.locate();
|
||||||
this._lastUserRequest = undefined;
|
|
||||||
|
|
||||||
|
if (!self._isActive.data) {
|
||||||
if (this._currentGPSLocation.data.latlng[0] === 0 && this._currentGPSLocation.data.latlng[1] === 0) {
|
self._isActive.setData(true);
|
||||||
console.debug("Not moving to GPS-location: it is null island")
|
Utils.DoEvery(60000, () => {
|
||||||
return;
|
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();
|
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
17
Svg.ts
File diff suppressed because one or more lines are too long
32
UI/Base/CenterFlexedElement.ts
Normal file
32
UI/Base/CenterFlexedElement.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,11 +5,11 @@ import Combine from "./Base/Combine";
|
||||||
* A button floating above the map, in a uniform style
|
* A button floating above the map, in a uniform style
|
||||||
*/
|
*/
|
||||||
export default class MapControlButton extends Combine {
|
export default class MapControlButton extends Combine {
|
||||||
|
constructor(contents: BaseUIElement) {
|
||||||
constructor(contents: BaseUIElement) {
|
super([contents]);
|
||||||
super([contents]);
|
this.SetClass(
|
||||||
this.SetClass("relative block rounded-full w-10 h-10 p-1 pointer-events-auto z-above-map subtle-background")
|
"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);");
|
);
|
||||||
}
|
this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,36 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "direction_masked.svg",
|
"path": "direction_masked.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "direction_outline.svg",
|
"path": "direction_outline.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "direction_stroke.svg",
|
"path": "direction_stroke.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "SocialImageForeground.svg",
|
"path": "SocialImageForeground.svg",
|
||||||
"license": "CC-BY-SA",
|
"license": "CC-BY-SA",
|
||||||
"sources": [
|
"sources": ["https://mapcomplete.osm.be"]
|
||||||
"https://mapcomplete.osm.be"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "add.svg",
|
"path": "add.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "addSmall.svg",
|
"path": "addSmall.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
|
@ -56,33 +42,25 @@
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "arrow-left-smooth.svg",
|
"path": "arrow-left-smooth.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "arrow-right-smooth.svg",
|
"path": "arrow-right-smooth.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "back.svg",
|
"path": "back.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Github"],
|
||||||
"Github"
|
|
||||||
],
|
|
||||||
"path": "bug.svg",
|
"path": "bug.svg",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"sources": [
|
"sources": [
|
||||||
|
@ -93,35 +71,26 @@
|
||||||
{
|
{
|
||||||
"path": "camera-plus.svg",
|
"path": "camera-plus.svg",
|
||||||
"license": "CC-BY-SA 3.0",
|
"license": "CC-BY-SA 3.0",
|
||||||
"authors": [
|
"authors": ["Dave Gandy", "Pieter Vander Vennet"],
|
||||||
"Dave Gandy",
|
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"sources": [
|
"sources": [
|
||||||
"https://fontawesome.com/",
|
"https://fontawesome.com/",
|
||||||
"https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg"
|
"https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "checkmark.svg",
|
"path": "checkmark.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "circle.svg",
|
"path": "circle.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet"],
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"path": "clock.svg",
|
"path": "clock.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": []
|
"sources": []
|
||||||
|
@ -163,9 +132,7 @@
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Dave Gandy"],
|
||||||
"Dave Gandy"
|
|
||||||
],
|
|
||||||
"path": "delete_icon.svg",
|
"path": "delete_icon.svg",
|
||||||
"license": "CC-BY-SA",
|
"license": "CC-BY-SA",
|
||||||
"sources": [
|
"sources": [
|
||||||
|
@ -197,9 +164,7 @@
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["The Tango Desktop Project"],
|
||||||
"The Tango Desktop Project"
|
|
||||||
],
|
|
||||||
"path": "floppy.svg",
|
"path": "floppy.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": [
|
"sources": [
|
||||||
|
@ -220,29 +185,19 @@
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Timothy Miller"],
|
||||||
"Timothy Miller"
|
|
||||||
],
|
|
||||||
"path": "home.svg",
|
"path": "home.svg",
|
||||||
"license": "CC-BY-SA 3.0",
|
"license": "CC-BY-SA 3.0",
|
||||||
"sources": [
|
"sources": ["https://commons.wikimedia.org/wiki/File:Home-icon.svg"]
|
||||||
"https://commons.wikimedia.org/wiki/File:Home-icon.svg"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Timothy Miller"],
|
||||||
"Timothy Miller"
|
|
||||||
],
|
|
||||||
"path": "home_white_bg.svg",
|
"path": "home_white_bg.svg",
|
||||||
"license": "CC-BY-SA 3.0",
|
"license": "CC-BY-SA 3.0",
|
||||||
"sources": [
|
"sources": ["https://commons.wikimedia.org/wiki/File:Home-icon.svg"]
|
||||||
"https://commons.wikimedia.org/wiki/File:Home-icon.svg"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["JOSM Team"],
|
||||||
"JOSM Team"
|
|
||||||
],
|
|
||||||
"path": "josm_logo.svg",
|
"path": "josm_logo.svg",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
"sources": [
|
"sources": [
|
||||||
|
@ -265,9 +220,7 @@
|
||||||
{
|
{
|
||||||
"path": "Ornament-Horiz-0.svg",
|
"path": "Ornament-Horiz-0.svg",
|
||||||
"license": "CC-BY",
|
"license": "CC-BY",
|
||||||
"authors": [
|
"authors": ["Nightwolfdezines"],
|
||||||
"Nightwolfdezines"
|
|
||||||
],
|
|
||||||
"sources": [
|
"sources": [
|
||||||
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
]
|
]
|
||||||
|
@ -275,9 +228,7 @@
|
||||||
{
|
{
|
||||||
"path": "Ornament-Horiz-1.svg",
|
"path": "Ornament-Horiz-1.svg",
|
||||||
"license": "CC-BY",
|
"license": "CC-BY",
|
||||||
"authors": [
|
"authors": ["Nightwolfdezines"],
|
||||||
"Nightwolfdezines"
|
|
||||||
],
|
|
||||||
"sources": [
|
"sources": [
|
||||||
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
]
|
]
|
||||||
|
@ -285,9 +236,7 @@
|
||||||
{
|
{
|
||||||
"path": "Ornament-Horiz-2.svg",
|
"path": "Ornament-Horiz-2.svg",
|
||||||
"license": "CC-BY",
|
"license": "CC-BY",
|
||||||
"authors": [
|
"authors": ["Nightwolfdezines"],
|
||||||
"Nightwolfdezines"
|
|
||||||
],
|
|
||||||
"sources": [
|
"sources": [
|
||||||
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
]
|
]
|
||||||
|
@ -295,9 +244,7 @@
|
||||||
{
|
{
|
||||||
"path": "Ornament-Horiz-3.svg",
|
"path": "Ornament-Horiz-3.svg",
|
||||||
"license": "CC-BY",
|
"license": "CC-BY",
|
||||||
"authors": [
|
"authors": ["Nightwolfdezines"],
|
||||||
"Nightwolfdezines"
|
|
||||||
],
|
|
||||||
"sources": [
|
"sources": [
|
||||||
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
]
|
]
|
||||||
|
@ -305,9 +252,7 @@
|
||||||
{
|
{
|
||||||
"path": "Ornament-Horiz-4.svg",
|
"path": "Ornament-Horiz-4.svg",
|
||||||
"license": "CC-BY",
|
"license": "CC-BY",
|
||||||
"authors": [
|
"authors": ["Nightwolfdezines"],
|
||||||
"Nightwolfdezines"
|
|
||||||
],
|
|
||||||
"sources": [
|
"sources": [
|
||||||
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
]
|
]
|
||||||
|
@ -315,9 +260,7 @@
|
||||||
{
|
{
|
||||||
"path": "Ornament-Horiz-5.svg",
|
"path": "Ornament-Horiz-5.svg",
|
||||||
"license": "CC-BY",
|
"license": "CC-BY",
|
||||||
"authors": [
|
"authors": ["Nightwolfdezines"],
|
||||||
"Nightwolfdezines"
|
|
||||||
],
|
|
||||||
"sources": [
|
"sources": [
|
||||||
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
]
|
]
|
||||||
|
@ -325,9 +268,7 @@
|
||||||
{
|
{
|
||||||
"path": "Ornament-Horiz-6.svg",
|
"path": "Ornament-Horiz-6.svg",
|
||||||
"license": "CC-BY",
|
"license": "CC-BY",
|
||||||
"authors": [
|
"authors": ["Nightwolfdezines"],
|
||||||
"Nightwolfdezines"
|
|
||||||
],
|
|
||||||
"sources": [
|
"sources": [
|
||||||
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
|
||||||
]
|
]
|
||||||
|
@ -369,25 +310,16 @@
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Pieter Vander Vennet", " OSM"],
|
||||||
"Pieter Vander Vennet",
|
|
||||||
" OSM"
|
|
||||||
],
|
|
||||||
"path": "mapcomplete_logo.svg",
|
"path": "mapcomplete_logo.svg",
|
||||||
"license": "Logo; CC-BY-SA",
|
"license": "Logo; CC-BY-SA",
|
||||||
"sources": [
|
"sources": ["https://mapcomplete.osm.be"]
|
||||||
"https://mapcomplete.osm.be"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Mapillary"],
|
||||||
"Mapillary"
|
|
||||||
],
|
|
||||||
"path": "mapillary.svg",
|
"path": "mapillary.svg",
|
||||||
"license": "Logo; All rights reserved",
|
"license": "Logo; All rights reserved",
|
||||||
"sources": [
|
"sources": ["https://mapillary.com/"]
|
||||||
"https://mapillary.com/"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [],
|
"authors": [],
|
||||||
|
@ -411,32 +343,22 @@
|
||||||
"authors": [],
|
"authors": [],
|
||||||
"path": "osm-copyright.svg",
|
"path": "osm-copyright.svg",
|
||||||
"license": "logo; all rights reserved",
|
"license": "logo; all rights reserved",
|
||||||
"sources": [
|
"sources": ["https://www.OpenStreetMap.org"]
|
||||||
"https://www.OpenStreetMap.org"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["OpenStreetMap U.S. Chapter"],
|
||||||
"OpenStreetMap U.S. Chapter"
|
|
||||||
],
|
|
||||||
"path": "osm-logo-us.svg",
|
"path": "osm-logo-us.svg",
|
||||||
"license": "Logo",
|
"license": "Logo",
|
||||||
"sources": [
|
"sources": ["https://www.openstreetmap.us/"]
|
||||||
"https://www.openstreetmap.us/"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [],
|
"authors": [],
|
||||||
"path": "osm-logo.svg",
|
"path": "osm-logo.svg",
|
||||||
"license": "logo; all rights reserved",
|
"license": "logo; all rights reserved",
|
||||||
"sources": [
|
"sources": ["https://www.OpenStreetMap.org"]
|
||||||
"https://www.OpenStreetMap.org"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["GitHub Octicons"],
|
||||||
"GitHub Octicons"
|
|
||||||
],
|
|
||||||
"path": "pencil.svg",
|
"path": "pencil.svg",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"sources": [
|
"sources": [
|
||||||
|
@ -445,14 +367,10 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["@ tyskrat"],
|
||||||
"@ tyskrat"
|
|
||||||
],
|
|
||||||
"path": "phone.svg",
|
"path": "phone.svg",
|
||||||
"license": "CC-BY 3.0",
|
"license": "CC-BY 3.0",
|
||||||
"sources": [
|
"sources": ["https://www.onlinewebfonts.com/icon/1059"]
|
||||||
"https://www.onlinewebfonts.com/icon/1059"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [],
|
"authors": [],
|
||||||
|
@ -467,14 +385,10 @@
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["@fatih"],
|
||||||
"@fatih"
|
|
||||||
],
|
|
||||||
"path": "pop-out.svg",
|
"path": "pop-out.svg",
|
||||||
"license": "CC-BY 3.0",
|
"license": "CC-BY 3.0",
|
||||||
"sources": [
|
"sources": ["https://www.onlinewebfonts.com/icon/2151"]
|
||||||
"https://www.onlinewebfonts.com/icon/2151"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [],
|
"authors": [],
|
||||||
|
@ -489,9 +403,7 @@
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["OOjs UI Team and other contributors"],
|
||||||
"OOjs UI Team and other contributors"
|
|
||||||
],
|
|
||||||
"path": "search.svg",
|
"path": "search.svg",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"sources": [
|
"sources": [
|
||||||
|
@ -518,19 +430,13 @@
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["@felpgrc"],
|
||||||
"@felpgrc"
|
|
||||||
],
|
|
||||||
"path": "statistics.svg",
|
"path": "statistics.svg",
|
||||||
"license": "CC-BY 3.0",
|
"license": "CC-BY 3.0",
|
||||||
"sources": [
|
"sources": ["https://www.onlinewebfonts.com/icon/197818"]
|
||||||
"https://www.onlinewebfonts.com/icon/197818"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["MGalloway (WMF)"],
|
||||||
"MGalloway (WMF)"
|
|
||||||
],
|
|
||||||
"path": "translate.svg",
|
"path": "translate.svg",
|
||||||
"license": "CC-BY-SA 3.0",
|
"license": "CC-BY-SA 3.0",
|
||||||
"sources": [
|
"sources": [
|
||||||
|
@ -544,43 +450,45 @@
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Wikidata"],
|
||||||
"Wikidata"
|
|
||||||
],
|
|
||||||
"path": "wikidata.svg",
|
"path": "wikidata.svg",
|
||||||
"license": "Logo; All rights reserved",
|
"license": "Logo; All rights reserved",
|
||||||
"sources": [
|
"sources": ["https://www.wikidata.org"]
|
||||||
"https://www.wikidata.org"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Wikimedia"],
|
||||||
"Wikimedia"
|
|
||||||
],
|
|
||||||
"path": "wikimedia-commons-white.svg",
|
"path": "wikimedia-commons-white.svg",
|
||||||
"license": "Logo; All rights reserved",
|
"license": "Logo; All rights reserved",
|
||||||
"sources": [
|
"sources": ["https://commons.wikimedia.org"]
|
||||||
"https://commons.wikimedia.org"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Wikipedia"],
|
||||||
"Wikipedia"
|
|
||||||
],
|
|
||||||
"path": "wikipedia.svg",
|
"path": "wikipedia.svg",
|
||||||
"license": "Logo; All rights reserved",
|
"license": "Logo; All rights reserved",
|
||||||
"sources": [
|
"sources": ["https://www.wikipedia.org/"]
|
||||||
"https://www.wikipedia.org/"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"authors": [
|
"authors": ["Mapillary"],
|
||||||
"Mapillary"
|
|
||||||
],
|
|
||||||
"path": "mapillary_black.svg",
|
"path": "mapillary_black.svg",
|
||||||
"license": "Logo; All rights reserved",
|
"license": "Logo; All rights reserved",
|
||||||
"sources": [
|
"sources": ["https://www.mapillary.com/"]
|
||||||
"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
4
assets/svg/location.svg
Normal 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
3
assets/svg/min-zoom.svg
Normal 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
5
assets/svg/plus-zoom.svg
Normal 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 |
|
@ -48,7 +48,7 @@
|
||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--subtle-detail-color: #e5f5ff;
|
--subtle-detail-color: #007759;
|
||||||
--subtle-detail-color-contrast: black;
|
--subtle-detail-color-contrast: black;
|
||||||
--subtle-detail-color-light-contrast: lightgrey;
|
--subtle-detail-color-light-contrast: lightgrey;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue