-
Notifications
You must be signed in to change notification settings - Fork 75
Expand file tree
/
Copy pathpixelmapFeature.js
More file actions
387 lines (364 loc) · 12.3 KB
/
pixelmapFeature.js
File metadata and controls
387 lines (364 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
var $ = require('jquery');
var inherit = require('./inherit');
var feature = require('./feature');
var util = require('./util');
/**
* Pixelmap feature specification.
*
* @typedef {geo.feature.spec} geo.pixelmapFeature.spec
* @extends geo.feature.spec
* @property {string|Function|HTMLImageElement} [url] URL of a pixel map or an
* HTML Image element. The rgb data is interpreted as an index of the form
* 0xbbggrr. The alpha channel is ignored. An index of 0xffffff is treated
* as a no-data value for hit-tests.
* @property {geo.geoColor|Function} [color] The color that should be used
* for each data element. Data elements correspond to the indices in the
* pixel map. If an index is larger than the number of data elements, it will
* be transparent. If there is more data than there are indices, it is
* ignored.
* @property {geo.geoPosition|Function} [position] Position of the image.
* Default is (data). The position is an Object which specifies the corners
* of the quad: ll, lr, ur, ul. At least two opposite corners must be
* specified. The corners do not have to physically correspond to the order
* specified, but rather correspond to that part of the image map. If a
* corner is unspecified, it will use the x coordinate from one adjacent
* corner, the y coordinate from the other adjacent corner, and the average z
* value of those two corners. For instance, if ul is unspecified, it is
* {x: ll.x, y: ur.y}. Note that each quad is rendered as a pair of
* triangles: (ll, lr, ul) and (ur, ul, lr). Nothing special is done for
* quads that are not convex or quads that have substantially different
* transformations for those two triangles.
*/
/**
* Create a new instance of class pixelmapFeature
*
* @class
* @alias geo.pixelmapFeature
* @param {geo.pixelmapFeature.spec} arg Options object.
* @extends geo.feature
* @returns {geo.pixelmapFeature}
*/
var pixelmapFeature = function (arg) {
'use strict';
if (!(this instanceof pixelmapFeature)) {
return new pixelmapFeature(arg);
}
arg = arg || {};
feature.call(this, arg);
/**
* @private
*/
var m_this = this,
s_update = this._update,
m_modifiedIndexRange,
s_init = this._init;
this.featureType = 'pixelmap';
/**
* Get/Set position accessor.
*
* @param {geo.geoPosition|Function} [val] If not specified, return the
* current position accessor. If specified, use this for the position
* accessor and return `this`. See {@link geo.quadFeature.position} for
* for details on this position.
* @returns {geo.geoPosition|Function|this}
*/
this.position = function (val) {
if (val === undefined) {
return m_this.style('position');
} else if (val !== m_this.style('position')) {
m_this.style('position', val);
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Get/Set url accessor.
*
* @param {string|Function} [val] If not specified, return the current url
* accessor. If specified, use this for the url accessor and return
* `this`.
* @returns {string|Function|this}
*/
this.url = function (val) {
if (val === undefined) {
return m_this.style('url');
} else if (val !== m_this.style('url')) {
m_this.m_srcImage = m_this.m_info = undefined;
m_this.style('url', val);
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Get/Set color accessor.
*
* @param {geo.geoColor|Function} [val] The new color map accessor or
* `undefined` to get the current accessor.
* @returns {geo.geoColor|Function|this}
*/
this.color = function (val) {
if (val === undefined) {
return m_this.style('color');
} else if (val !== m_this.style('color')) {
m_this.style('color', val);
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Mark that an index's data value (and hence its color) has changed without
* marking all of the data array as changed. If this function is called
* without any parameters, it clears the tracked changes.
*
* @param {number} [idx] The lowest data index that has changed. If
* `undefined`, return the current tracked changed range.
* @param {number|'clear'} [idx2] If an index was specified in `idx` and
* this is specified, the highest index (inclusive) that has changed. If
* returning the tracked changed range and this is `clear`, clear the
* tracked range.
* @returns {this|number[]} When returning a range, this is the lowest and
* highest index values that have changed (inclusive), so their range is
* `[0, data.length)`.
*/
this.indexModified = function (idx, idx2) {
if (idx === undefined) {
const range = m_modifiedIndexRange;
if (idx2 === 'clear') {
m_modifiedIndexRange = undefined;
}
return range;
}
m_this.modified();
if (m_modifiedIndexRange === undefined) {
m_modifiedIndexRange = [idx, idx];
}
if (idx < m_modifiedIndexRange[0]) {
m_modifiedIndexRange[0] = idx;
}
if ((idx2 || idx) > m_modifiedIndexRange[1]) {
m_modifiedIndexRange[1] = (idx2 || idx);
}
return m_this;
};
/**
* Update.
*
* @returns {this}
*/
this._update = function () {
s_update.call(m_this);
if (m_this.buildTime().timestamp() <= m_this.dataTime().timestamp() ||
m_this.updateTime().timestamp() < m_this.timestamp()) {
m_this._build();
}
m_this.updateTime().modified();
return m_this;
};
/**
* Get the maximum index value from the pixelmap. This is a value present in
* the pixelmap.
*
* @returns {number?} The maximum index value.
*/
this.maxIndex = function () {
if (m_this.m_info) {
/* This isn't just m_info.mappedColors.length - 1, since there
* may be more data than actual indices. */
if (m_this.m_info.maxIndex === undefined) {
m_this.m_info.maxIndex = 0;
for (var idx in m_this.m_info.mappedColors) {
if (m_this.m_info.mappedColors.hasOwnProperty(idx)) {
m_this.m_info.maxIndex = Math.max(m_this.m_info.maxIndex, idx);
}
}
}
return m_this.m_info.maxIndex;
}
return undefined;
};
/**
* Given the loaded pixelmap image, create a canvas the size of the image.
* Compute a color for each distinct index and recolor the canvas based on
* these colors, then draw the resultant image as a quad.
*
* @fires geo.event.pixelmap.prepared
*/
this._computePixelmap = function () {
};
/**
* Build. Fetches the image if necessary.
*
* @returns {this}
*/
this._build = function () {
/* Set the build time at the start of the call. A build can result in
* drawing a quad, which can trigger a full layer update, which in turn
* checks if this feature is built. Setting the build time avoids calling
* this a second time. */
if (!m_this.m_srcImage) {
var src = m_this.style.get('url')();
if (util.isReadyImage(src)) {
/* we have an already loaded image, so we can just use it. */
m_this.m_srcImage = src;
m_this._computePixelmap();
} else if (src) {
var defer = $.Deferred(), prev_onload, prev_onerror;
if (src instanceof Image) {
/* we have an unloaded image. Hook to the load and error callbacks
* so that when it is loaded we can use it. */
m_this.m_srcImage = src;
prev_onload = src.onload;
prev_onerror = src.onerror;
} else {
/* we were given a url, so construct a new image */
m_this.m_srcImage = new Image();
// Only set the crossOrigin parameter if this is going across origins.
if (src.indexOf(':') >= 0 &&
src.indexOf('/') === src.indexOf(':') + 1) {
m_this.m_srcImage.crossOrigin = m_this.style.get('crossDomain')() || 'anonymous';
}
}
m_this.m_srcImage.onload = function () {
if (prev_onload) {
prev_onload.apply(m_this, arguments);
}
/* Only use this image if our pixelmap hasn't changed since we
* attached our handler */
if (m_this.style.get('url')() === src) {
m_this.m_info = undefined;
m_this._computePixelmap();
}
defer.resolve();
};
m_this.m_srcImage.onerror = function () {
if (prev_onerror) {
prev_onerror.apply(m_this, arguments);
}
defer.reject();
};
defer.promise(m_this);
m_this.layer().addPromise(m_this);
if (!(src instanceof Image)) {
m_this.m_srcImage.src = src;
}
}
} else if (m_this.m_info) {
m_this._computePixelmap();
} else {
m_this._computePixelmap();
} // else we need to regenerate the images for canvas
m_this.buildTime().modified();
return m_this;
};
/**
* Given the results of the quad search, determine which pixel index is
* found.
*
* @param {object} result An object with `index`: a list of quad indices,
* `found`: a list of quads that contain the specified coordinate, and
* `extra`: an object with keys that are quad indices and values that are
* objects with `basis.x` and `basis.y`, values from 0 - 1 relative to
* interior of the quad.
* @returns {geo.feature.searchResult} An object with a list of features and
* feature indices that are located at the specified point.
*/
this._pointSearchProcess = function (result) {
// use the last index by preference, since for tile layers, this is the
// topmosttile
let idxIdx = result.index.length - 1;
for (; idxIdx >= 0; idxIdx -= 1) {
if (result.extra[result.index[idxIdx]]._quad &&
result.extra[result.index[idxIdx]]._quad.image) {
let img = result.extra[result.index[idxIdx]]._quad.image;
if (result.extra[result.index[idxIdx]]._quad.m_srcImage) {
img = result.extra[result.index[idxIdx]]._quad.m_srcImage;
}
const basis = result.extra[result.index[idxIdx]].basis;
const x = Math.floor(basis.x * img.width);
const y = Math.floor(basis.y * img.height);
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 1;
const context = canvas.getContext('2d');
context.drawImage(img, x, y, 1, 1, 0, 0, 1, 1);
const pixel = context.getImageData(0, 0, 1, 1).data;
const idx = pixel[0] + pixel[1] * 256 + pixel[2] * 256 * 256;
if (idx === 16777215) {
continue;
}
result = {
index: [idx],
found: [m_this.data()[idx]]
};
return result;
}
}
return {index: [], found: []};
};
/**
* Initialize.
*
* @param {geo.pixelmapFeature.spec} arg
* @returns {this}
*/
this._init = function (arg) {
arg = arg || {};
s_init.call(m_this, arg);
var style = Object.assign(
{},
{
color: function (d, idx) {
return {
r: (idx & 0xFF) / 255,
g: ((idx >> 8) & 0xFF) / 255,
b: ((idx >> 16) & 0xFF) / 255,
a: 1
};
},
position: util.identityFunction
},
arg.style === undefined ? {} : arg.style
);
if (arg.position !== undefined) {
style.position = arg.position;
}
if (arg.url !== undefined) {
style.url = arg.url;
}
if (arg.color !== undefined) {
style.color = arg.color;
}
m_this.style(style);
m_this.dataTime().modified();
if (arg.quadFeature) {
m_this.m_srcImage = true;
m_this._computePixelmap();
}
return m_this;
};
return this;
};
/**
* Create a pixelmapFeature from an object.
*
* @see {@link geo.feature.create}
* @param {geo.layer} layer The layer to add the feature to
* @param {geo.pixelmapFeature.spec} spec The object specification
* @returns {geo.pixelmapFeature|null}
*/
pixelmapFeature.create = function (layer, spec) {
'use strict';
spec = spec || {};
spec.type = 'pixelmap';
return feature.create(layer, spec);
};
pixelmapFeature.capabilities = {
/* core feature name -- support in any manner */
feature: 'pixelmap',
/* support for image-based lookup */
lookup: 'pixelmap.lookup'
};
inherit(pixelmapFeature, feature);
module.exports = pixelmapFeature;