initial commit

This commit is contained in:
recrof 2025-07-18 14:49:10 +02:00
parent 8aad8fe6c5
commit 4bf1157f26
25 changed files with 20223 additions and 1 deletions

View file

@ -1,2 +1,13 @@
# map.meshcore.dev
Official MeshCore Map
Official MeshCore Map (frontend)
## Installation
This is fully static and build-free site, cloning it to web location that can serve static content should be enough.
It uses backend api deployed on https://meshcore.dev/api/v1/nodes, so you will need to edit `apiUrl` in `src/map.js`.
## Libraries used
* [Vue3](https://github.com/vuejs/core)
* [Beer.css](https://github.com/beercss/beercss)
* [Leaflet](https://github.com/Leaflet/Leaflet)
* [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster)
* [Material icons](https://fonts.google.com/icons)

203
css/style.css Normal file
View file

@ -0,0 +1,203 @@
@font-face {
font-family: Material Symbols Outlined;
font-style: normal;
font-weight: 400;
font-display: block;
src: url(https://cdn.jsdelivr.net/npm/beercss@3.9.4/dist/cdn/material-symbols-outlined.woff2) format("woff2")
}
[v-cloak] {
display: none !important;
}
body { margin: 0; }
body:has(dialog[open])::after {
content: '';
position: absolute;
z-index: 1049;
top: 0;
left: 0;
bottom: 0;
right: 0;
backdrop-filter: blur(10px);
}
dialog[open] {
z-index: 1050 !important;
}
#map { height: 100vh; }
table.node-info {
table-layout: fixed;
width: 100%;
}
table.node-info td {
padding: 2px 0;
}
table.node-info tr td:first-child {
width: 150px;
}
table.node-info tr td:last-child {
overflow: hidden;
text-overflow: ellipsis;
}
.search-results {
max-height: 90vh;
overflow-y: auto;
overflow-x: hidden;
}
.search-results li {
overflow: hidden;
}
.search-pkey {
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
}
#app {
display: flex;
justify-content: center;
}
.add-dialog {
z-index: 1050;
padding: 15px;
max-height: 80vh;
}
.add-dialog .page {
text-align: center;
}
.add-dialog .page img {
max-width: 50%;
max-height: 300px;
}
@media (max-width: 800px) {
.add-dialog {
width: 95%;
max-height: 100vh;
}
}
.pointer-help {
cursor: help
}
.stats {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
display: flex;
align-self: center;
align-items: center;
flex-direction: row;
flex-wrap: wrap;
column-gap: 15px;
row-gap: 32px;
height: 30px;
width: 100%;
min-width: 700px;
overflow-y: hidden;
overflow-x: auto;
color: #000;
background-color: #ffffff55;
backdrop-filter: blur(10px);
padding-left: 15px;
}
.stats > span {
line-height: 0;
display: flex;
flex-direction: row;
align-items: center;
}
.stats i.node-type {
background-color: #667a8c;
border-radius: 50%;
font-size: 14px;
color: #fff;
margin-left: 7px;
margin-right: 4px;
}
.stats a img {
width: 16px;
min-inline-size: 0px !important;
}
.search {
position: fixed;
z-index: 1000;
display: flex;
flex-direction: column;
padding: 10px;
top: 30px;
max-width: 600px;
flex-grow: 1;
width: calc(100% - 20px);
align-self: center;
justify-content: center;
gap: 5px;
}
.search .field {
width: 100%;
display: flex;
}
.search .submit {
width: 12px;
height: 35px;
}
.search .filter {
margin-top: 5px;
}
.search-text b {
background-color: #ee0;
}
button.manual-add {
position: fixed;
z-index: 1000;
display: block;
bottom: 55px;
right: 10px;
width: 50px;
height: 50px;
border-radius: 5px;
}
button.manual-add::after {
font-size: 40px;
content: '+';
color: #fff;
}
.leaflet-bottom {
position: fixed !important;
}
.leaflet-control-layers-list {
margin-block-start: 0 !important;
}
.leaflet-bottom .leaflet-control-layers {
margin-bottom: 55px !important;
}
.marker-cluster span {
color: #000;
font-weight: 800;
}
.leaflet-popup-close-button span {
font-size: 30px;
padding: 5px;
}
.leaflet-container a.leaflet-popup-close-button {
width: auto !important;
height: auto !important;
}

BIN
img/button_contact.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
img/button_self.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

1
img/node_types/1.svg Normal file
View file

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512"><style>.a{fill:#667b89}.b{fill:#fff}</style><path fill-rule="evenodd" class="a" d="m256 512c-141.6 0-256-114.4-256-256 0-141.6 114.4-256 256-256 141.6 0 256 114.4 256 256 0 141.6-114.4 256-256 256z"/><path class="b" d="m256.1 256.1c34.5 0 62.4-27.9 62.4-62.4 0-34.5-27.9-62.4-62.4-62.4-34.5 0-62.4 27.9-62.4 62.4 0 34.5 27.9 62.4 62.4 62.4zm0 31.2c-41.6 0-124.8 20.9-124.8 62.4v31.1h249.5v-31.1c0-41.5-83.1-62.4-124.7-62.4z"/></svg>

After

Width:  |  Height:  |  Size: 533 B

1
img/node_types/2.svg Normal file
View file

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512"><style>.a{fill:#667b89}.b{fill:#fff}</style><path fill-rule="evenodd" class="a" d="m256 512c-141.6 0-256-114.4-256-256 0-141.6 114.4-256 256-256 141.6 0 256 114.4 256 256 0 141.6-114.4 256-256 256z"/><path fill-rule="evenodd" class="b" d="m196.7 284l15-15c-12.5-12.5-18.7-28.7-18.7-43.6 0-16.2 6.2-32.4 18.7-43.7l-15-14.9c-16.2 16.2-24.9 37.4-24.9 58.6 0 21.2 8.7 42.4 24.9 58.6zm147.1-147.1l-15 14.9c19.9 20 29.9 47.4 29.9 73.6 0 26.2-10 53.6-29.9 73.5l15 15c24.9-24.9 36.1-56.1 36.1-88.5 0-32.4-12.5-63.6-36.1-88.5zm-162 14.9l-15-14.9c-23.7 24.9-36.1 56.1-36.1 88.5 0 32.4 12.4 63.6 36.1 88.5l15-15c-20-19.9-29.9-47.3-29.9-73.5 0-26.2 9.9-53.6 29.9-73.6zm132 132.2c16.2-16.2 25-37.4 25-58.6-1.3-21.2-8.8-42.4-25-58.6l-14.9 14.9c12.5 12.5 18.7 28.7 18.7 43.7 0 16.2-6.2 32.4-18.7 43.6zm-27.4-58.6c0-17.2-14-31.1-31.2-31.1-17.2 0-31.1 13.9-31.1 31.1 0 9.5 4.2 17.7 10.8 23.5l-42 126.1h24.9l8.4-24.9h58.2l8.2 24.9h24.9l-42-126.1c6.6-5.8 10.9-14 10.9-23.5zm-52 99.7l20.8-62.3 20.8 62.3z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
img/node_types/3.svg Normal file
View file

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512"><style>.a{fill:#667b89}.b{fill:#fff}</style><path fill-rule="evenodd" class="a" d="m256 512c-141.6 0-256-114.4-256-256 0-141.6 114.4-256 256-256 141.6 0 256 114.4 256 256 0 141.6-114.4 256-256 256z"/><path class="b" d="m256 265.4c20.4 0 38.4 4.9 53 11.2 13.5 6 22 19.5 22 34.2v20.2h-150v-20.1c0-14.8 8.5-28.3 22-34.1 14.6-6.5 32.6-11.4 53-11.4zm-100 3.1c13.8 0 25-11.2 25-25 0-13.7-11.2-25-25-25-13.7 0-25 11.3-25 25 0 13.8 11.3 25 25 25zm14.1 13.8c-4.6-0.8-9.2-1.3-14.1-1.3-12.4 0-24.1 2.6-34.7 7.3-9.3 4-15.3 13-15.3 23.1v19.6h56.3v-20.1c0-10.4 2.8-20.1 7.8-28.6zm185.9-13.8c13.8 0 25-11.2 25-25 0-13.8-11.2-25-25-25-13.8 0-25 11.2-25 25 0 13.8 11.2 25 25 25zm50 42.9c0-10.1-6-19.1-15.2-23.1-10.7-4.7-22.4-7.3-34.8-7.3-4.9 0-9.5 0.5-14.1 1.3 5 8.5 7.8 18.2 7.8 28.6v20.1h56.3zm-150-130.4c20.8 0 37.5 16.8 37.5 37.5 0 20.8-16.8 37.5-37.5 37.5-20.7 0-37.5-16.7-37.5-37.5 0-20.7 16.8-37.5 37.5-37.5z"/></svg>

After

Width:  |  Height:  |  Size: 1,008 B

1
img/node_types/4.svg Normal file
View file

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512"><style>.a{fill:#667b89}.b{fill:#fff}</style><path fill-rule="evenodd" class="a" d="m256 512c-141.6 0-256-114.4-256-256 0-141.6 114.4-256 256-256 141.6 0 256 114.4 256 256 0 141.6-114.4 256-256 256z"/><path class="b" d="m381 242.1v-27.8h-27.8v-27.7c0-15.3-12.5-27.8-27.8-27.8h-27.7v-27.8h-27.8v27.8h-27.8v-27.8h-27.8v27.8h-27.7c-15.3 0-27.8 12.5-27.8 27.8v27.7h-27.8v27.8h27.8v27.8h-27.8v27.8h27.8v27.7c0 15.3 12.5 27.8 27.8 27.8h27.7v27.8h27.8v-27.8h27.8v27.8h27.8v-27.8h27.7c15.3 0 27.8-12.5 27.8-27.8v-27.7h27.8v-27.8h-27.8v-27.8zm-55.5 83.3h-138.9v-138.8h138.9zm-27.8-111.2h-83.3v83.4h83.3zm-27.8 55.5h-27.8v-27.7h27.8z"/></svg>

After

Width:  |  Height:  |  Size: 732 B

BIN
img/share_contact.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
img/share_self.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

99
index.html Normal file
View file

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="./css/style.css" />
<link rel="stylesheet" href="./lib/css/beer.css" />
<link rel="stylesheet" href="./lib/css/leaflet.css" />
<link rel="stylesheet" href="./lib/css/MarkerCluster.css" />
<link rel="stylesheet" href="./lib/css/MarkerCluster.Default.css" />
<script src="./lib/beer.min.js" type="module"></script>
<script src="./lib/leaflet.js"></script>
<script src="./lib/leaflet.markercluster.js"></script>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<title>MeshCore Node Map</title>
</head>
<body class="light">
<div class="container">
<div id="map"></div>
<div id="app" v-if="app.nodes" v-cloak>
<dialog class="add-dialog background no-round" ref="dialogAddNode">
<h6>Add node / Replace node</h6>
<div>
<div class="tabs">
<a class="active" data-ui="#add-self" tabindex="0">Your Node</a>
<a data-ui="#add-contact" tabindex="0">From Contacts</a>
</div>
<div class="page active" id="add-self">
<img src="./img/button_self.jpg">
<img src="./img/share_self.jpg">
</div>
<div class="page" id="add-contact">
<img src="./img/button_contact.jpg">
<img src="./img/share_contact.jpg">
</div>
</div>
<div class="small-space"></div>
<div>
Please paste your meshcore link.<br>
If you use link with same public key as node already on the map, it will get replaced.
</div>
<div class="field border"><input placeholder="meshcore:// link" v-model="app.link"></div>
<nav class="right-align">
<button class="transparent link" @click="dialogAddNode.close()">Cancel</button>
<button class="transparent link" @click="addNode">Submit</button>
</nav>
</dialog>
<button class="manual-add square round light-blue extra" title="add node from meshcore:// link" @click="dialogAddNode.showModal()"><i class="extra">add</i></button>
<div class="stats">
<span title="Stats">
<i>monitoring</i>
</span>
<span v-for="stat in stats" v-html="stat"></span>
</a>
</div>
<form class="search no-margin" action="javascript:;">
<div class="field border no-margin">
<input type="text" class="background" list="nodes" v-model="app.search" placeholder="Search Nodes">
<!--button class="filter circle transparent" data-ui="#node-filter">
<i>filter_alt</i>
</button>
<menu class="left no-wrap" id="node-filter" data-ui="#node-filter">
<li>
<label class="checkbox">
<input type="checkbox" value="1" v-model="app.nodeFilter"><span>Clients</span>
</label>
</li>
<li>
<label class="checkbox">
<input type="checkbox" value="2" v-model="app.nodeFilter"><span>Repeaters</span>
</label>
</li>
<li>
<label class="checkbox">
<input type="checkbox" value="3" v-model="app.nodeFilter"><span>Room Servers</span>
</label>
</li>
<li>
<label class="checkbox">
<input type="checkbox" value="3" v-model="app.nodeFilter"><span>Sensors</span>
</label>
</li>
</menu-->
</div>
<article class="search-results no-margin no-padding" v-if="searchResults?.length > 0">
<ul class="list no-space border">
<li v-for="node in searchResults" tabindex="0" @click="showNode(node)" @keyup.enter="showNode(node)">
<img :src="`./img/node_types/${node.type}.svg`" width="32">
<div class="max search-text">
<h6 class="small" v-html="highlightString(node.adv_name, app.search)"></h6>
<div class="search-pkey" v-html="highlightString(node.public_key, app.search)"></div>
</div>
</li>
</ul>
</article>
</form>
</div>
</div>
</body>
<script type="module" src="./src/map.js"></script>
</html>

54
lib/Helpers.ts Normal file
View file

@ -0,0 +1,54 @@
const DEC2HEX = (() => {
const alphabet = '0123456789abcdef';
const dec2hex16 = [...alphabet];
const dec2hex256 = new Array<string> (256);
for (let i = 0; i < 256; i++) {
dec2hex256[i] = `${dec2hex16[(i >>> 4) & 0xF]}${dec2hex16[i & 0xF]}`;
}
return dec2hex256;
})();
const HEX2DEC = (() => {
const hex2dec: Record<string, number> = {};
for ( let i = 0; i < 256; i++ ) {
const hex = DEC2HEX[i];
const firstLower = hex[0];
const firstUpper = firstLower.toUpperCase();
const lastLower = hex[1];
const lastUpper = lastLower.toUpperCase();
hex2dec[hex] = i;
hex2dec[`${firstLower}${lastUpper}`] = i;
hex2dec[`${firstUpper}${lastLower}`] = i;
hex2dec[`${firstUpper}${lastUpper}`] = i;
}
return hex2dec;
})();
export function uint8ArrayConcat(arrays: Uint8Array[]) {
const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for(const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return result;
}
export function hexToUint8Array(hexString: string, maxLength?: number): Uint8Array {
const length = maxLength ?? hexString.length / 2;
const result = new Uint8Array(length);
for (let i = 0; i < length; i++) {
result[i] = HEX2DEC[hexString.slice (i * 2, (i * 2) + 2)];
}
return result;
}

1
lib/beer.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,60 @@
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
/* IE 6-8 fallback colors */
.leaflet-oldie .marker-cluster-small {
background-color: rgb(181, 226, 140);
}
.leaflet-oldie .marker-cluster-small div {
background-color: rgb(110, 204, 57);
}
.leaflet-oldie .marker-cluster-medium {
background-color: rgb(241, 211, 87);
}
.leaflet-oldie .marker-cluster-medium div {
background-color: rgb(240, 194, 12);
}
.leaflet-oldie .marker-cluster-large {
background-color: rgb(253, 156, 115);
}
.leaflet-oldie .marker-cluster-large div {
background-color: rgb(241, 128, 23);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}

14
lib/css/MarkerCluster.css Normal file
View file

@ -0,0 +1,14 @@
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
.leaflet-cluster-spider-leg {
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}

1
lib/css/beer.css Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

661
lib/css/leaflet.css Normal file
View file

@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

BIN
lib/images/layers-2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

661
lib/leaflet.css Normal file
View file

@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

6
lib/leaflet.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

18216
lib/vue.esm-browser.js Normal file

File diff suppressed because it is too large Load diff

0
lib/vue.esm-browser.min.js vendored Normal file
View file

228
src/map.js Normal file
View file

@ -0,0 +1,228 @@
import { createApp, reactive, ref, computed, onMounted } from '../lib/vue.esm-browser.js';
const apiUrl = '/api/v1/nodes';
const keyOrder = ['adv_name', 'type', 'link', 'last_advert', 'public_key', 'coords', 'params' ]
const humanLabel = {
coords: 'Coordinates',
adv_name: 'Name',
last_advert: 'Last heard',
public_key: 'Public key',
type: 'Node type',
params: 'Radio params',
link: 'Meshcore link',
};
const types = {
'1': 'Client',
'2': 'Repeater',
'3': 'Room Server',
'4': 'Sensor'
};
const humanValue = {
last_advert(val) {
return new Date(val).toLocaleString();
},
coords(val) {
return `<a target="_blank" href="https://google.com/maps/place/${val.replace(' ', '')}">${val}</a>`;
},
type(val) {
return types[val];
},
link(val) {
return `<a href="javascript:navigator.clipboard.writeText('${val}')">Copy to clipboard</a>`
},
params(val) {
return Object.entries(val).map(([key, val]) => `${key}=${val}`).join(', ')
}
}
function clearLocationHash () {
history.pushState('', document.title, location.pathname + location.search);
}
function getTable(node) {
return '<table class="node-info"><tbody>'+
'<tr>' + keyOrder.flatMap(key => node[key] ? [`<td><b>${humanLabel[key]}</b></td><td>${ humanValue[key] ? humanValue[key](node[key]) : node[key] }</td>`] : [] ).join('</tr><tr>') + '</tr>'+
'</tbody></table>';
}
window.isNewerThan = (date, days) => {
const daysMs = 1000 * 3600 * 24 * days;
const dateMs = new Date(date).getTime();
return dateMs > Date.now() - daysMs;
}
const deletionMailUrl = new URL('mailto:recrof@gmail.com');
deletionMailUrl.searchParams.append('subject', 'MeshCore Map node deletion request');
deletionMailUrl.searchParams.append('body',
'Please delete my node from MeshCore Map database\n'+
'MeshCore link: <please insert meshcore:// link here>\n'
);
const appAttribution = `
App: recrof, <a target="_blank" href="https://www.paypal.com/donate/?business=DREHF5HM265ES&no_recurring=0&item_name=If+you+enjoy+my+work%2C+you+can+support+me+here%3A&currency_code=EUR">
<strong>support my work</strong></a> |
<a target="_blank" href="${deletionMailUrl.toString().replaceAll('+', '%20')}"><strong>Node deletion request</strong></a>
`;
const baseMapSelected = localStorage.getItem('baseMapSelected') || 'OpenStreetMap';
const baseMaps = {
'OpenStreetMap': L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: `Tiles: &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> | ${appAttribution}`
}),
'Esri Satellite': L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 18,
attribution: `Tiles: &copy; Esri | Sources: Esri, DigitalGlobe, GeoEye, i-cubed, USDA FSA, USGS, AEX, Getmapping, Aerogrid, IGN, IGP, swisstopo, GIS Users | ${appAttribution}`,
}),
};
let initCoords = { lat: 7, lon: 25, zoom: 3 };
const urlParams = Object.fromEntries(new URLSearchParams(location.search));
if(!(isNaN(urlParams.lat) || isNaN(urlParams.lon) || isNaN(urlParams.zoom))) {
initCoords = urlParams
}
const map = window.leafletMap = leaflet.map('map', {
minZoom: 2,
maxBounds: [
[-90, -180], // top left
[90, 200], // bottom right
],
layers: baseMaps[baseMapSelected],
zoomControl: false
}).setView([initCoords.lat, initCoords.lon], initCoords.zoom);
map.on('baselayerchange', function(ev) {
localStorage.setItem('baseMapSelected', ev.name);
});
L.control.layers(baseMaps, null, { position: 'bottomleft' }).addTo(map);
// map.zoomControl.setPosition('bottomleft');
const icons = Object.fromEntries([1, 2, 3, 4].map(id => [id, L.icon({
iconUrl: `img/node_types/${id}.svg`,
iconSize: [32, 32],
iconAnchor: [17, 17],
popupAnchor: [0, -16],
})]));
createApp({
setup() {
const dialogAddNode = ref();
const app = window.app = reactive({
nodes: null,
search: '',
link: '',
nodeFilter: [1, 2, 3, 4]
});
const stats = computed(() => {
const nodes = app.nodes;
if(!nodes) return [];
const result = [];
result.push(`
<span>total: <b>${nodes.length}</b></span>&nbsp;|
<i class="node-type pointer-help" title="Total client nodes">person</i><b>${nodes.filter(n => n.type === 1).length}</b>&nbsp;|
<i class="node-type pointer-help" title="Total repeater nodes">cell_tower</i><b>${nodes.filter(n => n.type === 2).length}</b>&nbsp;|
<i class="node-type pointer-help" title="Total room server nodes">forum</i><b>${nodes.filter(n => n.type === 3).length}</b>
`);
result.push(`<span class="pointer-help" title="Nodes added in last 24 hours">24h: <b>${app.nodes.filter(n => isNewerThan(n.inserted_date, 1)).length}</b></span>`);
result.push(`<span class="pointer-help" title="Nodes added in last 7 days">7d: <b>${app.nodes.filter(n => isNewerThan(n.inserted_date, 7)).length}</b></span>`);
result.push(`<span class="pointer-help" title="Nodes added in last 30 days">30d: <b>${app.nodes.filter(n => isNewerThan(n.inserted_date, 30)).length}</b></span>`);
return result;
});
const searchResults = computed(() => {
if(!app.search) { return [] }
return app.nodes.filter(
node => node.adv_name.toLowerCase().includes(app.search.toLowerCase()) || node.public_key.startsWith(app.search)
).toSorted(
(a, b) => a.adv_name.localeCompare(b.adv_name)
).slice(0, 20);
})
async function refreshMap(refresh, noDownload) {
if(!noDownload) {
const nodesReq = await fetch(apiUrl);
app.nodes = await nodesReq.json();
}
const markers = L.markerClusterGroup();
for(const node of app.nodes) {
const marker = L.marker([node.adv_lat, node.adv_lon], { icon: icons[node.type.toString()], title: node.adv_name });
node.marker = marker;
node.coords = `${node.adv_lat.toFixed(4)}, ${node.adv_lon.toFixed(4)}`;
node.lastAdvertDate = new Date(node.last_advert);
const popup = L.popup({ minWidth: 350, maxWidth: 350, content: getTable(node) });
marker.bindPopup(popup);
markers.addLayer(marker);
}
if(refresh) {
map.eachLayer(layer => layer.clearLayers());
}
map.addLayer(markers);
}
async function addNode() {
if(!(app.link && app.link.startsWith('meshcore://'))) {
alert('Please paste valid meshcore link.');
return;
};
const res = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
links: [ app.link ],
radio: {}
})
});
const reply = await res.json();
alert(reply.message || reply.error);
clearLocationHash();
location.reload();
}
function showNode(node) {
node.marker.openPopup();
map.flyTo(node.marker.getLatLng(), 19);
app.search = '';
}
function highlightString(source, toHighlight) {
const escapedSource = source.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
const matchIndex = source.toLowerCase().indexOf(toHighlight.toLowerCase());
const highlightString = matchIndex >= 0 ? source.substring(matchIndex, matchIndex + toHighlight.length) : toHighlight;
return escapedSource.replace(highlightString, `<b>${highlightString}</b>`);
}
refreshMap();
map.on('moveend', function(e) {
const pos = map.getCenter();
const zoom = map.getZoom();
history.replaceState({}, '', `/?lat=${pos.lat.toFixed(4)}&lon=${pos.lng.toFixed(4)}&zoom=${zoom}`);
});
onMounted(() => {
if(location.hash === '#add-new-node') {
dialogAddNode.value.showModal();
dialogAddNode.value.addEventListener("close", () => clearLocationHash());
}
})
window.refreshMap = refreshMap;
return {
app, refreshMap, addNode,
stats, searchResults,
showNode, dialogAddNode, highlightString
}
},
}).mount('#app')