Add tests and a tutorial.
MODIFY
scripts/datastore.js
scripts/datastore.js
@@ -6,7 +6,7 @@ var tar = require('tar');
var registry = {
'AdderallCities2015.csv': 'c3e984482cc6db1193a6dca2a55396a2baad8541a5c8c679f33750b76f766f40a119ec3e63abbabcd095d752c3df8ce21bf24cbe629502121f24ba90b4b0674c',
- 'base-images.tgz': '2dc58216c90c4d0d2c0f22593b804c0d5f4b39ceacf79e5cd8ebe1673f42bf95dd1bc3072861ac93aa4b371a67c1166607863dbd0ff50d3f8c483fe5f032f724',
+ 'base-images.tgz': '072fc031039b3c4cf0a1a810e6181043db411749f990e5382a734fa1067850857c0ef7174919923d739406e9d8cd0380b5519da332ea7b6b18d8a0cb776d61db',
'blue.jpg': '867b1f3c568289efc7d0dba97d827a2bc4d83a7465cebcb3b5aec7bac6a38cf70d037d1814402bc97ad1f2f6737cfb5ce97db0a4fb53a716e77fd3ba57a7ab3b',
'cities.csv': '5a665e5feda24f28e5cf4ed0801b67e73bbcf3ea781b2e50d11284214e67b25b68e6a1c48da46e5e4d4d0c54c2ec18f88d292224b4541fb279396cf7b94beac9',
'earthquakes.json': 'f098b6437411384b552419b4a36264c1bb3fed816ccfe9545145175e0b92a0b7ad5ebdcb9dddd0a12a90499143ffa471c02f6e049be5b973db607ff066892500',
@@ -19,7 +19,7 @@ var registry = {
'oahu-dense.json': '692a44ec4a18b16d1530403a9a2bf286ff2ef7d45fa58f555c278f91a0fa708b5626a3f38955e06a11cdc06a1009e859328687c1f32a2169e0c8ef1b518418c1',
'oahu.json': 'e44282c44fa95f0b40c2135ee94ff40a755771f3b4bf9acda0eef0048cd0fb29b3a71352e2eecdca6cfc35fafde96ffb1a9658c9fee2346071808a9123c26cb5',
'oahu-medium.json': '83375c5c2678b11d8de7f59fd8e2f9b889d3ec20f4a81279d996711bfc0942bd9dcdce149cbca995930459c26b3bf58be60569687085adf361ff805436b75aab',
- 'pixelmap_0_0_0.png': 'c9be347bb323dca21a8ec4708b2c4343d7861546606f7d531b305d22d7b5be1e3bc7754cc407d117eda7ec3e31e2e0197d296cd80d40f0b32c17db0ad4db4292',
+ 'pixelmap_0_0_0.png': '67bc541c8f26de5b644d756bc517b31ef4187c1c609da5291fc446fa7e2ddcf40c75b93cdb6f2e579be596ac1215c5af802c9555474723d3b10713851fbb58e3',
'pixelmap_1_0_0.png': '41ca6c789bd410b0ea4dd814c92eb6047c098fcc6a8f77c200604e2bff7b7847f6c740e3a411a957a03edc0dfe6ee1383f38c34dceacdfa3e9f6408b7187ee25',
'pixelmap_1_0_1.png': 'a27aa667e482cd7a7800cbe9c2401b9fbf387bdc67a49b17be04f7aa2846e522aa64383383fb3413d9dc7206e3b90910ccc8d4454ac1b08ebcedc68182c49e99',
'pixelmap_1_1_0.png': 'b3f020355093ca6fa450550fe9680f549a6dc327a568d439d59e620ee3f9a43b6c88913e5d9344e65bd896cce5494b864051359faaaa4704433d5d0c41da2a43',

MODIFY
src/canvas/pixelmapFeature.js
src/canvas/pixelmapFeature.js
@@ -4,6 +4,25 @@ var pixelmapFeature = require('../pixelmapFeature');
var geo_event = require('../event');
var util = require('../util');
+/**
+ * Pixelmap feature information record.
+ *
+ * @typedef {object} geo.pixelmapFeature.info
+ * @property {number} width The width of the source image.
+ * @property {number} height The width of the source image.
+ * @property {context} context The HTMLCanvasElement context used for handling
+ * the pixelmap.
+ * @property {ImageData} imageData The context's image data.
+ * @property {number[]} indices An array, one per pixel, of the index value in
+ * the image. This decodes the pixel value to the corresponding integer.
+ * @property number} area The number of pixels in the image. This is
+ * `width * height`.
+ * @property {object[]} mappedColors This has one entry for each distinct index
+ * value. Each entry has `first` and `last` with the first and last pixel
+ * locations where that index occurs. Note that last is the inclusive value
+ * of the location (so its maximum possible value is `size - 1`).
+ */
+
/**
* Create a new instance of class pixelmapFeature.
*

MODIFY
src/pixelmapFeature.js
src/pixelmapFeature.js
@@ -30,25 +30,6 @@ var util = require('./util');
* transformations for those two triangles.
*/
-/**
- * Pixelmap feature information record.
- *
- * @typedef {object} geo.pixelmapFeature.info
- * @property {number} width The width of the source image.
- * @property {number} height The width of the source image.
- * @property {context} context The HTMLCanvasElement context used for handling
- * the pixelmap.
- * @property {ImageData} imageData The context's image data.
- * @property {number[]} indices An array, one per pixel, of the index value in
- * the image. This decodes the pixel value to the corresponding integer.
- * @property number} area The number of pixels in the image. This is
- * `width * height`.
- * @property {object[]} mappedColors This has one entry for each distinct index
- * value. Each entry has `first` and `last` with the first and last pixel
- * locations where that index occurs. Note that last is the inclusive value
- * of the location (so its maximum possible value is `size - 1`).
- */
-
/**
* Create a new instance of class pixelmapFeature
*

MODIFY
src/pixelmapLayer.js
src/pixelmapLayer.js
@@ -15,6 +15,9 @@ var pixelmapFeature = require('./pixelmapFeature');
* 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 {object} [style] An optional style object that could contain
+ * `color` or other style values.
+ * @property {array} [data] A new data array.
*/
/**
@@ -26,7 +29,7 @@ var pixelmapFeature = require('./pixelmapFeature');
* @alias geo.pixelmapLayer
* @extends geo.tileLayer
*
- * @param {geo.tileLayer.spec} [arg] Specification for the layer.
+ * @param {geo.pixelmapLayer.spec} [arg] Specification for the layer.
*/
var pixelmapLayer = function (arg) {
@@ -104,21 +107,13 @@ var pixelmapLayer = function (arg) {
m_pixelmapFeature.dataTime().modified();
return s_dataTimeModified();
};
- const s_modified = m_this.modified;
- m_this.modified = () => {
- m_pixelmapFeature.modified();
- return s_modified();
- };
- const s_geoOn = m_this.geoOn;
- const s_geoOff = m_this.geoOff;
- m_this.geoOn = (event, handler) => {
- m_pixelmapFeature.geoOn(event, handler);
- return s_geoOn(event, handler);
- };
- m_this.geoOff = (event, handler) => {
- m_pixelmapFeature.geoOff(event, handler);
- return s_geoOff(event, handler);
+ ['modified', 'geoOn', 'geoOff', 'geoOnce'].forEach((funcName) => {
+ const superFunc = m_this[funcName];
+ m_this[funcName] = function () {
+ m_pixelmapFeature[funcName].apply(this, arguments);
+ return superFunc.apply(this, arguments);
};
+ });
return m_this;
};

MODIFY
src/tileLayer.js
src/tileLayer.js
@@ -676,7 +676,7 @@ var tileLayer = function (arg) {
this._getTiles = function (maxLevel, bounds, sorted, onlyIfChanged) {
var i, j, tiles = [], index, nTilesLevel,
start, end, indexRange, source, center, changed = false, old, level,
- minLevel = (m_this._options.keepLower ? m_this._options.minLevel : maxLevel);
+ minLevel = (m_this._options.keepLower ? m_this._options.minLevel : Math.max(maxLevel, m_this._options.minLevel));
if (maxLevel < minLevel) {
maxLevel = minLevel;
}
@@ -1532,7 +1532,8 @@ var tileLayer = function (arg) {
* finished, purge all but the current layer. This is important for
* semi-transparent layers. */
if ((doneLoading || m_this._isCovered(tile)) &&
- zoom !== tile.index.level) {
+ zoom !== tile.index.level &&
+ (zoom >= m_this._options.minLevel || tile.index.level !== m_this._options.minLevel)) {
return true;
}
}

MODIFY
src/webgl/pixelmapFeature.js
src/webgl/pixelmapFeature.js
@@ -47,12 +47,14 @@ var webgl_pixelmapFeature = function (arg) {
this.pointSearch = function (geo, gcs) {
if (m_quadFeature && m_this.m_info) {
let result = m_quadFeature.pointSearch(geo, gcs);
- if (result.index.length === 1 &&
- result.extra && result.extra[result.index[0]].basis &&
- result.extra[result.index[0]]._quad &&
- result.extra[result.index[0]]._quad.image) {
- const img = result.extra[result.index[0]]._quad.image;
- const basis = result.extra[result.index[0]].basis;
+ // 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) {
+ const img = result.extra[result.index[idxIdx]]._quad.image;
+ 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');
@@ -68,6 +70,7 @@ var webgl_pixelmapFeature = function (arg) {
return result;
}
}
+ }
return {index: [], found: []};
};

ADD
tests/gl-cases/pixelmapLayer.js
+var $ = require('jquery');
+var geo = require('../test-utils').geo;
+
+describe('webglPixelmapLayer', function () {
+ var imageTest = require('../image-test');
+
+ var map, layer;
+
+ beforeEach(function () {
+ imageTest.prepareImageTest();
+ });
+
+ afterEach(function () {
+ map.exit();
+ });
+
+ /**
+ * Create a tiled pixelmap layer.
+ *
+ * @param {object} layerParams Optional layer parameters.
+ */
+ function createPixelmap(layerParams) {
+ var params = geo.util.pixelCoordinateParams('#map', 4096, 4096, 2048, 2048);
+ params.layer.url = '/data/pixelmap_{z}_{x}_{y}.png';
+ params.layer.data = new Array(5112).fill(0);
+ params.layer.style = {
+ color: (d, i) => {
+ if (i % 2) {
+ return 'yellow';
+ }
+ i = Math.floor(i / 2);
+ switch (i % 3) {
+ case 0: return 'red';
+ case 1: return 'green';
+ case 2: return 'blue';
+ }
+ }
+ };
+ if (layerParams) {
+ params.layer = $.extend({}, params.layer, layerParams);
+ }
+ map = geo.map(params.map);
+ layer = map.createLayer('pixelmap', params.layer);
+ map.draw();
+ }
+
+ it('basic', function (done) {
+ createPixelmap();
+ expect(layer instanceof geo.pixelmapLayer).toBe(true);
+ expect(layer.features().length).toBe(2);
+ imageTest.imageTest('pixelmapLayer', null, 0.0015, done, map.onIdle, 5000, 2);
+ });
+ it('color in params', function () {
+ createPixelmap({style: undefined, color: 'black'});
+ expect(layer.style.get('color')(0, 0)).toEqual({r: 0, b: 0, g: 0});
+ });
+ it('geoOn and geoOff', function (done) {
+ createPixelmap();
+ var click = false;
+ layer.geoOn(geo.event.feature.mouseclick, () => {
+ click = true;
+ });
+ // wait for the tiles to be available
+ map.onIdle(() => {
+ expect(click).toBe(false);
+ map.interactor().simulateEvent('mousedown', {map: {x: 390, y: 200}});
+ map.interactor().simulateEvent('mouseup', {map: {x: 390, y: 200}});
+ expect(click).toBe(true);
+ click = false;
+ layer.geoOff(geo.event.feature.mouseclick);
+ map.interactor().simulateEvent('mousedown', {map: {x: 390, y: 200}});
+ map.interactor().simulateEvent('mouseup', {map: {x: 390, y: 200}});
+ expect(click).toBe(false);
+ done();
+ });
+ });
+ it('geospatial', function (done) {
+ map = geo.map({node: '#map'});
+ layer = map.createLayer('pixelmap', {
+ url: geo.osmLayer.tileSources['stamen-toner-lite'].url,
+ data: new Array(5112).fill(0),
+ color: 'black'
+ });
+ map.draw();
+ map.onIdle(() => {
+ expect(layer instanceof geo.pixelmapLayer).toBe(true);
+ expect(layer.features().length).toBe(2);
+ done();
+ });
+ });
+});

ADD
tutorials/tiled-pixelmap/index.pug
+extends ../common/index.pug
+
+block mainTutorial
+ :markdown-it
+ # Tutorial - Tiled Pixelmap
+
+ A pixelmap is a way of representing categorical data. It uses a lossless
+ image where, instead of the pixels representing colors, they represent
+ category indices. This is usually compactly represented as a integer split
+ across the red, green, and blue channels, where the category index is
+ computed as `(red value) + (green value * 256) + (blue value * 65536)`.
+
+ For large datasets, a single image might be too large to display at once.
+ Much like tile layers used for maps and large images, a pixelmap can also
+ be rendered from a tiled image using the `pixelmapLayer`. The
+ `pixelmapLayer` is essentially a hybrid between a tile image layer and a
+ pixelmap feature.
+
+ First, let's create our map in the appropriate image coordinates for our
+ pixelmap tile image.
+
+ +codeblock('javascript', 1, true).
+ // We happen to know that our data is an image that is 4096 x 4096 and is
+ // divided into tiles that are 2048 x 2048. Get paraemters for a map and
+ // tile layer with appropriate values.
+ var params = geo.util.pixelCoordinateParams(
+ '#map', 4096, 4096, 2048, 2048);
+ // Now create the map
+ map = geo.map(params.map);
+
+ :markdown-it
+ We can use some of the parameters generated by
+ `geo.util.pixelCoordinateParams` for the pixelmap tile layer. We need to
+ modify the url to find the image tiles, create an appropriate data array
+ with the categorical data, and determine how to map colors to the
+ categories.
+
+ Our categorical image data was generated from a small portion of a
+ histology image using a superpixel algorithm. After dividing the source
+ image into superpixels, the edge of each superpixel was located and set as
+ a separate index value. These happen to always have an index value one
+ greater than the superpixel itself (so the main superpixels are indices
+ 0, 2, 4, ..., and the corresponding edges are indices 1, 3, 5, ...). This
+ data set happens to have 5112 distinct index values. We start with a data
+ array of all zeroes.
+
+ +codeblock('javascript', 2, 1, true).
+ // modify the suggest layer parameters
+ params.layer.url = '../../data/pixelmap_{z}_{y}_{x}.png';
+ // if we use transparency, we don't want to keep lower tile levels -- they
+ // will show through
+ params.layer.keepLower = false;
+ // start with data that is all zeroes
+ params.layer.data = new Array(5112).fill(0);
+ // color our edges yellow and just cycle through red, green, and blue for
+ // the superpixels
+ params.layer.style = {
+ color: (d, i) => {
+ if (i % 2) {
+ return 'yellow';
+ }
+ i = Math.floor(i / 2);
+ switch (i % 3) {
+ case 0: return 'red';
+ case 1: return 'green';
+ case 2: return 'blue';
+ }
+ }
+ };
+ // create our pixelmap layer and draw it
+ var pixelmap = map.createLayer('pixelmap', params.layer).draw();
+
+ :markdown-it
+ It is more useful to allow the user to modify the data. Change our
+ coloring so that superpixels start transparent with yellow edges and cycle
+ through red, green, and blue as they are selected.
+
+ +codeblock('javascript', 3, 2).
+ pixelmap.style('color', (d, i) => {
+ switch (d) {
+ case 1: return i % 2 ? '#ff0000': '#ff000080';
+ case 2: return i % 2 ? '#008000': '#00800080';
+ case 3: return i % 2 ? '#0000ff': '#0000ff80';
+ default: return i % 2 ? '#ffff00' : '#00000000';
+ }
+ }).draw();
+ pixelmap.geoOn(geo.event.feature.mouseclick, function (evt) {
+ // use the index of the superpixel rather than its border, regardless of
+ // which was clicked.
+ var index = evt.index - evt.index % 2;
+ var data = pixelmap.data();
+ // change the value of the superpixel and its border
+ data[index] = data[index + 1] = (data[index] + 1) % 4;
+ // since we changed the data directory, just tell the layer that it has
+ // been modified and to redraw.
+ pixelmap.modified().draw();
+ });
+
+ :markdown-it
+ We might want to make it easier for a user to set many superpixels to the
+ same state at once. If the user holds down shift, then the color that the
+ first superpixel that is clicked on is applied to any superpixel entered
+ until the mouse button is released. By using shift, the core pan feature
+ isn't affected.
+
+ +codeblock('javascript', 4, 3).
+ // disable the default action for left mouse button + shift
+ map.interactor().removeAction(geo.geo_action.zoomselect);
+ // if the mouse moves, decide whether we should handle it
+ var newValue = undefined;
+ pixelmap.geoOn(geo.event.feature.mousemove, function (evt) {
+ // if shift or the left button aren't down, stop setting values
+ if (!evt.mouse.modifiers.shift || !evt.mouse.buttons.left) {
+ newValue = undefined;
+ return;
+ }
+ // determine which superpixel to modify
+ var index = evt.index - evt.index % 2;
+ var data = pixelmap.data();
+ // if we just started a movement with shift, update the value and record it
+ if (newValue === undefined) {
+ data[index] = data[index + 1] = (data[index] + 1) % 4;
+ newValue = data[index];
+ pixelmap.modified().draw();
+ }
+ // if we are moving and things have changed, update
+ if (data[index] !== newValue) {
+ data[index] = data[index + 1] = newValue;
+ pixelmap.modified().draw();
+ }
+ });

ADD
tutorials/tiled-pixelmap/thumb.jpg
tutorials/tiled-pixelmap/thumb.jpg
Binary files /dev/null and b/tutorials/tiled-pixelmap/thumb.jpg differ

ADD
tutorials/tiled-pixelmap/tutorial.json
+{
+ "title": "Tile Pixelmap",
+ "hideNavbar": true,
+ "level": 1,
+ "tutorialCss": [],
+ "tutorialJs": [],
+ "about": {
+ "text": "Render large categorical data via a tiled pixelmap."
+ }
+}

Add tests and a tutorial.