/**
# This file part of: VisiOmatic
* @file Support for VisiOmatic layers in Leaflet
* @requires util/VUtil.js
* @requires util/RGB.js
* @requires crs/WCS.js
* @copyright (c) 2014-2024 CNRS/IAP/CFHT/SorbonneU/CEA/AIM/UParisSaclay
* @author Emmanuel Bertin <bertin@cfht.hawaii.edu>
*/
import {
Browser,
DomUtil,
TileLayer,
Util,
latLngBounds
} from 'leaflet';
import {VUtil} from '../util';
import {rgb as rgbin} from '../util';
import {WCS} from '../crs';
export const VTileLayer = TileLayer.extend( /** @lends VTileLayer */ {
options: {
title: null,
setTitleBar: false,
crs: null,
center: null,
fov: null,
minZoom: 0,
maxZoom: null,
maxNativeZoom: 18,
noWrap: true,
brightness: null,
contrast: null,
colorSaturation: null,
gamma: null,
cMap: 'grey',
invertCMap: false,
quality: null,
mixingMode: 'color',
channelColors: [],
channelLabels: [],
channelLabelMatch: '.*',
channelUnits: [],
minMaxValues: [],
defaultChannel: 0,
framerate: null,
sesameURL: 'https://cdsweb.u-strasbg.fr/cgi-bin/nph-sesame',
credentials: null
/*
pane: 'tilePane',
opacity: 1,
attribution: <String>,
zIndex: <Number>,
bounds: <LatLngBounds>
unloadInvisibleTiles: L.Browser.mobile,
updateWhenIdle: Browser.mobile,
updateInterval: 150,
tms: <Boolean>,
zoomReverse: <Number>,
detectRetina: <Number>,
*/
},
/**
Default _server_ rendering parameters (to shorten tile query strings).
* @type {object}
* @property {number} brightness
Default brightness level.
* @property {number} contrast
Default contrast factor.
* @property {number} colorSaturation
Default image color saturation.
* @property {number} gamma
Default display gamma.
* @property {string} cMap
Default colormap.
* @property {boolean} invertCMap
Default colormap inversion switch.
* @property {number[]} minValue
Default lower clipping limits for channels.
* @property {number[]} maxValue
Default upper clipping limits for channels.
* @property {RGB[]} channelColors
Default color mixing matrix.
* @property {number} quality
Default JPEG encoding quality.
* @property {number} framerate
Default animation framerate.
*/
visioDefault: {
brightness: 0.,
contrast: 1.,
colorSaturation: 1.5,
gamma: 2.2,
cMap: 'grey',
invertCMap: false,
minValue: [],
maxValue: [],
channelColors: [
[''],
['#FFFFFF'],
['#00BAFF', '#FFBA00'],
['#0000FF', '#00BA00', '#FF0000'],
['#0000E0', '#00BA88', '#88BA00', '#E00000'],
['#0000CA', '#007BA8', '#00CA00', '#A87B00', '#CA0000'],
['#0000BA', '#00719B', '#009B71', '#719B00', '#9B7100', '#BA0000']
],
quality: 90,
framerate: 1
},
/**
* Create a layer with tiled image data queried from a VisiOmatic server.
* @extends leaflet.TileLayer
* @memberof module:layer/VTileLayer.js
* @constructs
* @param {string} url - URL of the tile server
* @param {object} [options] - Options.
* @param {?string} [options.title=null]
Layer title. Defaults to the image filename with extension removed
followed by the OBJECT name if available.
* @param {boolean} [options.setTitleBar=false]
Update the document / webpage title using the layer title.
* @param {?(leaflet.CRS|WCS)} [options.crs=null]
Coordinate Reference or World Coordinate System: extracted from the data
header if available or raw pixel coordinates otherwise.
* @param {?string} [options.center=null]
World coordinates (either in RA,Dec decimal form or in
``hh:mm:ss.s±dd:mm:ss.s`` sexagesimal format), or any
[Sesame]{@link http://cds.u-strasbg.fr/cgi-bin/Sesame}-compliant identifier
defining the initial centering of the map upon layer initialization.
Sexagesimal coordinates and identifier strings are sent to the
Sesame resolver service for conversion to decimal coordinates. Assume x,y
pixel coordinates if WCS information is missing. Defaults to image center.
* @param {?number} [options.fov=null]
Field of View (FoV) covered by the map upon later initialization, in world
coordinates (degrees, or pixel coordinates if WCS information is missing).
Defaults to the full FoV.
* @param {number} [options.minZoom=0]
Minimum zoom factor.
* @param {?number} options.maxZoom
Maximum zoom factor.
* @param {number} [options.maxNativeZoom=18]
Maximum native zoom factor (including resampling).
* @param {boolean} [options.noWrap=true]
Deactivate layer wrapping.
* @param {number} options.brightness
Brightness level.
* @param {number} options.contrast
Contrast factor.
* @param {number} options.colorSaturation
Color saturation for multi-channel data (0.0: B&W, >1.0: enhance).
* @param {number} options.gamma
Display gamma.
* @param {string} [options.cMap='grey']
Colormap for single channels or channel combinations. Valid colormaps are
``'grey'``, ``'jet'``, ``'cold'`` and ``'hot'``.
* @param {boolean} [options.invertCMap=false]
Invert Colormap or color mix (like a negative).
* @param {number} options.quality
JPEG encoding quality in percent.
* @param {string} [options.mixingMode='color']
Channel mixing mode. Valid modes are ``'mono'`` (single-channel) and
``'color'``.
* @param {RGB[]} [options.channelColors=[]]
RGB contribution of each channel to the mixing matrix. Defaults to
``rgb(0.,0.,1.), rgb(0.,1.,0.), rgb(1.,0.,0.), rgb(0.,0.,0.), ...]``
* @param {string[]} [options.channelLabels=[]]
Channel labels. Defaults to ``['Channel #1', 'Channel #2', ...]``.
* @param {string} [options.channelLabelMatch='.*']
Regular expression matching the labels of channels that are given a color by
default.
* @param {string[]} [options.channelUnits=[]]
Channel units. Defaults to ``['ADU','ADU',...]``.
* @param {number[][]} [options.minMaxValues=[]]
Pairs of lower, higher clipping limits for every channel. Defaults to values
extracted from the data header if available or
``[[0.,255.], [0.,255.], ...]`` otherwise.
* @param {number} [options.channel=0]
Default active channel index in mono-channel mixing mode.
* @param {number} options.framerate
Default animation framerate.
* @param {string} [options.sesameURL='https://cdsweb.u-strasbg.fr/cgi-bin/nph-sesame']
URL of the [Sesame]{@link http://cds.u-strasbg.fr/cgi-bin/Sesame} resolver
service.
* @param {?string} [options.credentials=null]
For future use.
* @returns {VTileLayer} VisiOmatic TileLayer instance.
* @example
* const map = L.map('map'),
* url = '/tiles?FIF=example.fits',
* layer = new VTileLayer(url, {cmap: 'jet'});
* layer.addTo(map);
*/
initialize: function (url, options) {
this.type = 'tilelayer';
this._url = url.replace(/\&.*$/g, '');
options = Util.setOptions(this, options);
// detecting retina displays, adjusting tileSize and zoom levels
if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) {
options.tileSize = Math.floor(options.tileSize / 2);
options.zoomOffset++;
options.minZoom = Math.max(0, options.minZoom);
options.maxZoom--;
}
if (typeof options.subdomains === 'string') {
options.subdomains = options.subdomains.split('');
}
this.tileSize = {x: 256, y: 256};
/**
* VisiOmatic-specific TileLayer properties.
* @type {object}
* @instance
* @property {string} imageName
Image name (e.g., a filename).
* @property {string} objectName
Object name.
* @property {number[][]} imageSize
Image sizes at every resolution.
* @property {object[]} gridSize
Grid sizes at every resolution.
* @property {number} bpp
Image depth in bits per pixel.
* @property {string} mixingMode
Current color mixing mode (``'mono'`` or ``'color'``).
* @property {number} channel
Current image channel index.
* @property {number} nChannel
Number of image channels.
* @property {number} minZoom
Minimum zoom factor (tile resolution).
* @property {number} maxZoom
Maximum zoom factor (tile resolution).
* @property {number} brightness
Current image brightness level.
* @property {number} contrast
Current image contrast factor.
* @property {number} colorSaturation
Current image color saturation.
* @property {number} gamma
Current image display gamma.
* @property {string} cMap
Current color map.
* @property {boolean} invertCMap
Current colormap inversion switch status.
* @property {number[]} backgroundLevel
Background level for every channel.
* @property {number[]} backgroundMAD
Background MAD for every channel.
* @property {number[]} minValue
Current lower clipping limit for every channel.
* @property {number[]} maxValue
Current upper clipping limit for every channel.
* @property {number[][]} mix
Current color mixing matrix.
* @property {RGB[]} rgb
Current color mixing matrix as RGB mixes.
* @property {string[]} channelLabels
Label for every image channel.
* @property {boolean[]} channelFlags
Display activation flag for every channel.
* @property {string[]} channelUnits
Pixel value unit for every image channel.
* @property {number} quality
Current JPEG encoding quality in %.
* @property {number} framerate
Current animation framerate.
*/
this.visio = {
imageName: "",
objectName: "",
imageSize: [[this.tileSize]],
gridSize: [{x: 1, y: 1}],
bpp: 8,
mixingMode: options.mixingMode, // 'mono' or 'color'
channel: 0,
nChannel: 1,
minZoom: options.minZoom,
maxZoom: options.maxZoom,
brightness: options.brightness,
contrast: options.contrast,
colorSaturation: options.colorSaturation,
gamma: options.gamma,
cMap: options.cMap,
invertCMap: options.invertCMap,
backgroundLevel: [0.],
backgroundMAD: [1.],
minValue: [0.],
maxValue: [255.],
mix: [[]],
rgb: [],
channelLabels: [],
channelFlags: [],
channelUnits: [],
quality: options.quality,
framerate: options.framerate
}
this.getMetaData(this._url);
// for https://github.com/Leaflet/Leaflet/issues/137
if (!Browser.android) {
this.on('tileunload', this._onTileRemove);
}
return this;
},
/**
* Get metadata describing the tiled image at the provided URL.
* @async
* @param {string} url - The full tile URL.
* @fires metaload
*/
getMetaData: async function (url) {
const res = await fetch(url + '&INFO', {method: 'GET'});
const meta = await res.json();
if (res.status == 200 && meta['type'] == 'visiomatic') {
const options = this.options,
visio = this.visio,
visioDefault = this.visioDefault,
maxsize = {x: meta.full_size[0], y: meta.full_size[1]};
this.tileSize = {x: meta.tile_size[0], y: meta.tile_size[1]};
options.tileSize = this.tileSize.x;
visio.maxZoom = meta.tile_levels - 1;
if (visio.minZoom > options.minZoom) {
options.minZoom = visio.minZoom;
}
if (!options.maxZoom) {
options.maxZoom = visio.maxZoom + 6;
}
if (options.maxNativeZoom > visio.maxZoom) {
options.maxNativeZoom = visio.maxZoom;
}
// Set grid sizes
for (let z = 0; z <= visio.maxZoom; z++) {
visio.imageSize[z] = {
x: Math.floor(maxsize.x / Math.pow(2, visio.maxZoom - z)),
y: Math.floor(maxsize.y / Math.pow(2, visio.maxZoom - z))
};
visio.gridSize[z] = {
x: Math.ceil(visio.imageSize[z].x / this.tileSize.x),
y: Math.ceil(visio.imageSize[z].y / this.tileSize.y)
};
}
// (Virtual) grid sizes for extra zooming
for (let z = visio.maxZoom; z <= options.maxZoom; z++) {
visio.gridSize[z] = visio.gridSize[visio.maxZoom];
}
// Set pixel bpp
visio.bpp = meta.bits_per_channel;
// Number of channels
nchannel = visio.nChannel = meta.channels;
// Default brightness
if (meta.brightness) {
visioDefault.brightness = meta.brightness;
}
if (!visio.brightness) {
visio.brightness = visioDefault.brightness;
}
// Default contrast
if (meta.contrast) {
visioDefault.contrast = meta.contrast;
}
if (!visio.contrast) {
visio.contrast = visioDefault.contrast;
}
// Default color saturation
if (meta.color_saturation) {
visioDefault.colorSaturation = meta.color_saturation;
}
if (!visio.colorSaturation) {
visio.colorSaturation = visioDefault.colorSaturation;
}
// Default display gamma
if (meta.gamma) {
visioDefault.gamma = meta.gamma;
}
if (!visio.gamma) {
visio.gamma = visioDefault.gamma;
}
// Default compression quality
if (meta.quality) {
visioDefault.quality = meta.quality;
}
if (!visio.quality) {
visio.quality = visioDefault.quality;
}
// Default animation framerate
if (meta.framerate) {
visioDefault.framerate = meta.framerate;
}
if (!visio.framerate) {
visio.framerate = visioDefault.framerate;
}
// Image filename
if (meta.image_name) {
visio.imageName = meta.image_name;
}
// Object name
if (meta.object_name) {
visio.objectName = meta.object_name;
}
// Layer title
this._title = options.title ?
options.title
: (
visio.imageName ? (
visio.objectName ? (
visio.imageName.replace(
/(\.fits)|(\.fit)|(\.fz)/g,
''
) + ' - ' + visio.objectName
) : visio.imageName
) : 'VisiOmatic'
);
// Update Title bar is requested
if (options.setTitleBar) {
document.title = this._title;
}
// Images
images = meta.images;
// Background level and MAD values
for (let c = 0; c < nchannel; c++) {
visio.backgroundLevel[c] = images[0].background_level[c];
visio.backgroundMAD[c] = images[0].background_mad[c];
}
// Min and max pixel values
for (let c = 0; c < nchannel; c++) {
visioDefault.minValue[c] = images[0].min_max[c][0];
visioDefault.maxValue[c] = images[0].min_max[c][1];
}
// Override min and max pixel values based on user provided options
const minmax = options.minMaxValues;
if (minmax.length) {
for (let c = 0; c < nchannel; c++) {
if (minmax[c] !== undefined && minmax[c].length) {
visio.minValue[c] = minmax[c][0];
visio.maxValue[c] = minmax[c][1];
} else {
visio.minValue[c] = visioDefault.minValue[c];
visio.maxValue[c] = visioDefault.maxValue[c];
}
}
} else {
for (let c = 0; c < nchannel; c++) {
visio.minValue[c] = visioDefault.minValue[c];
visio.maxValue[c] = visioDefault.maxValue[c];
}
}
// Default channel
visio.channel = options.defaultChannel;
// Channel labels
const inlabels = options.channelLabels,
ninlabel = inlabels.length,
labels = visio.channelLabels,
inunits = options.channelUnits,
ninunits = inunits.length,
units = visio.channelUnits,
key = VUtil.readFITSKey;
let label = 'Channel';
if (nchannel === 1 && (filter = images[0].header['FILTER'])) {
label = filter;
}
for (let c = 0; c < nchannel; c++) {
if (c < ninlabel) {
labels[c] = inlabels[c];
} else {
labels[c] = nchannel > 1 ? 'Channel #' + (c + 1).toString()
: filter;
}
}
// Channel Units
const imageUnit = (meta.header && (unit=meta.header['BUNIT'])) ?
unit : 'ADU';
// Copy those units that have been provided
for (const c in inunits) {
units[c] = inunits[c];
}
// Fill out units that are not provided with a default string
for (let c = 0; c < nchannel; c++) {
if (!units[c]) {
units[c] = imageUnit;
}
}
// Initialize mixing matrix depending on arguments and the number of channels
const mix = visio.mix,
colors = options.channelColors,
rgb = visio.rgb,
re = new RegExp(options.channelLabelMatch),
channelflags = visio.channelFlags;
let cc = 0,
nchanon = 0;
for (var c = 0; c < nchannel; c++) {
channelflags[c] = re.test(labels[c]);
if (channelflags[c]) {
nchanon++;
}
}
if (nchanon >= visioDefault.channelColors.length) {
nchanon = visioDefault.channelColors.length - 1;
}
if (colors.length) {
// Dispatch input colors
for (const c in colors) {
// Copy RGB triplet
rgb[c] = rgbin(colors[c][0], colors[c][1], colors[c][2]);
mix[c] = [];
this.rgbToMix(c);
}
} else {
// Dispatch default colors
rgb[0] = rgbin(visioDefault.channelColors[3][0]);
rgb[Math.floor(nchannel / 2)] = rgbin(visioDefault.channelColors[3][1]);
rgb[nchannel -1] = rgbin(visioDefault.channelColors[3][2]);
/*
for (const c = 0; c < nchannel; c++) {
if (channelflags[c] && cc < nchanon) {
rgb[c] = rgbin(visioDefault.channelColors[nchanon][cc++]);
// Compute the current row of the mixing matrix
mix[c] = [];
this.rgbToMix(c);
}
*/
}
if (options.bounds) {
options.bounds = latLngBounds(options.bounds);
}
this.wcs = options.crs ? options.crs : new WCS(
meta.header,
meta.images,
{
nzoom: visio.maxZoom + 1
}
);
visio.metaReady = true;
/**
* Fired when the image metadata have been loaded.
* @event metaload
* @memberof VTileLayer
*/
this.fire('metaload');
} else {
alert(
'VisiOmatic metadata query error: ' + meta.detail[0].msg + '.'
);
}
},
/**
* Get color for the given channel.
* @param {number} channel - Input channel.
* @return {string} color string.
*/
getChannelColor: function(channel) {
const rgb = this.visio.rgb
return channel in rgb ? this.visio.rgb[channel].toStr() : '';
},
/**
* Update the color mixing matrix with the RGB contribution of a given
channel.
* @param {number} channel - Input channel.
* @param {RGB | false} rgb - RGB color. False deletes the channel.
*/
rgbToMix: function (channel, rgb) {
const visio = this.visio;
if (rgb) {
visio.rgb[channel] = rgb.clone();
} else if (rgb == false) {
delete visio.rgb[channel];
delete visio.mix[channel];
return;
} else {
rgb = visio.rgb[channel];
}
const cr = this._gammaCorr(rgb.r),
cg = this._gammaCorr(rgb.g),
cb = this._gammaCorr(rgb.b),
lum = (cr + cg + cb) / 3.0,
alpha = visio.colorSaturation / 3.0;
visio.mix[channel] = [];
visio.mix[channel][0] = lum + alpha * (2.0 * cr - cg - cb);
visio.mix[channel][1] = lum + alpha * (2.0 * cg - cr - cb);
visio.mix[channel][2] = lum + alpha * (2.0 * cb - cr - cg);
return;
},
/**
* @summary
Switch the layer to ``'mono'`` mixing mode for the current channel.
@desc
The current channel index defines the color mixing matrix elements in
``'mono'`` mode
*/
setMono: function () {
this.visio.mixingMode = 'mono';
},
/**
* @summary
Switch the layer to ``'color'`` mixing mode.
@desc
RGB colors and saturation settings define mixing matrix elements in
``'color'`` mode
*/
setColor: function () {
this.visio.mixingMode = 'color';
},
/**
* @summary
Update the color mixing matrix using the current color and
saturation settings.
@desc
RGB colors and saturation settings define mixing matrix elements in
``'color'`` mode
* @param {VTileLayer} layer - VisiOmatic layer ("this" is used if not provided)
*/
updateMix: function (layer) {
const _this = layer ? layer : this,
visio = _this.visio;
if (visio.mixingMode === 'color') {
for (const c in visio.rgb) {
_this.rgbToMix(c);
}
}
},
/**
* Apply gamma expansion to the provided input value.
* @private
* @param {number} val - Input value.
* @return {number} gamma-compressed value.
*/
_gammaCorr: function (val) {
return val > 0.0 ? Math.pow(val, this.visio.gamma) : 0.0;
},
/**
* Decode the input string as a 'keyword:value' pair.
* @private
* @deprecated since version 3.0
* @param {string} str - Input string.
* @param {string} keyword - Input keyword.
* @param {string} regexp - Regular expression for decoding the value.
* @return {*} Decoded output.
*/
_readVisioKey: function (str, keyword, regexp) {
const reg = new RegExp(keyword + ':' + regexp);
return reg.exec(str);
},
/**
* Update layer attribute and redraw layer content.
* @private
* @param {string} attr
Name of the (numerical) layer attribute to be updated.
* @param {*} value
New value.
* @param {UI~layerCallback} [fn]
Optional additional callback function.
*/
_setAttr: function (
attr,
value,
fn=undefined
) {
const attrarr = attr.split(/\[|\]/);
if (attrarr[1]) {
this.visio[attrarr[0]][parseInt(attrarr[1], 10)] = value;
} else {
this.visio[attrarr[0]] = value;
}
if (fn) {
fn(this);
}
this.redraw();
},
/**
* Add the layer to the map.
* @override
* @param {object} map - Leaflet map to add the layer to.
* @listens metaload
*/
addTo: function (map) {
if (this.visio.metaReady) {
// VisioMatic data are ready so we can go
this._addToMap(map);
}
else {
// Wait for metadata request to complete
this._loadActivity = DomUtil.create(
'div',
'visiomatic-layer-activity',
map._controlContainer
);
this.once('metaload', function () {
this._addToMap(map);
map._controlContainer.removeChild(this._loadActivity);
}, this);
}
return this;
},
/**
* Executed once the layer to be added to the map is ready.
* @override
* @private
* @param {object} map - Leaflet map to add the layer to.
*/
_addToMap: function (map) {
const newcrs = this.wcs,
curcrs = map.options.crs,
prevcrs = map._prevcrs,
maploadedflag = map._loaded;
var zoom,
center;
if (maploadedflag) {
curcrs._prevLatLng = map.getCenter();
curcrs._prevZoom = map.getZoom();
}
map._prevcrs = map.options.crs = newcrs;
TileLayer.prototype.addTo.call(this, map);
// Go to previous layers' coordinates if applicable
if (prevcrs && newcrs !== curcrs && maploadedflag &&
newcrs.pixelFlag === curcrs.pixelFlag) {
center = curcrs._prevLatLng;
zoom = curcrs._prevZoom;
const prevpixscale = prevcrs.pixelScale(zoom, center),
newpixscale = newcrs.pixelScale(zoom, center);
if (prevpixscale > 1e-20 && newpixscale > 1e-20) {
zoom += Math.round(Math.LOG2E *
Math.log(newpixscale / prevpixscale));
}
// Else go back to previous recorded position for the new layer
} else if (newcrs._prevLatLng) {
center = newcrs._prevLatLng;
zoom = newcrs._prevZoom;
} else if (this.options.center) {
// Default center coordinates and zoom
const latlng = (typeof this.options.center === 'string') ?
newcrs.parseCoords(decodeURI(this.options.center)) :
this.options.center;
if (latlng) {
if (this.options.fov) {
zoom = newcrs.fovToZoom(map, this.options.fov, latlng);
}
map.setView(latlng, zoom, {reset: true, animate: false});
} else {
// If not, ask Sesame@CDS!
VUtil.requestURL(
this.options.sesameURL + '/-oI/A?' + this.options.center,
'getting coordinates for ' + this.options.center,
function (_this, httpRequest) {
if (httpRequest.readyState === 4) {
if (httpRequest.status === 200) {
const str = httpRequest.responseText,
newLatlng = newcrs.parseCoords(str);
if (newLatlng) {
if (_this.options.fov) {
zoom = newcrs.fovToZoom(
map,
_this.options.fov,
newLatlng
);
}
map.setView(
newLatlng,
zoom,
{reset: true, animate: false}
);
} else {
map.setView(
newcrs.crval,
zoom,
{reset: true, animate: false}
);
alert(str + ': Unknown location');
}
} else {
map.setView(
newcrs.crval,
zoom,
{reset: true, animate: false}
);
alert('Error with Sesame query at CDS');
}
}
}, this, 10
);
}
} else {
map.setView(
newcrs.centerLatLng,
zoom,
{reset: true, animate: false}
);
}
},
/**
* Tell if a tile at the given coordinates should be loaded.
* @override
* @private
* @param {point} coords - Tile coordinates.
* @return {boolean} ``true`` if tile should be loaded, ``false`` otherwise.
*/
_isValidTile: function (coords) {
const crs = this._map.options.crs;
if (!crs.infinite) {
// don't load tile if it's out of bounds and not wrapped
const bounds = this._globalTileRange;
if ((!crs.wrapLng && (coords.x < bounds.min.x ||
coords.x > bounds.max.x)) ||
(!crs.wrapLat && (coords.y < bounds.min.y ||
coords.y > bounds.max.y))) {
return false;
}
}
// don't load tile if it's out of the tile grid
const z = this._getZoomForUrl(),
wcoords = coords.clone();
this._wrapCoords(wcoords);
if (wcoords.x < 0 || wcoords.x >= this.visio.gridSize[z].x ||
wcoords.y < 0 || wcoords.y >= this.visio.gridSize[z].y) {
return false;
}
if (!this.options.bounds) { return true; }
// don't load tile if it doesn't intersect the bounds in options
return latLngBounds(this.options.bounds).intersects(
this._tileCoordsToBounds(coords)
);
},
/**
* Create a tile at the given coordinates.
* @override
* @param {point} coords
Tile coordinates.
* @param {boolean} done
Callback function called when the tile has been loaded.
* @return {object} The new tile.
*/
createTile: function (coords, done) {
const tile = TileLayer.prototype.createTile.call(this, coords, done);
tile.coords = coords;
return tile;
},
/**
* Generate the settings part of a tile query URL based on current settings.
* @return {string} The tile settings URL.
*/
getTileSettingsURL: function() {
const visio = this.visio,
visioDefault = this.visioDefault;
let str = this._url;
if (visio.cMap !== visioDefault.cMap) {
str += '&CMP=' + visio.cMap;
}
if (visio.invertCMap !== visioDefault.invertCMap) {
str += '&INV';
}
if (visio.brightness !== visioDefault.brightness) {
str += '&BRT=' + visio.brightness.toString();
}
if (visio.contrast !== visioDefault.contrast) {
str += '&CNT=' + visio.contrast.toString();
}
if (visio.gamma !== visioDefault.gamma) {
str += '&GAM=' + visio.gamma.toFixed(4);
}
const nchannel = visio.nChannel,
mix = visio.mix;
if (visio.mixingMode === 'color') {
for (let c = 0; c < visio.nChannel; c++) {
if (visio.minValue[c] !== visioDefault.minValue[c] ||
visio.maxValue[c] !== visioDefault.maxValue[c]) {
str += '&MINMAX=' + (c + 1).toString() + ':' +
visio.minValue[c].toString() + ',' +
visio.maxValue[c].toString();
}
}
for (const m in mix) {
str += '&MIX=' + (parseInt(m, 10) + 1).toString() + ':';
for (let n = 0; n < 3; n++) {
if (n) { str += ','; }
str += mix[m][n].toFixed(3);
}
}
} else {
const chan = visio.channel;
let chanp1 = chan + 1;
if (chanp1 > nchannel) {
chanp1 = 1;
}
str += '&CHAN=' + chanp1.toString();
if (visio.minValue[chan] !== visioDefault.minValue[chan] ||
visio.maxValue[chan] !== visioDefault.maxValue[chan]) {
str += '&MINMAX=' + chanp1.toString() + ':' +
visio.minValue[chan].toString() + ',' +
visio.maxValue[chan].toString();
}
}
if (visio.quality !== visioDefault.quality) {
str += '&QLT=' + visio.quality.toString();
}
return str;
},
/**
* Generate a tile URL from its coordinates
* @override
* @param {point} coords - Tile coordinates.
* @return {string} The tile URL.
*/
getTileUrl: function (coords) {
const visio = this.visio,
visioDefault = this.visioDefault,
z = this._getZoomForUrl();
return this.getTileSettingsURL() + '&JTL=' + z.toString() + ',' +
(coords.x + visio.gridSize[z].x * coords.y).toString();
},
/**
* Initialize a tile.
* @override
* @private
* @param {object} tile - The tile.
*/
_initTile: function (tile) {
DomUtil.addClass(tile, 'leaflet-tile');
// Force pixels to be visible at high zoom factos whenever possible
if (this._tileZoom >= this.options.maxNativeZoom) {
tile.style.imageRendering = 'pixelated';
}
tile.onselectstart = Util.falseFn;
tile.onmousemove = Util.falseFn;
// update opacity on tiles in IE7-8 because of filter inheritance problems
if (Browser.ielt9 && this.options.opacity < 1) {
DomUtil.setOpacity(tile, this.options.opacity);
}
// without this hack, tiles disappear after zoom on Chrome for Android
// https://github.com/Leaflet/Leaflet/issues/2078
if (Browser.android && !Browser.android23) {
tile.style.WebkitBackfaceVisibility = 'hidden';
}
},
/**
* Replace a tile.
* @see {@link https://github.com/Leaflet/Leaflet/issues/6659}
* @private
* @param {object} tile - The tile.
* @param {string} url - The tile URL.
*/
_refreshTileUrl: function(tile, url) {
// Use an image in background so that we only replace the actual tile,
// once image is loaded in cache!
const img = new Image();
img.onload = function() {
L.Util.requestAnimFrame(function() {
tile.el.src = url;
});
};
img.src = url;
},
/**
* Redraw a tile without flickering.
* @see {@link https://github.com/Leaflet/Leaflet/issues/6659}
* @override
*/
redraw: function() {
// Prevent _tileOnLoad/_tileReady re-triggering a opacity animation
const wasAnimated = this._map._fadeAnimated;
this._map._fadeAnimated = false;
for (var key in this._tiles) {
tile = this._tiles[key];
if (tile.current && tile.active) {
const oldsrc = tile.el.src,
newsrc = this.getTileUrl(tile.coords);
if (oldsrc != newsrc) {
// L.DomEvent.off(tile, 'load', this._tileOnLoad);
// ... this doesnt work!
this._refreshTileUrl(tile, newsrc);
}
}
}
if (wasAnimated) {
setTimeout(function() { map._fadeAnimated = wasAnimated; }, 1000000);
}
}
});
/**
* Instantiate a VisiOmatic tile layer.
* @function
* @param {string} url - URL of the tile server.
* @param {object} [options] - Options: see {@link VTileLayer}.
* @returns {VTileLayer} VisiOmatic TileLayer instance.
*/
export const vTileLayer = function (url, options) {
return new VTileLayer(url, options);
};
source