Fix leader length, add transparent pixels option

This commit is contained in:
Dave Conway-Jones 2021-11-05 13:02:10 +00:00
parent 4fb075d28c
commit 0a04418eee
No known key found for this signature in database
GPG Key ID: 88BA2B8A411BE9FF
7 changed files with 164 additions and 3 deletions

View File

@ -1,5 +1,6 @@
### Change Log for Node-RED Worldmap ### Change Log for Node-RED Worldmap
- v2.21.4 - fix speed leader length. Add transparentPixels option.
- v2.21.3 - Add zoom to bounds action. Adjust map layers max zoom levels. - v2.21.3 - Add zoom to bounds action. Adjust map layers max zoom levels.
- v2.21.2 - Expand ship nav to ship navigation. - v2.21.2 - Expand ship nav to ship navigation.
- v2.21.1 - Fix ui check callback to not use . - v2.21.1 - Fix ui check callback to not use .

View File

@ -11,6 +11,7 @@ map web page for plotting "things" on.
### Updates ### Updates
- v2.21.4 - fix speed leader length. Add transparentPixels option..
- v2.21.3 - Add zoom to bounds action. Adjust map layers max zoom levels. - v2.21.3 - Add zoom to bounds action. Adjust map layers max zoom levels.
- v2.21.2 - Expand ship nav to ship navigation. - v2.21.2 - Expand ship nav to ship navigation.
- v2.21.1 - Fix ui check callback to not use . - v2.21.1 - Fix ui check callback to not use .

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red-contrib-web-worldmap", "name": "node-red-contrib-web-worldmap",
"version": "2.21.3", "version": "2.21.4",
"description": "A Node-RED node to provide a web page of a world map for plotting things on.", "description": "A Node-RED node to provide a web page of a world map for plotting things on.",
"dependencies": { "dependencies": {
"@turf/bezier-spline": "~6.5.0", "@turf/bezier-spline": "~6.5.0",

View File

@ -72,6 +72,7 @@
<script src="leaflet/leaflet.latlng-graticule.js"></script> <script src="leaflet/leaflet.latlng-graticule.js"></script>
<script src="leaflet/VectorTileLayer.umd.min.js"></script> <script src="leaflet/VectorTileLayer.umd.min.js"></script>
<script src="leaflet/Semicircle.js"></script> <script src="leaflet/Semicircle.js"></script>
<script src="leaflet/L.TileLayer.PixelFilter.js"></script>
<script src="leaflet/dialog-polyfill.js"></script> <script src="leaflet/dialog-polyfill.js"></script>
<script src="images/emoji.js"></script> <script src="images/emoji.js"></script>

View File

@ -0,0 +1,152 @@
/*
* L.TileLayer.PixelFilter
* https://github.com/greeninfo/L.TileLayer.PixelFilter
* http://greeninfo-network.github.io/L.TileLayer.PixelFilter/
*/
L.tileLayerPixelFilter = function (url, options) {
return new L.TileLayer.PixelFilter(url, options);
}
L.TileLayer.PixelFilter = L.TileLayer.extend({
// the constructor saves settings and throws a fit if settings are bad, as typical
// then adds the all-important 'tileload' event handler which basically "detects" an unmodified tile and performs the pxiel-swap
initialize: function (url, options) {
options = L.extend({}, L.TileLayer.prototype.options, {
matchRGBA: null,
missRGBA: null,
pixelCodes: [],
crossOrigin: 'Anonymous', // per issue 15, this is how you do it in Leaflet 1.x
}, options);
L.TileLayer.prototype.initialize.call(this, url, options);
L.setOptions(this, options);
// go ahead and save our settings
this.setMatchRGBA(this.options.matchRGBA);
this.setMissRGBA(this.options.missRGBA);
this.setPixelCodes(this.options.pixelCodes);
// and add our tile-load event hook which triggers us to do the pixel-swap
this.on('tileload', function (event) {
this.applyFiltersToTile(event.tile);
});
},
// settings setters
setMatchRGBA: function (rgba) {
// save the setting
if (rgba !== null && (typeof rgba !== 'object' || typeof rgba.length !== 'number' || rgba.length !== 4) ) throw "L.TileLayer.PixelSwap expected matchRGBA to be RGBA [r,g,b,a] array or else null";
this.options.matchRGBA = rgba;
// force a redraw, which means new tiles, which mean new tileload events; the circle of life
this.redraw(true);
},
setMissRGBA: function (rgba) {
// save the setting
if (rgba !== null && (typeof rgba !== 'object' || typeof rgba.length !== 'number' || rgba.length !== 4) ) throw "L.TileLayer.PixelSwap expected missRGBA to be RGBA [r,g,b,a] array or else null";
this.options.missRGBA = rgba;
// force a redraw, which means new tiles, which mean new tileload events; the circle of life
this.redraw(true);
},
setPixelCodes: function (pixelcodes) {
// save the setting
if (typeof pixelcodes !== 'object' || typeof pixelcodes.length !== 'number') throw "L.TileLayer.PixelSwap expected pixelCodes to be a list of triplets: [ [r,g,b], [r,g,b], ... ]";
this.options.pixelCodes = pixelcodes;
// force a redraw, which means new tiles, which mean new tileload events; the circle of life
this.redraw(true);
},
// extend the _createTile function to add the .crossOrigin attribute, since loading tiles from a separate service is a pretty common need
// and the Canvas is paranoid about cross-domain image data. see issue #5
// this is really only for Leaflet 0.7; as of 1.0 L.TileLayer has a crossOrigin setting which we define as a layer option
_createTile: function () {
var tile = L.TileLayer.prototype._createTile.call(this);
tile.crossOrigin = "Anonymous";
return tile;
},
// the heavy lifting to do the pixel-swapping
// called upon 'tileload' and passed the IMG element
// tip: when the tile is saved back to the IMG element that counts as a tileload event too! thus an infinite loop, as wel as comparing the pixelCodes against already-replaced pixels!
// so, we tag the already-swapped tiles so we know when to quit
// if the layer is redrawn, it's a new IMG element and that means it would not yet be tagged
applyFiltersToTile: function (imgelement) {
// already processed, see note above
if (imgelement.getAttribute('data-PixelFilterDone')) return;
// copy the image data onto a canvas for manipulation
var width = imgelement.width;
var height = imgelement.height;
var canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
var context = canvas.getContext("2d");
context.drawImage(imgelement, 0, 0);
// create our target imagedata
var output = context.createImageData(width, height);
// extract out our RGBA trios into separate numbers, so we don't have to use rgba[i] a zillion times
var matchRGBA = this.options.matchRGBA, missRGBA = this.options.missRGBA;
if (matchRGBA !== null) {
var match_r = matchRGBA[0], match_g = matchRGBA[1], match_b = matchRGBA[2], match_a = matchRGBA[3];
}
if (missRGBA !== null) {
var miss_r = missRGBA[0], miss_g = missRGBA[1], miss_b = missRGBA[2], miss_a = missRGBA[3];
}
// go over our pixel-code list and generate the list of integers that we'll use for RGB matching
// 1000000*R + 1000*G + B = 123123123 which is an integer, and finding an integer inside an array is a lot faster than finding an array inside an array
var pixelcodes = [];
for (var i=0, l=this.options.pixelCodes.length; i<l; i++) {
var value = 1000000 * this.options.pixelCodes[i][0] + 1000 * this.options.pixelCodes[i][1] + this.options.pixelCodes[i][2];
pixelcodes.push(value);
}
// iterate over the pixels (each one is 4 bytes, RGBA)
// and see if they are on our list (recall the "addition" thing so we're comparing integers in an array for performance)
// per issue #5 catch a failure here, which is likely a cross-domain problem
try {
var pixels = context.getImageData(0, 0, width, height).data;
} catch(e) {
throw "L.TileLayer.PixelFilter getImageData() failed. Likely a cross-domain issue?";
}
for(var i = 0, n = pixels.length; i < n; i += 4) {
var r = pixels[i ];
var g = pixels[i+1];
var b = pixels[i+2];
var a = pixels[i+3];
// bail condition: if the alpha is 0 then it's already transparent, likely nodata, and we should skip it
if (a == 0) {
output.data[i ] = 255;
output.data[i+1] = 255;
output.data[i+2] = 255;
output.data[i+3] = 0;
continue;
}
// default to matching, so that if we are not in fact filtering by code it's an automatic hit
// number matching trick: 1000000*R + 1000*G + 1*B = 123,123,123 a simple number that either is or isn't on the list
var match = true;
if (pixelcodes.length) {
var sum = 1000000 * r + 1000 * g + b;
if (-1 === pixelcodes.indexOf(sum)) match = false;
}
// did it match? either way we push a R, a G, and a B onto the image blob
// if the target RGBA is a null, then we push exactly the same RGBA as we found in the source pixel
output.data[i ] = match ? (matchRGBA===null ? r : match_r) : (missRGBA===null ? r : miss_r);
output.data[i+1] = match ? (matchRGBA===null ? g : match_g) : (missRGBA===null ? g : miss_g);
output.data[i+2] = match ? (matchRGBA===null ? b : match_b) : (missRGBA===null ? b : miss_b);
output.data[i+3] = match ? (matchRGBA===null ? a : match_a) : (missRGBA===null ? a : miss_a);
}
// write the image back to the canvas, and assign its base64 back into the on-screen tile to visualize the change
// tag the tile as having already been updated, so we don't process a 'load' event again and re-process a tile that surely won't match any target RGB codes, in an infinite loop!
context.putImageData(output, 0, 0);
imgelement.setAttribute('data-PixelFilterDone', true);
imgelement.src = canvas.toDataURL();
}
});

View File

@ -1983,7 +1983,7 @@ function setMarker(data) {
else if (data.heading !== undefined) { track = data.heading; } else if (data.heading !== undefined) { track = data.heading; }
else if (data.bearing !== undefined) { track = data.bearing; } else if (data.bearing !== undefined) { track = data.bearing; }
if (track != undefined) { // if there is a heading if (track != undefined) { // if there is a heading
if (data.speed != null && !data.length) { // and a speed - lets convert to a leader length if (data.speed != null && data.length === undefined) { // and a speed - lets convert to a leader length
data.length = parseFloat(data.speed || "0") * 60; data.length = parseFloat(data.speed || "0") * 60;
var re1 = new RegExp('kn|knot|kt','i'); var re1 = new RegExp('kn|knot|kt','i');
var re2 = new RegExp('kph|kmh','i'); var re2 = new RegExp('kph|kmh','i');
@ -1992,7 +1992,7 @@ function setMarker(data) {
else if ( re2.test(""+data.speed) ) { data.length = data.length * 0.44704; } else if ( re2.test(""+data.speed) ) { data.length = data.length * 0.44704; }
else if ( re3.test(""+data.speed) ) { data.length = data.length * 0.277778; } else if ( re3.test(""+data.speed) ) { data.length = data.length * 0.277778; }
} }
if (data.length != null) { if (data.length !== undefined) {
if (polygons[data.name] != null && !polygons[data.name].hasOwnProperty("_layers")) { if (polygons[data.name] != null && !polygons[data.name].hasOwnProperty("_layers")) {
map.removeLayer(polygons[data.name]); map.removeLayer(polygons[data.name]);
} }
@ -2452,6 +2452,11 @@ function doCommand(cmd) {
else if (cmd.map.url.slice(-4).toLowerCase() === ".pbf") { else if (cmd.map.url.slice(-4).toLowerCase() === ".pbf") {
overlays[cmd.map.overlay] = VectorTileLayer(cmd.map.url, cmd.map.opt); overlays[cmd.map.overlay] = VectorTileLayer(cmd.map.url, cmd.map.opt);
} }
else if (cmd.map.hasOwnProperty("transparentPixels")) {
cmd.map.opt.pixelCodes = cmd.map.transparentPixels;
cmd.map.opt.matchRGBA = [ 0,0,0,0 ];
overlays[cmd.map.overlay] = L.tileLayerPixelFilter(cmd.map.url, cmd.map.opt);
}
else { else {
overlays[cmd.map.overlay] = L.tileLayer(cmd.map.url, cmd.map.opt); overlays[cmd.map.overlay] = L.tileLayer(cmd.map.url, cmd.map.opt);
} }

View File

@ -42,6 +42,7 @@
<script src="leaflet/Leaflet.Coordinates.js"></script> <script src="leaflet/Leaflet.Coordinates.js"></script>
<script src="leaflet/leaflet.latlng-graticule.js"></script> <script src="leaflet/leaflet.latlng-graticule.js"></script>
<script src="leaflet/Semicircle.js"></script> <script src="leaflet/Semicircle.js"></script>
<script src="leaflet/L.TileLayer.PixelFilter.js"></script>
<script src="leaflet/dialog-polyfill.js"></script> <script src="leaflet/dialog-polyfill.js"></script>
<script src="images/emoji.js"></script> <script src="images/emoji.js"></script>