gridviz 3.0.32 → 3.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gridviz",
3
- "version": "3.0.32",
3
+ "version": "3.0.34",
4
4
  "description": "Visualization tool for gridded statistics",
5
5
  "keywords": [
6
6
  "statistics",
@@ -30,7 +30,7 @@ export class Drawable {
30
30
  * The function parameter is the zoom level.
31
31
  * (see CanvasRenderingContext2D: globalCompositeOperation property)
32
32
  * @type {function(number):GlobalCompositeOperation} */
33
- this.blendOperation = opts.blendOperation || ((z) => 'source-over')
33
+ this.blendOperation = opts.blendOperation //|| ((z) => 'source-over')
34
34
 
35
35
  /** @type {(function(number):string)|undefined} */
36
36
  this.filterColor = opts.filterColor // (z) => "#eee7"
@@ -39,7 +39,7 @@ export class Drawable {
39
39
  }
40
40
 
41
41
  /**
42
- * Draw layer filter.
42
+ * Draw filter.
43
43
  *
44
44
  * @param {import("./GeoCanvas.js").GeoCanvas} geoCanvas The canvas where to draw the layer.
45
45
  * @returns {void}
package/src/core/Map.js CHANGED
@@ -182,14 +182,19 @@ export class Map {
182
182
  const z = this.geoCanvas.view.z
183
183
  this.updateExtentGeo()
184
184
 
185
+ const ctx = this.geoCanvas.offscreenCtx
186
+
185
187
  //go through the layers
186
188
  for (const layer of this.layers) {
187
189
  //check if layer is visible
188
190
  if (layer.visible && !layer.visible(z)) continue
189
191
 
190
192
  //set layer alpha and blend mode
191
- this.geoCanvas.offscreenCtx.globalAlpha = layer.alpha ? layer.alpha(z) : 1.0
192
- if (layer.blendOperation) this.geoCanvas.offscreenCtx.globalCompositeOperation = layer.blendOperation(z)
193
+ if (layer.alpha || layer.blendOperation) {
194
+ ctx.save()
195
+ if (layer.alpha) ctx.globalAlpha = layer.alpha(z)
196
+ if (layer.blendOperation) ctx.globalCompositeOperation = layer.blendOperation(z)
197
+ }
193
198
 
194
199
  //set affin transform to draw with geographical coordinates
195
200
  this.geoCanvas.setCanvasTransform()
@@ -201,8 +206,7 @@ export class Map {
201
206
  if (layer.filterColor) layer.drawFilter(this.geoCanvas)
202
207
 
203
208
  //restore default alpha and blend operation
204
- this.geoCanvas.offscreenCtx.globalAlpha = 1.0
205
- this.geoCanvas.offscreenCtx.globalCompositeOperation = this.defaultGlobalCompositeOperation
209
+ if (layer.alpha || layer.blendOperation) ctx.restore()
206
210
  }
207
211
 
208
212
  // one drawImage call: draw the offscreen canvas to the main canvas
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ export { SegmentStyle } from './style/SegmentStyle.js'
23
23
  export { TextStyle } from './style/TextStyle.js'
24
24
  export { PillarStyle } from './style/PillarStyle.js'
25
25
  export { SideStyle } from './style/SideStyle.js'
26
+ export { ShadingRayStyle } from './style/ShadingRayStyle.js'
26
27
  export { ShadingStyle } from './style/ShadingStyle.js'
27
28
  export { SideCategoryStyle } from './style/SideCategoryStyle.js'
28
29
  export { DotDensityStyle } from './style/DotDensityStyle.js'
@@ -35,6 +36,7 @@ export { NinjaStarStyle } from './style/NinjaStarStyle.js'
35
36
  export { TimeSeriesStyle } from './style/TimeSeriesStyle.js'
36
37
  export { IsoFenceStyle } from './style/IsoFenceStyle.js'
37
38
  export { ImageStyle } from './style/ImageStyle.js'
39
+ export { Interpolator } from './style/Interpolator.js'
38
40
 
39
41
  // export additional layers
40
42
  export { GridLayer } from './layer/GridLayer.js'
@@ -62,7 +64,7 @@ export { TernaryLegend } from './legend/TernaryLegend.js'
62
64
  export * from './utils/stretching.js'
63
65
  export * from './utils/scale.js'
64
66
  export * from './utils/ternary.js'
65
- export { nice } from './utils/utils.js'
67
+ export { nice, cellsToMatrix } from './utils/utils.js'
66
68
 
67
69
  import { GeoCanvas } from './core/GeoCanvas.js'
68
70
  export const getParameterByName = GeoCanvas.getParameterByName
@@ -30,6 +30,7 @@ export class GeoJSONLayer extends Layer {
30
30
  this.preprocess = opts.preprocess
31
31
 
32
32
  //for points
33
+
33
34
  /**
34
35
  * @private
35
36
  * @type {function(object,number):string} */
@@ -39,36 +40,32 @@ export class GeoJSONLayer extends Layer {
39
40
  * @private
40
41
  * @type {function(object,number):number} */
41
42
  this.size = opts.size || ((f, z) => 10)
42
- /**
43
- * @private
44
- * @type {function(object,number):string} */
45
- this.strokeStyle = opts.strokeStyle || ((f, z) => 'red')
46
- /**
47
- * @private
48
- * @type {function(object,number):string} */
49
- this.fillStyle = opts.fillStyle || ((f, z) => 'black')
50
- /**
51
- * In pixel
52
- * @private
53
- * @type {function(object,number):number} */
54
- this.lineWidth = opts.lineWidth || ((f, z) => 2)
55
43
 
56
- //for lines
44
+ // for lines and polygon outlines
57
45
 
58
46
  /**
59
47
  * @private
60
48
  * @type {function(object,number):string} */
61
- this.color = opts.color || ((f, z) => 'gray')
49
+ this.strokeStyle = opts.strokeStyle || opts.color || ((f, z) => 'gray')
62
50
  /**
63
51
  * In pixel
64
52
  * @private
65
53
  * @type {function(object,number):number} */
66
- this.width = opts.width || ((f, z) => 2)
54
+ this.lineWidth = opts.lineWidth || opts.width || ((f, z) => 2)
67
55
  /**
68
56
  * @private
69
57
  * @type {function(object,number):Array.<number>|undefined} */
70
58
  this.lineDash = opts.lineDash || ((f, z) => undefined)
71
59
 
60
+
61
+ // for polygon filling and point symbol filling
62
+ /**
63
+ * @private
64
+ * @type {function(object,number):string} */
65
+ this.fillStyle = opts.fillStyle || ((f, z) => 'black')
66
+
67
+
68
+
72
69
  /**
73
70
  * @private
74
71
  * @type {Array.<object> | undefined} */
@@ -96,81 +93,136 @@ export class GeoJSONLayer extends Layer {
96
93
  const z = geoCanvas.view.z
97
94
  const ctx = geoCanvas.offscreenCtx
98
95
 
99
- for (const f of this.fs) {
100
- const gt = f.geometry.type
101
-
102
- if (gt == 'Point' || gt == 'MultiPoint') {
103
-
104
- //get style parameters for the point feature
105
- const shape = this.shape(f, z)
106
- if (!shape || shape == 'none') continue
107
- const size = this.size(f, z) * z
108
- if (!size) continue
109
- const strokeStyle = this.strokeStyle(f, z)
110
- const fillStyle = this.fillStyle(f, z)
111
- const lineWidth = this.lineWidth(f, z) * z
112
-
113
- //set canvas drawing parameters
114
- if (strokeStyle) ctx.strokeStyle = strokeStyle
115
- if (fillStyle) ctx.fillStyle = fillStyle
116
- if (lineWidth) ctx.lineWidth = lineWidth
117
-
118
- let cs = f.geometry.coordinates
119
- if (gt == 'Point') cs = [cs]
120
-
121
- if (shape == 'circle') {
122
- //draw circle - fill and stroke
123
- for (const c of cs) {
124
- ctx.beginPath()
125
- ctx.arc(c[0], c[1], size / 2, 0, 2 * Math.PI, false)
126
- if (fillStyle) ctx.fill()
127
- if (strokeStyle && lineWidth) ctx.stroke()
128
- }
129
- } else if (shape == 'square') {
130
- //draw square - fill and stroke
131
- for (const c of cs) {
132
- ctx.beginPath()
133
- ctx.rect(c[0] - size / 2, c[1] - size / 2, size, size)
134
- if (fillStyle) ctx.fill()
135
- if (strokeStyle && lineWidth) ctx.stroke()
136
- }
137
- } else {
138
- console.error('Unexpected shape for point geojson: ' + shape)
96
+ // draw each feature
97
+ for (const f of this.fs)
98
+ this.drawGeometry(f, f.geometry, f.geometry.type, z, ctx)
99
+
100
+ }
101
+
102
+ /**
103
+ * @private
104
+ * Draw a geometry on the canvas, according to its type and the layer style.
105
+ * @param {object} f The feature to draw
106
+ * @param {object} geom The geometry of the feature to draw
107
+ * @param {string} gt The geometry type (e.g. 'Point', 'LineString', etc.)
108
+ * @param {number} z The current zoom level of the map
109
+ * @param {CanvasRenderingContext2D} ctx The canvas context to draw on
110
+ * @returns {void}
111
+ */
112
+ drawGeometry(f, geom, gt, z, ctx) {
113
+ if (gt == 'Point' || gt == 'MultiPoint') {
114
+
115
+ //get style parameters for the point feature
116
+ const shape = this.shape(f, z)
117
+ if (!shape || shape == 'none') return
118
+ const size = this.size(f, z) * z
119
+ if (!size) return
120
+
121
+ const strokeStyle = this.strokeStyle(f, z)
122
+ const fillStyle = this.fillStyle(f, z)
123
+ const lineWidth = this.lineWidth(f, z) * z
124
+
125
+ //set canvas drawing parameters
126
+ ctx.save()
127
+ if (strokeStyle) ctx.strokeStyle = strokeStyle
128
+ if (fillStyle) ctx.fillStyle = fillStyle
129
+ if (lineWidth) ctx.lineWidth = lineWidth
130
+
131
+ let cs = geom.coordinates
132
+ if (gt == 'Point') cs = [cs]
133
+
134
+ if (shape == 'circle') {
135
+ //draw circle - fill and stroke
136
+ for (const c of cs) {
137
+ ctx.beginPath()
138
+ ctx.arc(c[0], c[1], size / 2, 0, 2 * Math.PI, false)
139
+ if (fillStyle) ctx.fill()
140
+ if (strokeStyle && lineWidth) ctx.stroke()
141
+ }
142
+ } else if (shape == 'square') {
143
+ //draw square - fill and stroke
144
+ for (const c of cs) {
145
+ ctx.beginPath()
146
+ ctx.rect(c[0] - size / 2, c[1] - size / 2, size, size)
147
+ if (fillStyle) ctx.fill()
148
+ if (strokeStyle && lineWidth) ctx.stroke()
139
149
  }
140
- } else if (gt == 'LineString' || gt == 'MultiLineString') {
150
+ } else {
151
+ console.error('Unexpected shape for point GeoJSON: ' + shape)
152
+ }
153
+ ctx.restore()
154
+
155
+ } else if (gt == 'LineString' || gt == 'MultiLineString') {
141
156
 
142
- //set color
143
- const col = this.color(f, z)
144
- if (!col || col == 'none') continue
145
- ctx.strokeStyle = col
157
+ const col = this.strokeStyle(f, z)
158
+ if (!col || col === 'none') return
146
159
 
147
- //set linewidth
148
- const wP = this.width(f, z)
149
- if (!wP || wP < 0) continue
150
- ctx.lineWidth = wP * z
160
+ const wP = this.lineWidth(f, z)
161
+ if (!wP || wP < 0) return
151
162
 
152
- //set line dash
153
- const ldP = this.lineDash(f, z)
154
- if (ldP) ctx.setLineDash(ldP)
163
+ ctx.save()
164
+ ctx.strokeStyle = col
165
+ ctx.lineWidth = wP * z
166
+ ctx.setLineDash(this.lineDash(f, z) || [])
167
+
168
+ //
169
+ const css = gt === 'LineString' ? [geom.coordinates] : geom.coordinates
170
+
171
+ for (const cs of css) {
172
+ if (cs.length < 2) continue
173
+ ctx.beginPath()
174
+ ctx.moveTo(cs[0][0], cs[0][1])
175
+ for (let i = 1; i < cs.length; i++) ctx.lineTo(cs[i][0], cs[i][1])
176
+ ctx.stroke()
177
+ }
155
178
 
156
- let css = f.geometry.coordinates
157
- if (gt == 'LineString') css = [css]
179
+ ctx.restore()
158
180
 
159
- //draw lines
160
- for (const cs of css) {
181
+ } else if (gt == 'Polygon' || gt == 'MultiPolygon') {
182
+ ctx.save()
183
+
184
+ // A Polygon is [ outerRing, ...holes ]. A MultiPolygon is [ [ outerRing, ...holes ], ... ]
185
+ const polygons = gt === 'Polygon' ? [geom.coordinates] : geom.coordinates
186
+
187
+ ctx.beginPath()
188
+
189
+ for (const rings of polygons) {
190
+ for (const cs of rings) {
161
191
  if (cs.length < 2) continue
162
- ctx.beginPath()
163
192
  ctx.moveTo(cs[0][0], cs[0][1])
164
193
  for (let i = 1; i < cs.length; i++) ctx.lineTo(cs[i][0], cs[i][1])
194
+ ctx.closePath()
195
+ }
196
+ }
197
+
198
+ // Fill
199
+ const fillStyle = this.fillStyle(f, z)
200
+ if (fillStyle && fillStyle !== 'none') {
201
+ ctx.fillStyle = fillStyle
202
+ ctx.fill('evenodd') // even-odd punches holes correctly regardless of ring winding order
203
+ }
204
+
205
+ // Stroke (optional, only if style provides a width)
206
+ const wP = this.lineWidth(f, z)
207
+ if (wP && wP > 0) {
208
+ const strokeStyle = this.strokeStyle?.(f, z) ?? fillStyle
209
+ if (strokeStyle && strokeStyle !== 'none') {
210
+ ctx.strokeStyle = strokeStyle
211
+ ctx.lineWidth = wP * z
212
+ ctx.setLineDash(this.lineDash?.(f, z) || [])
165
213
  ctx.stroke()
166
214
  }
167
- } else {
168
- console.log('Unsupported geometry type in GeoJSONLayer: ' + gt)
169
215
  }
170
- }
216
+ ctx.restore()
217
+
218
+ } else if (gt == 'GeometryCollection') {
219
+ //recursive call for each geometry in the collection
220
+ for (const g of geom.geometries)
221
+ this.drawGeometry(f, g, g.type, z, ctx)
171
222
 
172
- //...
173
- ctx.setLineDash([])
223
+ } else {
224
+ console.log('Unexpected geometry type in GeoJSONLayer: ' + gt)
225
+ }
174
226
  }
175
227
 
176
228
  /**
@@ -180,7 +232,7 @@ export class GeoJSONLayer extends Layer {
180
232
  */
181
233
  async load(callback) {
182
234
  if (!this.url) {
183
- console.log('Failed loading boundaries: No URL specified. ' + this.url)
235
+ console.log('Failed loading GeoJSON data: No URL specified. ' + this.url)
184
236
  this.loadingStatus = 'failed'
185
237
  this.labels = []
186
238
  return
@@ -216,7 +268,7 @@ export class GeoJSONLayer extends Layer {
216
268
  //redraw
217
269
  if (callback) callback()
218
270
  } catch (error) {
219
- console.log('Failed loading boundaries from ' + this.url)
271
+ console.log('Failed loading GeoJSON from ' + this.url)
220
272
  this.fs = []
221
273
  this.loadingStatus = 'failed'
222
274
  }
@@ -60,15 +60,26 @@ export class GridLayer extends Layer {
60
60
  //update dataset view cache
61
61
  dsc.updateViewCache(geoCanvas.extGeo)
62
62
 
63
+
64
+ //set layer alpha and blend mode
65
+ if (this.alpha || this.blendOperation) {
66
+ ctx.save()
67
+ if (this.alpha) ctx.globalAlpha = this.alpha(z)
68
+ if (this.blendOperation) ctx.globalCompositeOperation = this.blendOperation(z)
69
+ }
70
+
63
71
  //draw cells, style by style
64
72
  for (const s of this.styles) {
65
73
  //check if style is visible
66
74
  if (s.visible && !s.visible(z)) continue
67
75
 
68
- //set style alpha and blend mode
69
- //TODO: multiply by layer alpha ?
70
- ctx.globalAlpha = s.alpha ? s.alpha(z) : 1.0
71
- if (s.blendOperation) ctx.globalCompositeOperation = s.blendOperation(z)
76
+ // set style alpha and blend mode
77
+ //TODO: multiply style alpha by layer alpha ?
78
+ if (s.alpha || s.blendOperation) {
79
+ ctx.save()
80
+ if (s.alpha) ctx.globalAlpha = s.alpha(z)
81
+ if (s.blendOperation) ctx.globalCompositeOperation = s.blendOperation(z)
82
+ }
72
83
 
73
84
  //set affin transform to draw with geographical coordinates
74
85
  geoCanvas.setCanvasTransform()
@@ -78,8 +89,14 @@ export class GridLayer extends Layer {
78
89
 
79
90
  //draw style filter
80
91
  if (s.filterColor) s.drawFilter(geoCanvas)
92
+
93
+ //restore default alpha and blend operation - before style change
94
+ if (s.alpha || s.blendOperation) ctx.restore()
81
95
  }
82
96
 
97
+ //restore default alpha and blend operation - before layer change
98
+ if (this.alpha || this.blendOperation) ctx.restore()
99
+
83
100
  //add legend element
84
101
  if (legend) {
85
102
  for (const s of this.styles) {
@@ -19,7 +19,7 @@ export class TernaryLegend extends Legend {
19
19
  this.classifier = opts.classifier
20
20
 
21
21
  this.width = opts.width || 150
22
- this.selectionColor = this.selectionColor || 'red'
22
+ this.selectionColor = opts.selectionColor || 'red'
23
23
  this.tooltip = opts.tooltip
24
24
  this.texts = opts.texts
25
25
 
@@ -0,0 +1,258 @@
1
+ //@ts-check
2
+ 'use strict'
3
+
4
+ import { Style } from '../core/Style.js'
5
+ import { cellsToMatrix } from '../utils/utils.js'
6
+
7
+ /**
8
+ * @module style
9
+ * @author Julien Gaffuri
10
+ */
11
+ export class Interpolator extends Style {
12
+ /** @param {object} opts */
13
+ constructor(opts) {
14
+ super(opts)
15
+ opts = opts || {}
16
+
17
+ /** The cell value to interpolate
18
+ * @type { function } } */
19
+ this.value = opts.value //(c) => elevation
20
+
21
+ /** The target resolution. As a function (resolution, z) => targetResolution
22
+ * NB: Try to make sure that resolution/targetResolution is an integer.
23
+ * @type { function } } */
24
+ this.targetResolution = opts.targetResolution || ((r, z) => r / 5)
25
+
26
+ /** the property name to store the interpolated value in the cell
27
+ * @type { string } } */
28
+ this.interpolatedProperty = opts.interpolatedProperty || 'value'
29
+
30
+ // the interpolation method: currently only 'bilinear' is supported
31
+ //this.method = opts.method || 'bilinear' // 'nearest', 'bilinear'
32
+
33
+ /** when a neighbor cell has no value, take zero value or average of surrounding cells
34
+ * @type { string } } */
35
+ this.nodataHandling = opts.nodataHandling || "zero" // 'zero', 'average'
36
+
37
+ /** The styles to represent the interpolated grid.
38
+ * @type {Array.<Style>} */
39
+ this.styles = opts.styles || []
40
+ }
41
+
42
+
43
+ /**
44
+ *
45
+ * @param {Array.<import("../core/Dataset.js").Cell>} cells
46
+ * @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
47
+ * @param {number} resolution
48
+ */
49
+ draw(cells, geoCanvas, resolution) {
50
+
51
+ //filter cells
52
+ if (this.filter) cells = cells.filter(this.filter)
53
+ if (cells.length == 0) return;
54
+
55
+ //
56
+ const z = geoCanvas.view.z
57
+
58
+ //get view scale
59
+ //const viewScale = this.viewScale ? this.viewScale(cells, resolution, z) : undefined
60
+
61
+ //index cells by y and x
62
+ let m = cellsToMatrix(cells, resolution, c => this.value(c))
63
+ const x0 = m.x0, y0 = m.y0
64
+
65
+ //get target resolution and scale factor
66
+ let targetResolution = this.targetResolution(resolution, z)
67
+ const scaleFactor = Math.round(resolution / targetResolution)
68
+ targetResolution = resolution / scaleFactor
69
+
70
+ // compute ray casting shadow
71
+ m = bilinearInterpolator(m, scaleFactor, this.nodataHandling);
72
+
73
+ // make cells
74
+ cells = []
75
+ const ra = (resolution - targetResolution) / 2
76
+ for (let i = 0; i < m.length; i++) {
77
+ const row = m[i]
78
+ const y = y0 - i * targetResolution + ra
79
+ for (let j = 0; j < row.length; j++) {
80
+ const v = row[j]
81
+ if (!v) continue
82
+ const c = { x: x0 + j * targetResolution + ra, y: y }
83
+ c[this.interpolatedProperty] = v
84
+ cells.push(c)
85
+ }
86
+ }
87
+
88
+ //draw smoothed cells from styles
89
+ const ctx = geoCanvas.offscreenCtx
90
+ for (let s of this.styles) {
91
+
92
+ //check if style is visible
93
+ if (s.visible && !s.visible(z)) continue
94
+
95
+ //set style alpha and blend mode
96
+ //TODO: multiply by layer alpha ?
97
+ if (s.alpha || s.blendOperation) {
98
+ ctx.save()
99
+ if (s.alpha) ctx.globalAlpha = s.alpha(z)
100
+ if (s.blendOperation) ctx.globalCompositeOperation = s.blendOperation(z)
101
+ }
102
+
103
+ //set affin transform to draw with geographical coordinates
104
+ geoCanvas.setCanvasTransform()
105
+
106
+ //draw with style
107
+ s.draw(cells, geoCanvas, targetResolution)
108
+
109
+ //draw style filter
110
+ if (s.filterColor) s.drawFilter(geoCanvas)
111
+
112
+ //restore ctx
113
+ if (s.alpha || s.blendOperation) ctx.restore()
114
+ }
115
+
116
+ }
117
+ }
118
+
119
+
120
+ const interp = (topLeft, topRight, bottomLeft, bottomRight, xRatio, yRatio) => {
121
+ const top = topLeft + (topRight - topLeft) * xRatio;
122
+ const bottom = bottomLeft + (bottomRight - bottomLeft) * xRatio;
123
+ return top + (bottom - top) * yRatio;
124
+ }
125
+
126
+
127
+ function bilinearInterpolator(grid, scaleFactor = 5, nodataHandling = "zero") {
128
+
129
+ if (scaleFactor === 1) return grid;
130
+
131
+ // input grid dimensions
132
+ const rows = grid.length;
133
+ if (rows === 0) return [];
134
+ const cols = grid[0].length;
135
+
136
+ // output grid dimensions
137
+ const fineRows = rows * scaleFactor;
138
+ const fineCols = cols * scaleFactor;
139
+ const fineGrid = Array(fineRows).fill().map(() => Array(fineCols).fill(undefined));
140
+
141
+ // check if scale factor is even
142
+ const sfIsEven = scaleFactor % 2 === 0
143
+
144
+ //compute output grid values
145
+ for (let i = 0; i < fineRows; i++) {
146
+
147
+ const coarseI = Math.min(Math.floor(i / scaleFactor), rows - 2);
148
+ const yRatio = (i % scaleFactor) / scaleFactor;
149
+ const i_ = coarseI * scaleFactor + (scaleFactor / 2)
150
+
151
+ for (let j = 0; j < fineCols; j++) {
152
+ const coarseJ = Math.min(Math.floor(j / scaleFactor), cols - 2);
153
+ const xRatio = (j % scaleFactor) / scaleFactor;
154
+ const j_ = coarseJ * scaleFactor + (scaleFactor / 2)
155
+
156
+ // get four corner values
157
+ const topLeft = grid[coarseI][coarseJ];
158
+ const topRight = grid[coarseI][coarseJ + 1];
159
+ const bottomLeft = grid[coarseI + 1][coarseJ];
160
+ const bottomRight = grid[coarseI + 1][coarseJ + 1];
161
+
162
+
163
+ if (sfIsEven) {
164
+ if (topLeft === undefined && i <= i_ && j <= j_) continue
165
+ if (topRight === undefined && i <= i_ && j >= j_) continue
166
+ if (bottomLeft === undefined && i >= i_ && j <= j_) continue
167
+ if (bottomRight === undefined && i >= i_ && j >= j_) continue
168
+ } else {
169
+ if (topLeft === undefined && i < i_ && j < j_) continue
170
+ if (topRight === undefined && i < i_ && j > j_) continue
171
+ if (bottomLeft === undefined && i > i_ && j < j_) continue
172
+ if (bottomRight === undefined && i > i_ && j > j_) continue
173
+ }
174
+
175
+ if(nodataHandling === "zero") {
176
+ // replace undefined values by zero
177
+ fineGrid[i][j] = interp(topLeft || 0, topRight || 0, bottomLeft || 0, bottomRight || 0, xRatio, yRatio)
178
+ continue
179
+ }
180
+
181
+ // general case: bilinear interpolation
182
+ // If all four are defined, interpolate normally
183
+ if (topLeft !== undefined && topRight !== undefined && bottomLeft !== undefined && bottomRight !== undefined) {
184
+ fineGrid[i][j] = interp(topLeft, topRight, bottomLeft, bottomRight, xRatio, yRatio)
185
+ continue
186
+ }
187
+
188
+ // If only one is not defined, use average value of 2 adjacents and interpolate
189
+ if (topLeft === undefined && topRight !== undefined && bottomLeft !== undefined && bottomRight !== undefined) {
190
+ const v = topRight + bottomLeft
191
+ fineGrid[i][j] = interp(v / 2, topRight, bottomLeft, bottomRight, xRatio, yRatio)
192
+ continue
193
+ }
194
+ if (topLeft !== undefined && topRight === undefined && bottomLeft !== undefined && bottomRight !== undefined) {
195
+ const v = topLeft + bottomRight
196
+ fineGrid[i][j] = interp(topLeft, v / 2, bottomLeft, bottomRight, xRatio, yRatio)
197
+ continue
198
+ }
199
+ if (topLeft !== undefined && topRight !== undefined && bottomLeft === undefined && bottomRight !== undefined) {
200
+ const v = topLeft + bottomRight
201
+ fineGrid[i][j] = interp(topLeft, topRight, v / 2, bottomRight, xRatio, yRatio)
202
+ continue
203
+ }
204
+ if (topLeft !== undefined && topRight !== undefined && bottomLeft !== undefined && bottomRight === undefined) {
205
+ const v = topRight + bottomLeft
206
+ fineGrid[i][j] = interp(topLeft, topRight, bottomLeft, v / 2, xRatio, yRatio)
207
+ continue
208
+ }
209
+
210
+ // If only two diagonally opposite points are defined, interpolate along diagonal
211
+ if (topLeft !== undefined && bottomRight !== undefined) {
212
+ const t = (xRatio + yRatio) * Math.SQRT1_2
213
+ fineGrid[i][j] = topLeft * (1 - t) + bottomRight * t;
214
+ continue
215
+ }
216
+ if (topRight !== undefined && bottomLeft !== undefined) {
217
+ const t = (1 - xRatio + yRatio) * Math.SQRT1_2
218
+ fineGrid[i][j] = topRight * (1 - t) + bottomLeft * t;
219
+ continue
220
+ }
221
+
222
+ // If only two adjacent points are defined, interpolate between them
223
+ if (topLeft !== undefined && bottomLeft !== undefined) {
224
+ fineGrid[i][j] = bottomLeft * yRatio + topLeft * (1 - yRatio);
225
+ continue
226
+ }
227
+ if (topRight !== undefined && bottomRight !== undefined) {
228
+ fineGrid[i][j] = bottomRight * yRatio + topRight * (1 - yRatio);
229
+ continue
230
+ }
231
+ if (topLeft !== undefined && topRight !== undefined) {
232
+ fineGrid[i][j] = topRight * xRatio + topLeft * (1 - xRatio);
233
+ }
234
+ if (bottomLeft !== undefined && bottomRight !== undefined) {
235
+ fineGrid[i][j] = bottomRight * xRatio + bottomLeft * (1 - xRatio);
236
+ continue
237
+ }
238
+
239
+ // If only one point is defined, use that value
240
+ if (topLeft !== undefined) {
241
+ fineGrid[i][j] = topLeft;
242
+ continue
243
+ }
244
+ if (topRight !== undefined) {
245
+ fineGrid[i][j] = topRight;
246
+ continue
247
+ }
248
+ if (bottomLeft !== undefined) {
249
+ fineGrid[i][j] = bottomLeft;
250
+ continue
251
+ }
252
+ if (bottomRight !== undefined) {
253
+ fineGrid[i][j] = bottomRight;
254
+ }
255
+ }
256
+ }
257
+ return fineGrid;
258
+ }