@mapwhit/tilerenderer 0.48.0 → 0.49.0

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.
@@ -1,4 +1,4 @@
1
- const ShelfPack = require('@mapbox/shelf-pack');
1
+ const potpack = require('potpack');
2
2
 
3
3
  const { RGBAImage } = require('../util/image');
4
4
  const { ImagePosition } = require('./image_atlas');
@@ -26,9 +26,8 @@ class ImageManager {
26
26
  constructor() {
27
27
  this.images = {};
28
28
 
29
- this.shelfPack = new ShelfPack(64, 64, { autoResize: true });
30
29
  this.patterns = {};
31
- this.atlasImage = new RGBAImage({ width: 64, height: 64 });
30
+ this.atlasImage = new RGBAImage({ width: 1, height: 1 });
32
31
  this.dirty = true;
33
32
  }
34
33
 
@@ -56,12 +55,7 @@ class ImageManager {
56
55
  removeImage(id) {
57
56
  assert(this.images[id]);
58
57
  delete this.images[id];
59
-
60
- const pattern = this.patterns[id];
61
- if (pattern) {
62
- this.shelfPack.unref(pattern.bin);
63
- delete this.patterns[id];
64
- }
58
+ delete this.patterns[id];
65
59
  }
66
60
 
67
61
  listImages() {
@@ -89,10 +83,8 @@ class ImageManager {
89
83
  // Pattern stuff
90
84
 
91
85
  getPixelSize() {
92
- return {
93
- width: this.shelfPack.w,
94
- height: this.shelfPack.h
95
- };
86
+ const { width, height } = this.atlasImage;
87
+ return { width, height };
96
88
  }
97
89
 
98
90
  getPattern(id) {
@@ -106,36 +98,13 @@ class ImageManager {
106
98
  return null;
107
99
  }
108
100
 
109
- const width = image.data.width + padding * 2;
110
- const height = image.data.height + padding * 2;
111
-
112
- const bin = this.shelfPack.packOne(width, height);
113
- if (!bin) {
114
- return null;
115
- }
116
-
117
- this.atlasImage.resize(this.getPixelSize());
118
-
119
- const src = image.data;
120
- const dst = this.atlasImage;
121
-
122
- const x = bin.x + padding;
123
- const y = bin.y + padding;
124
- const w = src.width;
125
- const h = src.height;
126
-
127
- RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x, y }, { width: w, height: h });
128
-
129
- // Add 1 pixel wrapped padding on each side of the image.
130
- RGBAImage.copy(src, dst, { x: 0, y: h - 1 }, { x: x, y: y - 1 }, { width: w, height: 1 }); // T
131
- RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x, y: y + h }, { width: w, height: 1 }); // B
132
- RGBAImage.copy(src, dst, { x: w - 1, y: 0 }, { x: x - 1, y: y }, { width: 1, height: h }); // L
133
- RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x + w, y: y }, { width: 1, height: h }); // R
134
-
135
- this.dirty = true;
136
-
101
+ const w = image.data.width + padding * 2;
102
+ const h = image.data.height + padding * 2;
103
+ const bin = { w, h, x: 0, y: 0 };
137
104
  const position = new ImagePosition(bin, image);
138
105
  this.patterns[id] = { bin, position };
106
+ this._updatePatternAtlas();
107
+
139
108
  return position;
140
109
  }
141
110
 
@@ -150,6 +119,37 @@ class ImageManager {
150
119
 
151
120
  this.atlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
152
121
  }
122
+
123
+ _updatePatternAtlas() {
124
+ const bins = [];
125
+ for (const id in this.patterns) {
126
+ bins.push(this.patterns[id].bin);
127
+ }
128
+
129
+ const { w, h } = potpack(bins);
130
+
131
+ const dst = this.atlasImage;
132
+ dst.resize({ width: w ?? 1, height: h ?? 1 });
133
+
134
+ for (const id in this.patterns) {
135
+ const { bin } = this.patterns[id];
136
+ const x = bin.x + padding;
137
+ const y = bin.y + padding;
138
+ const src = this.images[id].data;
139
+ const w = src.width;
140
+ const h = src.height;
141
+
142
+ RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x, y }, { width: w, height: h });
143
+
144
+ // Add 1 pixel wrapped padding on each side of the image.
145
+ RGBAImage.copy(src, dst, { x: 0, y: h - 1 }, { x: x, y: y - 1 }, { width: w, height: 1 }); // T
146
+ RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x, y: y + h }, { width: w, height: 1 }); // B
147
+ RGBAImage.copy(src, dst, { x: w - 1, y: 0 }, { x: x - 1, y: y }, { width: 1, height: h }); // L
148
+ RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x + w, y: y }, { width: 1, height: h }); // R
149
+ }
150
+
151
+ this.dirty = true;
152
+ }
153
153
  }
154
154
 
155
155
  module.exports = ImageManager;
@@ -19,6 +19,12 @@ const layout = new Properties({
19
19
  default: false,
20
20
  expression: { parameters: ['zoom'] }
21
21
  }),
22
+ 'symbol-z-order': new DataConstantProperty({
23
+ type: 'enum',
24
+ values: ['viewport-y', 'source'],
25
+ default: 'viewport-y',
26
+ expression: { parameters: ['zoom'] }
27
+ }),
22
28
  'icon-allow-overlap': new DataConstantProperty({
23
29
  type: 'boolean',
24
30
  default: false,
@@ -1,6 +1,15 @@
1
1
  const assert = require('assert');
2
2
 
3
- const { ObjectType, ValueType, StringType, NumberType, BooleanType, checkSubtype, toString } = require('../types');
3
+ const {
4
+ ObjectType,
5
+ ValueType,
6
+ StringType,
7
+ NumberType,
8
+ BooleanType,
9
+ checkSubtype,
10
+ toString,
11
+ array
12
+ } = require('../types');
4
13
  const RuntimeError = require('../runtime_error');
5
14
  const { typeOf } = require('../values');
6
15
 
@@ -20,13 +29,39 @@ class Assertion {
20
29
  static parse(args, context) {
21
30
  if (args.length < 2) return context.error('Expected at least one argument.');
22
31
 
32
+ let i = 1;
33
+ let type;
34
+
23
35
  const name = args[0];
24
- assert(types[name], name);
36
+ if (name === 'array') {
37
+ let itemType;
38
+ if (args.length > 2) {
39
+ const type = args[1];
40
+ if (typeof type !== 'string' || !(type in types) || type === 'object')
41
+ return context.error('The item type argument of "array" must be one of string, number, boolean', 1);
42
+ itemType = types[type];
43
+ i++;
44
+ } else {
45
+ itemType = ValueType;
46
+ }
25
47
 
26
- const type = types[name];
48
+ let N;
49
+ if (args.length > 3) {
50
+ if (args[2] !== null && (typeof args[2] !== 'number' || args[2] < 0 || args[2] !== Math.floor(args[2]))) {
51
+ return context.error('The length argument to "array" must be a positive integer literal', 2);
52
+ }
53
+ N = args[2];
54
+ i++;
55
+ }
56
+
57
+ type = array(itemType, N);
58
+ } else {
59
+ assert(types[name], name);
60
+ type = types[name];
61
+ }
27
62
 
28
63
  const parsed = [];
29
- for (let i = 1; i < args.length; i++) {
64
+ for (; i < args.length; i++) {
30
65
  const input = context.parse(args[i], i, ValueType);
31
66
  if (!input) return null;
32
67
  parsed.push(input);
@@ -62,7 +97,19 @@ class Assertion {
62
97
  }
63
98
 
64
99
  serialize() {
65
- return [this.type.kind].concat(this.args.map(arg => arg.serialize()));
100
+ const type = this.type;
101
+ const serialized = [type.kind];
102
+ if (type.kind === 'array') {
103
+ const itemType = type.itemType;
104
+ if (itemType.kind === 'string' || itemType.kind === 'number' || itemType.kind === 'boolean') {
105
+ serialized.push(itemType.kind);
106
+ const N = type.N;
107
+ if (typeof N === 'number' || this.args.length > 1) {
108
+ serialized.push(N);
109
+ }
110
+ }
111
+ }
112
+ return serialized.concat(this.args.map(arg => arg.serialize()));
66
113
  }
67
114
  }
68
115
 
@@ -18,7 +18,6 @@ const Let = require('./let');
18
18
  const Var = require('./var');
19
19
  const Literal = require('./literal');
20
20
  const Assertion = require('./assertion');
21
- const ArrayAssertion = require('./array');
22
21
  const Coercion = require('./coercion');
23
22
  const At = require('./at');
24
23
  const Match = require('./match');
@@ -39,7 +38,7 @@ const expressions = {
39
38
  '<': LessThan,
40
39
  '>=': GreaterThanOrEqual,
41
40
  '<=': LessThanOrEqual,
42
- array: ArrayAssertion,
41
+ array: Assertion,
43
42
  at: At,
44
43
  boolean: Assertion,
45
44
  case: Case,
@@ -4,7 +4,6 @@ const { checkSubtype } = require('./types');
4
4
  const ParsingError = require('./parsing_error');
5
5
  const Literal = require('./definitions/literal');
6
6
  const Assertion = require('./definitions/assertion');
7
- const ArrayAssertion = require('./definitions/array');
8
7
  const Coercion = require('./definitions/coercion');
9
8
  const EvaluationContext = require('./evaluation_context');
10
9
  const { CollatorExpression } = require('./definitions/collator');
@@ -83,16 +82,13 @@ class ParsingContext {
83
82
  (expected.kind === 'string' ||
84
83
  expected.kind === 'number' ||
85
84
  expected.kind === 'boolean' ||
86
- expected.kind === 'object') &&
85
+ expected.kind === 'object' ||
86
+ expected.kind === 'array') &&
87
87
  actual.kind === 'value'
88
88
  ) {
89
89
  if (!options.omitTypeAnnotations) {
90
90
  parsed = new Assertion(expected, [parsed]);
91
91
  }
92
- } else if (expected.kind === 'array' && actual.kind === 'value') {
93
- if (!options.omitTypeAnnotations) {
94
- parsed = new ArrayAssertion(expected, parsed);
95
- }
96
92
  } else if (expected.kind === 'color' && (actual.kind === 'value' || actual.kind === 'string')) {
97
93
  if (!options.omitTypeAnnotations) {
98
94
  parsed = new Coercion(expected, [parsed]);
@@ -188,8 +184,7 @@ function isConstant(expression) {
188
184
  return false;
189
185
  }
190
186
 
191
- const isTypeAnnotation =
192
- expression instanceof Coercion || expression instanceof Assertion || expression instanceof ArrayAssertion;
187
+ const isTypeAnnotation = expression instanceof Coercion || expression instanceof Assertion;
193
188
 
194
189
  let childrenConstant = true;
195
190
  expression.eachChild(child => {
@@ -0,0 +1,197 @@
1
+ const { isExpressionFilter } = require('./index');
2
+
3
+ module.exports = convertFilter;
4
+
5
+ /**
6
+ * Convert the given legacy filter to (the JSON representation of) an
7
+ * equivalent expression
8
+ * @private
9
+ */
10
+ function convertFilter(filter) {
11
+ return _convertFilter(filter, {});
12
+ }
13
+
14
+ /*
15
+ * Convert the given filter to an expression, storing the expected types for
16
+ * any feature properties referenced in expectedTypes.
17
+ *
18
+ * These expected types are needed in order to construct preflight type checks
19
+ * needed for handling 'any' filters. A preflight type check is necessary in
20
+ * order to mimic legacy filters' semantics around expected type mismatches.
21
+ * For example, consider the legacy filter:
22
+ *
23
+ * ["any", ["all", [">", "y", 0], [">", "y", 0]], [">", "x", 0]]
24
+ *
25
+ * Naively, we might convert this to the expression:
26
+ *
27
+ * ["any", ["all", [">", ["get", "y"], 0], [">", ["get", "z"], 0]], [">", ["get", "x"], 0]]
28
+ *
29
+ * But if we tried to evaluate this against, say `{x: 1, y: null, z: 0}`, the
30
+ * [">", ["get", "y"], 0] would cause an evaluation error, leading to the
31
+ * entire filter returning false. Legacy filter semantics, though, ask for
32
+ * [">", "y", 0] to simply return `false` when `y` is of the wrong type,
33
+ * allowing the subsequent terms of the outer "any" expression to be evaluated
34
+ * (resulting, in this case, in a `true` value, because x > 0).
35
+ *
36
+ * We account for this by inserting a preflight type-checking expression before
37
+ * each "any" term, allowing us to avoid evaluating the actual converted filter
38
+ * if any type mismatches would cause it to produce an evalaution error:
39
+ *
40
+ * ["any",
41
+ * ["case",
42
+ * ["all", ["==", ["typeof", ["get", "y"]], "number"], ["==", ["typeof", ["get", "z"], "number]],
43
+ * ["all", [">", ["get", "y"], 0], [">", ["get", "z"], 0]],
44
+ * false
45
+ * ],
46
+ * ["case",
47
+ * ["==", ["typeof", ["get", "x"], "number"]],
48
+ * [">", ["get", "x"], 0],
49
+ * false
50
+ * ]
51
+ * ]
52
+ *
53
+ * An alternative, possibly more direct approach would be to use type checks
54
+ * in the conversion of each comparison operator, so that the converted version
55
+ * of each individual ==, >=, etc. would mimic the legacy filter semantics. The
56
+ * downside of this approach is that it can lead to many more type checks than
57
+ * would otherwise be necessary: outside the context of an "any" expression,
58
+ * bailing out due to a runtime type error (expression semantics) and returning
59
+ * false (legacy filter semantics) are equivalent: they cause the filter to
60
+ * produce a `false` result.
61
+ */
62
+ function _convertFilter(filter, expectedTypes) {
63
+ if (isExpressionFilter(filter)) {
64
+ return filter;
65
+ }
66
+
67
+ if (!filter) return true;
68
+ const op = filter[0];
69
+ if (filter.length <= 1) return op !== 'any';
70
+
71
+ let converted;
72
+
73
+ if (op === '==' || op === '!=' || op === '<' || op === '>' || op === '<=' || op === '>=') {
74
+ const [, property, value] = filter;
75
+ converted = convertComparisonOp(property, value, op, expectedTypes);
76
+ } else if (op === 'any') {
77
+ const children = filter.slice(1).map(f => {
78
+ const types = {};
79
+ const child = _convertFilter(f, types);
80
+ const typechecks = runtimeTypeChecks(types);
81
+ return typechecks === true ? child : ['case', typechecks, child, false];
82
+ });
83
+ return ['any'].concat(children);
84
+ } else if (op === 'all') {
85
+ const children = filter.slice(1).map(f => _convertFilter(f, expectedTypes));
86
+ return ['all'].concat(children);
87
+ } else if (op === 'none') {
88
+ return ['!', _convertFilter(['any'].concat(filter.slice(1)), {})];
89
+ } else if (op === 'in') {
90
+ converted = convertInOp(filter[1], filter.slice(2));
91
+ } else if (op === '!in') {
92
+ converted = convertInOp(filter[1], filter.slice(2), true);
93
+ } else if (op === 'has') {
94
+ converted = convertHasOp(filter[1]);
95
+ } else if (op === '!has') {
96
+ converted = ['!', convertHasOp(filter[1])];
97
+ } else {
98
+ converted = true;
99
+ }
100
+
101
+ return converted;
102
+ }
103
+
104
+ // Given a set of feature properties and an expected type for each one,
105
+ // construct an boolean expression that tests whether each property has the
106
+ // right type.
107
+ // E.g.: for {name: 'string', population: 'number'}, return
108
+ // [ 'all',
109
+ // ['==', ['typeof', ['get', 'name'], 'string']],
110
+ // ['==', ['typeof', ['get', 'population'], 'number]]
111
+ // ]
112
+ function runtimeTypeChecks(expectedTypes) {
113
+ const conditions = [];
114
+ for (const property in expectedTypes) {
115
+ const get = property === '$id' ? ['id'] : ['get', property];
116
+ conditions.push(['==', ['typeof', get], expectedTypes[property]]);
117
+ }
118
+ if (conditions.length === 0) return true;
119
+ if (conditions.length === 1) return conditions[0];
120
+ return ['all'].concat(conditions);
121
+ }
122
+
123
+ function convertComparisonOp(property, value, op, expectedTypes) {
124
+ let get;
125
+ if (property === '$type') {
126
+ return [op, ['geometry-type'], value];
127
+ }
128
+ if (property === '$id') {
129
+ get = ['id'];
130
+ } else {
131
+ get = ['get', property];
132
+ }
133
+
134
+ if (expectedTypes && value !== null) {
135
+ const type = typeof value;
136
+ expectedTypes[property] = type;
137
+ }
138
+
139
+ if (op === '==' && property !== '$id' && value === null) {
140
+ return [
141
+ 'all',
142
+ ['has', property], // missing property != null for legacy filters
143
+ ['==', get, null]
144
+ ];
145
+ }
146
+ if (op === '!=' && property !== '$id' && value === null) {
147
+ return [
148
+ 'any',
149
+ ['!', ['has', property]], // missing property != null for legacy filters
150
+ ['!=', get, null]
151
+ ];
152
+ }
153
+
154
+ return [op, get, value];
155
+ }
156
+
157
+ function convertInOp(property, values, negate = false) {
158
+ if (values.length === 0) return negate;
159
+
160
+ let get;
161
+ if (property === '$type') {
162
+ get = ['geometry-type'];
163
+ } else if (property === '$id') {
164
+ get = ['id'];
165
+ } else {
166
+ get = ['get', property];
167
+ }
168
+
169
+ // Determine if the list of values to be searched is homogenously typed.
170
+ // If so (and if the type is string or number), then we can use a
171
+ // [match, input, [...values], true, false] construction rather than a
172
+ // bunch of `==` tests.
173
+ let uniformTypes = true;
174
+ const type = typeof values[0];
175
+ for (const value of values) {
176
+ if (typeof value !== type) {
177
+ uniformTypes = false;
178
+ break;
179
+ }
180
+ }
181
+
182
+ if (uniformTypes && (type === 'string' || type === 'number')) {
183
+ return ['match', get, values, !negate, negate];
184
+ }
185
+
186
+ return [negate ? 'all' : 'any'].concat(values.map(v => [negate ? '!=' : '==', get, v]));
187
+ }
188
+
189
+ function convertHasOp(property) {
190
+ if (property === '$type') {
191
+ return true;
192
+ }
193
+ if (property === '$id') {
194
+ return ['!=', ['id'], null];
195
+ }
196
+ return ['has', property];
197
+ }
@@ -5,6 +5,9 @@ module.exports = createFilter;
5
5
  createFilter.isExpressionFilter = isExpressionFilter;
6
6
 
7
7
  function isExpressionFilter(filter) {
8
+ if (filter === true || filter === false) {
9
+ return true;
10
+ }
8
11
  if (!Array.isArray(filter) || filter.length === 0) {
9
12
  return false;
10
13
  }
@@ -61,7 +64,7 @@ const filterSpec = {
61
64
  * @returns {Function} filter-evaluating function
62
65
  */
63
66
  function createFilter(filter) {
64
- if (!filter) {
67
+ if (filter === null || filter === undefined) {
65
68
  return () => true;
66
69
  }
67
70