remove src directory
127
src/admin.js
@ -1,127 +0,0 @@
|
|||||||
// node src/admin.js --purge-node-id 123
|
|
||||||
// node src/admin.js --purge-node-id '!AABBCCDD'
|
|
||||||
|
|
||||||
const commandLineArgs = require("command-line-args");
|
|
||||||
const commandLineUsage = require("command-line-usage");
|
|
||||||
|
|
||||||
// create prisma db client
|
|
||||||
const { PrismaClient } = require("@prisma/client");
|
|
||||||
const NodeIdUtil = require("./utils/node_id_util");
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
const optionsList = [
|
|
||||||
{
|
|
||||||
name: 'help',
|
|
||||||
alias: 'h',
|
|
||||||
type: Boolean,
|
|
||||||
description: 'Display this usage guide.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "purge-node-id",
|
|
||||||
type: String,
|
|
||||||
description: "Purges all records for the provided node id.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// parse command line args
|
|
||||||
const options = commandLineArgs(optionsList);
|
|
||||||
|
|
||||||
// show help
|
|
||||||
if(options.help){
|
|
||||||
const usage = commandLineUsage([
|
|
||||||
{
|
|
||||||
header: 'Meshtastic Map Admin',
|
|
||||||
content: 'Command line admin tool for the Meshtastic Map',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Options',
|
|
||||||
optionList: optionsList,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
console.log(usage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get options and fallback to default values
|
|
||||||
const purgeNodeId = options["purge-node-id"] ?? null;
|
|
||||||
|
|
||||||
async function purgeNodeById(nodeId) {
|
|
||||||
|
|
||||||
// convert to numeric id
|
|
||||||
nodeId = NodeIdUtil.convertToNumeric(nodeId);
|
|
||||||
|
|
||||||
// purge environment metrics
|
|
||||||
await prisma.environmentMetric.deleteMany({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// purge map reports
|
|
||||||
await prisma.mapReport.deleteMany({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// purge neighbour infos
|
|
||||||
await prisma.neighbourInfo.deleteMany({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// purge this node
|
|
||||||
await prisma.node.deleteMany({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// purge positions
|
|
||||||
await prisma.position.deleteMany({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// purge power metrics
|
|
||||||
await prisma.powerMetric.deleteMany({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// purge text messages
|
|
||||||
await prisma.textMessage.deleteMany({
|
|
||||||
where: {
|
|
||||||
from: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// purge traceroutes
|
|
||||||
await prisma.traceRoute.deleteMany({
|
|
||||||
where: {
|
|
||||||
from: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// purge waypoints
|
|
||||||
await prisma.waypoint.deleteMany({
|
|
||||||
where: {
|
|
||||||
from: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Node '${nodeId}' has been purged from the database.`);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
|
|
||||||
// purge node by id
|
|
||||||
if(purgeNodeId){
|
|
||||||
await purgeNodeById(purgeNodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
815
src/index.js
@ -1,815 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const express = require('express');
|
|
||||||
const compression = require('compression');
|
|
||||||
const protobufjs = require("protobufjs");
|
|
||||||
const commandLineArgs = require("command-line-args");
|
|
||||||
const commandLineUsage = require("command-line-usage");
|
|
||||||
|
|
||||||
// create prisma db client
|
|
||||||
const { PrismaClient } = require("@prisma/client");
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// return big ints as string when using JSON.stringify
|
|
||||||
BigInt.prototype.toJSON = function() {
|
|
||||||
return this.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const optionsList = [
|
|
||||||
{
|
|
||||||
name: 'help',
|
|
||||||
alias: 'h',
|
|
||||||
type: Boolean,
|
|
||||||
description: 'Display this usage guide.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "port",
|
|
||||||
type: Number,
|
|
||||||
description: "Port to serve web ui and api from.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// parse command line args
|
|
||||||
const options = commandLineArgs(optionsList);
|
|
||||||
|
|
||||||
// show help
|
|
||||||
if(options.help){
|
|
||||||
const usage = commandLineUsage([
|
|
||||||
{
|
|
||||||
header: 'Meshtastic Map',
|
|
||||||
content: 'A map of all Meshtastic nodes heard via MQTT.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Options',
|
|
||||||
optionList: optionsList,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
console.log(usage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get options and fallback to default values
|
|
||||||
const port = options["port"] ?? 8080;
|
|
||||||
|
|
||||||
// load protobufs
|
|
||||||
const root = new protobufjs.Root();
|
|
||||||
root.resolvePath = (origin, target) => path.join(__dirname, "protos", target);
|
|
||||||
root.loadSync('meshtastic/mqtt.proto');
|
|
||||||
const HardwareModel = root.lookupEnum("HardwareModel");
|
|
||||||
const Role = root.lookupEnum("Config.DeviceConfig.Role");
|
|
||||||
const RegionCode = root.lookupEnum("Config.LoRaConfig.RegionCode");
|
|
||||||
const ModemPreset = root.lookupEnum("Config.LoRaConfig.ModemPreset");
|
|
||||||
|
|
||||||
// appends extra info for node objects returned from api
|
|
||||||
function formatNodeInfo(node) {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
node_id_hex: "!" + node.node_id.toString(16),
|
|
||||||
hardware_model_name: HardwareModel.valuesById[node.hardware_model] ?? null,
|
|
||||||
role_name: Role.valuesById[node.role] ?? null,
|
|
||||||
region_name: RegionCode.valuesById[node.region] ?? null,
|
|
||||||
modem_preset_name: ModemPreset.valuesById[node.modem_preset] ?? null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// enable compression
|
|
||||||
app.use(compression());
|
|
||||||
|
|
||||||
// serve files inside the public folder from /
|
|
||||||
app.use('/', express.static(path.join(__dirname, 'public')));
|
|
||||||
|
|
||||||
app.get('/', async (req, res) => {
|
|
||||||
res.sendFile(path.join(__dirname, 'public/index.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api', async (req, res) => {
|
|
||||||
|
|
||||||
const links = [
|
|
||||||
{
|
|
||||||
"path": "/api",
|
|
||||||
"description": "This page",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/nodes",
|
|
||||||
"description": "All meshtastic nodes",
|
|
||||||
"params": {
|
|
||||||
"role": "Filter by role",
|
|
||||||
"hardware_model": "Filter by hardware model",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/nodes/:nodeId",
|
|
||||||
"description": "A specific meshtastic node",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/nodes/:nodeId/device-metrics",
|
|
||||||
"description": "Device metrics for a meshtastic node",
|
|
||||||
"params": {
|
|
||||||
"count": "How many results to return",
|
|
||||||
"time_from": "Only include metrics created after this unix timestamp (milliseconds)",
|
|
||||||
"time_to": "Only include metrics created before this unix timestamp (milliseconds)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/nodes/:nodeId/environment-metrics",
|
|
||||||
"description": "Environment metrics for a meshtastic node",
|
|
||||||
"params": {
|
|
||||||
"count": "How many results to return",
|
|
||||||
"time_from": "Only include metrics created after this unix timestamp (milliseconds)",
|
|
||||||
"time_to": "Only include metrics created before this unix timestamp (milliseconds)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/nodes/:nodeId/power-metrics",
|
|
||||||
"description": "Power metrics for a meshtastic node",
|
|
||||||
"params": {
|
|
||||||
"count": "How many results to return",
|
|
||||||
"time_from": "Only include metrics created after this unix timestamp (milliseconds)",
|
|
||||||
"time_to": "Only include metrics created before this unix timestamp (milliseconds)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/nodes/:nodeId/neighbours",
|
|
||||||
"description": "Neighbours for a meshtastic node",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/nodes/:nodeId/traceroutes",
|
|
||||||
"description": "Trace routes for a meshtastic node",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/nodes/:nodeId/position-history",
|
|
||||||
"description": "Position history for a meshtastic node",
|
|
||||||
"params": {
|
|
||||||
"time_from": "Only include positions created after this unix timestamp (milliseconds)",
|
|
||||||
"time_to": "Only include positions created before this unix timestamp (milliseconds)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/stats/hardware-models",
|
|
||||||
"description": "Database statistics about known hardware models",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/text-messages",
|
|
||||||
"description": "Text messages",
|
|
||||||
"params": {
|
|
||||||
"to": "Only include messages to this node id",
|
|
||||||
"from": "Only include messages from this node id",
|
|
||||||
"channel_id": "Only include messages for this channel id",
|
|
||||||
"gateway_id": "Only include messages gated to mqtt by this node id",
|
|
||||||
"last_id": "Only include messages before or after this id, based on results order",
|
|
||||||
"count": "How many results to return",
|
|
||||||
"order": "Order to return results in: asc, desc",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/text-messages/embed",
|
|
||||||
"description": "Text messages rendered as an embeddable HTML page.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/api/v1/waypoints",
|
|
||||||
"description": "Waypoints",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const linksHtml = links.map((link) => {
|
|
||||||
var line = `<li>`;
|
|
||||||
line += `<a href="${link.path}">${link.path}</a> - ${link.description}`;
|
|
||||||
line += `<ul>`;
|
|
||||||
for(const paramKey in (link.params ?? [])){
|
|
||||||
const paramDescription = link.params[paramKey];
|
|
||||||
line += "<li>";
|
|
||||||
line += `?${paramKey}: ${paramDescription}`;
|
|
||||||
line += `</li>`;
|
|
||||||
}
|
|
||||||
line += `</ul>`;
|
|
||||||
return line;
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
res.send(`<b>API Docs</b><br/><ul>${linksHtml}</ul>`);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/nodes', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// get query params
|
|
||||||
const role = req.query.role ? parseInt(req.query.role) : undefined;
|
|
||||||
const hardwareModel = req.query.hardware_model ? parseInt(req.query.hardware_model) : undefined;
|
|
||||||
|
|
||||||
// get nodes from db
|
|
||||||
const nodes = await prisma.node.findMany({
|
|
||||||
where: {
|
|
||||||
role: role,
|
|
||||||
hardware_model: hardwareModel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const nodesWithInfo = [];
|
|
||||||
for(const node of nodes){
|
|
||||||
nodesWithInfo.push(formatNodeInfo(node));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
nodes: nodesWithInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/nodes/:nodeId', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const nodeId = parseInt(req.params.nodeId);
|
|
||||||
|
|
||||||
// find node
|
|
||||||
const node = await prisma.node.findFirst({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// make sure node exists
|
|
||||||
if(!node){
|
|
||||||
res.status(404).json({
|
|
||||||
message: "Not Found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
node: formatNodeInfo(node),
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/nodes/:nodeId/device-metrics', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const nodeId = parseInt(req.params.nodeId);
|
|
||||||
const count = req.query.count ? parseInt(req.query.count) : undefined;
|
|
||||||
const timeFrom = req.query.time_from ? parseInt(req.query.time_from) : undefined;
|
|
||||||
const timeTo = req.query.time_to ? parseInt(req.query.time_to) : undefined;
|
|
||||||
|
|
||||||
// find node
|
|
||||||
const node = await prisma.node.findFirst({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// make sure node exists
|
|
||||||
if(!node){
|
|
||||||
res.status(404).json({
|
|
||||||
message: "Not Found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get latest device metrics
|
|
||||||
const deviceMetrics = await prisma.deviceMetric.findMany({
|
|
||||||
where: {
|
|
||||||
node_id: node.node_id,
|
|
||||||
created_at: {
|
|
||||||
gte: timeFrom ? new Date(timeFrom) : undefined,
|
|
||||||
lte: timeTo ? new Date(timeTo) : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
id: 'desc',
|
|
||||||
},
|
|
||||||
take: count,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
device_metrics: deviceMetrics,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/nodes/:nodeId/environment-metrics', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const nodeId = parseInt(req.params.nodeId);
|
|
||||||
const count = req.query.count ? parseInt(req.query.count) : undefined;
|
|
||||||
const timeFrom = req.query.time_from ? parseInt(req.query.time_from) : undefined;
|
|
||||||
const timeTo = req.query.time_to ? parseInt(req.query.time_to) : undefined;
|
|
||||||
|
|
||||||
// find node
|
|
||||||
const node = await prisma.node.findFirst({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// make sure node exists
|
|
||||||
if(!node){
|
|
||||||
res.status(404).json({
|
|
||||||
message: "Not Found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get latest environment metrics
|
|
||||||
const environmentMetrics = await prisma.environmentMetric.findMany({
|
|
||||||
where: {
|
|
||||||
node_id: node.node_id,
|
|
||||||
created_at: {
|
|
||||||
gte: timeFrom ? new Date(timeFrom) : undefined,
|
|
||||||
lte: timeTo ? new Date(timeTo) : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
id: 'desc',
|
|
||||||
},
|
|
||||||
take: count,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
environment_metrics: environmentMetrics,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/nodes/:nodeId/power-metrics', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const nodeId = parseInt(req.params.nodeId);
|
|
||||||
const count = req.query.count ? parseInt(req.query.count) : undefined;
|
|
||||||
const timeFrom = req.query.time_from ? parseInt(req.query.time_from) : undefined;
|
|
||||||
const timeTo = req.query.time_to ? parseInt(req.query.time_to) : undefined;
|
|
||||||
|
|
||||||
// find node
|
|
||||||
const node = await prisma.node.findFirst({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// make sure node exists
|
|
||||||
if(!node){
|
|
||||||
res.status(404).json({
|
|
||||||
message: "Not Found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get latest power metrics
|
|
||||||
const powerMetrics = await prisma.powerMetric.findMany({
|
|
||||||
where: {
|
|
||||||
node_id: node.node_id,
|
|
||||||
created_at: {
|
|
||||||
gte: timeFrom ? new Date(timeFrom) : undefined,
|
|
||||||
lte: timeTo ? new Date(timeTo) : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
id: 'desc',
|
|
||||||
},
|
|
||||||
take: count,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
power_metrics: powerMetrics,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/nodes/:nodeId/mqtt-metrics', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const nodeId = parseInt(req.params.nodeId);
|
|
||||||
|
|
||||||
// find node
|
|
||||||
const node = await prisma.node.findFirst({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// make sure node exists
|
|
||||||
if(!node){
|
|
||||||
res.status(404).json({
|
|
||||||
message: "Not Found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get mqtt topics published to by this node
|
|
||||||
const queryResult = await prisma.$queryRaw`select mqtt_topic, count(*) as packet_count, max(created_at) as last_packet_at from service_envelopes where gateway_id = ${nodeId} group by mqtt_topic order by packet_count desc;`;
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
mqtt_metrics: queryResult,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/nodes/:nodeId/neighbours', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const nodeId = parseInt(req.params.nodeId);
|
|
||||||
|
|
||||||
// find node
|
|
||||||
const node = await prisma.node.findFirst({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// make sure node exists
|
|
||||||
if(!node){
|
|
||||||
res.status(404).json({
|
|
||||||
message: "Not Found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get nodes from db that have this node as a neighbour
|
|
||||||
const nodesThatHeardUs = await prisma.node.findMany({
|
|
||||||
where: {
|
|
||||||
neighbours: {
|
|
||||||
array_contains: {
|
|
||||||
node_id: Number(nodeId),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
nodes_that_we_heard: node.neighbours.map((neighbour) => {
|
|
||||||
return {
|
|
||||||
...neighbour,
|
|
||||||
updated_at: node.neighbours_updated_at,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
nodes_that_heard_us: nodesThatHeardUs.map((nodeThatHeardUs) => {
|
|
||||||
const neighbourInfo = nodeThatHeardUs.neighbours.find((neighbour) => neighbour.node_id.toString() === node.node_id.toString());
|
|
||||||
return {
|
|
||||||
node_id: Number(nodeThatHeardUs.node_id),
|
|
||||||
snr: neighbourInfo.snr,
|
|
||||||
updated_at: nodeThatHeardUs.neighbours_updated_at,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/nodes/:nodeId/traceroutes', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const nodeId = parseInt(req.params.nodeId);
|
|
||||||
const count = req.query.count ? parseInt(req.query.count) : 10; // can't set to null because of $queryRaw
|
|
||||||
|
|
||||||
// find node
|
|
||||||
const node = await prisma.node.findFirst({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// make sure node exists
|
|
||||||
if(!node){
|
|
||||||
res.status(404).json({
|
|
||||||
message: "Not Found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get latest traceroutes
|
|
||||||
// We want replies where want_response is false and it will be "to" the
|
|
||||||
// requester.
|
|
||||||
const traceroutes = await prisma.$queryRaw`SELECT * FROM traceroutes WHERE want_response = false and \`to\` = ${node.node_id} and gateway_id is not null order by id desc limit ${count}`;
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
traceroutes: traceroutes.map((trace) => {
|
|
||||||
|
|
||||||
// ensure route is json array
|
|
||||||
if(typeof(trace.route) === "string"){
|
|
||||||
trace.route = JSON.parse(trace.route);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure route_back is json array
|
|
||||||
if(typeof(trace.route_back) === "string"){
|
|
||||||
trace.route_back = JSON.parse(trace.route_back);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure snr_towards is json array
|
|
||||||
if(typeof(trace.snr_towards) === "string"){
|
|
||||||
trace.snr_towards = JSON.parse(trace.snr_towards);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure snr_back is json array
|
|
||||||
if(typeof(trace.snr_back) === "string"){
|
|
||||||
trace.snr_back = JSON.parse(trace.snr_back);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trace;
|
|
||||||
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// defaults
|
|
||||||
const nowInMilliseconds = new Date().getTime();
|
|
||||||
const oneHourAgoInMilliseconds = new Date().getTime() - (3600 * 1000);
|
|
||||||
|
|
||||||
// get request params
|
|
||||||
const nodeId = parseInt(req.params.nodeId);
|
|
||||||
const timeFrom = req.query.time_from ? parseInt(req.query.time_from) : oneHourAgoInMilliseconds;
|
|
||||||
const timeTo = req.query.time_to ? parseInt(req.query.time_to) : nowInMilliseconds;
|
|
||||||
|
|
||||||
// find node
|
|
||||||
const node = await prisma.node.findFirst({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// make sure node exists
|
|
||||||
if(!node){
|
|
||||||
res.status(404).json({
|
|
||||||
message: "Not Found",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const positions = await prisma.position.findMany({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
created_at: {
|
|
||||||
gte: new Date(timeFrom),
|
|
||||||
lte: new Date(timeTo),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapReports = await prisma.mapReport.findMany({
|
|
||||||
where: {
|
|
||||||
node_id: nodeId,
|
|
||||||
created_at: {
|
|
||||||
gte: new Date(timeFrom),
|
|
||||||
lte: new Date(timeTo),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const positionHistory = []
|
|
||||||
|
|
||||||
positions.forEach((position) => {
|
|
||||||
positionHistory.push({
|
|
||||||
id: position.id,
|
|
||||||
node_id: position.node_id,
|
|
||||||
type: "position",
|
|
||||||
latitude: position.latitude,
|
|
||||||
longitude: position.longitude,
|
|
||||||
altitude: position.altitude,
|
|
||||||
gateway_id: position.gateway_id,
|
|
||||||
channel_id: position.channel_id,
|
|
||||||
created_at: position.created_at,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
mapReports.forEach((mapReport) => {
|
|
||||||
positionHistory.push({
|
|
||||||
node_id: mapReport.node_id,
|
|
||||||
type: "map_report",
|
|
||||||
latitude: mapReport.latitude,
|
|
||||||
longitude: mapReport.longitude,
|
|
||||||
altitude: mapReport.altitude,
|
|
||||||
created_at: mapReport.created_at,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// sort oldest to newest
|
|
||||||
positionHistory.sort((a, b) => a.created_at - b.created_at);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
position_history: positionHistory,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/stats/hardware-models', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// get nodes from db
|
|
||||||
const results = await prisma.node.groupBy({
|
|
||||||
by: ['hardware_model'],
|
|
||||||
orderBy: {
|
|
||||||
_count: {
|
|
||||||
hardware_model: 'desc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_count: {
|
|
||||||
hardware_model: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const hardwareModelStats = results.map((result) => {
|
|
||||||
return {
|
|
||||||
count: result._count.hardware_model,
|
|
||||||
hardware_model: result.hardware_model,
|
|
||||||
hardware_model_name: HardwareModel.valuesById[result.hardware_model] ?? "UNKNOWN",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
hardware_model_stats: hardwareModelStats,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/text-messages', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// get query params
|
|
||||||
const to = req.query.to ?? undefined;
|
|
||||||
const from = req.query.from ?? undefined;
|
|
||||||
const channelId = req.query.channel_id ?? undefined;
|
|
||||||
const gatewayId = req.query.gateway_id ?? undefined;
|
|
||||||
const directMessageNodeIds = req.query.direct_message_node_ids?.split(",") ?? undefined;
|
|
||||||
const lastId = req.query.last_id ? parseInt(req.query.last_id) : undefined;
|
|
||||||
const count = req.query.count ? parseInt(req.query.count) : 50;
|
|
||||||
const order = req.query.order ?? "asc";
|
|
||||||
|
|
||||||
// if direct message node ids are provided, there should be exactly two node ids
|
|
||||||
if(directMessageNodeIds !== undefined && directMessageNodeIds.length !== 2){
|
|
||||||
res.status(400).json({
|
|
||||||
message: "direct_message_node_ids requires 2 node ids separated by a comma.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// default where clauses that should always be used for filtering
|
|
||||||
var where = {
|
|
||||||
channel_id: channelId,
|
|
||||||
gateway_id: gatewayId,
|
|
||||||
// when ordered oldest to newest (asc), only get records after last id
|
|
||||||
// when ordered newest to oldest (desc), only get records before last id
|
|
||||||
id: order === "asc" ? {
|
|
||||||
gt: lastId,
|
|
||||||
} : {
|
|
||||||
lt: lastId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// if direct message node ids are provided, we expect exactly 2 node ids
|
|
||||||
if(directMessageNodeIds !== undefined && directMessageNodeIds.length === 2){
|
|
||||||
// filter message by "to -> from" or "from -> to"
|
|
||||||
const [firstNodeId, secondNodeId] = directMessageNodeIds;
|
|
||||||
where = {
|
|
||||||
AND: where,
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
to: firstNodeId,
|
|
||||||
from: secondNodeId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: secondNodeId,
|
|
||||||
from: firstNodeId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// filter by to and from
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
to: to,
|
|
||||||
from: from,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// get text messages from db
|
|
||||||
const textMessages = await prisma.textMessage.findMany({
|
|
||||||
where: where,
|
|
||||||
orderBy: {
|
|
||||||
id: order,
|
|
||||||
},
|
|
||||||
take: count,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
text_messages: textMessages,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/text-messages/embed', async (req, res) => {
|
|
||||||
res.sendFile(path.join(__dirname, 'public/text-messages-embed.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/waypoints', async (req, res) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// get waypoints from db
|
|
||||||
const waypoints = await prisma.waypoint.findMany({
|
|
||||||
orderBy: {
|
|
||||||
id: 'desc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ensure we only have the latest unique waypoints
|
|
||||||
// since ordered by newest first, older entries will be ignored
|
|
||||||
const uniqueWaypoints = [];
|
|
||||||
for(const waypoint of waypoints){
|
|
||||||
|
|
||||||
// skip if we already have a newer entry for this waypoint
|
|
||||||
if(uniqueWaypoints.find((w) => w.from === waypoint.from && w.waypoint_id === waypoint.waypoint_id)){
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// first time seeing this waypoint, add to unique list
|
|
||||||
uniqueWaypoints.push(waypoint);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// we only want waypoints that haven't expired yet
|
|
||||||
const nonExpiredWayPoints = uniqueWaypoints.filter((waypoint) => {
|
|
||||||
const nowInSeconds = Math.floor(Date.now() / 1000);
|
|
||||||
return waypoint.expire >= nowInSeconds;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
waypoints: nonExpiredWayPoints,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
res.status(500).json({
|
|
||||||
message: "Something went wrong, try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// start express server
|
|
||||||
const listener = app.listen(port, () => {
|
|
||||||
const port = listener.address().port;
|
|
||||||
console.log(`Server running at http://127.0.0.1:${port}`);
|
|
||||||
});
|
|
1390
src/mqtt.js
@ -1,7 +0,0 @@
|
|||||||
/*!
|
|
||||||
* chartjs-adapter-moment v1.0.1
|
|
||||||
* https://www.chartjs.org
|
|
||||||
* (c) 2022 chartjs-adapter-moment Contributors
|
|
||||||
* Released under the MIT license
|
|
||||||
*/
|
|
||||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})}));
|
|
@ -1,691 +0,0 @@
|
|||||||
function modulus(i, n) {
|
|
||||||
return ((i % n) + n) % n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function definedProps(obj) {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(obj).filter(([k, v]) => v !== undefined)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not a string is in the format '<number>m'
|
|
||||||
* @param {string} value
|
|
||||||
* @returns Boolean
|
|
||||||
*/
|
|
||||||
function isInMeters(value) {
|
|
||||||
return (
|
|
||||||
value
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.slice(value.toString().length - 1, value.toString().length) === 'm'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not a string is in the format '<number>%'
|
|
||||||
* @param {string} value
|
|
||||||
* @returns Boolean
|
|
||||||
*/
|
|
||||||
function isInPercent(value) {
|
|
||||||
return (
|
|
||||||
value
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.slice(value.toString().length - 1, value.toString().length) === '%'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not a string is in the format '<number>px'
|
|
||||||
* @param {string} value
|
|
||||||
* @returns Boolean
|
|
||||||
*/
|
|
||||||
function isInPixels(value) {
|
|
||||||
return (
|
|
||||||
value
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.slice(value.toString().length - 2, value.toString().length) === 'px'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pixelsToMeters(pixels, map) {
|
|
||||||
let refPoint1 = map.getCenter();
|
|
||||||
let xy1 = map.latLngToLayerPoint(refPoint1);
|
|
||||||
let xy2 = {
|
|
||||||
x: xy1.x + Number(pixels),
|
|
||||||
y: xy1.y,
|
|
||||||
};
|
|
||||||
let refPoint2 = map.layerPointToLatLng(xy2);
|
|
||||||
let derivedMeters = map.distance(refPoint1, refPoint2);
|
|
||||||
return derivedMeters;
|
|
||||||
}
|
|
||||||
|
|
||||||
L.Polyline.include({
|
|
||||||
/**
|
|
||||||
* Adds arrowheads to an L.polyline
|
|
||||||
* @param {object} options The options for the arrowhead. See documentation for details
|
|
||||||
* @returns The L.polyline instance that they arrowheads are attached to
|
|
||||||
*/
|
|
||||||
arrowheads: function (options = {}) {
|
|
||||||
// Merge user input options with default options:
|
|
||||||
const defaults = {
|
|
||||||
yawn: 60,
|
|
||||||
size: '15%',
|
|
||||||
frequency: 'allvertices',
|
|
||||||
proportionalToTotal: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.options.noClip = true;
|
|
||||||
|
|
||||||
let actualOptions = Object.assign({}, defaults, options);
|
|
||||||
this._arrowheadOptions = actualOptions;
|
|
||||||
|
|
||||||
this._hatsApplied = true;
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
buildVectorHats: function (options) {
|
|
||||||
// Reset variables from previous this._update()
|
|
||||||
if (this._arrowheads) {
|
|
||||||
this._arrowheads.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._ghosts) {
|
|
||||||
this._ghosts.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------- //
|
|
||||||
// ------------ FILTER THE OPTIONS ----------------------- //
|
|
||||||
/*
|
|
||||||
* The next 3 lines folds the options of the parent polyline into the default options for all polylines
|
|
||||||
* The options for the arrowhead are then folded in as well
|
|
||||||
* All options defined in parent polyline will be inherited by the arrowhead, unless otherwise specified in the arrowhead(options) call
|
|
||||||
*/
|
|
||||||
|
|
||||||
let defaultOptionsOfParent = Object.getPrototypeOf(
|
|
||||||
Object.getPrototypeOf(this.options)
|
|
||||||
);
|
|
||||||
|
|
||||||
// merge default options of parent polyline (this.options's prototype's prototype) with options passed to parent polyline (this.options).
|
|
||||||
let parentOptions = Object.assign({}, defaultOptionsOfParent, this.options);
|
|
||||||
|
|
||||||
// now merge in the options the user has put in the arrowhead call
|
|
||||||
let hatOptions = Object.assign({}, parentOptions, options);
|
|
||||||
|
|
||||||
// ...with a few exceptions:
|
|
||||||
hatOptions.smoothFactor = 1;
|
|
||||||
hatOptions.fillOpacity = 1;
|
|
||||||
hatOptions.fill = options.fill ? true : false;
|
|
||||||
hatOptions.interactive = false;
|
|
||||||
|
|
||||||
// ------------ FILTER THE OPTIONS END -------------------- //
|
|
||||||
// --------------------------------------------------------- //
|
|
||||||
|
|
||||||
// --------------------------------------------------------- //
|
|
||||||
// ------ LOOP THROUGH EACH POLYLINE SEGMENT --------------- //
|
|
||||||
// ------ TO CALCULATE HAT SIZES AND CAPTURE IN ARRAY ------ //
|
|
||||||
|
|
||||||
let size = options.size.toString(); // stringify if its a number
|
|
||||||
let allhats = []; // empty array to receive hat polylines
|
|
||||||
const { frequency, offsets } = options;
|
|
||||||
|
|
||||||
if (offsets?.start || offsets?.end) {
|
|
||||||
this._buildGhosts({ start: offsets.start, end: offsets.end });
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineToTrace = this._ghosts || this;
|
|
||||||
|
|
||||||
lineToTrace._parts.forEach((peice, index) => {
|
|
||||||
// Immutable variables for each peice
|
|
||||||
const latlngs = peice.map((point) => this._map.layerPointToLatLng(point));
|
|
||||||
|
|
||||||
const totalLength = (() => {
|
|
||||||
let total = 0;
|
|
||||||
for (var i = 0; i < peice.length - 1; i++) {
|
|
||||||
total += this._map.distance(latlngs[i], latlngs[i + 1]);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
})();
|
|
||||||
|
|
||||||
// TBD by options if tree below
|
|
||||||
let derivedLatLngs;
|
|
||||||
let derivedBearings;
|
|
||||||
let spacing;
|
|
||||||
let noOfPoints;
|
|
||||||
|
|
||||||
// Determining latlng and bearing arrays based on frequency choice:
|
|
||||||
if (!isNaN(frequency)) {
|
|
||||||
spacing = 1 / frequency;
|
|
||||||
noOfPoints = frequency;
|
|
||||||
} else if (isInPercent(frequency)) {
|
|
||||||
console.error(
|
|
||||||
'Error: arrowhead frequency option cannot be given in percent. Try another unit.'
|
|
||||||
);
|
|
||||||
} else if (isInMeters(frequency)) {
|
|
||||||
spacing = frequency.slice(0, frequency.length - 1) / totalLength;
|
|
||||||
noOfPoints = 1 / spacing;
|
|
||||||
// round things out for more even spacing:
|
|
||||||
noOfPoints = Math.floor(noOfPoints);
|
|
||||||
spacing = 1 / noOfPoints;
|
|
||||||
} else if (isInPixels(frequency)) {
|
|
||||||
spacing = (() => {
|
|
||||||
let chosenFrequency = frequency.slice(0, frequency.length - 2);
|
|
||||||
let derivedMeters = pixelsToMeters(chosenFrequency, this._map);
|
|
||||||
return derivedMeters / totalLength;
|
|
||||||
})();
|
|
||||||
|
|
||||||
noOfPoints = 1 / spacing;
|
|
||||||
|
|
||||||
// round things out for more even spacing:
|
|
||||||
noOfPoints = Math.floor(noOfPoints);
|
|
||||||
spacing = 1 / noOfPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.frequency === 'allvertices') {
|
|
||||||
derivedBearings = (() => {
|
|
||||||
let bearings = [];
|
|
||||||
for (var i = 1; i < latlngs.length; i++) {
|
|
||||||
let bearing =
|
|
||||||
L.GeometryUtil.angle(
|
|
||||||
this._map,
|
|
||||||
latlngs[modulus(i - 1, latlngs.length)],
|
|
||||||
latlngs[i]
|
|
||||||
) + 180;
|
|
||||||
bearings.push(bearing);
|
|
||||||
}
|
|
||||||
return bearings;
|
|
||||||
})();
|
|
||||||
|
|
||||||
derivedLatLngs = latlngs;
|
|
||||||
derivedLatLngs.shift();
|
|
||||||
} else if (options.frequency === 'endonly' && latlngs.length >= 2) {
|
|
||||||
derivedLatLngs = [latlngs[latlngs.length - 1]];
|
|
||||||
|
|
||||||
derivedBearings = [
|
|
||||||
L.GeometryUtil.angle(
|
|
||||||
this._map,
|
|
||||||
latlngs[latlngs.length - 2],
|
|
||||||
latlngs[latlngs.length - 1]
|
|
||||||
) + 180,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
derivedLatLngs = [];
|
|
||||||
let interpolatedPoints = [];
|
|
||||||
for (var i = 0; i < noOfPoints; i++) {
|
|
||||||
let interpolatedPoint = L.GeometryUtil.interpolateOnLine(
|
|
||||||
this._map,
|
|
||||||
latlngs,
|
|
||||||
spacing * (i + 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (interpolatedPoint) {
|
|
||||||
interpolatedPoints.push(interpolatedPoint);
|
|
||||||
derivedLatLngs.push(interpolatedPoint.latLng);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
derivedBearings = (() => {
|
|
||||||
let bearings = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < interpolatedPoints.length; i++) {
|
|
||||||
let bearing = L.GeometryUtil.angle(
|
|
||||||
this._map,
|
|
||||||
latlngs[interpolatedPoints[i].predecessor + 1],
|
|
||||||
latlngs[interpolatedPoints[i].predecessor]
|
|
||||||
);
|
|
||||||
bearings.push(bearing);
|
|
||||||
}
|
|
||||||
return bearings;
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
let hats = [];
|
|
||||||
|
|
||||||
// Function to build hats based on index and a given hatsize in meters
|
|
||||||
const pushHats = (size, localHatOptions = {}) => {
|
|
||||||
let yawn = localHatOptions.yawn ?? options.yawn;
|
|
||||||
|
|
||||||
let leftWingPoint = L.GeometryUtil.destination(
|
|
||||||
derivedLatLngs[i],
|
|
||||||
derivedBearings[i] - yawn / 2,
|
|
||||||
size
|
|
||||||
);
|
|
||||||
|
|
||||||
let rightWingPoint = L.GeometryUtil.destination(
|
|
||||||
derivedLatLngs[i],
|
|
||||||
derivedBearings[i] + yawn / 2,
|
|
||||||
size
|
|
||||||
);
|
|
||||||
|
|
||||||
let hatPoints = [
|
|
||||||
[leftWingPoint.lat, leftWingPoint.lng],
|
|
||||||
[derivedLatLngs[i].lat, derivedLatLngs[i].lng],
|
|
||||||
[rightWingPoint.lat, rightWingPoint.lng],
|
|
||||||
];
|
|
||||||
|
|
||||||
let hat = options.fill
|
|
||||||
? L.polygon(hatPoints, { ...hatOptions, ...localHatOptions })
|
|
||||||
: L.polyline(hatPoints, { ...hatOptions, ...localHatOptions });
|
|
||||||
|
|
||||||
hats.push(hat);
|
|
||||||
}; // pushHats()
|
|
||||||
|
|
||||||
// Function to build hats based on pixel input
|
|
||||||
const pushHatsFromPixels = (size, localHatOptions = {}) => {
|
|
||||||
let sizePixels = size.slice(0, size.length - 2);
|
|
||||||
let yawn = localHatOptions.yawn ?? options.yawn;
|
|
||||||
|
|
||||||
let derivedXY = this._map.latLngToLayerPoint(derivedLatLngs[i]);
|
|
||||||
|
|
||||||
let bearing = derivedBearings[i];
|
|
||||||
|
|
||||||
let thetaLeft = (180 - bearing - yawn / 2) * (Math.PI / 180),
|
|
||||||
thetaRight = (180 - bearing + yawn / 2) * (Math.PI / 180);
|
|
||||||
|
|
||||||
let dxLeft = sizePixels * Math.sin(thetaLeft),
|
|
||||||
dyLeft = sizePixels * Math.cos(thetaLeft),
|
|
||||||
dxRight = sizePixels * Math.sin(thetaRight),
|
|
||||||
dyRight = sizePixels * Math.cos(thetaRight);
|
|
||||||
|
|
||||||
let leftWingXY = {
|
|
||||||
x: derivedXY.x + dxLeft,
|
|
||||||
y: derivedXY.y + dyLeft,
|
|
||||||
};
|
|
||||||
let rightWingXY = {
|
|
||||||
x: derivedXY.x + dxRight,
|
|
||||||
y: derivedXY.y + dyRight,
|
|
||||||
};
|
|
||||||
|
|
||||||
let leftWingPoint = this._map.layerPointToLatLng(leftWingXY),
|
|
||||||
rightWingPoint = this._map.layerPointToLatLng(rightWingXY);
|
|
||||||
|
|
||||||
let hatPoints = [
|
|
||||||
[leftWingPoint.lat, leftWingPoint.lng],
|
|
||||||
[derivedLatLngs[i].lat, derivedLatLngs[i].lng],
|
|
||||||
[rightWingPoint.lat, rightWingPoint.lng],
|
|
||||||
];
|
|
||||||
|
|
||||||
let hat = options.fill
|
|
||||||
? L.polygon(hatPoints, { ...hatOptions, ...localHatOptions })
|
|
||||||
: L.polyline(hatPoints, { ...hatOptions, ...localHatOptions });
|
|
||||||
|
|
||||||
hats.push(hat);
|
|
||||||
}; // pushHatsFromPixels()
|
|
||||||
|
|
||||||
// ------- LOOP THROUGH POINTS IN EACH SEGMENT ---------- //
|
|
||||||
for (var i = 0; i < derivedLatLngs.length; i++) {
|
|
||||||
let { perArrowheadOptions, ...globalOptions } = options;
|
|
||||||
|
|
||||||
perArrowheadOptions = perArrowheadOptions ? perArrowheadOptions(i) : {};
|
|
||||||
perArrowheadOptions = Object.assign(
|
|
||||||
globalOptions,
|
|
||||||
definedProps(perArrowheadOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
size = perArrowheadOptions.size ?? size;
|
|
||||||
|
|
||||||
// ---- If size is chosen in meters -------------------------
|
|
||||||
if (isInMeters(size)) {
|
|
||||||
let hatSize = size.slice(0, size.length - 1);
|
|
||||||
pushHats(hatSize, perArrowheadOptions);
|
|
||||||
|
|
||||||
// ---- If size is chosen in percent ------------------------
|
|
||||||
} else if (isInPercent(size)) {
|
|
||||||
let sizePercent = size.slice(0, size.length - 1);
|
|
||||||
let hatSize = (() => {
|
|
||||||
if (
|
|
||||||
options.frequency === 'endonly' &&
|
|
||||||
options.proportionalToTotal
|
|
||||||
) {
|
|
||||||
return (totalLength * sizePercent) / 100;
|
|
||||||
} else {
|
|
||||||
let averageDistance = totalLength / (peice.length - 1);
|
|
||||||
return (averageDistance * sizePercent) / 100;
|
|
||||||
}
|
|
||||||
})(); // hatsize calculation
|
|
||||||
|
|
||||||
pushHats(hatSize, perArrowheadOptions);
|
|
||||||
|
|
||||||
// ---- If size is chosen in pixels --------------------------
|
|
||||||
} else if (isInPixels(size)) {
|
|
||||||
pushHatsFromPixels(options.size, perArrowheadOptions);
|
|
||||||
|
|
||||||
// ---- If size unit is not given -----------------------------
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
'Error: Arrowhead size unit not defined. Check your arrowhead options.'
|
|
||||||
);
|
|
||||||
} // if else block for Size
|
|
||||||
} // for loop for each point witin a peice
|
|
||||||
|
|
||||||
allhats.push(...hats);
|
|
||||||
}); // forEach peice
|
|
||||||
|
|
||||||
// --------- LOOP THROUGH EACH POLYLINE END ---------------- //
|
|
||||||
// --------------------------------------------------------- //
|
|
||||||
|
|
||||||
let arrowheads = L.layerGroup(allhats);
|
|
||||||
this._arrowheads = arrowheads;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
getArrowheads: function () {
|
|
||||||
if (this._arrowheads) {
|
|
||||||
return this._arrowheads;
|
|
||||||
} else {
|
|
||||||
return console.error(
|
|
||||||
`Error: You tried to call '.getArrowheads() on a shape that does not have a arrowhead. Use '.arrowheads()' to add a arrowheads before trying to call '.getArrowheads()'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds ghost polylines that are clipped versions of the polylines based on the offsets
|
|
||||||
* If offsets are used, arrowheads are drawn from 'this._ghosts' rather than 'this'
|
|
||||||
*/
|
|
||||||
_buildGhosts: function ({ start, end }) {
|
|
||||||
if (start || end) {
|
|
||||||
let latlngs = this.getLatLngs();
|
|
||||||
|
|
||||||
latlngs = Array.isArray(latlngs[0]) ? latlngs : [latlngs];
|
|
||||||
|
|
||||||
const newLatLngs = latlngs.map((segment) => {
|
|
||||||
// Get total distance of original latlngs
|
|
||||||
const totalLength = (() => {
|
|
||||||
let total = 0;
|
|
||||||
for (var i = 0; i < segment.length - 1; i++) {
|
|
||||||
total += this._map.distance(segment[i], segment[i + 1]);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Modify latlngs to end at interpolated point
|
|
||||||
if (start) {
|
|
||||||
let endOffsetInMeters = (() => {
|
|
||||||
if (isInMeters(start)) {
|
|
||||||
return Number(start.slice(0, start.length - 1));
|
|
||||||
} else if (isInPixels(start)) {
|
|
||||||
let pixels = Number(start.slice(0, start.length - 2));
|
|
||||||
return pixelsToMeters(pixels, this._map);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
let newStart = L.GeometryUtil.interpolateOnLine(
|
|
||||||
this._map,
|
|
||||||
segment,
|
|
||||||
endOffsetInMeters / totalLength
|
|
||||||
);
|
|
||||||
|
|
||||||
segment = segment.slice(
|
|
||||||
newStart.predecessor === -1 ? 1 : newStart.predecessor + 1,
|
|
||||||
segment.length
|
|
||||||
);
|
|
||||||
segment.unshift(newStart.latLng);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end) {
|
|
||||||
let endOffsetInMeters = (() => {
|
|
||||||
if (isInMeters(end)) {
|
|
||||||
return Number(end.slice(0, end.length - 1));
|
|
||||||
} else if (isInPixels(end)) {
|
|
||||||
let pixels = Number(end.slice(0, end.length - 2));
|
|
||||||
return pixelsToMeters(pixels, this._map);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
let newEnd = L.GeometryUtil.interpolateOnLine(
|
|
||||||
this._map,
|
|
||||||
segment,
|
|
||||||
(totalLength - endOffsetInMeters) / totalLength
|
|
||||||
);
|
|
||||||
|
|
||||||
segment = segment.slice(0, newEnd.predecessor + 1);
|
|
||||||
segment.push(newEnd.latLng);
|
|
||||||
}
|
|
||||||
|
|
||||||
return segment;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._ghosts = L.polyline(newLatLngs, {
|
|
||||||
...this.options,
|
|
||||||
color: 'rgba(0,0,0,0)',
|
|
||||||
stroke: 0,
|
|
||||||
smoothFactor: 0,
|
|
||||||
interactive: false,
|
|
||||||
});
|
|
||||||
this._ghosts.addTo(this._map);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteArrowheads: function () {
|
|
||||||
if (this._arrowheads) {
|
|
||||||
this._arrowheads.remove();
|
|
||||||
delete this._arrowheads;
|
|
||||||
delete this._arrowheadOptions;
|
|
||||||
this._hatsApplied = false;
|
|
||||||
}
|
|
||||||
if (this._ghosts) {
|
|
||||||
this._ghosts.remove();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_update: function () {
|
|
||||||
if (!this._map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._clipPoints();
|
|
||||||
this._simplifyPoints();
|
|
||||||
this._updatePath();
|
|
||||||
|
|
||||||
if (this._hatsApplied) {
|
|
||||||
this.buildVectorHats(this._arrowheadOptions);
|
|
||||||
this._map.addLayer(this._arrowheads);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
remove: function () {
|
|
||||||
if (this._arrowheads) {
|
|
||||||
this._arrowheads.remove();
|
|
||||||
}
|
|
||||||
if (this._ghosts) {
|
|
||||||
this._ghosts.remove();
|
|
||||||
}
|
|
||||||
return this.removeFrom(this._map || this._mapToAdd);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
L.LayerGroup.include({
|
|
||||||
removeLayer: function (layer) {
|
|
||||||
var id = layer in this._layers ? layer : this.getLayerId(layer);
|
|
||||||
|
|
||||||
if (this._map && this._layers[id]) {
|
|
||||||
if (this._layers[id]._arrowheads) {
|
|
||||||
this._layers[id]._arrowheads.remove();
|
|
||||||
}
|
|
||||||
this._map.removeLayer(this._layers[id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this._layers[id];
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
onRemove: function (map, layer) {
|
|
||||||
for (var layer in this._layers) {
|
|
||||||
if (this._layers[layer]) {
|
|
||||||
this._layers[layer].remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.eachLayer(map.removeLayer, map);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
L.Map.include({
|
|
||||||
removeLayer: function (layer) {
|
|
||||||
var id = L.Util.stamp(layer);
|
|
||||||
|
|
||||||
if (layer._arrowheads) {
|
|
||||||
layer._arrowheads.remove();
|
|
||||||
}
|
|
||||||
if (layer._ghosts) {
|
|
||||||
layer._ghosts.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._layers[id]) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._loaded) {
|
|
||||||
layer.onRemove(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layer.getAttribution && this.attributionControl) {
|
|
||||||
this.attributionControl.removeAttribution(layer.getAttribution());
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this._layers[id];
|
|
||||||
|
|
||||||
if (this._loaded) {
|
|
||||||
this.fire('layerremove', { layer: layer });
|
|
||||||
layer.fire('remove');
|
|
||||||
}
|
|
||||||
|
|
||||||
layer._map = layer._mapToAdd = null;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
L.GeoJSON.include({
|
|
||||||
geometryToLayer: function (geojson, options) {
|
|
||||||
var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
|
|
||||||
coords = geometry ? geometry.coordinates : null,
|
|
||||||
layers = [],
|
|
||||||
pointToLayer = options && options.pointToLayer,
|
|
||||||
_coordsToLatLng =
|
|
||||||
(options && options.coordsToLatLng) || L.GeoJSON.coordsToLatLng,
|
|
||||||
latlng,
|
|
||||||
latlngs,
|
|
||||||
i,
|
|
||||||
len;
|
|
||||||
|
|
||||||
if (!coords && !geometry) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (geometry.type) {
|
|
||||||
case 'Point':
|
|
||||||
latlng = _coordsToLatLng(coords);
|
|
||||||
return this._pointToLayer(pointToLayer, geojson, latlng, options);
|
|
||||||
|
|
||||||
case 'MultiPoint':
|
|
||||||
for (i = 0, len = coords.length; i < len; i++) {
|
|
||||||
latlng = _coordsToLatLng(coords[i]);
|
|
||||||
layers.push(
|
|
||||||
this._pointToLayer(pointToLayer, geojson, latlng, options)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new L.FeatureGroup(layers);
|
|
||||||
|
|
||||||
case 'LineString':
|
|
||||||
case 'MultiLineString':
|
|
||||||
latlngs = L.GeoJSON.coordsToLatLngs(
|
|
||||||
coords,
|
|
||||||
geometry.type === 'LineString' ? 0 : 1,
|
|
||||||
_coordsToLatLng
|
|
||||||
);
|
|
||||||
var polyline = new L.Polyline(latlngs, options);
|
|
||||||
if (options.arrowheads) {
|
|
||||||
polyline.arrowheads(options.arrowheads);
|
|
||||||
}
|
|
||||||
return polyline;
|
|
||||||
|
|
||||||
case 'Polygon':
|
|
||||||
case 'MultiPolygon':
|
|
||||||
latlngs = L.GeoJSON.coordsToLatLngs(
|
|
||||||
coords,
|
|
||||||
geometry.type === 'Polygon' ? 1 : 2,
|
|
||||||
_coordsToLatLng
|
|
||||||
);
|
|
||||||
return new L.Polygon(latlngs, options);
|
|
||||||
|
|
||||||
case 'GeometryCollection':
|
|
||||||
for (i = 0, len = geometry.geometries.length; i < len; i++) {
|
|
||||||
var layer = this.geometryToLayer(
|
|
||||||
{
|
|
||||||
geometry: geometry.geometries[i],
|
|
||||||
type: 'Feature',
|
|
||||||
properties: geojson.properties,
|
|
||||||
},
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
if (layer) {
|
|
||||||
layers.push(layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new L.FeatureGroup(layers);
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error('Invalid GeoJSON object.');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addData: function (geojson) {
|
|
||||||
var features = L.Util.isArray(geojson) ? geojson : geojson.features,
|
|
||||||
i,
|
|
||||||
len,
|
|
||||||
feature;
|
|
||||||
|
|
||||||
if (features) {
|
|
||||||
for (i = 0, len = features.length; i < len; i++) {
|
|
||||||
// only add this if geometry or geometries are set and not null
|
|
||||||
feature = features[i];
|
|
||||||
if (
|
|
||||||
feature.geometries ||
|
|
||||||
feature.geometry ||
|
|
||||||
feature.features ||
|
|
||||||
feature.coordinates
|
|
||||||
) {
|
|
||||||
this.addData(feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = this.options;
|
|
||||||
|
|
||||||
if (options.filter && !options.filter(geojson)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
var layer = this.geometryToLayer(geojson, options);
|
|
||||||
if (!layer) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
layer.feature = L.GeoJSON.asFeature(geojson);
|
|
||||||
|
|
||||||
layer.defaultOptions = layer.options;
|
|
||||||
this.resetStyle(layer);
|
|
||||||
|
|
||||||
if (options.onEachFeature) {
|
|
||||||
options.onEachFeature(geojson, layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.addLayer(layer);
|
|
||||||
},
|
|
||||||
|
|
||||||
_pointToLayer: function (pointToLayerFn, geojson, latlng, options) {
|
|
||||||
return pointToLayerFn
|
|
||||||
? pointToLayerFn(geojson, latlng)
|
|
||||||
: new L.Marker(
|
|
||||||
latlng,
|
|
||||||
options && options.markersInheritOptions && options
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,807 +0,0 @@
|
|||||||
// Packaging/modules magic dance.
|
|
||||||
(function (factory) {
|
|
||||||
var L;
|
|
||||||
if (typeof define === 'function' && define.amd) {
|
|
||||||
// AMD
|
|
||||||
define(['leaflet'], factory);
|
|
||||||
} else if (typeof module !== 'undefined') {
|
|
||||||
// Node/CommonJS
|
|
||||||
L = require('leaflet');
|
|
||||||
module.exports = factory(L);
|
|
||||||
} else {
|
|
||||||
// Browser globals
|
|
||||||
if (typeof window.L === 'undefined')
|
|
||||||
throw 'Leaflet must be loaded first';
|
|
||||||
factory(window.L);
|
|
||||||
}
|
|
||||||
}(function (L) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
L.Polyline._flat = L.LineUtil.isFlat || L.Polyline._flat || function (latlngs) {
|
|
||||||
// true if it's a flat array of latlngs; false if nested
|
|
||||||
return !L.Util.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @fileOverview Leaflet Geometry utilities for distances and linear referencing.
|
|
||||||
* @name L.GeometryUtil
|
|
||||||
*/
|
|
||||||
|
|
||||||
L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
|
|
||||||
|
|
||||||
/**
|
|
||||||
Shortcut function for planar distance between two {L.LatLng} at current zoom.
|
|
||||||
|
|
||||||
@tutorial distance-length
|
|
||||||
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {L.LatLng} latlngA geographical point A
|
|
||||||
@param {L.LatLng} latlngB geographical point B
|
|
||||||
@returns {Number} planar distance
|
|
||||||
*/
|
|
||||||
distance: function (map, latlngA, latlngB) {
|
|
||||||
return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Shortcut function for planar distance between a {L.LatLng} and a segment (A-B).
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {L.LatLng} latlng - The position to search
|
|
||||||
@param {L.LatLng} latlngA geographical point A of the segment
|
|
||||||
@param {L.LatLng} latlngB geographical point B of the segment
|
|
||||||
@returns {Number} planar distance
|
|
||||||
*/
|
|
||||||
distanceSegment: function (map, latlng, latlngA, latlngB) {
|
|
||||||
var p = map.latLngToLayerPoint(latlng),
|
|
||||||
p1 = map.latLngToLayerPoint(latlngA),
|
|
||||||
p2 = map.latLngToLayerPoint(latlngB);
|
|
||||||
return L.LineUtil.pointToSegmentDistance(p, p1, p2);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Shortcut function for converting distance to readable distance.
|
|
||||||
@param {Number} distance distance to be converted
|
|
||||||
@param {String} unit 'metric' or 'imperial'
|
|
||||||
@returns {String} in yard or miles
|
|
||||||
*/
|
|
||||||
readableDistance: function (distance, unit) {
|
|
||||||
var isMetric = (unit !== 'imperial'),
|
|
||||||
distanceStr;
|
|
||||||
if (isMetric) {
|
|
||||||
// show metres when distance is < 1km, then show km
|
|
||||||
if (distance > 1000) {
|
|
||||||
distanceStr = (distance / 1000).toFixed(2) + ' km';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
distanceStr = distance.toFixed(1) + ' m';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
distance *= 1.09361;
|
|
||||||
if (distance > 1760) {
|
|
||||||
distanceStr = (distance / 1760).toFixed(2) + ' miles';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
distanceStr = distance.toFixed(1) + ' yd';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return distanceStr;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns true if the latlng belongs to segment A-B
|
|
||||||
@param {L.LatLng} latlng - The position to search
|
|
||||||
@param {L.LatLng} latlngA geographical point A of the segment
|
|
||||||
@param {L.LatLng} latlngB geographical point B of the segment
|
|
||||||
@param {?Number} [tolerance=0.2] tolerance to accept if latlng belongs really
|
|
||||||
@returns {boolean}
|
|
||||||
*/
|
|
||||||
belongsSegment: function(latlng, latlngA, latlngB, tolerance) {
|
|
||||||
tolerance = tolerance === undefined ? 0.2 : tolerance;
|
|
||||||
var hypotenuse = latlngA.distanceTo(latlngB),
|
|
||||||
delta = latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse;
|
|
||||||
return delta/hypotenuse < tolerance;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns total length of line
|
|
||||||
* @tutorial distance-length
|
|
||||||
*
|
|
||||||
* @param {L.Polyline|Array<L.Point>|Array<L.LatLng>} coords Set of coordinates
|
|
||||||
* @returns {Number} Total length (pixels for Point, meters for LatLng)
|
|
||||||
*/
|
|
||||||
length: function (coords) {
|
|
||||||
var accumulated = L.GeometryUtil.accumulatedLengths(coords);
|
|
||||||
return accumulated.length > 0 ? accumulated[accumulated.length-1] : 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of accumulated length along a line.
|
|
||||||
* @param {L.Polyline|Array<L.Point>|Array<L.LatLng>} coords Set of coordinates
|
|
||||||
* @returns {Array<Number>} Array of accumulated lengths (pixels for Point, meters for LatLng)
|
|
||||||
*/
|
|
||||||
accumulatedLengths: function (coords) {
|
|
||||||
if (typeof coords.getLatLngs == 'function') {
|
|
||||||
coords = coords.getLatLngs();
|
|
||||||
}
|
|
||||||
if (coords.length === 0)
|
|
||||||
return [];
|
|
||||||
var total = 0,
|
|
||||||
lengths = [0];
|
|
||||||
for (var i = 0, n = coords.length - 1; i< n; i++) {
|
|
||||||
total += coords[i].distanceTo(coords[i+1]);
|
|
||||||
lengths.push(total);
|
|
||||||
}
|
|
||||||
return lengths;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the closest point of a {L.LatLng} on the segment (A-B)
|
|
||||||
|
|
||||||
@tutorial closest
|
|
||||||
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {L.LatLng} latlng - The position to search
|
|
||||||
@param {L.LatLng} latlngA geographical point A of the segment
|
|
||||||
@param {L.LatLng} latlngB geographical point B of the segment
|
|
||||||
@returns {L.LatLng} Closest geographical point
|
|
||||||
*/
|
|
||||||
closestOnSegment: function (map, latlng, latlngA, latlngB) {
|
|
||||||
var maxzoom = map.getMaxZoom();
|
|
||||||
if (maxzoom === Infinity)
|
|
||||||
maxzoom = map.getZoom();
|
|
||||||
var p = map.project(latlng, maxzoom),
|
|
||||||
p1 = map.project(latlngA, maxzoom),
|
|
||||||
p2 = map.project(latlngB, maxzoom),
|
|
||||||
closest = L.LineUtil.closestPointOnSegment(p, p1, p2);
|
|
||||||
return map.unproject(closest, maxzoom);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the closest point of a {L.LatLng} on a {L.Circle}
|
|
||||||
|
|
||||||
@tutorial closest
|
|
||||||
|
|
||||||
@param {L.LatLng} latlng - The position to search
|
|
||||||
@param {L.Circle} circle - A Circle defined by a center and a radius
|
|
||||||
@returns {L.LatLng} Closest geographical point on the circle circumference
|
|
||||||
*/
|
|
||||||
closestOnCircle: function (circle, latLng) {
|
|
||||||
const center = circle.getLatLng();
|
|
||||||
const circleRadius = circle.getRadius();
|
|
||||||
const radius = typeof circleRadius === 'number' ? circleRadius : circleRadius.radius;
|
|
||||||
const x = latLng.lng;
|
|
||||||
const y = latLng.lat;
|
|
||||||
const cx = center.lng;
|
|
||||||
const cy = center.lat;
|
|
||||||
// dx and dy is the vector from the circle's center to latLng
|
|
||||||
const dx = x - cx;
|
|
||||||
const dy = y - cy;
|
|
||||||
|
|
||||||
// distance between the point and the circle's center
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
||||||
|
|
||||||
// Calculate the closest point on the circle by adding the normalized vector to the center
|
|
||||||
const tx = cx + (dx / distance) * radius;
|
|
||||||
const ty = cy + (dy / distance) * radius;
|
|
||||||
|
|
||||||
return new L.LatLng(ty, tx);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the closest latlng on layer.
|
|
||||||
|
|
||||||
Accept nested arrays
|
|
||||||
|
|
||||||
@tutorial closest
|
|
||||||
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {Array<L.LatLng>|Array<Array<L.LatLng>>|L.PolyLine|L.Polygon} layer - Layer that contains the result
|
|
||||||
@param {L.LatLng} latlng - The position to search
|
|
||||||
@param {?boolean} [vertices=false] - Whether to restrict to path vertices.
|
|
||||||
@returns {L.LatLng} Closest geographical point or null if layer param is incorrect
|
|
||||||
*/
|
|
||||||
closest: function (map, layer, latlng, vertices) {
|
|
||||||
|
|
||||||
var latlngs,
|
|
||||||
mindist = Infinity,
|
|
||||||
result = null,
|
|
||||||
i, n, distance, subResult;
|
|
||||||
|
|
||||||
if (layer instanceof Array) {
|
|
||||||
// if layer is Array<Array<T>>
|
|
||||||
if (layer[0] instanceof Array && typeof layer[0][0] !== 'number') {
|
|
||||||
// if we have nested arrays, we calc the closest for each array
|
|
||||||
// recursive
|
|
||||||
for (i = 0; i < layer.length; i++) {
|
|
||||||
subResult = L.GeometryUtil.closest(map, layer[i], latlng, vertices);
|
|
||||||
if (subResult && subResult.distance < mindist) {
|
|
||||||
mindist = subResult.distance;
|
|
||||||
result = subResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} else if (layer[0] instanceof L.LatLng
|
|
||||||
|| typeof layer[0][0] === 'number'
|
|
||||||
|| typeof layer[0].lat === 'number') { // we could have a latlng as [x,y] with x & y numbers or {lat, lng}
|
|
||||||
layer = L.polyline(layer);
|
|
||||||
} else {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we don't have here a Polyline, that means layer is incorrect
|
|
||||||
// see https://github.com/makinacorpus/Leaflet.GeometryUtil/issues/23
|
|
||||||
if (! ( layer instanceof L.Polyline ) )
|
|
||||||
return result;
|
|
||||||
|
|
||||||
// deep copy of latlngs
|
|
||||||
latlngs = JSON.parse(JSON.stringify(layer.getLatLngs().slice(0)));
|
|
||||||
|
|
||||||
// add the last segment for L.Polygon
|
|
||||||
if (layer instanceof L.Polygon) {
|
|
||||||
// add the last segment for each child that is a nested array
|
|
||||||
var addLastSegment = function(latlngs) {
|
|
||||||
if (L.Polyline._flat(latlngs)) {
|
|
||||||
latlngs.push(latlngs[0]);
|
|
||||||
} else {
|
|
||||||
for (var i = 0; i < latlngs.length; i++) {
|
|
||||||
addLastSegment(latlngs[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
addLastSegment(latlngs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have a multi polygon / multi polyline / polygon with holes
|
|
||||||
// use recursive to explore and return the good result
|
|
||||||
if ( ! L.Polyline._flat(latlngs) ) {
|
|
||||||
for (i = 0; i < latlngs.length; i++) {
|
|
||||||
// if we are at the lower level, and if we have a L.Polygon, we add the last segment
|
|
||||||
subResult = L.GeometryUtil.closest(map, latlngs[i], latlng, vertices);
|
|
||||||
if (subResult.distance < mindist) {
|
|
||||||
mindist = subResult.distance;
|
|
||||||
result = subResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Lookup vertices
|
|
||||||
if (vertices) {
|
|
||||||
for(i = 0, n = latlngs.length; i < n; i++) {
|
|
||||||
var ll = latlngs[i];
|
|
||||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
|
||||||
if (distance < mindist) {
|
|
||||||
mindist = distance;
|
|
||||||
result = ll;
|
|
||||||
result.distance = distance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep the closest point of all segments
|
|
||||||
for (i = 0, n = latlngs.length; i < n-1; i++) {
|
|
||||||
var latlngA = latlngs[i],
|
|
||||||
latlngB = latlngs[i+1];
|
|
||||||
distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB);
|
|
||||||
if (distance <= mindist) {
|
|
||||||
mindist = distance;
|
|
||||||
result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
|
|
||||||
result.distance = distance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the closest layer to latlng among a list of layers.
|
|
||||||
|
|
||||||
@tutorial closest
|
|
||||||
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {Array<L.ILayer>} layers Set of layers
|
|
||||||
@param {L.LatLng} latlng - The position to search
|
|
||||||
@returns {object} ``{layer, latlng, distance}`` or ``null`` if list is empty;
|
|
||||||
*/
|
|
||||||
closestLayer: function (map, layers, latlng) {
|
|
||||||
var mindist = Infinity,
|
|
||||||
result = null,
|
|
||||||
ll = null,
|
|
||||||
distance = Infinity;
|
|
||||||
|
|
||||||
for (var i = 0, n = layers.length; i < n; i++) {
|
|
||||||
var layer = layers[i];
|
|
||||||
if (layer instanceof L.LayerGroup) {
|
|
||||||
// recursive
|
|
||||||
var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
|
|
||||||
if (subResult.distance < mindist) {
|
|
||||||
mindist = subResult.distance;
|
|
||||||
result = subResult;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (layer instanceof L.Circle){
|
|
||||||
ll = this.closestOnCircle(layer, latlng);
|
|
||||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
|
||||||
} else
|
|
||||||
// Single dimension, snap on points, else snap on closest
|
|
||||||
if (typeof layer.getLatLng == 'function') {
|
|
||||||
ll = layer.getLatLng();
|
|
||||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ll = L.GeometryUtil.closest(map, layer, latlng);
|
|
||||||
if (ll) distance = ll.distance; // Can return null if layer has no points.
|
|
||||||
}
|
|
||||||
if (distance < mindist) {
|
|
||||||
mindist = distance;
|
|
||||||
result = {layer: layer, latlng: ll, distance: distance};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the n closest layers to latlng among a list of input layers.
|
|
||||||
|
|
||||||
@param {L.Map} map - Leaflet map to be used for this method
|
|
||||||
@param {Array<L.ILayer>} layers - Set of layers
|
|
||||||
@param {L.LatLng} latlng - The position to search
|
|
||||||
@param {?Number} [n=layers.length] - the expected number of output layers.
|
|
||||||
@returns {Array<object>} an array of objects ``{layer, latlng, distance}`` or ``null`` if the input is invalid (empty list or negative n)
|
|
||||||
*/
|
|
||||||
nClosestLayers: function (map, layers, latlng, n) {
|
|
||||||
n = typeof n === 'number' ? n : layers.length;
|
|
||||||
|
|
||||||
if (n < 1 || layers.length < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var results = [];
|
|
||||||
var distance, ll;
|
|
||||||
|
|
||||||
for (var i = 0, m = layers.length; i < m; i++) {
|
|
||||||
var layer = layers[i];
|
|
||||||
if (layer instanceof L.LayerGroup) {
|
|
||||||
// recursive
|
|
||||||
var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
|
|
||||||
results.push(subResult);
|
|
||||||
} else {
|
|
||||||
if (layer instanceof L.Circle){
|
|
||||||
ll = this.closestOnCircle(layer, latlng);
|
|
||||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
|
||||||
} else
|
|
||||||
// Single dimension, snap on points, else snap on closest
|
|
||||||
if (typeof layer.getLatLng == 'function') {
|
|
||||||
ll = layer.getLatLng();
|
|
||||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ll = L.GeometryUtil.closest(map, layer, latlng);
|
|
||||||
if (ll) distance = ll.distance; // Can return null if layer has no points.
|
|
||||||
}
|
|
||||||
results.push({layer: layer, latlng: ll, distance: distance});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.sort(function(a, b) {
|
|
||||||
return a.distance - b.distance;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (results.length > n) {
|
|
||||||
return results.slice(0, n);
|
|
||||||
} else {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all layers within a radius of the given position, in an ascending order of distance.
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {Array<ILayer>} layers - A list of layers.
|
|
||||||
@param {L.LatLng} latlng - The position to search
|
|
||||||
@param {?Number} [radius=Infinity] - Search radius in pixels
|
|
||||||
@return {object[]} an array of objects including layer within the radius, closest latlng, and distance
|
|
||||||
*/
|
|
||||||
layersWithin: function(map, layers, latlng, radius) {
|
|
||||||
radius = typeof radius == 'number' ? radius : Infinity;
|
|
||||||
|
|
||||||
var results = [];
|
|
||||||
var ll = null;
|
|
||||||
var distance = 0;
|
|
||||||
|
|
||||||
for (var i = 0, n = layers.length; i < n; i++) {
|
|
||||||
var layer = layers[i];
|
|
||||||
|
|
||||||
if (typeof layer.getLatLng == 'function') {
|
|
||||||
ll = layer.getLatLng();
|
|
||||||
distance = L.GeometryUtil.distance(map, latlng, ll);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ll = L.GeometryUtil.closest(map, layer, latlng);
|
|
||||||
if (ll) distance = ll.distance; // Can return null if layer has no points.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ll && distance < radius) {
|
|
||||||
results.push({layer: layer, latlng: ll, distance: distance});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortedResults = results.sort(function(a, b) {
|
|
||||||
return a.distance - b.distance;
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortedResults;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the closest position from specified {LatLng} among specified layers,
|
|
||||||
with a maximum tolerance in pixels, providing snapping behaviour.
|
|
||||||
|
|
||||||
@tutorial closest
|
|
||||||
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {Array<ILayer>} layers - A list of layers to snap on.
|
|
||||||
@param {L.LatLng} latlng - The position to snap
|
|
||||||
@param {?Number} [tolerance=Infinity] - Maximum number of pixels.
|
|
||||||
@param {?boolean} [withVertices=true] - Snap to layers vertices or segment points (not only vertex)
|
|
||||||
@returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded.
|
|
||||||
*/
|
|
||||||
closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) {
|
|
||||||
tolerance = typeof tolerance == 'number' ? tolerance : Infinity;
|
|
||||||
withVertices = typeof withVertices == 'boolean' ? withVertices : true;
|
|
||||||
|
|
||||||
var result = L.GeometryUtil.closestLayer(map, layers, latlng);
|
|
||||||
if (!result || result.distance > tolerance)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// If snapped layer is linear, try to snap on vertices (extremities and middle points)
|
|
||||||
if (withVertices && typeof result.layer.getLatLngs == 'function') {
|
|
||||||
var closest = L.GeometryUtil.closest(map, result.layer, result.latlng, true);
|
|
||||||
if (closest.distance < tolerance) {
|
|
||||||
result.latlng = closest;
|
|
||||||
result.distance = L.GeometryUtil.distance(map, closest, latlng);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the Point located on a segment at the specified ratio of the segment length.
|
|
||||||
@param {L.Point} pA coordinates of point A
|
|
||||||
@param {L.Point} pB coordinates of point B
|
|
||||||
@param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive.
|
|
||||||
@returns {L.Point} the interpolated point.
|
|
||||||
*/
|
|
||||||
interpolateOnPointSegment: function (pA, pB, ratio) {
|
|
||||||
return L.point(
|
|
||||||
(pA.x * (1 - ratio)) + (ratio * pB.x),
|
|
||||||
(pA.y * (1 - ratio)) + (ratio * pB.y)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the coordinate of the point located on a line at the specified ratio of the line length.
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {Array<L.LatLng>|L.PolyLine} latlngs Set of geographical points
|
|
||||||
@param {Number} ratio the length ratio, expressed as a decimal between 0 and 1, inclusive
|
|
||||||
@returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline
|
|
||||||
(-1 if the interpolated point is the first vertex)
|
|
||||||
*/
|
|
||||||
interpolateOnLine: function (map, latLngs, ratio) {
|
|
||||||
latLngs = (latLngs instanceof L.Polyline) ? latLngs.getLatLngs() : latLngs;
|
|
||||||
var n = latLngs.length;
|
|
||||||
if (n < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the ratio is between 0 and 1;
|
|
||||||
ratio = Math.max(Math.min(ratio, 1), 0);
|
|
||||||
|
|
||||||
if (ratio === 0) {
|
|
||||||
return {
|
|
||||||
latLng: latLngs[0] instanceof L.LatLng ? latLngs[0] : L.latLng(latLngs[0]),
|
|
||||||
predecessor: -1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (ratio == 1) {
|
|
||||||
return {
|
|
||||||
latLng: latLngs[latLngs.length -1] instanceof L.LatLng ? latLngs[latLngs.length -1] : L.latLng(latLngs[latLngs.length -1]),
|
|
||||||
predecessor: latLngs.length - 2
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// project the LatLngs as Points,
|
|
||||||
// and compute total planar length of the line at max precision
|
|
||||||
var maxzoom = map.getMaxZoom();
|
|
||||||
if (maxzoom === Infinity)
|
|
||||||
maxzoom = map.getZoom();
|
|
||||||
var pts = [];
|
|
||||||
var lineLength = 0;
|
|
||||||
for(var i = 0; i < n; i++) {
|
|
||||||
pts[i] = map.project(latLngs[i], maxzoom);
|
|
||||||
if(i > 0)
|
|
||||||
lineLength += pts[i-1].distanceTo(pts[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var ratioDist = lineLength * ratio;
|
|
||||||
|
|
||||||
// follow the line segments [ab], adding lengths,
|
|
||||||
// until we find the segment where the points should lie on
|
|
||||||
var cumulativeDistanceToA = 0, cumulativeDistanceToB = 0;
|
|
||||||
for (var i = 0; cumulativeDistanceToB < ratioDist; i++) {
|
|
||||||
var pointA = pts[i], pointB = pts[i+1];
|
|
||||||
|
|
||||||
cumulativeDistanceToA = cumulativeDistanceToB;
|
|
||||||
cumulativeDistanceToB += pointA.distanceTo(pointB);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pointA == undefined && pointB == undefined) { // Happens when line has no length
|
|
||||||
var pointA = pts[0], pointB = pts[1], i = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute the ratio relative to the segment [ab]
|
|
||||||
var segmentRatio = ((cumulativeDistanceToB - cumulativeDistanceToA) !== 0) ? ((ratioDist - cumulativeDistanceToA) / (cumulativeDistanceToB - cumulativeDistanceToA)) : 0;
|
|
||||||
var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment(pointA, pointB, segmentRatio);
|
|
||||||
return {
|
|
||||||
latLng: map.unproject(interpolatedPoint, maxzoom),
|
|
||||||
predecessor: i-1
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns a float between 0 and 1 representing the location of the
|
|
||||||
closest point on polyline to the given latlng, as a fraction of total line length.
|
|
||||||
(opposite of L.GeometryUtil.interpolateOnLine())
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {L.PolyLine} polyline Polyline on which the latlng will be search
|
|
||||||
@param {L.LatLng} latlng The position to search
|
|
||||||
@returns {Number} Float between 0 and 1
|
|
||||||
*/
|
|
||||||
locateOnLine: function (map, polyline, latlng) {
|
|
||||||
var latlngs = polyline.getLatLngs();
|
|
||||||
if (latlng.equals(latlngs[0]))
|
|
||||||
return 0.0;
|
|
||||||
if (latlng.equals(latlngs[latlngs.length-1]))
|
|
||||||
return 1.0;
|
|
||||||
|
|
||||||
var point = L.GeometryUtil.closest(map, polyline, latlng, false),
|
|
||||||
lengths = L.GeometryUtil.accumulatedLengths(latlngs),
|
|
||||||
total_length = lengths[lengths.length-1],
|
|
||||||
portion = 0,
|
|
||||||
found = false;
|
|
||||||
for (var i=0, n = latlngs.length-1; i < n; i++) {
|
|
||||||
var l1 = latlngs[i],
|
|
||||||
l2 = latlngs[i+1];
|
|
||||||
portion = lengths[i];
|
|
||||||
if (L.GeometryUtil.belongsSegment(point, l1, l2, 0.001)) {
|
|
||||||
portion += l1.distanceTo(point);
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
throw "Could not interpolate " + latlng.toString() + " within " + polyline.toString();
|
|
||||||
}
|
|
||||||
return portion / total_length;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns a clone with reversed coordinates.
|
|
||||||
@param {L.PolyLine} polyline polyline to reverse
|
|
||||||
@returns {L.PolyLine} polyline reversed
|
|
||||||
*/
|
|
||||||
reverse: function (polyline) {
|
|
||||||
return L.polyline(polyline.getLatLngs().slice(0).reverse());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns a sub-part of the polyline, from start to end.
|
|
||||||
If start is superior to end, returns extraction from inverted line.
|
|
||||||
@param {L.Map} map Leaflet map to be used for this method
|
|
||||||
@param {L.PolyLine} polyline Polyline on which will be extracted the sub-part
|
|
||||||
@param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive
|
|
||||||
@param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive
|
|
||||||
@returns {Array<L.LatLng>} new polyline
|
|
||||||
*/
|
|
||||||
extract: function (map, polyline, start, end) {
|
|
||||||
if (start > end) {
|
|
||||||
return L.GeometryUtil.extract(map, L.GeometryUtil.reverse(polyline), 1.0-start, 1.0-end);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bound start and end to [0-1]
|
|
||||||
start = Math.max(Math.min(start, 1), 0);
|
|
||||||
end = Math.max(Math.min(end, 1), 0);
|
|
||||||
|
|
||||||
var latlngs = polyline.getLatLngs(),
|
|
||||||
startpoint = L.GeometryUtil.interpolateOnLine(map, polyline, start),
|
|
||||||
endpoint = L.GeometryUtil.interpolateOnLine(map, polyline, end);
|
|
||||||
// Return single point if start == end
|
|
||||||
if (start == end) {
|
|
||||||
var point = L.GeometryUtil.interpolateOnLine(map, polyline, end);
|
|
||||||
return [point.latLng];
|
|
||||||
}
|
|
||||||
// Array.slice() works indexes at 0
|
|
||||||
if (startpoint.predecessor == -1)
|
|
||||||
startpoint.predecessor = 0;
|
|
||||||
if (endpoint.predecessor == -1)
|
|
||||||
endpoint.predecessor = 0;
|
|
||||||
var result = latlngs.slice(startpoint.predecessor+1, endpoint.predecessor+1);
|
|
||||||
result.unshift(startpoint.latLng);
|
|
||||||
result.push(endpoint.latLng);
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns true if first polyline ends where other second starts.
|
|
||||||
@param {L.PolyLine} polyline First polyline
|
|
||||||
@param {L.PolyLine} other Second polyline
|
|
||||||
@returns {bool}
|
|
||||||
*/
|
|
||||||
isBefore: function (polyline, other) {
|
|
||||||
if (!other) return false;
|
|
||||||
var lla = polyline.getLatLngs(),
|
|
||||||
llb = other.getLatLngs();
|
|
||||||
return (lla[lla.length-1]).equals(llb[0]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns true if first polyline starts where second ends.
|
|
||||||
@param {L.PolyLine} polyline First polyline
|
|
||||||
@param {L.PolyLine} other Second polyline
|
|
||||||
@returns {bool}
|
|
||||||
*/
|
|
||||||
isAfter: function (polyline, other) {
|
|
||||||
if (!other) return false;
|
|
||||||
var lla = polyline.getLatLngs(),
|
|
||||||
llb = other.getLatLngs();
|
|
||||||
return (lla[0]).equals(llb[llb.length-1]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns true if first polyline starts where second ends or start.
|
|
||||||
@param {L.PolyLine} polyline First polyline
|
|
||||||
@param {L.PolyLine} other Second polyline
|
|
||||||
@returns {bool}
|
|
||||||
*/
|
|
||||||
startsAtExtremity: function (polyline, other) {
|
|
||||||
if (!other) return false;
|
|
||||||
var lla = polyline.getLatLngs(),
|
|
||||||
llb = other.getLatLngs(),
|
|
||||||
start = lla[0];
|
|
||||||
return start.equals(llb[0]) || start.equals(llb[llb.length-1]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns horizontal angle in degres between two points.
|
|
||||||
@param {L.Point} a Coordinates of point A
|
|
||||||
@param {L.Point} b Coordinates of point B
|
|
||||||
@returns {Number} horizontal angle
|
|
||||||
*/
|
|
||||||
computeAngle: function(a, b) {
|
|
||||||
return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns slope (Ax+B) between two points.
|
|
||||||
@param {L.Point} a Coordinates of point A
|
|
||||||
@param {L.Point} b Coordinates of point B
|
|
||||||
@returns {Object} with ``a`` and ``b`` properties.
|
|
||||||
*/
|
|
||||||
computeSlope: function(a, b) {
|
|
||||||
var s = (b.y - a.y) / (b.x - a.x),
|
|
||||||
o = a.y - (s * a.x);
|
|
||||||
return {'a': s, 'b': o};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns LatLng of rotated point around specified LatLng center.
|
|
||||||
@param {L.LatLng} latlngPoint: point to rotate
|
|
||||||
@param {double} angleDeg: angle to rotate in degrees
|
|
||||||
@param {L.LatLng} latlngCenter: center of rotation
|
|
||||||
@returns {L.LatLng} rotated point
|
|
||||||
*/
|
|
||||||
rotatePoint: function(map, latlngPoint, angleDeg, latlngCenter) {
|
|
||||||
var maxzoom = map.getMaxZoom();
|
|
||||||
if (maxzoom === Infinity)
|
|
||||||
maxzoom = map.getZoom();
|
|
||||||
var angleRad = angleDeg*Math.PI/180,
|
|
||||||
pPoint = map.project(latlngPoint, maxzoom),
|
|
||||||
pCenter = map.project(latlngCenter, maxzoom),
|
|
||||||
x2 = Math.cos(angleRad)*(pPoint.x-pCenter.x) - Math.sin(angleRad)*(pPoint.y-pCenter.y) + pCenter.x,
|
|
||||||
y2 = Math.sin(angleRad)*(pPoint.x-pCenter.x) + Math.cos(angleRad)*(pPoint.y-pCenter.y) + pCenter.y;
|
|
||||||
return map.unproject(new L.Point(x2,y2), maxzoom);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the bearing in degrees clockwise from north (0 degrees)
|
|
||||||
from the first L.LatLng to the second, at the first LatLng
|
|
||||||
@param {L.LatLng} latlng1: origin point of the bearing
|
|
||||||
@param {L.LatLng} latlng2: destination point of the bearing
|
|
||||||
@returns {float} degrees clockwise from north.
|
|
||||||
*/
|
|
||||||
bearing: function(latlng1, latlng2) {
|
|
||||||
var rad = Math.PI / 180,
|
|
||||||
lat1 = latlng1.lat * rad,
|
|
||||||
lat2 = latlng2.lat * rad,
|
|
||||||
lon1 = latlng1.lng * rad,
|
|
||||||
lon2 = latlng2.lng * rad,
|
|
||||||
y = Math.sin(lon2 - lon1) * Math.cos(lat2),
|
|
||||||
x = Math.cos(lat1) * Math.sin(lat2) -
|
|
||||||
Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
|
|
||||||
|
|
||||||
var bearing = ((Math.atan2(y, x) * 180 / Math.PI) + 360) % 360;
|
|
||||||
return bearing >= 180 ? bearing-360 : bearing;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the point that is a distance and heading away from
|
|
||||||
the given origin point.
|
|
||||||
@param {L.LatLng} latlng: origin point
|
|
||||||
@param {float} heading: heading in degrees, clockwise from 0 degrees north.
|
|
||||||
@param {float} distance: distance in meters
|
|
||||||
@returns {L.latLng} the destination point.
|
|
||||||
Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html
|
|
||||||
for a great reference and examples.
|
|
||||||
*/
|
|
||||||
destination: function(latlng, heading, distance) {
|
|
||||||
heading = (heading + 360) % 360;
|
|
||||||
var rad = Math.PI / 180,
|
|
||||||
radInv = 180 / Math.PI,
|
|
||||||
R = L.CRS.Earth.R, // approximation of Earth's radius
|
|
||||||
lon1 = latlng.lng * rad,
|
|
||||||
lat1 = latlng.lat * rad,
|
|
||||||
rheading = heading * rad,
|
|
||||||
sinLat1 = Math.sin(lat1),
|
|
||||||
cosLat1 = Math.cos(lat1),
|
|
||||||
cosDistR = Math.cos(distance / R),
|
|
||||||
sinDistR = Math.sin(distance / R),
|
|
||||||
lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 *
|
|
||||||
sinDistR * Math.cos(rheading)),
|
|
||||||
lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR *
|
|
||||||
cosLat1, cosDistR - sinLat1 * Math.sin(lat2));
|
|
||||||
lon2 = lon2 * radInv;
|
|
||||||
lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2;
|
|
||||||
return L.latLng([lat2 * radInv, lon2]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns the the angle of the given segment and the Equator in degrees,
|
|
||||||
clockwise from 0 degrees north.
|
|
||||||
@param {L.Map} map: Leaflet map to be used for this method
|
|
||||||
@param {L.LatLng} latlngA: geographical point A of the segment
|
|
||||||
@param {L.LatLng} latlngB: geographical point B of the segment
|
|
||||||
@returns {Float} the angle in degrees.
|
|
||||||
*/
|
|
||||||
angle: function(map, latlngA, latlngB) {
|
|
||||||
var pointA = map.latLngToContainerPoint(latlngA),
|
|
||||||
pointB = map.latLngToContainerPoint(latlngB),
|
|
||||||
angleDeg = Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI + 90;
|
|
||||||
angleDeg += angleDeg < 0 ? 360 : 0;
|
|
||||||
return angleDeg;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Returns a point snaps on the segment and heading away from the given origin point a distance.
|
|
||||||
@param {L.Map} map: Leaflet map to be used for this method
|
|
||||||
@param {L.LatLng} latlngA: geographical point A of the segment
|
|
||||||
@param {L.LatLng} latlngB: geographical point B of the segment
|
|
||||||
@param {float} distance: distance in meters
|
|
||||||
@returns {L.latLng} the destination point.
|
|
||||||
*/
|
|
||||||
destinationOnSegment: function(map, latlngA, latlngB, distance) {
|
|
||||||
var angleDeg = L.GeometryUtil.angle(map, latlngA, latlngB),
|
|
||||||
latlng = L.GeometryUtil.destination(latlngA, angleDeg, distance);
|
|
||||||
return L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return L.GeometryUtil;
|
|
||||||
|
|
||||||
}));
|
|
@ -1,13 +0,0 @@
|
|||||||
.leaflet-control-layers-group-name {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: .2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-layers-group {
|
|
||||||
margin-bottom: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-layers-scrollbar {
|
|
||||||
overflow-y: scroll;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
@ -1,374 +0,0 @@
|
|||||||
/* global L */
|
|
||||||
|
|
||||||
// A layer control which provides for layer groupings.
|
|
||||||
// Author: Ishmael Smyrnow
|
|
||||||
L.Control.GroupedLayers = L.Control.extend({
|
|
||||||
|
|
||||||
options: {
|
|
||||||
collapsed: true,
|
|
||||||
position: 'topright',
|
|
||||||
autoZIndex: true,
|
|
||||||
exclusiveGroups: [],
|
|
||||||
groupCheckboxes: false
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function (baseLayers, groupedOverlays, options) {
|
|
||||||
var i, j;
|
|
||||||
L.Util.setOptions(this, options);
|
|
||||||
|
|
||||||
this._layers = [];
|
|
||||||
this._lastZIndex = 0;
|
|
||||||
this._handlingClick = false;
|
|
||||||
this._groupList = [];
|
|
||||||
this._domGroups = [];
|
|
||||||
|
|
||||||
for (i in baseLayers) {
|
|
||||||
this._addLayer(baseLayers[i], i);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i in groupedOverlays) {
|
|
||||||
for (j in groupedOverlays[i]) {
|
|
||||||
this._addLayer(groupedOverlays[i][j], j, i, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onAdd: function (map) {
|
|
||||||
this._initLayout();
|
|
||||||
this._update();
|
|
||||||
|
|
||||||
map
|
|
||||||
.on('layeradd', this._onLayerChange, this)
|
|
||||||
.on('layerremove', this._onLayerChange, this);
|
|
||||||
|
|
||||||
return this._container;
|
|
||||||
},
|
|
||||||
|
|
||||||
onRemove: function (map) {
|
|
||||||
map
|
|
||||||
.off('layeradd', this._onLayerChange, this)
|
|
||||||
.off('layerremove', this._onLayerChange, this);
|
|
||||||
},
|
|
||||||
|
|
||||||
addBaseLayer: function (layer, name) {
|
|
||||||
this._addLayer(layer, name);
|
|
||||||
this._update();
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
addOverlay: function (layer, name, group) {
|
|
||||||
this._addLayer(layer, name, group, true);
|
|
||||||
this._update();
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeLayer: function (layer) {
|
|
||||||
var id = L.Util.stamp(layer);
|
|
||||||
var _layer = this._getLayer(id);
|
|
||||||
if (_layer) {
|
|
||||||
delete this._layers[this._layers.indexOf(_layer)];
|
|
||||||
}
|
|
||||||
this._update();
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
_getLayer: function (id) {
|
|
||||||
for (var i = 0; i < this._layers.length; i++) {
|
|
||||||
if (this._layers[i] && L.stamp(this._layers[i].layer) === id) {
|
|
||||||
return this._layers[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_initLayout: function () {
|
|
||||||
var className = 'leaflet-control-layers',
|
|
||||||
container = this._container = L.DomUtil.create('div', className);
|
|
||||||
|
|
||||||
// Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
|
|
||||||
container.setAttribute('aria-haspopup', true);
|
|
||||||
|
|
||||||
if (L.Browser.touch) {
|
|
||||||
L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
|
|
||||||
} else {
|
|
||||||
L.DomEvent.disableClickPropagation(container);
|
|
||||||
L.DomEvent.on(container, 'wheel', L.DomEvent.stopPropagation);
|
|
||||||
}
|
|
||||||
|
|
||||||
var form = this._form = L.DomUtil.create('form', className + '-list');
|
|
||||||
|
|
||||||
if (this.options.collapsed) {
|
|
||||||
if (!L.Browser.android) {
|
|
||||||
L.DomEvent
|
|
||||||
.on(container, 'mouseover', this._expand, this)
|
|
||||||
.on(container, 'mouseout', this._collapse, this);
|
|
||||||
}
|
|
||||||
var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container);
|
|
||||||
link.href = '#';
|
|
||||||
link.title = 'Layers';
|
|
||||||
|
|
||||||
if (L.Browser.touch) {
|
|
||||||
L.DomEvent
|
|
||||||
.on(link, 'click', L.DomEvent.stop)
|
|
||||||
.on(link, 'click', this._expand, this);
|
|
||||||
} else {
|
|
||||||
L.DomEvent.on(link, 'focus', this._expand, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._map.on('click', this._collapse, this);
|
|
||||||
// TODO keyboard accessibility
|
|
||||||
} else {
|
|
||||||
this._expand();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._baseLayersList = L.DomUtil.create('div', className + '-base', form);
|
|
||||||
this._separator = L.DomUtil.create('div', className + '-separator', form);
|
|
||||||
this._overlaysList = L.DomUtil.create('div', className + '-overlays', form);
|
|
||||||
|
|
||||||
container.appendChild(form);
|
|
||||||
},
|
|
||||||
|
|
||||||
_addLayer: function (layer, name, group, overlay) {
|
|
||||||
var id = L.Util.stamp(layer);
|
|
||||||
|
|
||||||
var _layer = {
|
|
||||||
layer: layer,
|
|
||||||
name: name,
|
|
||||||
overlay: overlay
|
|
||||||
};
|
|
||||||
this._layers.push(_layer);
|
|
||||||
|
|
||||||
group = group || '';
|
|
||||||
var groupId = this._indexOf(this._groupList, group);
|
|
||||||
|
|
||||||
if (groupId === -1) {
|
|
||||||
groupId = this._groupList.push(group) - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var exclusive = (this._indexOf(this.options.exclusiveGroups, group) !== -1);
|
|
||||||
|
|
||||||
_layer.group = {
|
|
||||||
name: group,
|
|
||||||
id: groupId,
|
|
||||||
exclusive: exclusive
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.options.autoZIndex && layer.setZIndex) {
|
|
||||||
this._lastZIndex++;
|
|
||||||
layer.setZIndex(this._lastZIndex);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_update: function () {
|
|
||||||
if (!this._container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._baseLayersList.innerHTML = '';
|
|
||||||
this._overlaysList.innerHTML = '';
|
|
||||||
this._domGroups.length = 0;
|
|
||||||
|
|
||||||
var baseLayersPresent = false,
|
|
||||||
overlaysPresent = false,
|
|
||||||
i, obj;
|
|
||||||
|
|
||||||
for (var i = 0; i < this._layers.length; i++) {
|
|
||||||
obj = this._layers[i];
|
|
||||||
this._addItem(obj);
|
|
||||||
overlaysPresent = overlaysPresent || obj.overlay;
|
|
||||||
baseLayersPresent = baseLayersPresent || !obj.overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none';
|
|
||||||
},
|
|
||||||
|
|
||||||
_onLayerChange: function (e) {
|
|
||||||
var obj = this._getLayer(L.Util.stamp(e.layer)),
|
|
||||||
type;
|
|
||||||
|
|
||||||
if (!obj) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._handlingClick) {
|
|
||||||
this._update();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.overlay) {
|
|
||||||
type = e.type === 'layeradd' ? 'overlayadd' : 'overlayremove';
|
|
||||||
} else {
|
|
||||||
type = e.type === 'layeradd' ? 'baselayerchange' : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type) {
|
|
||||||
this._map.fire(type, obj);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe)
|
|
||||||
_createRadioElement: function (name, checked) {
|
|
||||||
var radioHtml = '<input type="radio" class="leaflet-control-layers-selector" name="' + name + '"';
|
|
||||||
if (checked) {
|
|
||||||
radioHtml += ' checked="checked"';
|
|
||||||
}
|
|
||||||
radioHtml += '/>';
|
|
||||||
|
|
||||||
var radioFragment = document.createElement('div');
|
|
||||||
radioFragment.innerHTML = radioHtml;
|
|
||||||
|
|
||||||
return radioFragment.firstChild;
|
|
||||||
},
|
|
||||||
|
|
||||||
_addItem: function (obj) {
|
|
||||||
var label = document.createElement('label'),
|
|
||||||
input,
|
|
||||||
checked = this._map.hasLayer(obj.layer),
|
|
||||||
container,
|
|
||||||
groupRadioName;
|
|
||||||
|
|
||||||
if (obj.overlay) {
|
|
||||||
if (obj.group.exclusive) {
|
|
||||||
groupRadioName = 'leaflet-exclusive-group-layer-' + obj.group.id;
|
|
||||||
input = this._createRadioElement(groupRadioName, checked);
|
|
||||||
} else {
|
|
||||||
input = document.createElement('input');
|
|
||||||
input.type = 'checkbox';
|
|
||||||
input.className = 'leaflet-control-layers-selector';
|
|
||||||
input.defaultChecked = checked;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
input = this._createRadioElement('leaflet-base-layers', checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
input.layerId = L.Util.stamp(obj.layer);
|
|
||||||
input.groupID = obj.group.id;
|
|
||||||
L.DomEvent.on(input, 'click', this._onInputClick, this);
|
|
||||||
|
|
||||||
var name = document.createElement('span');
|
|
||||||
name.innerHTML = ' ' + obj.name;
|
|
||||||
|
|
||||||
label.appendChild(input);
|
|
||||||
label.appendChild(name);
|
|
||||||
|
|
||||||
if (obj.overlay) {
|
|
||||||
container = this._overlaysList;
|
|
||||||
|
|
||||||
var groupContainer = this._domGroups[obj.group.id];
|
|
||||||
|
|
||||||
// Create the group container if it doesn't exist
|
|
||||||
if (!groupContainer) {
|
|
||||||
groupContainer = document.createElement('div');
|
|
||||||
groupContainer.className = 'leaflet-control-layers-group';
|
|
||||||
groupContainer.id = 'leaflet-control-layers-group-' + obj.group.id;
|
|
||||||
|
|
||||||
var groupLabel = document.createElement('label');
|
|
||||||
groupLabel.className = 'leaflet-control-layers-group-label';
|
|
||||||
|
|
||||||
if (obj.group.name !== '' && !obj.group.exclusive) {
|
|
||||||
// ------ add a group checkbox with an _onInputClickGroup function
|
|
||||||
if (this.options.groupCheckboxes) {
|
|
||||||
var groupInput = document.createElement('input');
|
|
||||||
groupInput.type = 'checkbox';
|
|
||||||
groupInput.className = 'leaflet-control-layers-group-selector';
|
|
||||||
groupInput.groupID = obj.group.id;
|
|
||||||
groupInput.legend = this;
|
|
||||||
L.DomEvent.on(groupInput, 'click', this._onGroupInputClick, groupInput);
|
|
||||||
groupLabel.appendChild(groupInput);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var groupName = document.createElement('span');
|
|
||||||
groupName.className = 'leaflet-control-layers-group-name';
|
|
||||||
groupName.innerHTML = obj.group.name;
|
|
||||||
groupLabel.appendChild(groupName);
|
|
||||||
|
|
||||||
groupContainer.appendChild(groupLabel);
|
|
||||||
container.appendChild(groupContainer);
|
|
||||||
|
|
||||||
this._domGroups[obj.group.id] = groupContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
container = groupContainer;
|
|
||||||
} else {
|
|
||||||
container = this._baseLayersList;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.appendChild(label);
|
|
||||||
|
|
||||||
return label;
|
|
||||||
},
|
|
||||||
|
|
||||||
_onGroupInputClick: function () {
|
|
||||||
var i, input, obj;
|
|
||||||
|
|
||||||
var this_legend = this.legend;
|
|
||||||
this_legend._handlingClick = true;
|
|
||||||
|
|
||||||
var inputs = this_legend._form.getElementsByTagName('input');
|
|
||||||
var inputsLen = inputs.length;
|
|
||||||
|
|
||||||
for (i = 0; i < inputsLen; i++) {
|
|
||||||
input = inputs[i];
|
|
||||||
if (input.groupID === this.groupID && input.className === 'leaflet-control-layers-selector') {
|
|
||||||
input.checked = this.checked;
|
|
||||||
obj = this_legend._getLayer(input.layerId);
|
|
||||||
if (input.checked && !this_legend._map.hasLayer(obj.layer)) {
|
|
||||||
this_legend._map.addLayer(obj.layer);
|
|
||||||
} else if (!input.checked && this_legend._map.hasLayer(obj.layer)) {
|
|
||||||
this_legend._map.removeLayer(obj.layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this_legend._handlingClick = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
_onInputClick: function () {
|
|
||||||
var i, input, obj,
|
|
||||||
inputs = this._form.getElementsByTagName('input'),
|
|
||||||
inputsLen = inputs.length;
|
|
||||||
|
|
||||||
this._handlingClick = true;
|
|
||||||
|
|
||||||
for (i = 0; i < inputsLen; i++) {
|
|
||||||
input = inputs[i];
|
|
||||||
if (input.className === 'leaflet-control-layers-selector') {
|
|
||||||
obj = this._getLayer(input.layerId);
|
|
||||||
|
|
||||||
if (input.checked && !this._map.hasLayer(obj.layer)) {
|
|
||||||
this._map.addLayer(obj.layer);
|
|
||||||
} else if (!input.checked && this._map.hasLayer(obj.layer)) {
|
|
||||||
this._map.removeLayer(obj.layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._handlingClick = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
_expand: function () {
|
|
||||||
L.DomUtil.addClass(this._container, 'leaflet-control-layers-expanded');
|
|
||||||
// permits to have a scrollbar if overlays heighter than the map.
|
|
||||||
var acceptableHeight = this._map._size.y - (this._container.offsetTop * 4);
|
|
||||||
if (acceptableHeight < this._form.clientHeight) {
|
|
||||||
L.DomUtil.addClass(this._form, 'leaflet-control-layers-scrollbar');
|
|
||||||
this._form.style.height = acceptableHeight + 'px';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_collapse: function () {
|
|
||||||
this._container.className = this._container.className.replace(' leaflet-control-layers-expanded', '');
|
|
||||||
},
|
|
||||||
|
|
||||||
_indexOf: function (arr, obj) {
|
|
||||||
for (var i = 0, j = arr.length; i < j; i++) {
|
|
||||||
if (arr[i] === obj) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
L.control.groupedLayers = function (baseLayers, groupedOverlays, options) {
|
|
||||||
return new L.Control.GroupedLayers(baseLayers, groupedOverlays, options);
|
|
||||||
};
|
|
@ -1,60 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
.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,227 +0,0 @@
|
|||||||
(function (factory, window) {
|
|
||||||
if (typeof define === 'function' && define.amd) {
|
|
||||||
define(['leaflet'], factory);
|
|
||||||
} else if (typeof exports === 'object') {
|
|
||||||
module.exports = factory(require('leaflet'));
|
|
||||||
}
|
|
||||||
if (typeof window !== 'undefined' && window.L) {
|
|
||||||
window.L.PolylineOffset = factory(L);
|
|
||||||
}
|
|
||||||
}(function (L) {
|
|
||||||
|
|
||||||
function forEachPair(list, callback) {
|
|
||||||
if (!list || list.length < 1) { return; }
|
|
||||||
for (var i = 1, l = list.length; i < l; i++) {
|
|
||||||
callback(list[i-1], list[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Find the coefficients (a,b) of a line of equation y = a.x + b,
|
|
||||||
or the constant x for vertical lines
|
|
||||||
Return null if there's no equation possible
|
|
||||||
*/
|
|
||||||
function lineEquation(pt1, pt2) {
|
|
||||||
if (pt1.x === pt2.x) {
|
|
||||||
return pt1.y === pt2.y ? null : { x: pt1.x };
|
|
||||||
}
|
|
||||||
|
|
||||||
var a = (pt2.y - pt1.y) / (pt2.x - pt1.x);
|
|
||||||
return {
|
|
||||||
a: a,
|
|
||||||
b: pt1.y - a * pt1.x,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Return the intersection point of two lines defined by two points each
|
|
||||||
Return null when there's no unique intersection
|
|
||||||
*/
|
|
||||||
function intersection(l1a, l1b, l2a, l2b) {
|
|
||||||
var line1 = lineEquation(l1a, l1b);
|
|
||||||
var line2 = lineEquation(l2a, l2b);
|
|
||||||
|
|
||||||
if (line1 === null || line2 === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line1.hasOwnProperty('x')) {
|
|
||||||
return line2.hasOwnProperty('x')
|
|
||||||
? null
|
|
||||||
: {
|
|
||||||
x: line1.x,
|
|
||||||
y: line2.a * line1.x + line2.b,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (line2.hasOwnProperty('x')) {
|
|
||||||
return {
|
|
||||||
x: line2.x,
|
|
||||||
y: line1.a * line2.x + line1.b,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line1.a === line2.a) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var x = (line2.b - line1.b) / (line1.a - line2.a);
|
|
||||||
return {
|
|
||||||
x: x,
|
|
||||||
y: line1.a * x + line1.b,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function translatePoint(pt, dist, heading) {
|
|
||||||
return {
|
|
||||||
x: pt.x + dist * Math.cos(heading),
|
|
||||||
y: pt.y + dist * Math.sin(heading),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var PolylineOffset = {
|
|
||||||
offsetPointLine: function(points, distance) {
|
|
||||||
var offsetSegments = [];
|
|
||||||
|
|
||||||
forEachPair(points, L.bind(function(a, b) {
|
|
||||||
if (a.x === b.x && a.y === b.y) { return; }
|
|
||||||
|
|
||||||
// angles in (-PI, PI]
|
|
||||||
var segmentAngle = Math.atan2(a.y - b.y, a.x - b.x);
|
|
||||||
var offsetAngle = segmentAngle - Math.PI/2;
|
|
||||||
|
|
||||||
offsetSegments.push({
|
|
||||||
offsetAngle: offsetAngle,
|
|
||||||
original: [a, b],
|
|
||||||
offset: [
|
|
||||||
translatePoint(a, distance, offsetAngle),
|
|
||||||
translatePoint(b, distance, offsetAngle)
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}, this));
|
|
||||||
|
|
||||||
return offsetSegments;
|
|
||||||
},
|
|
||||||
|
|
||||||
offsetPoints: function(pts, options) {
|
|
||||||
var offsetSegments = this.offsetPointLine(L.LineUtil.simplify(pts, options.smoothFactor), options.offset);
|
|
||||||
return this.joinLineSegments(offsetSegments, options.offset);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Join 2 line segments defined by 2 points each with a circular arc
|
|
||||||
*/
|
|
||||||
joinSegments: function(s1, s2, offset) {
|
|
||||||
// TODO: different join styles
|
|
||||||
return this.circularArc(s1, s2, offset)
|
|
||||||
.filter(function(x) { return x; })
|
|
||||||
},
|
|
||||||
|
|
||||||
joinLineSegments: function(segments, offset) {
|
|
||||||
var joinedPoints = [];
|
|
||||||
var first = segments[0];
|
|
||||||
var last = segments[segments.length - 1];
|
|
||||||
|
|
||||||
if (first && last) {
|
|
||||||
joinedPoints.push(first.offset[0]);
|
|
||||||
forEachPair(segments, L.bind(function(s1, s2) {
|
|
||||||
joinedPoints = joinedPoints.concat(this.joinSegments(s1, s2, offset));
|
|
||||||
}, this));
|
|
||||||
joinedPoints.push(last.offset[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return joinedPoints;
|
|
||||||
},
|
|
||||||
|
|
||||||
segmentAsVector: function(s) {
|
|
||||||
return {
|
|
||||||
x: s[1].x - s[0].x,
|
|
||||||
y: s[1].y - s[0].y,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getSignedAngle: function(s1, s2) {
|
|
||||||
const a = this.segmentAsVector(s1);
|
|
||||||
const b = this.segmentAsVector(s2);
|
|
||||||
return Math.atan2(a.x * b.y - a.y * b.x, a.x * b.x + a.y * b.y);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Interpolates points between two offset segments in a circular form
|
|
||||||
*/
|
|
||||||
circularArc: function(s1, s2, distance) {
|
|
||||||
// if the segments are the same angle,
|
|
||||||
// there should be a single join point
|
|
||||||
if (s1.offsetAngle === s2.offsetAngle) {
|
|
||||||
return [s1.offset[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
const signedAngle = this.getSignedAngle(s1.offset, s2.offset);
|
|
||||||
// for inner angles, just find the offset segments intersection
|
|
||||||
if ((signedAngle * distance > 0) &&
|
|
||||||
(signedAngle * this.getSignedAngle(s1.offset, [s1.offset[0], s2.offset[1]]) > 0)) {
|
|
||||||
return [intersection(s1.offset[0], s1.offset[1], s2.offset[0], s2.offset[1])];
|
|
||||||
}
|
|
||||||
|
|
||||||
// draws a circular arc with R = offset distance, C = original meeting point
|
|
||||||
var points = [];
|
|
||||||
var center = s1.original[1];
|
|
||||||
// ensure angles go in the anti-clockwise direction
|
|
||||||
var rightOffset = distance > 0;
|
|
||||||
var startAngle = rightOffset ? s2.offsetAngle : s1.offsetAngle;
|
|
||||||
var endAngle = rightOffset ? s1.offsetAngle : s2.offsetAngle;
|
|
||||||
// and that the end angle is bigger than the start angle
|
|
||||||
if (endAngle < startAngle) {
|
|
||||||
endAngle += Math.PI * 2;
|
|
||||||
}
|
|
||||||
var step = Math.PI / 8;
|
|
||||||
for (var alpha = startAngle; alpha < endAngle; alpha += step) {
|
|
||||||
points.push(translatePoint(center, distance, alpha));
|
|
||||||
}
|
|
||||||
points.push(translatePoint(center, distance, endAngle));
|
|
||||||
|
|
||||||
return rightOffset ? points.reverse() : points;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modify the L.Polyline class by overwriting the projection function
|
|
||||||
L.Polyline.include({
|
|
||||||
_projectLatlngs: function (latlngs, result, projectedBounds) {
|
|
||||||
var isFlat = latlngs.length > 0 && latlngs[0] instanceof L.LatLng;
|
|
||||||
|
|
||||||
if (isFlat) {
|
|
||||||
var ring = latlngs.map(L.bind(function(ll) {
|
|
||||||
var point = this._map.latLngToLayerPoint(ll);
|
|
||||||
if (projectedBounds) {
|
|
||||||
projectedBounds.extend(point);
|
|
||||||
}
|
|
||||||
return point;
|
|
||||||
}, this));
|
|
||||||
|
|
||||||
// Offset management hack ---
|
|
||||||
if (this.options.offset) {
|
|
||||||
ring = L.PolylineOffset.offsetPoints(ring, this.options);
|
|
||||||
}
|
|
||||||
// Offset management hack END ---
|
|
||||||
|
|
||||||
result.push(ring.map(function (xy) {
|
|
||||||
return L.point(xy.x, xy.y);
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
latlngs.forEach(L.bind(function(ll) {
|
|
||||||
this._projectLatlngs(ll, result, projectedBounds);
|
|
||||||
}, this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
L.Polyline.include({
|
|
||||||
setOffset: function(offset) {
|
|
||||||
this.options.offset = offset;
|
|
||||||
this.redraw();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return PolylineOffset;
|
|
||||||
|
|
||||||
}, window));
|
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 618 B |
656
src/public/assets/js/leaflet@1.9.3/dist/leaflet.css
vendored
@ -1,656 +0,0 @@
|
|||||||
/* 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.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;
|
|
||||||
}
|
|
||||||
}
|
|
15
src/public/assets/js/moment@2.29.1/moment.min.js
vendored
16725
src/public/assets/js/vue@3.4.26/dist/vue.global.js
vendored
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 309 KiB |
Before Width: | Height: | Size: 159 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 281 KiB |
Before Width: | Height: | Size: 233 KiB |
Before Width: | Height: | Size: 256 KiB |
Before Width: | Height: | Size: 365 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 465 KiB |
Before Width: | Height: | Size: 350 KiB |
Before Width: | Height: | Size: 182 KiB |
Before Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 334 KiB |
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 451 KiB |
Before Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 702 KiB |
Before Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 252 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 514 KiB |
Before Width: | Height: | Size: 391 KiB |
Before Width: | Height: | Size: 10 KiB |
@ -1,386 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
||||||
<title>Meshtastic Messages</title>
|
|
||||||
|
|
||||||
<!-- tailwind css -->
|
|
||||||
<script src="/assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js"></script>
|
|
||||||
|
|
||||||
<!-- moment -->
|
|
||||||
<script src="/assets/js/moment@2.29.1/moment.min.js"></script>
|
|
||||||
|
|
||||||
<!-- vuejs -->
|
|
||||||
<script src="/assets/js/vue@3.4.26/dist/vue.global.js"></script>
|
|
||||||
|
|
||||||
<!-- axios -->
|
|
||||||
<script src="/assets/js/axios@1.6.8/dist/axios.min.js"></script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
/* used to prevent ui flicker before vuejs loads */
|
|
||||||
[v-cloak] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body class="h-full">
|
|
||||||
<div id="app" v-cloak>
|
|
||||||
<div class="h-full flex flex-col overflow-hidden">
|
|
||||||
|
|
||||||
<!-- empty state -->
|
|
||||||
<div v-if="messages.length === 0" class="flex h-full">
|
|
||||||
<div class="flex flex-col mx-auto my-auto p-4 text-gray-500 text-center">
|
|
||||||
<div class="mb-2 mx-auto">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-10">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="font-semibold">No Messages</div>
|
|
||||||
<div>There's no messages yet...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- note: must use flex-col-reverse to prevent ui scrolling when adding older messages to ui -->
|
|
||||||
<div v-show="messages.length > 0" id="messages" class="h-full flex flex-col-reverse p-3 overflow-y-auto">
|
|
||||||
|
|
||||||
<!-- messages -->
|
|
||||||
<div :key="message.id" v-for="message of reversedMessages" class="max-w-xl items-start my-1.5">
|
|
||||||
|
|
||||||
<div class="flex">
|
|
||||||
|
|
||||||
<div class="mr-2 mt-2">
|
|
||||||
<a target="_blank" :href="`/?node_id=${message.from}`">
|
|
||||||
<div class="flex rounded-full h-12 w-12 text-white shadow" :class="[ `bg-[${getNodeColour(message.from)}]`, `text-[${getNodeTextColour(message.from)}]` ]">
|
|
||||||
<div class="mx-auto my-auto drop-shadow-sm">{{ getNodeShortName(message.from) }}</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
|
|
||||||
<!-- sender -->
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
<a target="_blank" :href="`/?node_id=${message.from}`" class="hover:text-blue-500">
|
|
||||||
<span>{{ getNodeLongName(message.from) }}</span>
|
|
||||||
</a>
|
|
||||||
<span v-if="message.to.toString() !== '4294967295'">
|
|
||||||
<span> → </span>
|
|
||||||
<a target="_blank" :href="`/?node_id=${message.to}`" class="hover:text-blue-500">{{ getNodeName(message.to) }}</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- message -->
|
|
||||||
<div @click="message.is_details_expanded = !message.is_details_expanded" class="flex">
|
|
||||||
<div class="border border-gray-300 rounded-xl shadow overflow-hidden bg-[#efefef] divide-y">
|
|
||||||
<div class="w-full space-y-0.5 px-2.5 py-1" v-html="escapeMessageText(message.text)" style="white-space:pre-wrap;word-break:break-word;"></div>
|
|
||||||
<div v-if="message.is_details_expanded" class="text-xs text-gray-500 px-2 py-1">
|
|
||||||
<span :title="message.created_at">{{ formatMessageTimestamp(message.created_at) }}</span>
|
|
||||||
<span> • Gated by <a target="_blank" :href="`/?node_id=${message.gateway_id}`" class="hover:text-blue-500">{{ getNodeName(message.gateway_id) }}</a></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- load previous -->
|
|
||||||
<button v-show="!isLoadingPrevious && hasMorePrevious" id="load-previous" @click="loadPrevious" type="button" class="flex space-x-2 mx-auto bg-gray-200 px-3 py-1 hover:bg-gray-300 rounded-full shadow">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
||||||
</svg>
|
|
||||||
<span>Load Previous</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
Vue.createApp({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
|
|
||||||
to: null,
|
|
||||||
from: null,
|
|
||||||
channelId: null,
|
|
||||||
gatewayId: null,
|
|
||||||
|
|
||||||
isLoadingPrevious: false,
|
|
||||||
isLoadingMore: false,
|
|
||||||
shouldAutoScroll: true,
|
|
||||||
loadPreviousObserver: null,
|
|
||||||
hasMorePrevious: true,
|
|
||||||
|
|
||||||
messages: [],
|
|
||||||
nodesById: {},
|
|
||||||
|
|
||||||
moment: window.moment,
|
|
||||||
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted: function() {
|
|
||||||
|
|
||||||
// parse url params
|
|
||||||
const queryParams = new URLSearchParams(window.location.search);
|
|
||||||
this.to = queryParams.get('to');
|
|
||||||
this.from = queryParams.get('from');
|
|
||||||
this.channelId = queryParams.get('channel_id');
|
|
||||||
this.gatewayId = queryParams.get('gateway_id');
|
|
||||||
this.directMessageNodeIds = queryParams.get('direct_message_node_ids');
|
|
||||||
this.count = queryParams.get('count');
|
|
||||||
|
|
||||||
// listen for scrolling of messages list
|
|
||||||
document.getElementById("messages").addEventListener("scroll", (event) => {
|
|
||||||
|
|
||||||
// check if messages is scrolled to bottom
|
|
||||||
const element = event.target;
|
|
||||||
const isAtBottom = element.scrollTop === (element.scrollHeight - element.offsetHeight);
|
|
||||||
|
|
||||||
// we want to auto scroll if user is at bottom of messages list
|
|
||||||
this.shouldAutoScroll = isAtBottom;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// setup intersection observer
|
|
||||||
this.loadPreviousObserver = new IntersectionObserver((entries) => {
|
|
||||||
const loadMoreElement = entries[0];
|
|
||||||
if(loadMoreElement && loadMoreElement.isIntersecting){
|
|
||||||
this.loadPrevious();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.initialLoad();
|
|
||||||
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async initialLoad() {
|
|
||||||
|
|
||||||
// load 1 page of previous messages
|
|
||||||
await this.loadPrevious();
|
|
||||||
|
|
||||||
// scroll to bottom
|
|
||||||
this.scrollToBottom();
|
|
||||||
|
|
||||||
// setup auto loading previous
|
|
||||||
this.loadPreviousObserver.observe(document.querySelector("#load-previous"));
|
|
||||||
|
|
||||||
// load more every few seconds
|
|
||||||
setInterval(async () => {
|
|
||||||
await this.loadMore();
|
|
||||||
}, 2500);
|
|
||||||
|
|
||||||
},
|
|
||||||
async loadPrevious() {
|
|
||||||
|
|
||||||
// do nothing if already loading
|
|
||||||
if(this.isLoadingPrevious){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isLoadingPrevious = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
const response = await window.axios.get('/api/v1/text-messages', {
|
|
||||||
params: {
|
|
||||||
to: this.to,
|
|
||||||
from: this.from,
|
|
||||||
channel_id: this.channelId,
|
|
||||||
gateway_id: this.gatewayId,
|
|
||||||
direct_message_node_ids: this.directMessageNodeIds,
|
|
||||||
count: this.count,
|
|
||||||
order: "desc",
|
|
||||||
last_id: this.oldestMessageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// add messages to start of existing messages
|
|
||||||
const messages = response.data.text_messages;
|
|
||||||
for(const message of messages){
|
|
||||||
this.messages.unshift(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// no more previous to load if previous list is empty
|
|
||||||
if(messages.length === 0){
|
|
||||||
this.hasMorePrevious = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch node info
|
|
||||||
for(const message of messages){
|
|
||||||
await this.fetchNodeInfo(message.to);
|
|
||||||
await this.fetchNodeInfo(message.from);
|
|
||||||
await this.fetchNodeInfo(message.gateway_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// do nothing
|
|
||||||
} finally {
|
|
||||||
this.isLoadingPrevious = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
async loadMore() {
|
|
||||||
|
|
||||||
// do nothing if already loading
|
|
||||||
if(this.isLoadingMore){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isLoadingMore = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
const response = await window.axios.get('/api/v1/text-messages', {
|
|
||||||
params: {
|
|
||||||
to: this.to,
|
|
||||||
from: this.from,
|
|
||||||
channel_id: this.channelId,
|
|
||||||
gateway_id: this.gatewayId,
|
|
||||||
direct_message_node_ids: this.directMessageNodeIds,
|
|
||||||
count: this.count,
|
|
||||||
order: "asc",
|
|
||||||
last_id: this.latestMessageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// add messages to end of existing messages
|
|
||||||
const messages = response.data.text_messages;
|
|
||||||
for(const message of messages){
|
|
||||||
this.messages.push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// scroll to bottom
|
|
||||||
if(this.shouldAutoScroll){
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch node info
|
|
||||||
for(const message of messages){
|
|
||||||
await this.fetchNodeInfo(message.to);
|
|
||||||
await this.fetchNodeInfo(message.from);
|
|
||||||
await this.fetchNodeInfo(message.gateway_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// do nothing
|
|
||||||
} finally {
|
|
||||||
this.isLoadingMore = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
async fetchNodeInfo(nodeId) {
|
|
||||||
|
|
||||||
// do nothing if already fetched
|
|
||||||
if(nodeId in this.nodesById){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do nothing if broadcast address
|
|
||||||
if(nodeId.toString() === "4294967295"){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
const response = await window.axios.get(`/api/v1/nodes/${nodeId}`);
|
|
||||||
const node = response.data.node;
|
|
||||||
|
|
||||||
if(node){
|
|
||||||
this.nodesById[node.node_id] = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
scrollToBottom: function() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
var container = this.$el.querySelector("#messages");
|
|
||||||
container.scrollTop = container.scrollHeight;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getNodeHexId(nodeId) {
|
|
||||||
return "!" + parseInt(nodeId).toString(16);
|
|
||||||
},
|
|
||||||
getNodeName(nodeId) {
|
|
||||||
|
|
||||||
// find node by id
|
|
||||||
const node = this.nodesById[nodeId];
|
|
||||||
if(!node){
|
|
||||||
return this.getNodeHexId(nodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `[${node.short_name}] ${node.long_name}`;
|
|
||||||
|
|
||||||
},
|
|
||||||
getNodeShortName(nodeId) {
|
|
||||||
return this.nodesById[nodeId]?.short_name?.substring(0, 4) ?? "?";
|
|
||||||
},
|
|
||||||
getNodeLongName(nodeId) {
|
|
||||||
return this.nodesById[nodeId]?.long_name ?? this.getNodeHexId(nodeId);
|
|
||||||
},
|
|
||||||
getNodeColour(nodeId) {
|
|
||||||
// convert node id to a hex colour
|
|
||||||
return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0');
|
|
||||||
},
|
|
||||||
getNodeTextColour(nodeId) {
|
|
||||||
|
|
||||||
// extract rgb components
|
|
||||||
const r = (nodeId & 0xFF0000) >> 16;
|
|
||||||
const g = (nodeId & 0x00FF00) >> 8;
|
|
||||||
const b = nodeId & 0x0000FF;
|
|
||||||
|
|
||||||
// calculate brightness
|
|
||||||
const brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255;
|
|
||||||
|
|
||||||
// determine text color based on brightness
|
|
||||||
return brightness > 0.5 ? "#000000" : "#FFFFFF";
|
|
||||||
|
|
||||||
},
|
|
||||||
escapeMessageText(text) {
|
|
||||||
return text.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('\n', '<br/>');
|
|
||||||
},
|
|
||||||
formatMessageTimestamp(createdAt) {
|
|
||||||
return moment(new Date(createdAt)).local().format("DD/MMM/YYYY hh:mm:ss A");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
reversedMessages() {
|
|
||||||
// ensure a copy of the array is returned in reverse order
|
|
||||||
return this.messages.map((message) => message).reverse();
|
|
||||||
},
|
|
||||||
oldestMessageId() {
|
|
||||||
|
|
||||||
if(this.messages.length > 0){
|
|
||||||
return this.messages[0].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
},
|
|
||||||
latestMessageId() {
|
|
||||||
|
|
||||||
if(this.messages.length > 0){
|
|
||||||
return this.messages[this.messages.length - 1].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}).mount('#app');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,23 +0,0 @@
|
|||||||
class NodeIdUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the provided hex id to a numeric id, for example: !FFFFFFFF to 4294967295
|
|
||||||
* Anything else will be converted as is to a BigInt, for example "4294967295" to 4294967295
|
|
||||||
* @param hexIdOrNumber a node id in hex format with a prepended "!", or a numeric node id as a string or number
|
|
||||||
* @returns {bigint} the node id in numeric form
|
|
||||||
*/
|
|
||||||
static convertToNumeric(hexIdOrNumber) {
|
|
||||||
|
|
||||||
// check if this is a hex id, and convert to numeric
|
|
||||||
if(hexIdOrNumber.toString().startsWith("!")){
|
|
||||||
return BigInt('0x' + hexIdOrNumber.replaceAll("!", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert string or number to numeric
|
|
||||||
return BigInt(hexIdOrNumber);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = NodeIdUtil;
|
|
@ -1,9 +0,0 @@
|
|||||||
const NodeIdUtil = require("./node_id_util");
|
|
||||||
|
|
||||||
test('can convert hex id to numeric id', () => {
|
|
||||||
expect(NodeIdUtil.convertToNumeric("!FFFFFFFF")).toBe(BigInt(4294967295));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can convert numeric id to numeric id', () => {
|
|
||||||
expect(NodeIdUtil.convertToNumeric(4294967295)).toBe(BigInt(4294967295));
|
|
||||||
});
|
|
@ -1,66 +0,0 @@
|
|||||||
class PositionUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obfuscates the provided latitude or longitude down to the provided precision in bits.
|
|
||||||
* This is based on the same logic in the official meshtastic firmware.
|
|
||||||
* https://github.com/meshtastic/firmware/blob/0a93261c0646f93aea518cc0599e547e9dc0e997/src/modules/PositionModule.cpp#L187
|
|
||||||
*/
|
|
||||||
static setPositionPrecision(latitudeOrLongitudeInteger, precision) {
|
|
||||||
|
|
||||||
// check if we should use the provided precision
|
|
||||||
if(precision > 0 && precision < 32){
|
|
||||||
|
|
||||||
// apply bitmask to reduce precision of position to wanted bits
|
|
||||||
latitudeOrLongitudeInteger = latitudeOrLongitudeInteger & (0xFFFFFFFF << (32 - precision));
|
|
||||||
|
|
||||||
// we want the imprecise position to be the middle of the possible location
|
|
||||||
latitudeOrLongitudeInteger += (1 << (31 - precision));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return latitudeOrLongitudeInteger;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Truncates the provided latitude or longitude to a maximum of x decimal places
|
|
||||||
* e.g: 12.3456789 with 2 decimal places would be 12.34
|
|
||||||
* @param latitudeOrLongitudeString e.g: 12.3456789
|
|
||||||
* @param numberOfDecimalPlaces how many decimal places to allow in the result
|
|
||||||
* @returns {*|string|null}
|
|
||||||
*/
|
|
||||||
static truncateDecimalPlaces(latitudeOrLongitudeString, numberOfDecimalPlaces) {
|
|
||||||
|
|
||||||
// ensure value not null
|
|
||||||
if(latitudeOrLongitudeString == null){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// split into left and right side of decimal point
|
|
||||||
// e.g: -12.3456789 -> [-12, 3456789]
|
|
||||||
var [ leftOfDecimalPoint, rightOfDecimalPoint ] = latitudeOrLongitudeString.split(".");
|
|
||||||
|
|
||||||
// check if decimal places available
|
|
||||||
if(rightOfDecimalPoint != null){
|
|
||||||
|
|
||||||
// truncate decimal places to desired length
|
|
||||||
rightOfDecimalPoint = rightOfDecimalPoint.substring(0, numberOfDecimalPlaces);
|
|
||||||
|
|
||||||
// return modified position with decimal places, if available
|
|
||||||
if(rightOfDecimalPoint.length > 0){
|
|
||||||
return [ leftOfDecimalPoint, rightOfDecimalPoint ].join(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
// no decimal places available anymore, return left side without dot
|
|
||||||
return leftOfDecimalPoint;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// decimal places not available, return position as is
|
|
||||||
return latitudeOrLongitudeString;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = PositionUtil;
|
|
@ -1,47 +0,0 @@
|
|||||||
const PositionUtil = require("./position_util");
|
|
||||||
|
|
||||||
test('can truncate string position to provided decimal places', () => {
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12.3456789", 0)).toBe("12");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12.3456789", 1)).toBe("12.3");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12.3456789", 2)).toBe("12.34");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12.3456789", 3)).toBe("12.345");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12.3456789", 4)).toBe("12.3456");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12.3456789", 5)).toBe("12.34567");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12.3456789", 6)).toBe("12.345678");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12.3456789", 7)).toBe("12.3456789");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12.3", 7)).toBe("12.3");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces(null, 2)).toBe(null);
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("", 2)).toBe("");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("12", 2)).toBe("12");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("123", 2)).toBe("123");
|
|
||||||
expect(PositionUtil.truncateDecimalPlaces("1234.", 2)).toBe("1234");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can set integer position precision to provided bits', () => {
|
|
||||||
|
|
||||||
// these tests are using the auckland sky tower position
|
|
||||||
// auckland sky tower: -36.84844007222091, 174.76221115261924
|
|
||||||
// the outputs we are expecting, are the same values returned by the code in the meshtastic firmware
|
|
||||||
// https://github.com/meshtastic/firmware/blob/0a93261c0646f93aea518cc0599e547e9dc0e997/src/modules/PositionModule.cpp#L187
|
|
||||||
|
|
||||||
// set precision to 32 bits (within 0 meters)
|
|
||||||
// -36.8484400, 174.7622111 -> -36.8484400, 174.7622111
|
|
||||||
expect(PositionUtil.setPositionPrecision(-368484400, 32)).toBe(-368484400);
|
|
||||||
expect(PositionUtil.setPositionPrecision(1747622111, 32)).toBe(1747622111);
|
|
||||||
|
|
||||||
// set precision to 16 bits (within ~364 meters)
|
|
||||||
// -36.8484400, 174.7622111 -> -36.8476160, 174.7615744
|
|
||||||
expect(PositionUtil.setPositionPrecision(-368484400, 16)).toBe(-368476160);
|
|
||||||
expect(PositionUtil.setPositionPrecision(1747622111, 16)).toBe(1747615744);
|
|
||||||
|
|
||||||
// set precision to 13 bits (within ~2.9 kilometers)
|
|
||||||
// -36.8484400, 174.7622111 -> -36.8312320, 174.7714048
|
|
||||||
expect(PositionUtil.setPositionPrecision(-368484400, 13)).toBe(-368312320);
|
|
||||||
expect(PositionUtil.setPositionPrecision(1747622111, 13)).toBe(1747714048);
|
|
||||||
|
|
||||||
// set precision to 11 bits (within ~11.6 kilometers)
|
|
||||||
// -36.8484400, 174.7622111 -> -36.8050176, 174.7976192
|
|
||||||
expect(PositionUtil.setPositionPrecision(-368484400, 11)).toBe(-368050176);
|
|
||||||
expect(PositionUtil.setPositionPrecision(1747622111, 11)).toBe(1747976192);
|
|
||||||
|
|
||||||
});
|
|