Skip to content

Commit

Permalink
Restore quantize rounding.
Browse files Browse the repository at this point in the history
A better way to avoid aliasing issues is to ensure that during pre-quantization,
the fine quantization grid is aligned with the ultimate post-quantization grid.
  • Loading branch information
mbostock committed Mar 29, 2014
1 parent 77576be commit 1f19cc3
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 64 deletions.
24 changes: 18 additions & 6 deletions lib/topojson/post-quantize.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
var quantize = require("./quantize");

module.exports = function(topology, Q0, Q1) {
var k = topology.bbox.every(isFinite) ? (Q0 - 1) / (Q1 - 1) : 1, q = Q0
? quantize([0, 0, Q0, Q0], Q1 + 1)
: quantize(topology.bbox, Q1);
if (Q0) {
if (Q1 === Q0 || !topology.bbox.every(isFinite)) return topology;
var k = Q1 / Q0,
q = quantize(0, 0, k, k);

topology.transform.scale[0] /= k;
topology.transform.scale[1] /= k;
} else {
var x0 = isFinite(bbox[0]) ? bbox[0] : 0,
y0 = isFinite(bbox[1]) ? bbox[1] : 0,
x1 = isFinite(bbox[2]) ? bbox[2] : 0,
y1 = isFinite(bbox[3]) ? bbox[3] : 0,
kx = x1 - x0 ? (Q1 - 1) / (x1 - x0) : 1,
ky = y1 - y0 ? (Q1 - 1) / (y1 - y0) : 1,
q = quantize(-x0, -y0, kx, ky);

topology.transform = q.transform;
}

function quantizeGeometry(geometry) {
if (geometry && quantizeGeometryType.hasOwnProperty(geometry.type)) quantizeGeometryType[geometry.type](geometry);
Expand All @@ -25,8 +40,5 @@ module.exports = function(topology, Q0, Q1) {
return arc;
});

if (Q0) topology.transform.scale[0] *= k, topology.transform.scale[1] *= k;
else topology.transform = q.transform;

return topology;
};
12 changes: 10 additions & 2 deletions lib/topojson/pre-quantize.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
var quantize = require("./quantize");

module.exports = function(objects, bbox, Q) {
var q = quantize(bbox, Q);
module.exports = function(objects, bbox, Q0, Q1) {
if (arguments.length < 4) Q1 = Q0;

var x0 = isFinite(bbox[0]) ? bbox[0] : 0,
y0 = isFinite(bbox[1]) ? bbox[1] : 0,
x1 = isFinite(bbox[2]) ? bbox[2] : 0,
y1 = isFinite(bbox[3]) ? bbox[3] : 0,
kx = x1 - x0 ? (Q1 - 1) / (x1 - x0) * Q0 / Q1 : 1,
ky = y1 - y0 ? (Q1 - 1) / (y1 - y0) * Q0 / Q1 : 1,
q = quantize(-x0, -y0, kx, ky);

function quantizeGeometry(geometry) {
if (geometry && quantizeGeometryType.hasOwnProperty(geometry.type)) quantizeGeometryType[geometry.type](geometry);
Expand Down
67 changes: 33 additions & 34 deletions lib/topojson/quantize.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,42 @@
module.exports = function(bbox, Q) {
var x0 = isFinite(bbox[0]) ? bbox[0] : 0,
y0 = isFinite(bbox[1]) ? bbox[1] : 0,
x1 = isFinite(bbox[2]) ? bbox[2] : 0,
y1 = isFinite(bbox[3]) ? bbox[3] : 0,
kx = x1 - x0 ? (Q - 1) / (x1 - x0) : 1,
ky = y1 - y0 ? (Q - 1) / (y1 - y0) : 1;
module.exports = function(dx, dy, kx, ky) {

return {
point: function(coordinates) {
coordinates[0] = Math.floor((coordinates[0] - x0) * kx);
coordinates[1] = Math.floor((coordinates[1] - y0) * ky);
},
line: function(coordinates) {
var i = 0,
j = 1,
n = coordinates.length,
pi = coordinates[0],
pj,
px = pi[0] = Math.floor((pi[0] - x0) * kx),
py = pi[1] = Math.floor((pi[1] - y0) * ky),
x,
y;
function quantizePoint(coordinates) {
coordinates[0] = Math.round((coordinates[0] + dx) * kx);
coordinates[1] = Math.round((coordinates[1] + dy) * ky);
return coordinates;
}

function quantizeLine(coordinates) {
var i = 0,
j = 1,
n = coordinates.length,
pi = quantizePoint(coordinates[0]),
pj,
px = pi[0],
py = pi[1],
x,
y;

while (++i < n) {
pi = coordinates[i];
x = Math.floor((pi[0] - x0) * kx);
y = Math.floor((pi[1] - y0) * ky);
if (x !== px || y !== py) { // skip coincident points
pj = coordinates[j++];
pj[0] = px = x;
pj[1] = py = y;
}
while (++i < n) {
pi = quantizePoint(coordinates[i]);
x = pi[0];
y = pi[1];
if (x !== px || y !== py) { // skip coincident points
pj = coordinates[j++];
pj[0] = px = x;
pj[1] = py = y;
}
}

coordinates.length = j;
},
coordinates.length = j;
}

return {
point: quantizePoint,
line: quantizeLine,
transform: {
scale: [1 / kx, 1 / ky],
translate: [x0, y0]
translate: [-dx, -dy]
}
};
};
2 changes: 1 addition & 1 deletion lib/topojson/topology.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ module.exports = function(objects, options) {

// Pre-topology quantization.
if (Q0) {
transform = prequantize(objects, bbox, Q0);
transform = prequantize(objects, bbox, Q0, Q1);
if (verbose) {
console.warn("pre-quantization: " + transform.scale.map(function(degrees) { return system.formatDistance(degrees / 180 * Math.PI); }).join(" "));
}
Expand Down
36 changes: 36 additions & 0 deletions lib/topojson/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module.exports = function(dx, dy, kx, ky) {
return {
point: function(coordinates) {
coordinates[0] = Math.floor((coordinates[0] + dx) * kx);
coordinates[1] = Math.floor((coordinates[1] + dy) * ky);
},
line: function(coordinates) {
var i = 0,
j = 1,
n = coordinates.length,
pi = coordinates[0],
pj,
px = pi[0] = Math.floor((pi[0] + dx) * kx),
py = pi[1] = Math.floor((pi[1] + dy) * ky),
x,
y;

while (++i < n) {
pi = coordinates[i];
x = Math.floor((pi[0] + dx) * kx);
y = Math.floor((pi[1] + dy) * ky);
if (x !== px || y !== py) { // skip coincident points
pj = coordinates[j++];
pj[0] = px = x;
pj[1] = py = y;
}
}

coordinates.length = j;
},
transform: {
scale: [1 / kx, 1 / ky],
translate: [x0, y0]
}
};
};
30 changes: 15 additions & 15 deletions test/prequantize-test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
var vows = require("vows"),
assert = require("assert"),
prequantize = require("../lib/topojson/pre-quantize");
quantize = require("../lib/topojson/pre-quantize");

var suite = vows.describe("prequantize");
var suite = vows.describe("pre-quantize");

suite.addBatch({
"prequantize": {
"pre-quantize": {
"returns the quantization transform": function() {
assert.deepEqual(prequantize({}, [0, 0, 1, 1], 1e4), {
assert.deepEqual(quantize({}, [0, 0, 1, 1], 1e4), {
scale: [1 / 9999, 1 / 9999],
translate: [0, 0]
});
Expand All @@ -19,7 +19,7 @@ suite.addBatch({
coordinates: [[0, 0], [1, 0], [0, 1], [0, 0]]
}
};
prequantize(objects, [0, 0, 1, 1], 1e4);
quantize(objects, [0, 0, 1, 1], 1e4);
assert.deepEqual(objects.foo.coordinates, [[0, 0], [9999, 0], [0, 9999], [0, 0]]);
},
"observes the quantization parameter": function() {
Expand All @@ -29,7 +29,7 @@ suite.addBatch({
coordinates: [[0, 0], [1, 0], [0, 1], [0, 0]]
}
};
prequantize(objects, [0, 0, 1, 1], 10);
quantize(objects, [0, 0, 1, 1], 10);
assert.deepEqual(objects.foo.coordinates, [[0, 0], [9, 0], [0, 9], [0, 0]]);
},
"observes the bounding box": function() {
Expand All @@ -39,7 +39,7 @@ suite.addBatch({
coordinates: [[0, 0], [1, 0], [0, 1], [0, 0]]
}
};
prequantize(objects, [-1, -1, 2, 2], 10);
quantize(objects, [-1, -1, 2, 2], 10);
assert.deepEqual(objects.foo.coordinates, [[3, 3], [6, 3], [3, 6], [3, 3]]);
},
"applies to points as well as arcs": function() {
Expand All @@ -49,7 +49,7 @@ suite.addBatch({
coordinates: [[0, 0], [1, 0], [0, 1], [0, 0]]
}
};
prequantize(objects, [0, 0, 1, 1], 1e4);
quantize(objects, [0, 0, 1, 1], 1e4);
assert.deepEqual(objects.foo.coordinates, [[0, 0], [9999, 0], [0, 9999], [0, 0]]);
},
"skips coincident points in lines": function() {
Expand All @@ -59,7 +59,7 @@ suite.addBatch({
coordinates: [[0, 0], [0.9, 0.9], [1.1, 1.1], [2, 2]]
}
};
prequantize(objects, [0, 0, 2, 2], 3);
quantize(objects, [0, 0, 2, 2], 3);
assert.deepEqual(objects.foo.coordinates, [[0, 0], [1, 1], [2, 2]]);
},
"skips coincident points in polygons": function() {
Expand All @@ -69,7 +69,7 @@ suite.addBatch({
coordinates: [[[0, 0], [0.9, 0.9], [1.1, 1.1], [2, 2], [0, 0]]]
}
};
prequantize(objects, [0, 0, 2, 2], 3);
quantize(objects, [0, 0, 2, 2], 3);
assert.deepEqual(objects.foo.coordinates, [[[0, 0], [1, 1], [2, 2], [0, 0]]]);
},
"does not skip coincident points in points": function() {
Expand All @@ -79,8 +79,8 @@ suite.addBatch({
coordinates: [[0, 0], [0.9, 0.9], [1.1, 1.1], [2, 2], [0, 0]]
}
};
prequantize(objects, [0, 0, 2, 2], 3);
assert.deepEqual(objects.foo.coordinates, [[0, 0], [0, 0], [1, 1], [2, 2], [0, 0]]);
quantize(objects, [0, 0, 2, 2], 3);
assert.deepEqual(objects.foo.coordinates, [[0, 0], [1, 1], [1, 1], [2, 2], [0, 0]]);
},
"includes closing point in degenerate lines": function() {
var objects = {
Expand All @@ -89,17 +89,17 @@ suite.addBatch({
coordinates: [[1, 1], [1, 1], [1, 1]]
}
};
prequantize(objects, [0, 0, 2, 2], 3);
quantize(objects, [0, 0, 2, 2], 3);
assert.deepEqual(objects.foo.coordinates, [[1, 1], [1, 1]]);
},
"includes closing point in degenerate polygons": function() {
var objects = {
foo: {
type: "Polygon",
coordinates: [[[1.01, 1], [1.03, 1], [1.02, 1], [1.01, 1]]]
coordinates: [[[0.9, 1], [1.1, 1], [1.01, 1], [0.9, 1]]]
}
};
prequantize(objects, [0, 0, 2, 2], 3);
quantize(objects, [0, 0, 2, 2], 3);
assert.deepEqual(objects.foo.coordinates, [[[1, 1], [1, 1], [1, 1], [1, 1]]]);
}
}
Expand Down
12 changes: 6 additions & 6 deletions test/topology-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ suite.addBatch({
assert.deepEqual(topology.objects.foo, {type: "MultiPoint", coordinates: [[0, 0], [1, 1]]});
},

// Flooring allows us to apply additional post-quantization without introducing aliasing.
"quantization rounds down to the closest integer coordinate": function() {
var topology = topojson.topology({foo: {type: "LineString", coordinates: [[0.0, 0.0], [1.5, 1.5], [2.6, 2.6], [3.0, 3.0], [4.1, 4.1], [5.0, 5.0], [6.1, 6.1], [7.5, 7.5], [8.01, 8.01], [9.4, 9.4], [10, 10]]}}, {quantization: 11});
// Rounding is more accurate than flooring.
"quantization rounds to the closest integer coordinate to minimize error": function() {
var topology = topojson.topology({foo: {type: "LineString", coordinates: [[0.0, 0.0], [0.5, 0.5], [1.6, 1.6], [3.0, 3.0], [4.1, 4.1], [4.9, 4.9], [5.9, 5.9], [6.5, 6.5], [7.0, 7.0], [8.4, 8.4], [8.5, 8.5], [10, 10]]}}, {quantization: 11});
assert.deepEqual(topojson.feature(topology, topology.objects.foo).geometry.coordinates, [[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8], [9, 9], [10, 10]]);
assert.deepEqual(topology.arcs, [[[0, 0], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1], [1, 1]]]);
assert.deepEqual(topology.transform, {scale: [1, 1], translate: [0, 0]});
Expand Down Expand Up @@ -852,9 +852,9 @@ suite.addBatch({
[180, 60], [180, 30], [150, 0], [180, -30], [180, -60], [60, -60], [-60, -60], [-180, -60]
]]}}, {quantization: 8});
assert.deepEqual(topology.arcs, [
[[0, 7], [2, 0], [2, 0], [-4, 0]],
[[0, 0], [4, 0], [-2, 0], [-2, 0]],
[[0, 5], [6, -2], [-6, -2], [0, 4]]
[[0, 7], [2, 0], [3, 0], [-5, 0]],
[[0, 0], [5, 0], [-3, 0], [-2, 0]],
[[0, 5], [6, -1], [-6, -2], [1, 2], [-1, 1]]
]);
assert.deepEqual(topology.objects.polygon, {type: "Polygon", arcs: [[0], [1], [2]]});
}
Expand Down

0 comments on commit 1f19cc3

Please sign in to comment.