Add range rings capability

This commit is contained in:
Dave Conway-Jones 2019-11-25 13:48:43 +00:00
parent ec4068c839
commit 9d904f5b10
No known key found for this signature in database
GPG Key ID: 302A6725C594817F
7 changed files with 325 additions and 71 deletions

View File

@ -1,5 +1,6 @@
### Change Log for Node-RED Worldmap
- v2.2.0 - Add rangering arcs function
- v2.1.6 - Add legend command to allow inserting an html legend
- v2.1.5 - Fix squawk icon color handling
- v2.1.4 - Fix alt and speed as strings

View File

@ -1,7 +1,7 @@
# node-red-contrib-web-worldmap
[![npm version](https://badge.fury.io/js/node-red-contrib-web-worldmap.svg)](https://badge.fury.io/js/node-red-contrib-web-worldmap)
[![GitHub license](https://github.com/dceejay/redmap/blob/master/LICENSE)](https://img.shields.io/github/license/dceejay/redmap.svg)
[![GitHub license](https://img.shields.io/github/license/dceejay/redmap.svg)](https://github.com/dceejay/redmap/blob/master/LICENSE)
A <a href="https://nodered.org" target="mapinfo">Node-RED</a> node to provide a world
map web page for plotting "things" on.
@ -10,6 +10,7 @@ map web page for plotting "things" on.
### Updates
- v2.2.0 - Add rangerings arcs function
- v2.1.6 - Add legend command to allow inserting an html legend
- v2.1.5 - Fix squawk icon color handling
- v2.1.4 - Fix alt and speed as strings
@ -17,27 +18,8 @@ map web page for plotting "things" on.
- v2.1.2 - Fix layercontrol remove bug. Issue #116
- v2.1.1 - fix bug in repeated add with polygon
- v2.1.0 - add ui-worldmap node to make embedding in Dashboard easier. Let -in node specify connection actions only.
- v2.0.22 - fix SIDC missing property
- v2.0.21 - allow adding overlays without making them visible (visible:false). Issue #108
- v2.0.20 - ensure `fit` option is boolean, Issue #109. Fix track layers, Issue #110.
- v2.0.18 - Stop map contextmenu bleedthrough to marker. Add compress middleware.
- v2.0.17 - Let clear command also clear tracks from tracks node
- v2.0.16 - Revert use of ES6 import. Keep IE11 happy for while
- v2.0.13 - Fix tracks colour.
- v2.0.12 - Ensure default icon is in place if not specified (regression)
- v2.0.9 - Only update maxage on screen once it exists
- v2.0.8 - Drop beta flag, re-organise index, js and css files. Now using leaflet 1.4
- v2.0.7-beta - Switch Ruler control to be independent of Draw library.
- v2.0.6-beta - Re-enable editing of draw layer, add rectangles to lines and areas. Make individual objects editable.
- v2.0.5-beta - Fix clustering on zoom (update old library)
- v2.0.4-beta - Add helicopter icon. Correct Leaflet.Coordinates file name. Fix right contextmenu.
- v2.0.3-beta - Let circles have popups. Better drawing of ellipses
- v2.0.2-beta - Let lines and areas also have popups
- v2.0.1-beta - Add optional graticule
- v2.0.0-beta - Move to leaflet 1.4.x plus all plugins updated
- ...
see [CHANGELOG](https://github.com/dceejay/RedMap/blob/master/CHANGELOG.md) for full list.
- see [CHANGELOG](https://github.com/dceejay/RedMap/blob/master/CHANGELOG.md) for full list.
## Install
@ -45,7 +27,6 @@ Either use the Manage Palette option in the Node-RED Editor menu, or run the fol
npm i node-red-contrib-web-worldmap
## Usage
Plots "things" on a map. By default the map will be served from `{httpRoot}/worldmap`, but this
@ -225,6 +206,7 @@ a number of degrees.
msg.payload = { "name":"Bristol Channel", "lat":51.5, "lon":-2.9, "radius":[30000,70000], "tilt":45 };
### Options
Areas, Rectangles, Lines, Circles and Ellipses can also specify more optional properties:
@ -242,6 +224,31 @@ Areas, Rectangles, Lines, Circles and Ellipses can also specify more optional pr
Other properties can be found in the leaflet documentation.
### Arcs, Range Rings
You can add supplemental arc(s) to an icon by adding an **arc** property as below.
Supplemental means that you can also specify a line using a **bearing** and **length** property.
```
msg.payload = { name:"Camera01", icon:"fa-camera", lat:51.05, lon:-1.35,
bearing: 235,
length: 2200,
arc: {
ranges: [500,1000,2000],
pan: 228,
fov: 40,
color: '#aaaa00'
}
}
```
**ranges** can be a single number or an array of arc distances from the marker.
The **pan** is the bearing of the centre of the arc, and the **fov** (Field of view)
specifies the angle of the arc.
Defaults are shown above.
## Drawing
A single *right click* will allow you to add a point to the map - you must specify the `name` and optionally the `icon` and `layer`.
@ -251,6 +258,7 @@ Right-clicking on an icon will allow you to delete it.
If you select the **drawing** layer you can also add and edit polylines, polygons, rectangles and circles.
Once an item is drawn you can right click to edit or delete it. Double click the object to exit edit mode.
## Events from the map
The **worldmap in** node can be used to receive various events from the map. Examples of messages coming FROM the map include:

View File

@ -1,6 +1,6 @@
{
"name": "node-red-contrib-web-worldmap",
"version": "2.1.6",
"version": "2.2.0",
"description": "A Node-RED node to provide a web page of a world map for plotting things on.",
"dependencies": {
"cgi": "0.3.1",

View File

@ -67,6 +67,7 @@
<script src="leaflet/leaflet-omnivore.min.js"></script>
<script src="leaflet/Leaflet.Coordinates.js"></script>
<script src="leaflet/leaflet.latlng-graticule.js"></script>
<script src="leaflet/Semicircle.js"></script>
<script src="leaflet/dialog-polyfill.js"></script>
<script src="images/emoji.js"></script>

View File

@ -0,0 +1,196 @@
/**
* Semicircle extension for L.Circle.
* Jan Pieter Waagmeester <jieter@jieter.nl>
*
* This version is tested with leaflet 1.0.2
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['leaflet'], factory);
} else if (typeof module !== 'undefined' && typeof require !== 'undefined') {
// Node/CommonJS
module.exports = factory(require('leaflet'));
} else {
// Browser globals
if (typeof window.L === 'undefined') {
throw 'Leaflet must be loaded first';
}
factory(window.L);
}
})(function (L) {
var DEG_TO_RAD = Math.PI / 180;
// make sure 0 degrees is up (North) and convert to radians.
function fixAngle (angle) {
return (angle - 90) * DEG_TO_RAD;
}
// rotate point [x + r, y+r] around [x, y] by `angle` radians.
function rotated (p, angle, r) {
return p.add(
L.point(Math.cos(angle), Math.sin(angle)).multiplyBy(r)
);
}
L.Point.prototype.rotated = function (angle, r) {
return rotated(this, angle, r);
};
var semicircle = {
options: {
startAngle: 0,
stopAngle: 359.9999
},
startAngle: function () {
if (this.options.startAngle < this.options.stopAngle) {
return fixAngle(this.options.startAngle);
} else {
return fixAngle(this.options.stopAngle);
}
},
stopAngle: function () {
if (this.options.startAngle < this.options.stopAngle) {
return fixAngle(this.options.stopAngle);
} else {
return fixAngle(this.options.startAngle);
}
},
setStartAngle: function (angle) {
this.options.startAngle = angle;
return this.redraw();
},
setStopAngle: function (angle) {
this.options.stopAngle = angle;
return this.redraw();
},
setDirection: function (direction, degrees) {
if (degrees === undefined) {
degrees = 10;
}
this.options.startAngle = direction - (degrees / 2);
this.options.stopAngle = direction + (degrees / 2);
return this.redraw();
},
getDirection: function () {
return this.stopAngle() - (this.stopAngle() - this.startAngle()) / 2;
},
isSemicircle: function () {
var startAngle = this.options.startAngle,
stopAngle = this.options.stopAngle;
return (
!(startAngle === 0 && stopAngle > 359) &&
!(startAngle === stopAngle)
);
},
_containsPoint: function (p) {
function normalize (angle) {
while (angle <= -Math.PI) {
angle += 2.0 * Math.PI;
}
while (angle > Math.PI) {
angle -= 2.0 * Math.PI;
}
return angle;
}
var angle = Math.atan2(p.y - this._point.y, p.x - this._point.x);
var nStart = normalize(this.startAngle());
var nStop = normalize(this.stopAngle());
if (nStop <= nStart) {
nStop += 2.0 * Math.PI;
}
if (angle <= nStart) {
angle += 2.0 * Math.PI;
}
return (
nStart < angle && angle <= nStop &&
p.distanceTo(this._point) <= this._radius + this._clickTolerance()
);
}
};
L.SemiCircle = L.Circle.extend(semicircle);
L.SemiCircleMarker = L.CircleMarker.extend(semicircle);
L.semiCircle = function (latlng, options) {
return new L.SemiCircle(latlng, options);
};
L.semiCircleMarker = function (latlng, options) {
return new L.SemiCircleMarker(latlng, options);
};
var _updateCircleSVG = L.SVG.prototype._updateCircle;
var _updateCircleCanvas = L.Canvas.prototype._updateCircle;
L.SVG.include({
_updateCircle: function (layer) {
// If we want a circle, we use the original function
if (!(layer instanceof L.SemiCircle || layer instanceof L.SemiCircleMarker) ||
!layer.isSemicircle()) {
return _updateCircleSVG.call(this, layer);
}
if (layer._empty()) {
return this._setPath(layer, 'M0 0');
}
var p = layer._map.latLngToLayerPoint(layer._latlng),
r = layer._radius,
r2 = Math.round(layer._radiusY || r),
start = p.rotated(layer.startAngle(), r),
end = p.rotated(layer.stopAngle(), r);
var largeArc = (layer.options.stopAngle - layer.options.startAngle >= 180) ? '1' : '0';
var d = 'M' + p.x + ',' + p.y +
// line to first start point
'L' + start.x + ',' + start.y +
'A ' + r + ',' + r2 + ',0,' + largeArc + ',1,' + end.x + ',' + end.y +
' z';
this._setPath(layer, d);
}
});
L.Canvas.include({
_updateCircle: function (layer) {
// If we want a circle, we use the original function
if (!(layer instanceof L.SemiCircle || layer instanceof L.SemiCircleMarker) ||
!layer.isSemicircle()) {
return _updateCircleCanvas.call(this, layer);
}
if (!this._drawing || layer._empty()) { return; }
var p = layer._point,
ctx = this._ctx,
r = layer._radius,
s = (layer._radiusY || r) / r,
start = p.rotated(layer.startAngle(), r);
if (s !== 1) {
ctx.save();
ctx.scale(1, s);
}
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(start.x, start.y);
ctx.arc(p.x, p.y, r, layer.startAngle(), layer.stopAngle());
ctx.lineTo(p.x, p.y);
if (s !== 1) {
ctx.restore();
}
this._fillStroke(ctx, layer);
}
});
});

View File

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
var startpos = [51.03, -1.379]; // Start location - somewhere in UK :-)
var startzoom = 10;
@ -147,6 +148,23 @@ var yellowButton = L.easyButton('fa-square wm-yellow', function(btn) { console.l
var blackButton = L.easyButton('fa-square wm-black', function(btn) { console.log("BLACK",btn); })
var colorControl = L.easyBar([redButton,blueButton,greenButton,yellowButton,blackButton]);
function onLocationFound(e) {
var radius = e.accuracy;
//L.marker(e.latlng).addTo(map).bindPopup("You are within " + radius + " meters from this point").openPopup();
L.circle(e.latlng, radius, {color:"cyan", weight:4, opacity:0.8, fill:false, clickable:false}).addTo(map);
if (e.hasOwnProperty("heading")) {
var lengthAsDegrees = e.speed * 60 / 110540;
var ya = e.latlng.lat + Math.sin((90-e.heading)/180*Math.PI)*lengthAsDegrees*Math.cos(e.latlng.lng/180*Math.PI);
var xa = e.latlng.lng + Math.cos((90-e.heading)/180*Math.PI)*lengthAsDegrees;
var lla = new L.LatLng(ya,xa);
L.polygon([ e.latlng, lla ], {color:"cyan", weight:3, opacity:0.8, clickable:false}).addTo(map);
}
ws.send(JSON.stringify({action:"point", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5), point:"self", bearing:e.heading, speed:(e.speed*3.6 || undefined)}));
}
function onLocationError(e) { console.log(e.message); }
// Move some bits around if in an iframe
if (window.self !== window.top) {
console.log("IN an iframe");
@ -174,20 +192,7 @@ else {
L.easyButton( 'fa-crosshairs fa-lg', function() {
map.locate({setView:true, maxZoom:16});
}, "Locate me").addTo(map);
function onLocationFound(e) {
var radius = e.accuracy;
//L.marker(e.latlng).addTo(map).bindPopup("You are within " + radius + " meters from this point").openPopup();
L.circle(e.latlng, radius, {color:"cyan", weight:4, opacity:0.8, fill:false, clickable:false}).addTo(map);
if (e.hasOwnProperty("heading")) {
var lengthAsDegrees = e.speed * 60 / 110540;
var ya = e.latlng.lat + Math.sin((90-e.heading)/180*Math.PI)*lengthAsDegrees*Math.cos(e.latlng.lng/180*Math.PI);
var xa = e.latlng.lng + Math.cos((90-e.heading)/180*Math.PI)*lengthAsDegrees;
var lla = new L.LatLng(ya,xa);
L.polygon([ e.latlng, lla ], {color:"cyan", weight:3, opacity:0.8, clickable:false}).addTo(map);
}
ws.send(JSON.stringify({action:"point", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5), point:"self", bearing:e.heading, speed:(e.speed*3.6 || undefined)}));
}
function onLocationError(e) { console.log(e.message); }
map.on('locationfound', onLocationFound);
map.on('locationerror', onLocationError);
@ -512,8 +517,17 @@ function showMapCurrentZoom() {
polygons[key].setStyle({opacity:0});
}
}
try {
if (polygons[key].hasOwnProperty("_layers")) {
polygons[key].eachLayer(function(layer) { layer.redraw(); });
}
else {
polygons[key].redraw();
}
} catch(e) {
console.log(key,polygons[key],e)
}
}
}
},750);
}
@ -911,6 +925,26 @@ var editPoly = function(pname) {
editHandler.enable();
}
var rangerings = function(latlng, options) {
options = L.extend({
ranges: [250,500,750,1000],
pan: 0,
fov: 60,
color: '#910000'
}, options);
var rings = L.featureGroup();
if (typeof options.ranges === "number") { options.ranges = [ options.ranges ]; }
for (var i = 0; i < options.ranges.length; i++) {
L.semiCircle(latlng, {
radius: options.ranges[i],
fill: false,
color: options.color,
weight: 1
}).setDirection(options.pan, options.fov).addTo(rings);
}
return rings;
}
// the MAIN add something to map function
function setMarker(data) {
var rightmenu = function(m) {
@ -1019,6 +1053,7 @@ function setMarker(data) {
catch(e) { console.log("OOPS"); }
}
}
if (typeof polygons[data.name] != "undefined") { layers[lay].removeLayer(polygons[data.name]); }
if (data.hasOwnProperty("line") && Array.isArray(data.line)) {
@ -1052,19 +1087,23 @@ function setMarker(data) {
polygons[data.name] = polycirc;
}
}
else if (data.hasOwnProperty("arc")) {
if (data.hasOwnProperty("lat") && data.hasOwnProperty("lon")) {
polygons[data.name] = rangerings(new L.LatLng((data.lat*1), (data.lon*1)), data.arc);
}
}
if (polygons[data.name] !== undefined) {
polygons[data.name].lay = lay;
if (opt.clickable === true) {
var words = "<b>"+data.name+"</b>";
if (data.popup) { var words = words + "<br/>" + data.popup; }
if (data.popup) { words = words + "<br/>" + data.popup; }
polygons[data.name].bindPopup(words, {autoClose:false, closeButton:true, closeOnClick:false, minWidth:200});
}
//polygons[data.name] = rightmenu(polygons[data.name]); // DCJ Investigate
layers[lay].addLayer(polygons[data.name]);
}
if (typeof data.coordinates == "object") { ll = new L.LatLng(data.coordinates[1],data.coordinates[0]); }
else if (data.hasOwnProperty("position") && data.position.hasOwnProperty("lat") && data.position.hasOwnProperty("lon")) {
data.lat = data.position.lat*1;
@ -1444,6 +1483,7 @@ function setMarker(data) {
var llc = data.lineColor || data.color;
delete data.lat;
delete data.lon;
if (data.arc) { delete data.arc; }
if (data.layer) { delete data.layer; }
if (data.lineColor) { delete data.lineColor; }
if (data.color) { delete data.color; }
@ -1481,7 +1521,9 @@ function setMarker(data) {
if (data.bearing != null) { // if there is a heading
if (data.speed != null) { data.length = parseFloat(data.speed || "0") * 50; } // and a speed
if (data.length != null) {
if (polygons[data.name] != null) { map.removeLayer(polygons[data.name]); }
if (polygons[data.name] != null && !polygons[data.name].hasOwnProperty("_layers")) {
map.removeLayer(polygons[data.name]);
}
var x = ll.lng * 1; // X coordinate
var y = ll.lat * 1; // Y coordinate
var ll1 = ll;
@ -1490,16 +1532,16 @@ function setMarker(data) {
var polygon = null;
if (data.accuracy != null) {
data.accuracy = Number(data.accuracy);
var y2 = y + Math.sin((90-angle+data.accuracy)/180*Math.PI)*lengthAsDegrees*Math.cos(y/180*Math.PI);
var x2 = x + Math.cos((90-angle+data.accuracy)/180*Math.PI)*lengthAsDegrees;
var y2 = y + Math.sin((90-angle+data.accuracy)/180*Math.PI)*lengthAsDegrees;
var x2 = x + Math.cos((90-angle+data.accuracy)/180*Math.PI)*lengthAsDegrees/Math.cos(y/180*Math.PI);
var ll2 = new L.LatLng(y2,x2);
var y3 = y + Math.sin((90-angle-data.accuracy)/180*Math.PI)*lengthAsDegrees*Math.cos(y/180*Math.PI);
var x3 = x + Math.cos((90-angle-data.accuracy)/180*Math.PI)*lengthAsDegrees;
var y3 = y + Math.sin((90-angle-data.accuracy)/180*Math.PI)*lengthAsDegrees;
var x3 = x + Math.cos((90-angle-data.accuracy)/180*Math.PI)*lengthAsDegrees/Math.cos(y/180*Math.PI);
var ll3 = new L.LatLng(y3,x3);
polygon = L.polygon([ ll1, ll2, ll3 ], {weight:2, color:llc||'#f30', fillOpacity:0.06, clickable:false});
} else {
var ya = y + Math.sin((90-angle)/180*Math.PI)*lengthAsDegrees*Math.cos(y/180*Math.PI);
var xa = x + Math.cos((90-angle)/180*Math.PI)*lengthAsDegrees;
var ya = y + Math.sin((90-angle)/180*Math.PI)*lengthAsDegrees;
var xa = x + Math.cos((90-angle)/180*Math.PI)*lengthAsDegrees/Math.cos(y/180*Math.PI);
var lla = new L.LatLng(ya,xa);
polygon = L.polygon([ ll1, lla ], {weight:2, color:llc||'#f30', clickable:false});
}
@ -1509,7 +1551,12 @@ function setMarker(data) {
polygon.setStyle({opacity:0});
}
}
if (polygons[data.name] != null && polygons[data.name].hasOwnProperty("_layers")) {
polygons[data.name].addLayer(polygon);
}
else {
polygons[data.name] = polygon;
}
polygons[data.name].lay = lay;
layers[lay].addLayer(polygon);
}

View File

@ -41,6 +41,7 @@
<script src="leaflet/leaflet-omnivore.min.js"></script>
<script src="leaflet/Leaflet.Coordinates.js"></script>
<script src="leaflet/leaflet.latlng-graticule.js"></script>
<script src="leaflet/Semicircle.js"></script>
<script src="leaflet/dialog-polyfill.js"></script>
<script src="images/emoji.js"></script>