leaflet-geotiff
Version:
A LeafletJS plugin for displaying geoTIFF raster data.
309 lines (267 loc) • 12.5 kB
JavaScript
// Ideas from:
// https://github.com/ScanEx/Leaflet.imageTransform/blob/master/src/L.ImageTransform.js
// https://github.com/BenjaminVadant/leaflet-ugeojson
// Depends on:
// https://github.com/constantinius/geotiff.js
// Note this will only work with ESPG:4326 tiffs
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
var L = require('leaflet');
var GeoTIFF = require('geotiff');
}
L.LeafletGeotiff = L.ImageOverlay.extend({
options: {
arrowSize: 20,
band: 0,
image: 0,
opacity: 1,
renderer: null
},
initialize: function (url, options) {
if(typeof(GeoTIFF) === 'undefined'){
throw new Error("GeoTIFF not defined");
};
this._url = url;
this.raster = {};
L.Util.setOptions(this, options);
if (this.options.bounds) {
this._rasterBounds = L.latLngBounds(options.bounds);
}
if (this.options.renderer) {
this.options.renderer.setParent(this);
}
this._getData();
},
setURL: function(newURL) {
this._url = newURL;
this._getData();
},
onAdd: function (map) {
this._map = map;
if (!this._image) {
this._initImage();
}
map._panes.overlayPane.appendChild(this._image);
map.on('moveend', this._reset, this);
if (map.options.zoomAnimation && L.Browser.any3d) {
map.on('zoomanim', this._animateZoom, this);
}
this._reset();
},
onRemove: function (map) {
map.getPanes().overlayPane.removeChild(this._image);
map.off('moveend', this._reset, this);
if (map.options.zoomAnimation) {
map.off('zoomanim', this._animateZoom, this);
}
},
_getData: function() {
var self = this;
var request = new XMLHttpRequest();
request.onload = function() {
if (this.status >= 200 && this.status < 400) {
self._parseTIFF(this.response);
} //TODO else handle error
};
request.open("GET", this._url, true);
request.responseType = "arraybuffer";
request.send();
},
_parseTIFF: function (arrayBuffer) {
this.tiff = GeoTIFF.parse(arrayBuffer);
this.setBand(this.options.band);
if (!this.options.bounds) {
var image = this.tiff.getImage(this.options.image);
var meta = image.getFileDirectory();
var x_min = meta.ModelTiepoint[3];
var x_max = x_min + meta.ModelPixelScale[0]*meta.ImageWidth;
var y_min = meta.ModelTiepoint[4];
var y_max = y_min - meta.ModelPixelScale[1]*meta.ImageLength;
this._rasterBounds = L.latLngBounds([[y_min,x_min],[y_max,x_max]]);
this._reset();
}
},
setBand: function (band) {
this.options.band = band;
var image = this.tiff.getImage(this.options.image);
this.raster.data = image.readRasters({samples: [band]})[0];
this.raster.width = image.getWidth();
this.raster.height = image.getHeight();
this._reset()
},
getRasterArray: function () {
return this.raster.data;
},
getRasterCols: function () {
return this.raster.width;
},
getRasterRows: function () {
return this.raster.height;
},
getValueAtLatLng: function (lat, lng) {
try {
var x = Math.floor(this.raster.width*(lng - this._rasterBounds._southWest.lng)/(this._rasterBounds._northEast.lng - this._rasterBounds._southWest.lng));
var y = this.raster.height-Math.ceil(this.raster.height*(lat - this._rasterBounds._southWest.lat)/(this._rasterBounds._northEast.lat - this._rasterBounds._southWest.lat));
var i = y*this.raster.width+x;
return this.raster.data[i];
}
catch(err) {
return undefined;
}
},
_animateZoom: function (e) {
if (L.version >= "1.0") {
var scale = this._map.getZoomScale(e.zoom),
offset = this._map._latLngBoundsToNewLayerBounds(this._map.getBounds(), e.zoom, e.center).min;
L.DomUtil.setTransform(this._image, offset, scale);
} else {
var scale = this._map.getZoomScale(e.zoom),
nw = this._map.getBounds().getNorthWest(),
se = this._map.getBounds().getSouthEast(),
topLeft = this._map._latLngToNewLayerPoint(nw, e.zoom, e.center),
size = this._map._latLngToNewLayerPoint(se, e.zoom, e.center)._subtract(topLeft);
this._image.style[L.DomUtil.TRANSFORM] =
L.DomUtil.getTranslateString(topLeft) + ' scale(' + scale + ') ';
}
},
_reset: function () {
if (this.hasOwnProperty('_map')) {
if (this._rasterBounds) {
topLeft = this._map.latLngToLayerPoint(this._map.getBounds().getNorthWest()),
size = this._map.latLngToLayerPoint(this._map.getBounds().getSouthEast())._subtract(topLeft);
L.DomUtil.setPosition(this._image, topLeft);
this._image.style.width = size.x + 'px';
this._image.style.height = size.y + 'px';
this._drawImage();
};
};
},
setClip: function(clipLatLngs) {
this.options.clip = clipLatLngs;
this._reset();
},
_clipMaskToPixelPoints: function() {
if (this.options.clip) {
var topLeft = this._map.latLngToLayerPoint(this._map.getBounds().getNorthWest());
var pixelClipPoints = [];
for (var p = 0; p < this.options.clip.length; p++) {
var mercPoint = this._map.latLngToLayerPoint(this.options.clip[p]),
pixel = L.point(mercPoint.x - topLeft.x, mercPoint.y - topLeft.y);
pixelClipPoints.push(pixel);
}
this._pixelClipPoints = pixelClipPoints;
} else {
this._pixelClipPoints = undefined;
}
},
_drawImage: function () {
if (this.raster.hasOwnProperty('data')) {
var args = {};
topLeft = this._map.latLngToLayerPoint(this._map.getBounds().getNorthWest()),
size = this._map.latLngToLayerPoint(this._map.getBounds().getSouthEast())._subtract(topLeft);
args.rasterPixelBounds = L.bounds(this._map.latLngToContainerPoint(this._rasterBounds.getNorthWest()),this._map.latLngToContainerPoint(this._rasterBounds.getSouthEast()));
args.xStart = (args.rasterPixelBounds.min.x>0 ? args.rasterPixelBounds.min.x : 0);
args.xFinish = (args.rasterPixelBounds.max.x<size.x ? args.rasterPixelBounds.max.x : size.x);
args.yStart = (args.rasterPixelBounds.min.y>0 ? args.rasterPixelBounds.min.y : 0);
args.yFinish = (args.rasterPixelBounds.max.y<size.y ? args.rasterPixelBounds.max.y : size.y);
args.plotWidth = args.xFinish-args.xStart;
args.plotHeight = args.yFinish-args.yStart;
if ((args.plotWidth<=0) || (args.plotHeight<=0)) {
console.log(this.options.name,' is off screen.');
var plotCanvas = document.createElement("canvas");
plotCanvas.width = size.x;
plotCanvas.height = size.y;
var ctx = plotCanvas.getContext("2d");
ctx.globalAlpha = this.options.opacity;
ctx.clearRect(0, 0, plotCanvas.width, plotCanvas.height);
this._image.src = plotCanvas.toDataURL();
return;
}
args.xOrigin = this._map.getPixelBounds().min.x+args.xStart;
args.yOrigin = this._map.getPixelBounds().min.y+args.yStart;
args.lngSpan = (this._rasterBounds._northEast.lng - this._rasterBounds._southWest.lng)/this.raster.width;
args.latSpan = (this._rasterBounds._northEast.lat - this._rasterBounds._southWest.lat)/this.raster.height;
//Draw image data to canvas and pass to image element
var plotCanvas = document.createElement("canvas");
plotCanvas.width = size.x;
plotCanvas.height = size.y;
var ctx = plotCanvas.getContext("2d");
ctx.globalAlpha = this.options.opacity;
ctx.clearRect(0, 0, plotCanvas.width, plotCanvas.height);
this.options.renderer.render(this.raster, plotCanvas, ctx, args);
//Draw clipping polygon
if (this.options.clip) {
this._clipMaskToPixelPoints();
ctx.globalCompositeOperation = 'destination-out';
ctx.rect(args.xStart-10,args.yStart-10,args.plotWidth+20,args.plotHeight+20);
//Draw vertices in reverse order
for (var i = this._pixelClipPoints.length-1; i >= 0; i--) {
var pix = this._pixelClipPoints[i];
ctx['lineTo'](pix.x, pix.y);
}
ctx.closePath();
ctx.fill();
}
this._image.src = String(plotCanvas.toDataURL());
}
},
transform: function(rasterImageData, args) {
//Create image data and Uint32 views of data to speed up copying
var imageData;
try {
imageData = new ImageData(args.plotWidth, args.plotHeight);
} catch(e) { // For IE
imageData = document.createElement('canvas').getContext('2d').createImageData(args.plotWidth, args.plotHeight);
}
var outData = imageData.data;
var outPixelsU32 = new Uint32Array(outData.buffer);
var inData = rasterImageData.data;
var inPixelsU32 = new Uint32Array(inData.buffer);
var zoom = this._map.getZoom();
var scale = this._map.options.crs.scale(zoom);
var d = 57.29577951308232; //L.LatLng.RAD_TO_DEG;
var transformationA = this._map.options.crs.transformation._a;
var transformationB = this._map.options.crs.transformation._b;
var transformationC = this._map.options.crs.transformation._c;
var transformationD = this._map.options.crs.transformation._d;
if (L.version >= "1.0") {
transformationA = transformationA*this._map.options.crs.projection.R;
transformationC = transformationC*this._map.options.crs.projection.R;
}
for (var y=0;y<args.plotHeight;y++) {
var yUntransformed = ((args.yOrigin+y) / scale - transformationD) / transformationC;
var currentLat = (2 * Math.atan(Math.exp(yUntransformed)) - (Math.PI / 2)) * d;
var rasterY = this.raster.height-Math.ceil((currentLat - this._rasterBounds._southWest.lat)/args.latSpan);
for (var x=0;x<args.plotWidth;x++) {
//Location to draw to
var index = (y*args.plotWidth+x);
//Calculate lat-lng of (x,y)
//This code is based on leaflet code, unpacked to run as fast as possible
//Used to deal with TIF being EPSG:4326 (lat,lon) and map being EPSG:3857 (m E,m N)
var xUntransformed = ((args.xOrigin+x) / scale - transformationB) / transformationA;
var currentLng = xUntransformed * d;
var rasterX = Math.floor((currentLng - this._rasterBounds._southWest.lng)/args.lngSpan);
var rasterIndex = (rasterY*this.raster.width+rasterX);
//Copy pixel value
outPixelsU32[index] = inPixelsU32[rasterIndex];
}
}
return imageData;
}
});
L.LeafletGeotiffRenderer = L.Class.extend({
initialize: function(options) {
L.setOptions(this, options);
},
setParent: function(parent) {
this.parent = parent;
},
render: function(raster, canvas, ctx, args) {
throw new Error('Abstract class');
}
});
L.leafletGeotiff = function (url, options) {
return new L.LeafletGeotiff(url, options);
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = L;
}