Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An option to use a feature property as ID for feature state #8987

Merged
merged 12 commits into from
Dec 2, 2019
Prev Previous commit
Next Next commit
use state ids that can be cast as Float64 as is; update tests
  • Loading branch information
mourner committed Nov 29, 2019
commit b7935e53278d9d826050cc020e1f69566043bc47
8 changes: 4 additions & 4 deletions src/data/feature_position_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ export default class FeaturePositionMap {
}

add(id: mixed, index: number, start: number, end: number) {
this.ids.push(getIntegerId(id));
this.ids.push(getNumericId(id));
this.positions.push(index, start, end);
}

getPositions(id: mixed): Array<FeaturePosition> {
assert(this.indexed);

const intId = getIntegerId(id);
const intId = getNumericId(id);

// binary search for the first occurrence of id in this.ids;
// relies on ids/positions being sorted by id, which happens in serialization
Expand Down Expand Up @@ -82,9 +82,9 @@ export default class FeaturePositionMap {
}
}

export function getIntegerId(value: mixed) {
export function getNumericId(value: mixed) {
const numValue = +value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this incorrectly handles strings that can be converted to javascript numbers but not javascript integers:

  • integers javascript can't safely represent '9007199254740993'
  • decimals '1.2'
  • 'Infinity'

It also looks like users of this function expect integers but this can return a float if given a float. Is that ok because it should never be given a float?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially this was hashing all non-integer ids, but then I changed it because I thought there's no longer a need to have it limited to integer — those get saved in a Float64Array anyway, which supports decimals, values like Infinity, and values bigger than MAX_SAFE_INTEGER.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also looks like users of this function expect integers

Oh, actually, in addition to the above, this function being exported is a leftover from earlier — how things get stored in the position map should be an internal detail so there won't be "users" of this. I'll remove the "export".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those get saved in a Float64Array anyway, which supports decimals, values like Infinity, and values bigger than MAX_SAFE_INTEGER

Neither javascript numbers (which are 64 bit floats) or Float64Array can store integers > MAX_SAFE_INTEGER safely. Both '9007199254740993' and '9007199254740992' will look like the same id to this implementation. This might seem unlikely but we ran into this with iD editor because osm uses sequential 64 integers as ids.

Similar problems theoretically exist with decimals but seem less likely.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough — updated to hash as a string in case a numeric id is more than MAX_SAFE_INTEGER.

if (!isNaN(numValue) && numValue % 1 === 0) {
if (!isNaN(numValue)) {
return numValue;
}
return murmur3(String(value));
Expand Down
3 changes: 3 additions & 0 deletions src/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,9 @@ class Style extends Evented {
this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`)));
return;
}
if (target.id === undefined) {
this.fire(new ErrorEvent(new Error(`The feature id parameter must be provided.`)));
}

sourceCache.setFeatureState(sourceLayer, target.id, state);
}
Expand Down
116 changes: 34 additions & 82 deletions test/unit/ui/map.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,23 @@ test('Map', (t) => {
t.end();
});
});
t.test('works with string ids', (t) => {
const map = createMap(t, {
style: {
"version": 8,
"sources": {
"geojson": createStyleSource()
},
"layers": []
}
});
map.on('load', () => {
map.setFeatureState({source: 'geojson', id: 'foo'}, {'hover': true});
const fState = map.getFeatureState({source: 'geojson', id: 'foo'});
t.equal(fState.hover, true);
t.end();
});
});
t.test('parses feature id as an int', (t) => {
const map = createMap(t, {
style: {
Expand Down Expand Up @@ -1539,54 +1556,31 @@ test('Map', (t) => {
map.setFeatureState({source: 'vector', sourceLayer: "1"}, {'hover': true});
});
});
t.test('fires an error if id is less than zero', (t) => {
const map = createMap(t, {
style: {
"version": 8,
"sources": {
"vector": {
"type": "vector",
"tiles": ["http://example.com/{z}/{x}/{y}.png"]
}
},
"layers": []
}
});
map.on('load', () => {
map.on('error', ({error}) => {
t.match(error.message, /id/);
t.end();
});
map.setFeatureState({source: 'vector', sourceLayer: "1", id: -1}, {'hover': true});
});
});
t.test('fires an error if id cannot be parsed as an int', (t) => {
t.end();
});

t.test('#removeFeatureState', (t) => {

t.test('accepts "0" id', (t) => {
const map = createMap(t, {
style: {
"version": 8,
"sources": {
"vector": {
"type": "vector",
"tiles": ["http://example.com/{z}/{x}/{y}.png"]
}
"geojson": createStyleSource()
},
"layers": []
}
});
map.on('load', () => {
map.on('error', ({error}) => {
t.match(error.message, /id/);
t.end();
});
map.setFeatureState({source: 'vector', sourceLayer: "1", id: 'abc'}, {'hover': true});
map.setFeatureState({source: 'geojson', id: 0}, {'hover': true, 'click': true});
map.removeFeatureState({source: 'geojson', id: 0}, 'hover');
const fState = map.getFeatureState({source: 'geojson', id: 0});
t.equal(fState.hover, undefined);
t.equal(fState.click, true);
t.end();
});
});
t.end();
});

t.test('#removeFeatureState', (t) => {

t.test('accepts "0" id', (t) => {
t.test('accepts string id', (t) => {
const map = createMap(t, {
style: {
"version": 8,
Expand All @@ -1597,9 +1591,9 @@ test('Map', (t) => {
}
});
map.on('load', () => {
map.setFeatureState({source: 'geojson', id: 0}, {'hover': true, 'click': true});
map.removeFeatureState({source: 'geojson', id: 0}, 'hover');
const fState = map.getFeatureState({source: 'geojson', id: 0});
map.setFeatureState({source: 'geojson', id: 'foo'}, {'hover': true, 'click': true});
map.removeFeatureState({source: 'geojson', id: 'foo'}, 'hover');
const fState = map.getFeatureState({source: 'geojson', id: 'foo'});
t.equal(fState.hover, undefined);
t.equal(fState.click, true);
t.end();
Expand Down Expand Up @@ -1852,48 +1846,6 @@ test('Map', (t) => {
map.removeFeatureState({source: 'vector', sourceLayer: "1"}, {'hover': true});
});
});
t.test('removeFeatureState fires an error if id is less than zero', (t) => {
const map = createMap(t, {
style: {
"version": 8,
"sources": {
"vector": {
"type": "vector",
"tiles": ["http://example.com/{z}/{x}/{y}.png"]
}
},
"layers": []
}
});
map.on('load', () => {
map.on('error', ({error}) => {
t.match(error.message, /id/);
t.end();
});
map.removeFeatureState({source: 'vector', sourceLayer: "1", id: -1}, {'hover': true});
});
});
t.test('fires an error if id cannot be parsed as an int', (t) => {
const map = createMap(t, {
style: {
"version": 8,
"sources": {
"vector": {
"type": "vector",
"tiles": ["http://example.com/{z}/{x}/{y}.png"]
}
},
"layers": []
}
});
map.on('load', () => {
map.on('error', ({error}) => {
t.match(error.message, /id/);
t.end();
});
map.removeFeatureState({source: 'vector', sourceLayer: "1", id: 'abc'}, {'hover': true});
});
});
t.end();
});

Expand Down