You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1392 lines
42 KiB
JavaScript

import L from 'leaflet'
if (!L.DomUtil.setTransform) {
L.DomUtil.setTransform = function (el, offset, scale) {
var pos = offset || new L.Point(0, 0)
el.style[L.DomUtil.TRANSFORM] =
(L.Browser.ie3d
? 'translate(' + pos.x + 'px,' + pos.y + 'px)'
: 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + (scale ? ' scale(' + scale + ')' : '')
}
} // -- support for both 0.0.7 and 1.0.0 rc2 leaflet
L.ScalarLayer = (L.Layer ? L.Layer : L.Class).extend({
options: {
displayValues: true,
displayOptions: {
velocityType: 'Scalar',
position: 'bottomleft',
emptyString: 'No Scalar data',
},
minValue: 193,
maxValue: 328,
colorScale: null,
data: null,
},
_map: null,
_canvasLayer: null,
_scalar: null,
_context: null,
_timer: 0,
_mouseControl: null,
initialize: function initialize(options) {
console.log('==>', options)
L.setOptions(this, options)
},
onAdd: function onAdd(map) {
// determine where to add the layer
this._paneName = this.options.paneName || 'scalarPane' // fall back to overlayPane for leaflet < 1
var pane = map._panes.overlayPane
if (map.getPane) {
// attempt to get pane first to preserve parent (createPane voids this)
pane = map.getPane(this._paneName)
if (!pane) {
pane = map.createPane(this._paneName)
}
} // create canvas, add to map pane
this._canvasLayer = L.canvasLayer({
pane: pane,
}).delegate(this)
this._canvasLayer.addTo(map)
this._map = map
// this._map.on('mousemove',this._onMouseMove,this);
},
onRemove: function onRemove(map) {
this._destroyScalar()
},
setData: function setData(data) {
this.options.data = data
if (this._scalar) {
this._scalar.setData(data)
this._clearAndRestart()
}
this.fire('load')
},
setOpacity: function setOpacity(opacity) {
console.log('this._canvasLayer', this._canvasLayer)
this._canvasLayer.setOpacity(opacity)
},
setOptions: function setOptions(options) {
this.options = Object.assign(this.options, options)
if (options.hasOwnProperty('displayOptions')) {
this.options.displayOptions = Object.assign(
this.options.displayOptions,
options.displayOptions,
)
this._initMouseHandler(true)
}
if (options.hasOwnProperty('data')) this.options.data = options.data
if (this._scalar) {
this._scalar.setOptions(options)
if (options.hasOwnProperty('data')) this._scalar.setData(options.data)
this._clearAndRestart()
} // this.fire("load");
},
vectorToSpeed: function (uMs, vMs) {
return Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2))
},
_onMouseMove: function (e) {
var self = this
var pos = self._map.containerPointToLatLng(L.point(e.containerPoint.x, e.containerPoint.y))
var gridValue = self._scalar.interpolatePoint(pos.lng, pos.lat)
var htmlOut = gridValue ? gridValue.toFixed(2) : null
console.log('当前的值是', htmlOut)
},
/*------------------------------------ PRIVATE ------------------------------------------*/
onDrawLayer: function onDrawLayer(overlay, params) {
var self = this
if (!this._scalar) {
this._initScalar(this)
return
}
if (!this.options.data) {
return
}
if (this._timer) clearTimeout(self._timer)
this._timer = setTimeout(function () {
self._startScalar()
}, 0) // showing velocity is delayed
},
_startScalar: function _startScalar() {
var bounds = this._map.getBounds()
var size = this._map.getSize() // bounds, width, height, extent
this._scalar.start(
[
[0, 0],
[size.x, size.y],
],
size.x,
size.y,
[
[bounds._southWest.lng, bounds._southWest.lat],
[bounds._northEast.lng, bounds._northEast.lat],
],
)
},
_initScalar: function _initScalar(self) {
// scalar object, copy options
var options = Object.assign(
{
canvas: self._canvasLayer._canvas,
map: this._map,
},
self.options,
)
this._scalar = new Scalar(options) // prepare context global var, start drawing
this._context = this._canvasLayer._canvas.getContext('2d')
this._canvasLayer._canvas.classList.add('scalar-overlay')
this._canvasLayer._canvas.classList.add('leaflet-zoom-hide')
this.onDrawLayer()
this._map.on('movestart', self._hideScalar)
this._map.on('moveend', self._clearAndRestart)
this._map.on('zoomstart', self._hideScalar)
this._map.on('zoomend', self._clearAndRestart)
this._map.on('resize', self._clearScalar)
this._initMouseHandler(false)
},
_initMouseHandler: function _initMouseHandler(voidPrevious) {
if (voidPrevious) {
this._map.removeControl(this._mouseControl)
this._mouseControl = false
}
if (!this._mouseControl && this.options.displayValues) {
var options = this.options.displayOptions || {}
options['leafletVelocity'] = this
this._mouseControl = L.control.velocity(options).addTo(this._map)
}
},
// 拖拽时 隐藏 windy canvas
_hideScalar: function _hideWind() {
console.log('viewset')
var canvas = document.querySelector('.scalar-overlay')
if (canvas) {
canvas.style.visibility = 'hidden'
}
},
_clearAndRestart: function _clearAndRestart() {
if (this._context) this._context.clearRect(0, 0, 3000, 3000)
if (this._scalar) this._startScalar()
setTimeout(function () {
var canvas = document.querySelector('.scalar-overlay')
if (canvas) {
canvas.style.visibility = 'visible'
}
}, 0)
},
_clearScalar: function _clearScalar() {
if (this._scalar) this._scalar.stop()
if (this._context) this._context.clearRect(0, 0, 3000, 3000)
},
_destroyScalar: function _destroyScalar() {
if (this._timer) clearTimeout(this._timer)
if (this._scalar) this._scalar.stop()
if (this._context) this._context.clearRect(0, 0, 3000, 3000)
if (this._mouseControl) this._map.removeControl(this._mouseControl)
this._mouseControl = null
this._scalar = null
this._map.removeLayer(this._canvasLayer)
},
})
L.scalarLayer = function (options) {
return new L.ScalarLayer(options)
}
var µ = (function () {
'use strict'
var τ = 2 * Math.PI
var H = 0.000036 // 0.0000360°φ ~= 4m
var DEFAULT_CONFIG = 'current/wind/surface/level/orthographic'
var TOPOLOGY = isMobile() ? '/data/earth-topo-mobile.json?v2' : '/data/earth-topo.json?v2'
/**
* @returns {Boolean} true if the specified value is truthy.
*/
function isTruthy(x) {
return !!x
}
/**
* @returns {Boolean} true if the specified value is not null and not undefined.
*/
function isValue(x) {
return x !== null && x !== undefined
}
/**
* @returns {Object} the first argument if not null and not undefined, otherwise the second argument.
*/
function coalesce(a, b) {
return isValue(a) ? a : b
}
/**
* @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo
* of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation.
*/
function floorMod(a, n) {
var f = a - n * Math.floor(a / n) // HACK: when a is extremely close to an n transition, f can be equal to n. This is bad because f must be
// within range [0, n). Check for this corner case. Example: a:=-1e-16, n:=10. What is the proper fix?
return f === n ? 0 : f
}
/**
* @returns {Number} distance between two points having the form [x, y].
*/
function distance(a, b) {
var Δx = b[0] - a[0]
var Δy = b[1] - a[1]
return Math.sqrt(Δx * Δx + Δy * Δy)
}
/**
* @returns {Number} the value x clamped to the range [low, high].
*/
function clamp(x, low, high) {
return Math.max(low, Math.min(x, high))
}
/**
* @returns {number} the fraction of the bounds [low, high] covered by the value x, after clamping x to the
* bounds. For example, given bounds=[10, 20], this method returns 1 for x>=20, 0.5 for x=15 and 0
* for x<=10.
*/
function proportion(x, low, high) {
return (µ.clamp(x, low, high) - low) / (high - low)
}
/**
* @returns {number} the value p within the range [0, 1], scaled to the range [low, high].
*/
function spread(p, low, high) {
return p * (high - low) + low
}
/**
* Pad number with leading zeros. Does not support fractional or negative numbers.
*/
function zeroPad(n, width) {
var s = n.toString()
var i = Math.max(width - s.length, 0)
return new Array(i + 1).join('0') + s
}
/**
* @returns {String} the specified string with the first letter capitalized.
*/
function capitalize(s) {
return s.length === 0 ? s : s.charAt(0).toUpperCase() + s.substr(1)
}
/**
* @returns {Boolean} true if agent is probably firefox. Don't really care if this is accurate.
*/
function isFF() {
return /firefox/i.test(navigator.userAgent)
}
/**
* @returns {Boolean} true if agent is probably a mobile device. Don't really care if this is accurate.
*/
function isMobile() {
return /android|blackberry|iemobile|ipad|iphone|ipod|opera mini|webos/i.test(
navigator.userAgent,
)
}
function isEmbeddedInIFrame() {
return window != window.top
}
function toUTCISO(date) {
return (
date.getUTCFullYear() +
'-' +
zeroPad(date.getUTCMonth() + 1, 2) +
'-' +
zeroPad(date.getUTCDate(), 2) +
' ' +
zeroPad(date.getUTCHours(), 2) +
':00'
)
}
function toLocalISO(date) {
return (
date.getFullYear() +
'-' +
zeroPad(date.getMonth() + 1, 2) +
'-' +
zeroPad(date.getDate(), 2) +
' ' +
zeroPad(date.getHours(), 2) +
':00'
)
}
/**
* @returns {String} the string yyyyfmmfdd as yyyytmmtdd, where f and t are the "from" and "to" delimiters. Either
* delimiter may be the empty string.
*/
function ymdRedelimit(ymd, fromDelimiter, toDelimiter) {
if (!fromDelimiter) {
return ymd.substr(0, 4) + toDelimiter + ymd.substr(4, 2) + toDelimiter + ymd.substr(6, 2)
}
var parts = ymd.substr(0, 10).split(fromDelimiter)
return [parts[0], parts[1], parts[2]].join(toDelimiter)
}
/**
* @returns {String} the UTC year, month, and day of the specified date in yyyyfmmfdd format, where f is the
* delimiter (and may be the empty string).
*/
function dateToUTCymd(date, delimiter) {
return ymdRedelimit(date.toISOString(), '-', delimiter || '')
}
function dateToConfig(date) {
return {
date: µ.dateToUTCymd(date, '/'),
hour: µ.zeroPad(date.getUTCHours(), 2) + '00',
}
}
/**
* @returns {Object} an object to perform logging, if/when the browser supports it.
*/
function log() {
function format(o) {
return o && o.stack ? o + '\n' + o.stack : o
}
return {
debug: function debug(s) {
if (console && console.log) console.log(format(s))
},
info: function info(s) {
if (console && console.info) console.info(format(s))
},
error: function error(e) {
if (console && console.error) console.error(format(e))
},
time: function time(s) {
if (console && console.time) console.time(format(s))
},
timeEnd: function timeEnd(s) {
if (console && console.timeEnd) console.timeEnd(format(s))
},
}
}
/**
* @returns {width: (Number), height: (Number)} an object that describes the size of the browser's current view.
*/
function view() {
var w = window
var d = document && document.documentElement
var b = document && document.getElementsByTagName('body')[0]
var x = w.innerWidth || d.clientWidth || b.clientWidth
var y = w.innerHeight || d.clientHeight || b.clientHeight
return {
width: x,
height: y,
}
}
/**
* Removes all children of the specified DOM element.
*/
function removeChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild)
}
}
/**
* @returns {Object} clears and returns the specified Canvas element's 2d context.
*/
function clearCanvas(canvas) {
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height)
return canvas
}
function colorInterpolator(start, end) {
var r = start[0],
g = start[1],
b = start[2]
var Δr = end[0] - r,
Δg = end[1] - g,
Δb = end[2] - b
return function (i, a) {
return [Math.floor(r + i * Δr), Math.floor(g + i * Δg), Math.floor(b + i * Δb), a]
}
}
/**
* Produces a color style in a rainbow-like trefoil color space. Not quite HSV, but produces a nice
* spectrum. See http://krazydad.com/tutorials/makecolors.php.
*
* @param hue the hue rotation in the range [0, 1]
* @param a the alpha value in the range [0, 255]
* @returns {Array} [r, g, b, a]
*/
function sinebowColor(hue, a) {
// Map hue [0, 1] to radians [0, 5/6τ]. Don't allow a full rotation because that keeps hue == 0 and
// hue == 1 from mapping to the same color.
var rad = (hue * τ * 5) / 6
rad *= 0.75 // increase frequency to 2/3 cycle per rad
var s = Math.sin(rad)
var c = Math.cos(rad)
var r = Math.floor(Math.max(0, -c) * 255)
var g = Math.floor(Math.max(s, 0) * 255)
var b = Math.floor(Math.max(c, 0, -s) * 255)
return [r, g, b, a]
}
var BOUNDARY = 0.45
var fadeToWhite = colorInterpolator(sinebowColor(1.0, 0), [255, 255, 255])
/**
* Interpolates a sinebow color where 0 <= i <= j, then fades to white where j < i <= 1.
*
* @param i number in the range [0, 1]
* @param a alpha value in range [0, 255]
* @returns {Array} [r, g, b, a]
*/
function extendedSinebowColor(i, a) {
return i <= BOUNDARY
? sinebowColor(i / BOUNDARY, a)
: fadeToWhite((i - BOUNDARY) / (1 - BOUNDARY), a)
}
function asColorStyle(r, g, b, a) {
return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + a + ')'
}
/**
* @returns {Array} of wind colors and a method, indexFor, that maps wind magnitude to an index on the color scale.
*/
function windIntensityColorScale(step, maxWind) {
var result = []
for (var j = 85; j <= 255; j += step) {
result.push(asColorStyle(j, j, j, 1.0))
}
result.indexFor = function (m) {
// map wind speed to a style
return Math.floor((Math.min(m, maxWind) / maxWind) * (result.length - 1))
}
return result
}
/**
* Creates a color scale composed of the specified segments. Segments is an array of two-element arrays of the
* form [value, color], where value is the point along the scale and color is the [r, g, b] color at that point.
* For example, the following creates a scale that smoothly transitions from red to green to blue along the
* points 0.5, 1.0, and 3.5:
*
* [ [ 0.5, [255, 0, 0] ],
* [ 1.0, [0, 255, 0] ],
* [ 3.5, [0, 0, 255] ] ]
*
* @param segments array of color segments
* @returns {Function} a function(point, alpha) that returns the color [r, g, b, alpha] for the given point.
*/
function segmentedColorScale(segments) {
var points = [],
interpolators = [],
ranges = []
for (var i = 0; i < segments.length - 1; i++) {
points.push(segments[i + 1][0])
interpolators.push(colorInterpolator(segments[i][1], segments[i + 1][1]))
ranges.push([segments[i][0], segments[i + 1][0]])
}
return function (point, alpha) {
var i
for (i = 0; i < points.length - 1; i++) {
if (point <= points[i]) {
break
}
}
var range = ranges[i]
return interpolators[i](µ.proportion(point, range[0], range[1]), alpha)
}
}
/**
* Returns a human readable string for the provided coordinates.
*/
function formatCoordinates(λ, φ) {
return (
Math.abs(φ).toFixed(2) +
'° ' +
(φ >= 0 ? 'N' : 'S') +
', ' +
Math.abs(λ).toFixed(2) +
'° ' +
(λ >= 0 ? 'E' : 'W')
)
}
/**
* Returns a human readable string for the provided scalar in the given units.
*/
function formatScalar(value, units) {
return units.conversion(value).toFixed(units.precision)
}
/**
* Returns a human readable string for the provided rectangular wind vector in the given units.
* See http://mst.nerc.ac.uk/wind_vect_convs.html.
*/
function formatVector(wind, units) {
var d = (Math.atan2(-wind[0], -wind[1]) / τ) * 360 // calculate into-the-wind cardinal degrees
var wd = Math.round(((d + 360) % 360) / 5) * 5 // shift [-180, 180] to [0, 360], and round to nearest 5.
return wd.toFixed(0) + '° @ ' + formatScalar(wind[2], units)
}
/**
* Returns a promise for a JSON resource (URL) fetched via XHR. If the load fails, the promise rejects with an
* object describing the reason: {status: http-status-code, message: http-status-text, resource:}.
*/
function loadJson(resource) {} // var d = when.defer();
// d3.json(resource, function(error, result) {
// return error ?
// !error.status ?
// d.reject({status: -1, message: "Cannot load resource: " + resource, resource: resource}) :
// d.reject({status: error.status, message: error.statusText, resource: resource}) :
// d.resolve(result);
// });
// return d.promise;
/**
* Returns the distortion introduced by the specified projection at the given point.
*
* This method uses finite difference estimates to calculate warping by adding a very small amount (h) to
* both the longitude and latitude to create two lines. These lines are then projected to pixel space, where
* they become diagonals of triangles that represent how much the projection warps longitude and latitude at
* that location.
*
* <pre>
* (λ, φ+h) (, )
* . .
* | ==> \
* | \ __. (, )
* (λ, φ) .____. (λ+h, φ) (x, y) .--
* </pre>
*
* See:
* Map Projections: A Working Manual, Snyder, John P: pubs.er.usgs.gov/publication/pp1395
* gis.stackexchange.com/questions/5068/how-to-create-an-accurate-tissot-indicatrix
* www.jasondavies.com/maps/tissot
*
* @returns {Array} array of scaled derivatives [dx/, dy/, dx/, dy/]
*/
function distortion(projection, λ, φ, x, y) {
var = λ < 0 ? H : -H
var = φ < 0 ? H : -H
var = projection([λ + , φ])
var = projection([λ, φ + ]) // Meridian scale factor (see Snyder, equation 4-3), where R = 1. This handles issue where length of 1° λ
// changes depending on φ. Without this, there is a pinching effect at the poles.
var k = Math.cos((φ / 360) * τ)
return [([0] - x) / / k, ([1] - y) / / k, ([0] - x) / , ([1] - y) / ]
}
/**
* Returns a new agent. An agent executes tasks and stores the result of the most recently completed task.
*
* A task is a value or promise, or a function that returns a value or promise. After submitting a task to
* an agent using the submit() method, the task is evaluated and its result becomes the agent's value,
* replacing the previous value. If a task is submitted to an agent while an earlier task is still in
* progress, the earlier task is cancelled and its result ignored. Evaluation of a task may even be skipped
* entirely if cancellation occurs early enough.
*
* Agents are Backbone.js Event emitters. When a submitted task is accepted for invocation by an agent, a
* "submit" event is emitted. This event has the agent as its sole argument. When a task finishes and
* the agent's value changes, an "update" event is emitted, providing (value, agent) as arguments. If a task
* fails by either throwing an exception or rejecting a promise, a "reject" event having arguments (err, agent)
* is emitted. If an event handler throws an error, an "error" event having arguments (err, agent) is emitted.
*
* The current task can be cancelled by invoking the agent.cancel() method, and the cancel status is available
* as the Boolean agent.cancel.requested key. Within the task callback, the "this" context is set to the agent,
* so a task can know to abort execution by checking the this.cancel.requested key. Similarly, a task can cancel
* itself by invoking this.cancel().
*
* Example pseudocode:
* <pre>
* var agent = newAgent();
* agent.on("update", function(value) {
* console.log("task completed: " + value); // same as agent.value()
* });
*
* function someLongAsynchronousProcess(x) { // x === "abc"
* var d = when.defer();
* // some long process that eventually calls: d.resolve(result)
* return d.promise;
* }
*
* agent.submit(someLongAsynchronousProcess, "abc");
* </pre>
*
* @param [initial] initial value of the agent, if any
* @returns {Object}
*/
function newAgent(initial) {
/**
* @returns {Function} a cancel function for a task.
*/
function cancelFactory() {
return function cancel() {
cancel.requested = true
return agent
}
}
/**
* Invokes the specified task.
* @param cancel the task's cancel function.
* @param taskAndArguments the [task-function-or-value, arg0, arg1, ...] array.
*/
function runTask(cancel, taskAndArguments) {
//
// function run(args) {
// return cancel.requested ? null : _.isFunction(task) ? task.apply(agent, args) : task;
// }
//
// function accept(result) {
// if (!cancel.requested) {
// value = result;
// agent.trigger("update", result, agent);
// }
// }
//
// function reject(err) {
// if (!cancel.requested) { // ANNOYANCE: when cancelled, this task's error is silently suppressed
// agent.trigger("reject", err, agent);
// }
// }
//
// function fail(err) {
// agent.trigger("fail", err, agent);
// }
//
// try {
// // When all arguments are resolved, invoke the task then either accept or reject the result.
// var task = taskAndArguments[0];
// when.all(_.rest(taskAndArguments)).then(run).then(accept, reject).done(undefined, fail);
// agent.trigger("submit", agent);
// } catch (err) {
// fail(err);
// }
}
var _value = initial
var runTask_debounced = _.debounce(runTask, 0) // ignore multiple simultaneous submissions--reduces noise
var agent = {
/**
* @returns {Object} this agent's current value.
*/
value: function value() {
return _value
},
/**
* Cancels this agent's most recently submitted task.
*/
cancel: cancelFactory(),
/**
* Submit a new task and arguments to invoke the task with. The task may return a promise for
* asynchronous tasks, and all arguments may be either values or promises. The previously submitted
* task, if any, is immediately cancelled.
* @returns this agent.
*/
submit: function submit(task, arg0, arg1, and_so_on) {
// immediately cancel the previous task
this.cancel() // schedule the new task and update the agent with its associated cancel function
runTask_debounced((this.cancel = cancelFactory()), arguments)
return this
},
}
return _.extend(agent, Backbone.Events)
}
/**
* Parses a URL hash fragment:
*
* example: "2013/11/14/0900Z/wind/isobaric/1000hPa/orthographic=26.50,-153.00,1430/overlay=off"
* output: {date: "2013/11/14", hour: "0900", param: "wind", surface: "isobaric", level: "1000hPa",
* projection: "orthographic", orientation: "26.50,-153.00,1430", overlayType: "off"}
*
* grammar:
* hash := ( "current" | yyyy / mm / dd / hhhh "Z" ) / param / surface / level [ / option [ / option ... ] ]
* option := type [ "=" number [ "," number [ ... ] ] ]
*
* @param hash the hash fragment.
* @param projectionNames the set of allowed projections.
* @param overlayTypes the set of allowed overlays.
* @returns {Object} the result of the parse.
*/
function parse(hash, projectionNames, overlayTypes) {
var option,
result = {} // 1 2 3 4 5 6 7 8 9
var tokens =
/^(current|(\d{4})\/(\d{1,2})\/(\d{1,2})\/(\d{3,4})Z)\/(\w+)\/(\w+)\/(\w+)([\/].+)?/.exec(
hash,
)
if (tokens) {
var date =
tokens[1] === 'current'
? 'current'
: tokens[2] + '/' + zeroPad(tokens[3], 2) + '/' + zeroPad(tokens[4], 2)
var hour = isValue(tokens[5]) ? zeroPad(tokens[5], 4) : ''
result = {
date: date,
// "current" or "yyyy/mm/dd"
hour: hour,
// "hhhh" or ""
param: tokens[6],
// non-empty alphanumeric _
surface: tokens[7],
// non-empty alphanumeric _
level: tokens[8],
// non-empty alphanumeric _
projection: 'orthographic',
orientation: '',
topology: TOPOLOGY,
overlayType: 'default',
showGridPoints: false,
}
coalesce(tokens[9], '')
.split('/')
.forEach(function (segment) {
if ((option = /^(\w+)(=([\d\-.,]*))?$/.exec(segment))) {
if (projectionNames.has(option[1])) {
result.projection = option[1] // non-empty alphanumeric _
result.orientation = coalesce(option[3], '') // comma delimited string of numbers, or ""
}
} else if ((option = /^overlay=(\w+)$/.exec(segment))) {
if (overlayTypes.has(option[1]) || option[1] === 'default') {
result.overlayType = option[1]
}
} else if ((option = /^grid=(\w+)$/.exec(segment))) {
if (option[1] === 'on') {
result.showGridPoints = true
}
}
})
}
return result
}
/**
* A Backbone.js Model that persists its attributes as a human readable URL hash fragment. Loading from and
* storing to the hash fragment is handled by the sync method.
*/
var Configuration = Backbone.Model.extend({
id: 0,
_ignoreNextHashChangeEvent: false,
_projectionNames: null,
_overlayTypes: null,
/**
* @returns {String} this configuration converted to a hash fragment.
*/
toHash: function toHash() {
var attr = this.attributes
var dir = attr.date === 'current' ? 'current' : attr.date + '/' + attr.hour + 'Z'
var proj = [attr.projection, attr.orientation].filter(isTruthy).join('=')
var ol =
!isValue(attr.overlayType) || attr.overlayType === 'default'
? ''
: 'overlay=' + attr.overlayType
var grid = attr.showGridPoints ? 'grid=on' : ''
return [dir, attr.param, attr.surface, attr.level, ol, proj, grid].filter(isTruthy).join('/')
},
/**
* Synchronizes between the configuration model and the hash fragment in the URL bar. Invocations
* caused by "hashchange" events must have the {trigger: "hashchange"} option specified.
*/
sync: function sync(method, model, options) {
switch (method) {
case 'read':
if (options.trigger === 'hashchange' && model._ignoreNextHashChangeEvent) {
model._ignoreNextHashChangeEvent = false
return
}
model.set(
parse(
window.location.hash.substr(1) || DEFAULT_CONFIG,
model._projectionNames,
model._overlayTypes,
),
)
break
case 'update':
// Ugh. Setting the hash fires a hashchange event during the next event loop turn. Ignore it.
model._ignoreNextHashChangeEvent = true
window.location.hash = model.toHash()
break
}
},
})
/**
* A Backbone.js Model to hold the page's configuration as a set of attributes: date, layer, projection,
* orientation, etc. Changes to the configuration fire events which the page's components react to. For
* example, configuration.save({projection: "orthographic"}) fires an event which causes the globe to be
* re-rendered with an orthographic projection.
*
* All configuration attributes are persisted in a human readable form to the page's hash fragment (and
* vice versa). This allows deep linking and back-button navigation.
*
* @returns {Configuration} Model to represent the hash fragment, using the specified set of allowed projections.
*/
function buildConfiguration(projectionNames, overlayTypes) {
var result = new Configuration()
result._projectionNames = projectionNames
result._overlayTypes = overlayTypes
return result
}
return {
isTruthy: isTruthy,
isValue: isValue,
coalesce: coalesce,
floorMod: floorMod,
distance: distance,
clamp: clamp,
proportion: proportion,
spread: spread,
zeroPad: zeroPad,
capitalize: capitalize,
isFF: isFF,
isMobile: isMobile,
isEmbeddedInIFrame: isEmbeddedInIFrame,
toUTCISO: toUTCISO,
toLocalISO: toLocalISO,
ymdRedelimit: ymdRedelimit,
dateToUTCymd: dateToUTCymd,
dateToConfig: dateToConfig,
log: log,
view: view,
removeChildren: removeChildren,
clearCanvas: clearCanvas,
sinebowColor: sinebowColor,
extendedSinebowColor: extendedSinebowColor,
windIntensityColorScale: windIntensityColorScale,
segmentedColorScale: segmentedColorScale,
formatCoordinates: formatCoordinates,
formatScalar: formatScalar,
formatVector: formatVector,
loadJson: loadJson,
distortion: distortion,
newAgent: newAgent,
parse: parse,
buildConfiguration: buildConfiguration,
}
})()
var Scalar = function Scalar(params) {
// var SCALE = {
// bounds: [-20, 20],
// gradient: µ.segmentedColorScale([
// [-20, [37, 4, 42]],
// [-15, [41, 10, 130]],
// [-12, [81, 40, 40]],
// [-10, [192, 37, 149]], // -40 C/F
// [-5, [70, 215, 215]], // 0 F
// [0, [21, 84, 187]], // 0 C
// [5, [24, 132, 14]], // just above 0 C
// [10, [247, 251, 59]],
// [12, [235, 167, 21]],
// [15, [230, 71, 39]],
// [20, [88, 27, 67]]
// ])
// }
var view = {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
}
// var OVERLAY_ALPHA = Math.floor(0.4 * 255)//百分之40透明度
var OVERLAY_ALPHA = Math.floor(0.6 * 255)
var TRANSPARENT_BLACK = [0, 0, 0, 0] //default canvas's grid data color
var MIN_VELOCITY_INTENSITY = params.minValue || 193 // velocity at which particle intensity is minimum (m/s)
var MAX_VELOCITY_INTENSITY = params.maxValue || 328 // velocity at which particle intensity is maximum (m/s)
// var OPACITY = 0.97 // 默认 最大值最小值范围及颜色区间
var OPACITY = 1 // 默认 最大值最小值范围及颜色区间
var COLORSCALE = params.colorScale || [
[193, [90, 86, 143]],
[206, [72, 104, 181]],
[219, [69, 151, 168]],
[233.15, [81, 180, 98]], // -40 C/F
[255.372, [106, 192, 82]], // 0 F
[273.15, [177, 209, 67]], // 0 C
[275.15, [215, 206, 60]], // just above 0 C
[291, [214, 172, 64]],
[298, [213, 137, 72]],
[311, [205, 94, 93]],
[328, [144, 28, 79]],
]
var SCALE = {
bounds: [MIN_VELOCITY_INTENSITY, MAX_VELOCITY_INTENSITY],
gradient: {}, // 计算色值及step
}
var setColorScale = function setColorScale() {
let n = parseFloat(((MAX_VELOCITY_INTENSITY - MIN_VELOCITY_INTENSITY) / 10).toFixed(4)),
m = MIN_VELOCITY_INTENSITY
for (let i = 0; i < COLORSCALE.length; i++) {
COLORSCALE[i][0] = m
m += n
}
return COLORSCALE
}
SCALE.gradient = µ.segmentedColorScale(setColorScale())
var NULL_WIND_VECTOR = [NaN, NaN, null] // singleton for no wind in the form: [u, v, magnitude]
var builder
var grid
var gridData = params.data
var date
var λ0, φ0, Δλ, Δφ, ni, nj
var setData = function setData(data) {
gridData = data
} // 计算色值及step
var setColorScale = function setColorScale() {
var max = SCALE.bounds[1],
min = SCALE.bounds[0]
var n = Math.ceil((max - min) / 10)
var m = min
for (var i = 0; i < COLORSCALE.length; i++) {
COLORSCALE[i].unshift(m)
m += n
}
return COLORSCALE
}
var setOptions = function setOptions(options) {
if (options.hasOwnProperty('opacity')) OPACITY = options.opacity
} // 双线性插入标量
var bilinearInterpolateScalar = function bilinearInterpolateScalar(x, y, g00, g10, g01, g11) {
var rx = 1 - x
var ry = 1 - y
return g00 * rx * ry + g10 * x * ry + g01 * rx * y + g11 * x * y
} // 标量初始化
var createScalarBuilder = function createScalarBuilder(res) {
let _data, _data1, _data2
if (res.length > 1) {
_data1 = res[0]
_data2 = res[1]
_data = _data1.data.map(function (v, i) {
// 向量中有 负值,需取绝对值
return (Math.abs(v) + Math.abs(_data2.data[i])) / 2
})
} else {
_data1 = res[0]
_data = _data1.data.map(function (v, i) {
// 向量中有 负值,需取绝对值
return Math.abs(v)
})
}
// console.log('===>builder',_data)
return {
header: res[0].header,
data: function data(i) {
return _data[i]
},
interpolate: bilinearInterpolateScalar,
}
}
var buildGrid = function buildGrid(data, callback) {
builder = createScalarBuilder(data)
var header = builder.header
λ0 = header.lo1
φ0 = header.la1 // the grid's origin (e.g., 0.0E, 90.0N)
Δλ = header.dx
Δφ = header.dy // distance between grid points (e.g., 2.5 deg lon, 2.5 deg lat)
ni = header.nx
nj = header.ny // number of grid points W-E and N-S (e.g., 144 x 73)
date = new Date(header.refTime)
date.setHours(date.getHours() + header.forecastTime) // Scan mode 0 assumed. Longitude increases from λ0, and latitude decreases from φ0.
// http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table3-4.shtml
grid = []
var p = 0
var isContinuous = Math.floor(ni * Δλ) >= 360
for (var j = 0; j < nj; j++) {
var row = []
for (var i = 0; i < ni; i++, p++) {
row[i] = builder.data(p)
}
if (isContinuous) {
// For wrapped grids, duplicate first column as last column to simplify interpolation logic
row.push(row[0])
}
grid[j] = row
}
callback({
date: date,
interpolate: interpolate,
})
}
/**
* Get interpolated grid value from Lon/Lat position
* @param λ {Float} Longitude
* @param φ {Float} Latitude
* @returns {Object}
*/
var interpolate = function interpolate(λ, φ) {
if (!grid) return null
var i = floorMod(λ - λ0, 360) / Δλ // calculate longitude index in wrapped range [0, 360)
var j = (φ0 - φ) / Δφ // calculate latitude index in direction +90 to -90
var fi = Math.floor(i),
ci = fi + 1
var fj = Math.floor(j),
cj = fj + 1
var row
if ((row = grid[fj])) {
var g00 = row[fi]
var g10 = row[ci]
if (isValue(g00) && isValue(g10) && (row = grid[cj])) {
var g01 = row[fi]
var g11 = row[ci]
if (isValue(g01) && isValue(g11)) {
// All four points found, so interpolate the value.
return builder.interpolate(i - fi, j - fj, g00, g10, g01, g11)
}
}
}
return null
}
/**
* @returns {Boolean} true if the specified value is not null and not undefined.
*/
var isValue = function isValue(x) {
return x !== null && x !== undefined
}
/**
* @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo
* of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation.
*/
var floorMod = function floorMod(a, n) {
return a - n * Math.floor(a / n)
}
/**
* @returns {Number} the value x clamped to the range [low, high].
*/
var clamp = function clamp(x, range) {
return Math.max(range[0], Math.min(x, range[1]))
}
/**
* @returns {Boolean} true if agent is probably a mobile device. Don't really care if this is accurate.
*/
var isMobile = function isMobile() {
return /android|blackberry|iemobile|ipad|iphone|ipod|opera mini|webos/i.test(
navigator.userAgent,
)
} //
var createField = function createField(columns, bounds, callback) {
/**
* @returns {Array} wind vector [u, v, magnitude] at the point (x, y), or [NaN, NaN, null] if wind
* is undefined at that point.
*/
function field(x, y) {
var column = columns[Math.round(x)]
return (column && column[Math.round(y)]) || NULL_WIND_VECTOR
} // Frees the massive "columns" array for GC. Without this, the array is leaked (in Chrome) each time a new
// field is interpolated because the field closure's context is leaked, for reasons that defy explanation.
field.release = function () {
columns = []
}
field.randomize = function (o) {
// UNDONE: this method is terrible
var x, y
var safetyNet = 0
do {
x = Math.round(Math.floor(Math.random() * bounds.width) + bounds.x)
y = Math.round(Math.floor(Math.random() * bounds.height) + bounds.y)
} while (field(x, y)[2] === null && safetyNet++ < 30)
o.x = x
o.y = y
return o
} // console.log(bounds,field)
callback(bounds, field)
}
var buildBounds = function buildBounds(bounds, width, height) {
var upperLeft = bounds[0]
var lowerRight = bounds[1]
var x = Math.round(upperLeft[0]) //Math.max(Math.floor(upperLeft[0], 0), 0);
var y = Math.max(Math.floor(upperLeft[1], 0), 0)
var xMax = Math.min(Math.ceil(lowerRight[0], width), width - 1)
var yMax = Math.min(Math.ceil(lowerRight[1], height), height - 1)
return {
x: x,
y: y,
xMax: width,
yMax: yMax,
width: width,
height: height,
}
}
var deg2rad = function deg2rad(deg) {
return (deg / 180) * Math.PI
}
var invert = function invert(x, y, windy) {
var latlon = params.map.containerPointToLatLng(L.point(x, y))
return [latlon.lng, latlon.lat]
}
var project = function project(lat, lon, windy) {
var xy = params.map.latLngToContainerPoint(L.latLng(lat, lon))
return [xy.x, xy.y]
} //
var createMask = function createMask() {
// Create a detached canvas, ask the model to define the mask polygon, then fill with an opaque color.
var width = view.width,
height = view.height
var canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
var context = canvas.getContext('2d')
context.fillStyle = 'rgba(255, 0, 0, 1)'
context.fill()
var imageData = context.getImageData(0, 0, width, height)
var data = imageData.data // layout: [r, g, b, a, r, g, b, a, ...]
// console.log(data)
return {
imageData: imageData,
isVisible: function isVisible(x, y) {
var i = (y * width + x) * 4
return data[i + 3] > 0 // non-zero alpha means pixel is visible
},
set: function set(x, y, rgba) {
var i = (y * width + x) * 4
data[i] = rgba[0]
data[i + 1] = rgba[1]
data[i + 2] = rgba[2]
data[i + 3] = rgba[3]
return this
},
}
}
var interpolateField = function interpolateField(grid, bounds, extent, callback) {
var columns = []
var x = bounds.x
var mask = createMask()
function interpolateColumn(x) {
var column = []
for (var y = bounds.y; y <= bounds.yMax; y += 2) {
// if (mask.isVisible(x, y)){
var coord = invert(x, y)
var color = TRANSPARENT_BLACK
if (coord) {
var λ = coord[0],
φ = coord[1]
if (isFinite(λ)) {
var scalar = null
scalar = grid.interpolate(λ, φ)
if (isValue(scalar)) {
color = SCALE.gradient(scalar, OVERLAY_ALPHA)
}
}
}
mask
.set(x, y, color)
.set(x + 1, y, color)
.set(x, y + 1, color)
.set(x + 1, y + 1, color) // }
}
columns[x + 1] = columns[x] = column
}
;(function batchInterpolate() {
var start = Date.now()
while (x < bounds.width) {
interpolateColumn(x)
x += 2
if (Date.now() - start > 1000) {
//MAX_TASK_TIME) {
setTimeout(batchInterpolate, 25)
return
}
}
createField(columns, bounds, function () {
drawOverlay(mask)
})
})()
} // 绘制强度图层
var drawOverlay = function drawOverlay(field) {
if (!field) return
console.log('强度图层加载')
var canvas = document.querySelector('.scalar-overlay')
var ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, 3000, 3000) // field.imageData.data = data
ctx.putImageData(field.imageData, 0, 0)
console.log(field.imageData)
} // 开始绘制
var start = function start(bounds, width, height, extent) {
var mapBounds = {
south: deg2rad(extent[0][1]),
north: deg2rad(extent[1][1]),
east: deg2rad(extent[1][0]),
west: deg2rad(extent[0][0]),
width: width,
height: height,
}
stop() // build grid
buildGrid(gridData, function (grid) {
// interpolateField
interpolateField(
grid,
buildBounds(bounds, width, height),
mapBounds,
function (bounds, field) {
// animate the canvas with random points
// strength.field = field;
// animate(bounds, field);
},
)
})
}
var stop = function stop() {
if (scalar.field) scalar.field.release()
}
var scalar = {
params: params,
start: start,
stop: stop,
createField: createField,
interpolatePoint: interpolate,
setData: setData,
setOptions: setOptions,
}
return scalar
}
export default L