Merge branch 'immediateChanges' of https://github.com/openwfm/wrfxweb into mergeBranches
[wrfxweb.git] / fdds / js / components / simulationLayer.js
blob61368f326c95d0bfc5bf56ef3e112772c88e37e1
1 import { simVars } from '../simVars.js';
2 import { controllers } from './Controller.js';
3 import { map } from '../map.js';
4 import { utcToLocal } from '../util.js';
6 /** Layer for a specific domain.
7 * Contents
8 * 1. Initialization block
9 * 2. AddToAndRemovFromMap block
10 * 3. LoadTimestamp block
11 * 4. ColorbarMap block
14 export class SimulationLayer {
15 /** ===== Initialization block ===== */
16 constructor(layerName, domain, rasterInfo) {
17 let cs = rasterInfo.coords;
18 let layerURL = simVars.rasterBase + rasterInfo.raster;
19 let hasColorbar = ('colorbar' in rasterInfo);
21 this.layerName = layerName;
22 this.domain = domain;
23 this.hasColorbar = hasColorbar;
24 this.imageOverlay = L.imageOverlay(layerURL,
25 [[cs[0][1], cs[0][0]], [cs[2][1], cs[2][0]]],
27 attribution: simVars.organization,
28 opacity: 0.5,
29 interactive: true
30 });
31 this.preloadedRasters = {};
32 this.preloadedColorbars = {};
33 this.timestampsToColorbarMaps = {};
34 this.coordAndTimestampToColorbarValueCache = {};
36 this.imgCanvas = document.createElement('canvas');
37 this.imgCanvas.width = 1;
38 this.imgCanvas.height = 1;
40 this.clrbarCanvas = document.createElement('canvas');
43 /** ===== AddToAndRemoveFromMap block ===== */
44 addLayerToMap() {
45 let currentTimestamp = controllers.currentTimestamp.getValue();
46 let rastersNow = simVars.rasters[this.domain][currentTimestamp];
47 let rasterInfo = rastersNow[this.layerName];
48 let opacity = controllers.opacity.getValue();
50 this.imageOverlay.addTo(map);
51 if (!(simVars.overlayOrder.includes(this.layerName))) {
52 simVars.overlayOrder.push(this.layerName);
54 this.imageOverlay.bringToFront();
55 this.imageOverlay.setUrl(simVars.rasterBase + rasterInfo.raster);
56 this.imageOverlay.setOpacity(opacity);
57 if (this.hasColorbar) {
58 let cbURL = simVars.rasterBase + rasterInfo.colorbar;
59 controllers.colorbarURL.setValue(cbURL);
60 // simVars.setColorbarURL(cbURL);
61 // simVars.showColorbar();
62 simVars.displayedColorbar = this.layerName;
66 bringToFront() {
67 if (this.imageOverlay != null) {
68 this.imageOverlay.bringToFront();
72 removeLayer() {
73 this.imageOverlay.remove(map);
74 simVars.overlayOrder.splice(simVars.overlayOrder.indexOf(this.layerName), 1);
77 setOpacity(opacity) {
78 if (this.imageOverlay != null) {
79 this.imageOverlay.setOpacity(opacity);
83 /** ===== LoadingTimestamp block ===== */
84 dataArrayToLoadForTimestamp(timestamp) {
85 if (this.timestampIsPreloaded(timestamp)) {
86 return null;
88 let toLoad = [];
89 let raster = simVars.rasters[this.domain][timestamp];
90 let rasterInfo = raster[this.layerName];
91 let imgURL = simVars.rasterBase + rasterInfo.raster;
92 toLoad.push({
93 imageURL: imgURL,
94 timeStamp: timestamp,
95 layerName: this.layerName,
96 layerDomain: this.domain,
97 colorbar: false
98 });
99 if (this.hasColorbar) {
100 let colorbarURL = simVars.rasterBase + rasterInfo.colorbar;
101 toLoad.push({
102 imageURL: colorbarURL,
103 timeStamp: timestamp,
104 layerName: this.layerName,
105 layerDomain: this.domain,
106 colorbar: true
109 return toLoad;
112 timestampIsPreloaded(timestamp) {
113 let rasterCheck = this.preloadedRasters[timestamp] != null;
114 let colorbarCheck = true;
115 if (this.hasColorbar) {
116 colorbarCheck = this.preloadedColorbars[timestamp] != null;
118 return rasterCheck && colorbarCheck;
121 setLayerImagesToTimestamp(timestamp) {
122 let imageURL = this.getURLAtTimestamp(timestamp);
123 this.imageOverlay.setUrl(imageURL);
124 if (this.layerName == simVars.displayedColorbar) {
125 let colorbarURL = this.getColorbarURLAtTimestamp(timestamp);
126 if (colorbarURL != null) {
127 controllers.colorbarURL.setValue(colorbarURL);
128 // simVars.setColorbarURL(colorbarURL);
133 getURLAtTimestamp(timestamp) {
134 let rastersNow = simVars.rasters[this.domain][timestamp];
135 let rasterInfo = rastersNow[this.layerName];
136 let imageURL = simVars.rasterBase + rasterInfo.raster;
137 if (timestamp in this.preloadedRasters) {
138 imageURL = this.preloadedRasters[timestamp];
140 return imageURL;
143 getColorbarURLAtTimestamp(timestamp) {
144 if (!this.hasColorbar) {
145 return;
148 let rastersNow = simVars.rasters[this.domain][timestamp];
149 let rasterInfo = rastersNow[this.layerName];
150 let colorbarURL = simVars.rasterBase + rasterInfo.colorbar;
151 if (timestamp in this.preloadedColorbars) {
152 colorbarURL = this.preloadedColorbars[timestamp];
154 return colorbarURL;
157 clearCache() {
158 for (let timestamp in this.preloadedRasters) {
159 URL.revokeObjectURL(this.preloadedRasters[timestamp]);
161 this.preloadedRasters = {};
162 for (let timestamp in this.preloadedColorbars) {
163 URL.revokeObjectURL(this.preloadedRasters[timestamp]);
165 this.preloadedColorbars = {};
168 async setImageLoadedAtTimestamp(timestamp, imgURL, colorbar) {
169 if (!imgURL) {
170 this.imagePreloadedAtTimestamp(timestamp, '', colorbar);
171 return;
174 const img = new Image();
175 img.onload = () => {
176 this.imagePreloadedAtTimestamp(timestamp, imgURL, colorbar);
178 img.onerror = () => {
179 console.warn('Problem loading image at url: ' + imgURL);
181 img.src = imgURL;
184 imagePreloadedAtTimestamp(timestamp, imgURL, colorbar) {
185 if (colorbar) {
186 this.preloadedColorbars[timestamp] = imgURL;
187 } else {
188 this.preloadedRasters[timestamp] = imgURL;
192 /** ===== ColorbarMap block ===== */
193 async colorValueAtLocation(timestamp, coords) {
194 let key = timestamp + coords.join(',');
195 if (key in this.coordAndTimestampToColorbarValueCache) {
196 return this.coordAndTimestampToColorbarValueCache[key];
198 let [r, g, b] = await this.rgbValueAtLocation(timestamp, coords);
199 let colorValue = null;
200 if ((r + g + b) != 0) {
201 colorValue = await this.rgbValueToColorbarValue(timestamp, [r, g, b]);
203 this.coordAndTimestampToColorbarValueCache[key] = colorValue;
204 return colorValue;
207 async rgbValueAtLocation(timestamp, coords) {
208 return new Promise(resolve => {
209 let imgURL = this.getURLAtTimestamp(timestamp);
210 let [xCoord, yCoord] = coords;
211 let img = new Image();
213 img.onload = () => {
214 let imgX = Math.floor(xCoord * img.naturalWidth);
215 let imgY = Math.floor(yCoord * img.naturalHeight);
217 this.imgCanvas.getContext('2d').clearRect(0, 0, 1, 1);
219 this.imgCanvas.getContext('2d').drawImage(img, imgX, imgY, 1, 1, 0, 0, 1, 1);
220 let pixelData = this.imgCanvas.getContext('2d').getImageData(0, 0, 1, 1).data;
222 resolve([pixelData[0], pixelData[1], pixelData[2]])
224 img.onerror = () => {
225 console.warn('Problem loading image at url: ' + imgURL);
226 resolve([0, 0, 0]);
229 img.src = imgURL;
233 async rgbValueToColorbarValue(timestamp, rgbValue) {
234 let colorbarMap = await this.generateColorbarMap(timestamp);
235 let colorbarValue = this.findClosestKey(rgbValue, colorbarMap);
236 return colorbarValue;
239 async generateColorbarMap(timestamp) {
240 return new Promise(resolve => {
241 let colorbarMap = this.timestampsToColorbarMaps[timestamp];
242 if (colorbarMap != null) {
243 resolve(colorbarMap);
244 return;
246 let colorbarURL = this.getColorbarURLAtTimestamp(timestamp);
247 let colorbarImg = new Image();
248 colorbarImg.onload = () => {
249 colorbarMap = this.mapRGBsToColorbarValues(colorbarImg, timestamp);
250 this.timestampsToColorbarMaps[timestamp] = colorbarMap;
251 resolve(colorbarMap);
253 colorbarImg.onerror = () => {
254 console.warn('Problem loading colorbar at url: ' + colorbarURL);
255 resolve(colorbarMap);
257 colorbarImg.src = colorbarURL;
261 /** Iterates over all keys in clrbarMap and finds closest one to given rgb values. Returns relative
262 * location in clrbarMap. */
263 findClosestKey(rgb, clrbarMap) {
264 let [r, g, b] = rgb;
265 if (clrbarMap == null) {
266 return null;
268 if (r + g + b == 0) {
269 return 0;
271 const createKey = (r, g, b) => r + ',' + g + ',' + b;
272 const mapKey = (key) => key.split(',').map(str => parseInt(str));
273 let closestKey = createKey(r, g, b);
274 if (closestKey in clrbarMap){
275 return clrbarMap[closestKey];
277 let minDiff = 255*3 + 1;
278 for (let key in clrbarMap) {
279 let [rk, gk, bk] = mapKey(key);
280 let newDiff = Math.abs(r - rk) + Math.abs(g - gk) + Math.abs(b - bk);
281 if (newDiff < minDiff) {
282 minDiff = newDiff;
283 closestKey = createKey(rk, gk, bk);
286 return clrbarMap[closestKey];
289 mapRGBsToColorbarValues(colorbarImg, timestamp) {
290 this.drawColorbarCanvas(colorbarImg);
291 let clrbarMap = {};
292 if (!this.clrbarCanvas) {
293 return clrbarMap;
295 let [left, right] = this.getColorbarHorizontalBounds();
296 let horizontalCenterOfColorbar = Math.floor((right + left)/2);
297 let [top, bottom] = this.getColorbarVerticalBoundsAndMapRGBsToHeights(horizontalCenterOfColorbar, clrbarMap);
298 this.convertHeightsToProportionalHeights(clrbarMap, top, bottom);
300 clrbarMap.start = top;
301 clrbarMap.end = bottom;
302 clrbarMap.right = right;
303 clrbarMap.left = left;
305 this.convertProportionalHeightsToColorbarValues(clrbarMap, timestamp);
306 return clrbarMap;
309 getColorbarHorizontalBounds() {
310 let right = 0;
311 let left = 0;
312 let y = Math.round(this.clrbarCanvas.height / 2);
313 for (let x = this.clrbarCanvas.width - 1; x > 0; x--) {
314 let colorbarData = this.clrbarCanvas.getContext('2d').getImageData(x, y, 1, 1).data;
315 if (right == 0) {
316 if (colorbarData[0] + colorbarData[1] + colorbarData[2] != 0) {
317 right = x;
319 } else {
320 if (colorbarData[0] + colorbarData[1] + colorbarData[2] == 0) {
321 left = x;
322 break;
326 return [left, right];
329 getColorbarVerticalBoundsAndMapRGBsToHeights(horizontalCenterOfColorbar, clrbarMap) {
330 let top = 0;
331 let bottom = 0;
332 for (let j = 0; j < this.clrbarCanvas.height; j++) {
333 let [r, g, b, alpha] = this.clrbarCanvas.getContext('2d').getImageData(horizontalCenterOfColorbar, j, 1, 1).data;
334 if (top == 0) {
335 if (r + g + b != 0) {
336 top = j + 1;
338 } else {
339 if (r + g + b == 0) {
340 bottom = j - 1;
341 break;
344 clrbarMap[r + ',' + g + ',' + b] = j;
347 return [top, bottom];
350 convertHeightsToProportionalHeights(clrbarMap, top, bottom) {
351 const computeLocation = (key) => 1 - (clrbarMap[key] - top) / (bottom - top);
352 for (let rgbKey in clrbarMap) {
353 clrbarMap[rgbKey] = computeLocation(rgbKey);
357 convertProportionalHeightsToColorbarValues(clrbarMap, timestamp) {
358 if (simVars.displayedColorbar == null) {
359 return;
362 let levels = this.getColorbarLevelsAtTimestamp(timestamp);
363 if (!levels) {
364 return;
366 let levelMap = {};
367 let levelsAreStratified = this.areColorbarLevelsStratified(clrbarMap, levels, levelMap);
368 let [firstLevel, lastLevel] = this.mapColorbarTickLocationsToValues(clrbarMap, levels, levelMap);
370 let slope = (lastLevel[1] - firstLevel[1]) / (lastLevel[0] - firstLevel[0]);
371 const interpolate = (location) => {
372 if (!levelsAreStratified) {
373 let val = slope*(location - firstLevel[0]) + firstLevel[1];
374 return Math.round(val * 100) / 100;
376 // find closest key in levelMap
377 let closestKey = location;
378 let minDistance = 1;
379 for (let key in levelMap) {
380 let distance = Math.abs(key - location);
381 if (distance < minDistance) {
382 closestKey = key;
383 minDistance = distance;
386 return levelMap[closestKey];
388 for (let color in clrbarMap) {
389 clrbarMap[color] = interpolate(clrbarMap[color]);
393 getColorbarLevelsAtTimestamp(timestamp) {
394 let rastersAtTimestamp = simVars.rasters[this.domain][timestamp];
395 let rasterInfo = rastersAtTimestamp[this.layerName];
396 let levels = rasterInfo.levels;
398 if (!levels) {
399 simVars.noLevels.add(this.layerName, this.domain, utcToLocal(timestamp));
400 let index = simVars.sortedTimestamps.indexOf(timestamp);
401 let nearIndex = index == 0 ? 1 : index - 1;
402 let nearTimestamp = simVars.sortedTimestamps[nearIndex];
403 let nearRastersAtTime = simVars.rasters[this.domain][nearTimestamp];
404 let nearRasterInfo = nearRastersAtTime[this.layerName];
405 levels = nearRasterInfo.levels;
408 return levels;
411 areColorbarLevelsStratified(clrbarMap, levels, levelMap) {
412 let levelsAreStratified = false;
413 if (Object.keys(clrbarMap).length - 10 < levels.length) {
414 levelsAreStratified = true;
416 if (levelsAreStratified) {
417 levelMap[0] = 0;
419 return levelsAreStratified;
422 mapColorbarTickLocationsToValues(clrbarMap, levels, levelMap) {
423 let x = clrbarMap.left - 5;
424 let levelIndex = levels.length - 1;
425 let location;
426 let lastLevel = [];
427 const computeLocation = (y) => 1 - (y - clrbarMap.start) / (clrbarMap.end - clrbarMap.start);
428 for (let y = 0; y < this.clrbarCanvas.height; y++) {
429 if (levelIndex < 0) {
430 break;
432 let colorbarData = this.clrbarCanvas.getContext('2d').getImageData(x, y, 1, 1).data;
433 if (colorbarData[3] != 0) {
434 let markHeight = 0;
435 while (colorbarData[3] != 0 && y < this.clrbarCanvas.height) {
436 y += 1;
437 markHeight += 1;
438 colorbarData = this.clrbarCanvas.getContext('2d').getImageData(x, y, 1, 1).data;
440 if (levelIndex == 0) {
441 y = y - (markHeight - 1);
442 } else if (levelIndex < (levels.length - 1)) {
443 y = y - Math.floor((markHeight-1)/2);
446 location = computeLocation(y);
447 levelMap[location] = levels[levelIndex];
448 if (lastLevel.length == 0) {
449 lastLevel = [location, levels[levelIndex]];
451 levelIndex = levelIndex - 1;
452 y += 5;
455 let firstLevel = [location, levels[0]];
456 return [firstLevel, lastLevel];
459 drawColorbarCanvas(colorbarImg) {
460 this.clrbarCanvas.getContext('2d').clearRect(0, 0, this.clrbarCanvas.width, this.clrbarCanvas.height);
461 if (colorbarImg == null) {
462 return;
464 this.clrbarCanvas.width = colorbarImg.width;
465 this.clrbarCanvas.height = colorbarImg.height;
466 this.clrbarCanvas.getContext('2d').drawImage(colorbarImg, 0, 0, this.clrbarCanvas.width, this.clrbarCanvas.height);