From 2f498761801766af3a4366e72361a154e7c1f66f Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 28 Oct 2019 15:13:43 +0100 Subject: [PATCH 1/5] Expressions / add != operator & slightly better doc --- src/ol/style/expressions.js | 63 +++++++++++++++++--------- test/spec/ol/style/expressions.test.js | 2 + 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js index d2355648e7..a44aae4501 100644 --- a/src/ol/style/expressions.js +++ b/src/ol/style/expressions.js @@ -41,14 +41,15 @@ import {asArray, isStringColor} from '../color.js'; * between `output1` and `outputN`. * * * Logical operators: - * * `['<', value1, value2]` returns `1` if `value1` is strictly lower than value 2, or `0` otherwise. - * * `['<=', value1, value2]` returns `1` if `value1` is lower than or equals value 2, or `0` otherwise. - * * `['>', value1, value2]` returns `1` if `value1` is strictly greater than value 2, or `0` otherwise. - * * `['>=', value1, value2]` returns `1` if `value1` is greater than or equals value 2, or `0` otherwise. - * * `['==', value1, value2]` returns `1` if `value1` equals value 2, or `0` otherwise. - * * `['!', value1]` returns `0` if `value1` strictly greater than `0`, or `1` otherwise. - * * `['between', value1, value2, value3]` returns `1` if `value1` is contained between `value2` and `value3` - * (inclusively), or `0` otherwise. + * * `['<', value1, value2]` returns `true` if `value1` is strictly lower than value 2, or `false` otherwise. + * * `['<=', value1, value2]` returns `true` if `value1` is lower than or equals value 2, or `false` otherwise. + * * `['>', value1, value2]` returns `true` if `value1` is strictly greater than value 2, or `false` otherwise. + * * `['>=', value1, value2]` returns `true` if `value1` is greater than or equals value 2, or `false` otherwise. + * * `['==', value1, value2]` returns `true` if `value1` equals value 2, or `false` otherwise. + * * `['!=', value1, value2]` returns `true` if `value1` equals value 2, or `false` otherwise. + * * `['!', value1]` returns `false` if `value1` is `true` or greater than `0`, or `true` otherwise. + * * `['between', value1, value2, value3]` returns `true` if `value1` is contained between `value2` and `value3` + * (inclusively), or `false` otherwise. * * * Conversion operators: * * `['array', value1, ...valueN]` creates a numerical array from `number` values; please note that the amount of @@ -59,6 +60,7 @@ import {asArray, isStringColor} from '../color.js'; * * Values can either be literals or another operator, as they will be evaluated recursively. * Literal values can be of the following types: + * * `boolean` * * `number` * * `string` * * {@link module:ol/color~Color} @@ -252,9 +254,9 @@ function assertNumber(value) { throw new Error(`A numeric value was expected, got ${JSON.stringify(value)} instead`); } } -function assertNumbers(arr) { - for (let i = 0; i < arr.length; i++) { - assertNumber(arr[i]); +function assertNumbers(values) { + for (let i = 0; i < values.length; i++) { + assertNumber(values[i]); } } function assertString(value) { @@ -349,6 +351,7 @@ Operators['resolution'] = { return 'u_resolution'; } }; + Operators['*'] = { getReturnType: function(args) { return ValueTypes.NUMBER; @@ -421,6 +424,7 @@ Operators['^'] = { return `pow(${expressionToGlsl(context, args[0])}, ${expressionToGlsl(context, args[1])})`; } }; + Operators['>'] = { getReturnType: function(args) { return ValueTypes.BOOLEAN; @@ -461,16 +465,31 @@ Operators['<='] = { return `(${expressionToGlsl(context, args[0])} <= ${expressionToGlsl(context, args[1])})`; } }; -Operators['=='] = { - getReturnType: function(args) { - return ValueTypes.BOOLEAN; - }, - toGlsl: function(context, args) { - assertArgsCount(args, 2); - assertNumbers(args); - return `(${expressionToGlsl(context, args[0])} == ${expressionToGlsl(context, args[1])})`; - } -}; + +function getEqualOperator(operator) { + return { + getReturnType: function(args) { + return ValueTypes.BOOLEAN; + }, + toGlsl: function(context, args) { + assertArgsCount(args, 2); + + // find common type + let type = ValueTypes.ANY; + for (let i = 0; i < args.length; i++) { + type = type & getValueType(args[i]); + } + if (type === 0) { + throw new Error(`All arguments should be of compatible type, got ${JSON.stringify(args)} instead`); + } + + return `(${expressionToGlsl(context, args[0], type)} ${operator} ${expressionToGlsl(context, args[1], type)})`; + } + }; +} +Operators['=='] = getEqualOperator('=='); +Operators['!='] = getEqualOperator('!='); + Operators['!'] = { getReturnType: function(args) { return ValueTypes.BOOLEAN; @@ -494,6 +513,7 @@ Operators['between'] = { return `(${value} >= ${min} && ${value} <= ${max})`; } }; + Operators['array'] = { getReturnType: function(args) { return ValueTypes.NUMBER_ARRAY; @@ -526,6 +546,7 @@ Operators['color'] = { return `vec${args.length}(${parsedArgs.join(', ')})`; } }; + Operators['interpolate'] = { getReturnType: function(args) { let type = ValueTypes.COLOR | ValueTypes.NUMBER; diff --git a/test/spec/ol/style/expressions.test.js b/test/spec/ol/style/expressions.test.js index d2aa7bf273..029dbe8ae8 100644 --- a/test/spec/ol/style/expressions.test.js +++ b/test/spec/ol/style/expressions.test.js @@ -136,6 +136,7 @@ describe('ol.style.expressions', function() { expect(getValueType(['<', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); expect(getValueType(['<=', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); expect(getValueType(['==', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); + expect(getValueType(['!=', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); expect(getValueType(['between', ['get', 'attr4'], -4.0, 5.0])).to.eql(ValueTypes.BOOLEAN); expect(getValueType(['!', ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); expect(getValueType(['array', ['get', 'attr4'], 1, 2, 3])).to.eql(ValueTypes.NUMBER_ARRAY); @@ -170,6 +171,7 @@ describe('ol.style.expressions', function() { expect(expressionToGlsl(context, ['<', 10, ['get', 'attr4']])).to.eql('(10.0 < a_attr4)'); expect(expressionToGlsl(context, ['<=', 10, ['get', 'attr4']])).to.eql('(10.0 <= a_attr4)'); expect(expressionToGlsl(context, ['==', 10, ['get', 'attr4']])).to.eql('(10.0 == a_attr4)'); + expect(expressionToGlsl(context, ['!=', 10, ['get', 'attr4']])).to.eql('(10.0 != a_attr4)'); expect(expressionToGlsl(context, ['between', ['get', 'attr4'], -4.0, 5.0])).to.eql('(a_attr4 >= -4.0 && a_attr4 <= 5.0)'); expect(expressionToGlsl(context, ['!', ['get', 'attr4']])).to.eql('(!a_attr4)'); expect(expressionToGlsl(context, ['array', ['get', 'attr4'], 1, 2, 3])).to.eql('vec4(a_attr4, 1.0, 2.0, 3.0)'); From 2a2783c086fb38fa577d7e7a50fa1868e007c13c Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 28 Oct 2019 15:17:08 +0100 Subject: [PATCH 2/5] ShaderBuilder / better handling of strings variables/attributes Now values which are not mentioned in the style are still added to the string literals mapping. Also an error will be thrown if a style references a missing variable. --- src/ol/webgl/ShaderBuilder.js | 14 +++++-- test/spec/ol/webgl/shaderbuilder.test.js | 50 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/ol/webgl/ShaderBuilder.js b/src/ol/webgl/ShaderBuilder.js index e3a141e187..bea9a05926 100644 --- a/src/ol/webgl/ShaderBuilder.js +++ b/src/ol/webgl/ShaderBuilder.js @@ -3,7 +3,7 @@ * @module ol/webgl/ShaderBuilder */ -import {expressionToGlsl, ValueTypes} from '../style/expressions.js'; +import {expressionToGlsl, stringToGlsl, ValueTypes} from '../style/expressions.js'; /** * @typedef {Object} VaryingDescription @@ -445,8 +445,14 @@ export function parseLiteralStyle(style) { fragContext.variables.forEach(function(varName) { builder.addUniform(`float u_${varName}`); uniforms[`u_${varName}`] = function() { - return style.variables && style.variables[varName] !== undefined ? - style.variables[varName] : 0; + if (!style.variables || style.variables[varName] === undefined) { + throw new Error(`The following variable is missing from the style: ${varName}`); + } + let value = style.variables[varName]; + if (typeof value === 'string') { + value = parseFloat(stringToGlsl(vertContext, value)); + } + return value !== undefined ? value : -9999999; // to avoid matching with the first string literal }; }); @@ -481,7 +487,7 @@ export function parseLiteralStyle(style) { callback: function(feature) { let value = feature.get(attributeName); if (typeof value === 'string') { - value = vertContext.stringLiteralsMap[value]; + value = parseFloat(stringToGlsl(vertContext, value)); } return value !== undefined ? value : -9999999; // to avoid matching with the first string literal } diff --git a/test/spec/ol/webgl/shaderbuilder.test.js b/test/spec/ol/webgl/shaderbuilder.test.js index f2b9145c96..4b08699ebb 100644 --- a/test/spec/ol/webgl/shaderbuilder.test.js +++ b/test/spec/ol/webgl/shaderbuilder.test.js @@ -402,6 +402,56 @@ void main(void) { expect(result.attributes).to.eql([]); expect(result.uniforms).to.have.property('u_ratio'); }); + + it('correctly adds string variables to the string literals mapping', function() { + const result = parseLiteralStyle({ + variables: { + mySize: 'abcdef' + }, + symbol: { + symbolType: 'square', + size: ['match', ['var', 'mySize'], 'abc', 10, 'def', 20, 30], + color: 'red' + } + }); + + expect(result.uniforms['u_mySize']()).to.be.greaterThan(0); + }); + + it('throws when a variable is requested but not present in the style', function(done) { + const result = parseLiteralStyle({ + variables: {}, + symbol: { + symbolType: 'square', + size: ['var', 'mySize'], + color: 'red' + } + }); + + try { + result.uniforms['u_mySize'](); + } catch (e) { + done(); + } + done(true); + }); + + it('throws when a variable is requested but the style does not have a variables dict', function(done) { + const result = parseLiteralStyle({ + symbol: { + symbolType: 'square', + size: ['var', 'mySize'], + color: 'red' + } + }); + + try { + result.uniforms['u_mySize'](); + } catch (e) { + done(); + } + done(true); + }); }); }); From 501c90b0a26e20ae74f40b591223e7f5db0ea8e6 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 28 Oct 2019 15:52:29 +0100 Subject: [PATCH 3/5] Expressions / introduced the case operator This operator is used for if/else control flow --- src/ol/style/expressions.js | 39 +++++++++++- test/spec/ol/style/expressions.test.js | 88 ++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js index a44aae4501..db4cdfe087 100644 --- a/src/ol/style/expressions.js +++ b/src/ol/style/expressions.js @@ -27,6 +27,9 @@ import {asArray, isStringColor} from '../color.js'; * * `['^', value1, value1]` returns the value of `value1` raised to the `value2` power * * * Transform operators: + * * `['case', condition1, output1, ...conditionN, outputN, fallback]` selects the first output whose corresponding + * condition evaluates to `true`. If no match is found, returns the `fallback` value. + * All conditions should be `boolean`, output and fallback can be any kind. * * `['match', input, match1, output1, ...matchN, outputN, fallback]` compares the `input` value against all * provided `matchX` values, returning the output associated with the first valid match. If no match is found, * returns the `fallback` value. @@ -289,6 +292,11 @@ function assertArgsEven(args) { throw new Error(`An even amount of arguments was expected, got ${args} instead`); } } +function assertArgsOdd(args) { + if (args.length % 2 === 0) { + throw new Error(`An even amount of arguments was expected, got ${args} instead`); + } +} function assertUniqueInferredType(args, types) { if (!isTypeUnique(types)) { throw new Error(`Could not infer only one type from the following expression: ${JSON.stringify(args)}`); @@ -601,7 +609,6 @@ Operators['match'] = { assertArgsEven(args); assertArgsMinCount(args, 4); - // compute input/output types const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY; const outputType = Operators['match'].getReturnType(args) & typeHint; assertUniqueInferredType(args, outputType); @@ -617,3 +624,33 @@ Operators['match'] = { return result; } }; +Operators['case'] = { + getReturnType: function(args) { + let type = ValueTypes.ANY; + for (let i = 1; i < args.length; i += 2) { + type = type & getValueType(args[i]); + } + type = type & getValueType(args[args.length - 1]); + return type; + }, + toGlsl: function(context, args, opt_typeHint) { + assertArgsOdd(args); + assertArgsMinCount(args, 3); + + const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY; + const outputType = Operators['case'].getReturnType(args) & typeHint; + assertUniqueInferredType(args, outputType); + for (let i = 0; i < args.length - 1; i += 2) { + assertBoolean(args[i]); + } + + const fallback = expressionToGlsl(context, args[args.length - 1], outputType); + let result = null; + for (let i = args.length - 3; i >= 0; i -= 2) { + const condition = expressionToGlsl(context, args[i]); + const output = expressionToGlsl(context, args[i + 1], outputType); + result = `(${condition} ? ${output} : ${result || fallback})`; + } + return result; + } +}; diff --git a/test/spec/ol/style/expressions.test.js b/test/spec/ol/style/expressions.test.js index 029dbe8ae8..bff0248ddd 100644 --- a/test/spec/ol/style/expressions.test.js +++ b/test/spec/ol/style/expressions.test.js @@ -294,6 +294,94 @@ describe('ol.style.expressions', function() { }); }); + describe('case operator', function() { + let context; + + beforeEach(function () { + context = { + variables: [], + attributes: [], + stringLiteralsMap: {} + }; + }); + + it('correctly guesses the output type', function() { + expect(getValueType(['case', true, 0, false, [3, 4, 5], 'green'])) + .to.eql(ValueTypes.NONE); + expect(getValueType(['case', true, 0, false, 1, 2])) + .to.eql(ValueTypes.NUMBER); + expect(getValueType(['case', true, [0, 0, 0], true, [1, 2, 3], ['get', 'attr'], [4, 5, 6, 7], [8, 9, 0]])) + .to.eql(ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY); + expect(getValueType(['case', true, 'red', true, 'yellow', ['get', 'attr'], 'green', 'white'])) + .to.eql(ValueTypes.COLOR | ValueTypes.STRING); + expect(getValueType(['case', true, [0, 0], false, [1, 1], [2, 2]])) + .to.eql(ValueTypes.NUMBER_ARRAY); + }); + + it('throws if no single output type could be inferred', function() { + let thrown = false; + try { + expressionToGlsl(context, ['case', false, 'red', true, 'yellow', 'green'], ValueTypes.COLOR); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(false); + + try { + expressionToGlsl(context, ['case', true, 'red', true, 'yellow', 'green']); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + + thrown = false; + try { + expressionToGlsl(context, ['case', true, 'red', false, 'yellow', 'green'], ValueTypes.NUMBER); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + + thrown = false; + try { + expressionToGlsl(context, ['case', true, 'red', false, 'yellow', 'not_a_color'], ValueTypes.COLOR); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + }); + + it('throws if invalid argument count', function() { + let thrown = false; + try { + expressionToGlsl(context, ['case', true, 0, false, 1]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + + thrown = false; + try { + expressionToGlsl(context, ['case', true, 0]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + + try { + expressionToGlsl(context, ['case', false]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + }); + + it('correctly parses the expression (colors)', function() { + expect(expressionToGlsl(context, ['case', ['>', ['get', 'attr'], 3], 'red', ['>', ['get', 'attr'], 1], 'yellow', 'white'], ValueTypes.COLOR)) + .to.eql('((a_attr > 3.0) ? vec4(1.0, 0.0, 0.0, 1.0) : ((a_attr > 1.0) ? vec4(1.0, 1.0, 0.0, 1.0) : vec4(1.0, 1.0, 1.0, 1.0)))'); + }); + }); + describe('match operator', function() { let context; From b96e70e9521aa0d41daf1a25b2d982e40ba62bfb Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 28 Oct 2019 15:53:55 +0100 Subject: [PATCH 4/5] Expressions / renamed mod to % to be more in line with MB style spec --- examples/filter-points-webgl.js | 2 +- src/ol/style/expressions.js | 4 ++-- test/spec/ol/style/expressions.test.js | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/filter-points-webgl.js b/examples/filter-points-webgl.js index fa09676c8b..b2d39f0bd6 100644 --- a/examples/filter-points-webgl.js +++ b/examples/filter-points-webgl.js @@ -18,7 +18,7 @@ const period = 12; // animation period in seconds const animRatio = ['^', ['/', - ['mod', + ['%', ['+', ['time'], [ diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js index db4cdfe087..25921154b8 100644 --- a/src/ol/style/expressions.js +++ b/src/ol/style/expressions.js @@ -23,7 +23,7 @@ import {asArray, isStringColor} from '../color.js'; * * `['+', value1, value1]` adds `value1` and `value2` * * `['-', value1, value1]` subtracts `value2` from `value1` * * `['clamp', value, low, high]` clamps `value` between `low` and `high` - * * `['mod', value1, value1]` returns the result of `value1 % value2` (modulo) + * * `['%', value1, value1]` returns the result of `value1 % value2` (modulo) * * `['^', value1, value1]` returns the value of `value1` raised to the `value2` power * * * Transform operators: @@ -412,7 +412,7 @@ Operators['clamp'] = { return `clamp(${expressionToGlsl(context, args[0])}, ${min}, ${max})`; } }; -Operators['mod'] = { +Operators['%'] = { getReturnType: function(args) { return ValueTypes.NUMBER; }, diff --git a/test/spec/ol/style/expressions.test.js b/test/spec/ol/style/expressions.test.js index bff0248ddd..d3d15fdf48 100644 --- a/test/spec/ol/style/expressions.test.js +++ b/test/spec/ol/style/expressions.test.js @@ -130,7 +130,7 @@ describe('ol.style.expressions', function() { expect(getValueType(['*', ['get', 'size'], 12])).to.eql(ValueTypes.NUMBER); expect(getValueType(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql(ValueTypes.NUMBER); expect(getValueType(['^', 10, 2])).to.eql(ValueTypes.NUMBER); - expect(getValueType(['mod', ['time'], 10])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['%', ['time'], 10])).to.eql(ValueTypes.NUMBER); expect(getValueType(['>', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); expect(getValueType(['>=', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); expect(getValueType(['<', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); @@ -165,7 +165,7 @@ describe('ol.style.expressions', function() { expect(expressionToGlsl(context, ['+', ['*', ['get', 'size'], 0.001], 12])).to.eql('((a_size * 0.001) + 12.0)'); expect(expressionToGlsl(context, ['/', ['-', ['get', 'size'], 20], 100])).to.eql('((a_size - 20.0) / 100.0)'); expect(expressionToGlsl(context, ['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql('clamp(a_attr2, a_attr3, 20.0)'); - expect(expressionToGlsl(context, ['^', ['mod', ['time'], 10], 2])).to.eql('pow(mod(u_time, 10.0), 2.0)'); + expect(expressionToGlsl(context, ['^', ['%', ['time'], 10], 2])).to.eql('pow(mod(u_time, 10.0), 2.0)'); expect(expressionToGlsl(context, ['>', 10, ['get', 'attr4']])).to.eql('(10.0 > a_attr4)'); expect(expressionToGlsl(context, ['>=', 10, ['get', 'attr4']])).to.eql('(10.0 >= a_attr4)'); expect(expressionToGlsl(context, ['<', 10, ['get', 'attr4']])).to.eql('(10.0 < a_attr4)'); @@ -297,7 +297,7 @@ describe('ol.style.expressions', function() { describe('case operator', function() { let context; - beforeEach(function () { + beforeEach(function() { context = { variables: [], attributes: [], @@ -619,7 +619,7 @@ describe('ol.style.expressions', function() { ['linear'], ['^', ['/', - ['mod', + ['%', ['+', ['time'], [ From 6c0dd6152daad279aa00cc5aac0a8dd82db35530 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 28 Oct 2019 15:54:20 +0100 Subject: [PATCH 5/5] Modified the icon-sprite-webgl example to allow filtering on a string attribute --- examples/icon-sprite-webgl.html | 4 + examples/icon-sprite-webgl.js | 137 ++++++++++++++++++++------------ 2 files changed, 90 insertions(+), 51 deletions(-) diff --git a/examples/icon-sprite-webgl.html b/examples/icon-sprite-webgl.html index 2af2b66f59..2a6bce2201 100644 --- a/examples/icon-sprite-webgl.html +++ b/examples/icon-sprite-webgl.html @@ -20,3 +20,7 @@ cloak: ---
Current sighting:
+
+ Filter by UFO shape: + +
diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index 434473ca36..45d78a80fd 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -10,6 +10,22 @@ import WebGLPointsLayer from '../src/ol/layer/WebGLPoints.js'; const key = 'pk.eyJ1IjoidHNjaGF1YiIsImEiOiJjaW5zYW5lNHkxMTNmdWttM3JyOHZtMmNtIn0.CDIBD8H-G2Gf-cPkIuWtRg'; +const map = new Map({ + layers: [ + new TileLayer({ + source: new TileJSON({ + url: 'https://api.tiles.mapbox.com/v4/mapbox.world-dark.json?secure&access_token=' + key, + crossOrigin: 'anonymous' + }) + }) + ], + target: document.getElementById('map'), + view: new View({ + center: [0, 4000000], + zoom: 2 + }) +}); + const vectorSource = new Vector({ features: [], attributions: 'National UFO Reporting Center' @@ -20,6 +36,15 @@ const newColor = [180, 255, 200]; const size = 16; const style = { + variables: { + filterShape: 'all' + }, + filter: [ + 'case', + ['!=', ['var', 'filterShape'], 'all'], + ['==', ['get', 'shape'], ['var', 'filterShape']], + true + ], symbol: { symbolType: 'image', src: 'data/ufo_shapes.png', @@ -51,61 +76,71 @@ const style = { } }; -function loadData() { - const client = new XMLHttpRequest(); - client.open('GET', 'data/csv/ufo_sighting_data.csv'); - client.onload = function() { - const csv = client.responseText; - const features = []; - - let prevIndex = csv.indexOf('\n') + 1; // scan past the header line - - let curIndex; - while ((curIndex = csv.indexOf('\n', prevIndex)) != -1) { - const line = csv.substr(prevIndex, curIndex - prevIndex).split(','); - prevIndex = curIndex + 1; - - const coords = fromLonLat([parseFloat(line[5]), parseFloat(line[4])]); - - // only keep valid points - if (isNaN(coords[0]) || isNaN(coords[1])) { - continue; - } - - features.push(new Feature({ - datetime: line[0], - year: parseInt(/[0-9]{4}/.exec(line[0])[0]), // extract the year as int - shape: line[2], - duration: line[3], - geometry: new Point(coords) - })); - } - vectorSource.addFeatures(features); - }; - client.send(); +// key is shape name, value is sightings count +const shapeTypes = { + all: 0 +}; +const shapeSelect = document.getElementById('shape-filter'); +shapeSelect.addEventListener('input', function() { + style.variables.filterShape = shapeSelect.options[shapeSelect.selectedIndex].value; + map.render(); +}); +function fillShapeSelect() { + Object.keys(shapeTypes) + .sort(function(a, b) { + return shapeTypes[b] - shapeTypes[a]; + }) + .forEach(function(shape) { + const option = document.createElement('option'); + option.text = `${shape} (${shapeTypes[shape]} sightings)`; + option.value = shape; + shapeSelect.appendChild(option); + }); } -loadData(); +const client = new XMLHttpRequest(); +client.open('GET', 'data/csv/ufo_sighting_data.csv'); +client.onload = function() { + const csv = client.responseText; + const features = []; -const map = new Map({ - layers: [ - new TileLayer({ - source: new TileJSON({ - url: 'https://api.tiles.mapbox.com/v4/mapbox.world-dark.json?secure&access_token=' + key, - crossOrigin: 'anonymous' - }) - }), - new WebGLPointsLayer({ - source: vectorSource, - style: style - }) - ], - target: document.getElementById('map'), - view: new View({ - center: [0, 4000000], - zoom: 2 + let prevIndex = csv.indexOf('\n') + 1; // scan past the header line + + let curIndex; + while ((curIndex = csv.indexOf('\n', prevIndex)) != -1) { + const line = csv.substr(prevIndex, curIndex - prevIndex).split(','); + prevIndex = curIndex + 1; + + const coords = fromLonLat([parseFloat(line[5]), parseFloat(line[4])]); + + // only keep valid points + if (isNaN(coords[0]) || isNaN(coords[1])) { + continue; + } + + const shape = line[2]; + shapeTypes[shape] = (shapeTypes[shape] ? shapeTypes[shape] : 0) + 1; + shapeTypes['all']++; + + features.push(new Feature({ + datetime: line[0], + year: parseInt(/[0-9]{4}/.exec(line[0])[0]), // extract the year as int + shape: shape, + duration: line[3], + geometry: new Point(coords) + })); + } + vectorSource.addFeatures(features); + fillShapeSelect(); +}; +client.send(); + +map.addLayer( + new WebGLPointsLayer({ + source: vectorSource, + style: style }) -}); +); const info = document.getElementById('info'); map.on('pointermove', function(evt) {