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

Non-destructive topology creation. #4

Merged
merged 13 commits into from
Mar 31, 2017
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ See [shapefile](https://github.com/mbostock/shapefile) for converting ESRI shape
If you use NPM, `npm install topojson-server`. Otherwise, download the [latest release](https://github.com/topojson/topojson-server/releases/latest). You can also load directly from [unpkg](https://unpkg.com). AMD, CommonJS, and vanilla environments are supported. In vanilla, a `topojson` global is exported:

```html
<script src="https://unpkg.com/topojson-server@2"></script>
<script src="https://unpkg.com/topojson-server@3"></script>
<script>

var topology = topojson.topology({foo: geojson});
Expand All @@ -23,11 +23,9 @@ var topology = topojson.topology({foo: geojson});

<a name="topology" href="#topology">#</a> topojson.<b>topology</b>(<i>objects</i>[, <i>quantization</i>]) [<>](https://github.com/topojson/topojson-server/blob/master/src/topology.js "Source")

Converts the specified [GeoJSON *objects*](http://geojson.org/geojson-spec.html#geojson-objects) to TopoJSON.
Returns a TopoJSON topology for the specified [GeoJSON *objects*](http://geojson.org/geojson-spec.html#geojson-objects). The returned topology makes a shallow copy of the input *objects*: the identifier, bounding box, properties and coordinates of input objects may be shared with the output topology.

**CAUTION:** The input *objects* are modified **in-place** and should not be referenced after calling this method; this is a destructive operation!

If a *quantization* parameter is specified, the input geometry is quantized prior to computing the topology, and the returned topology is quantized, and its arcs are [delta-encoded](https://github.com/topojson/topojson-specification/blob/master/README.md#213-arcs). Quantization is recommended to improve the quality of the topology if the input geometry is messy (*i.e.*, small floating point error means that adjacent boundaries do not have identical values); typical values are powers of ten, such as 1e4, 1e5 or 1e6. See also [topojson.quantize](https://github.com/topojson/topojson-client/blob/master/README.md#quantize) to quantize a topology after it has been constructed, without altering the topological relationships.
If a *quantization* parameter is specified, the input geometry is quantized prior to computing the topology, the returned topology is quantized, and its arcs are [delta-encoded](https://github.com/topojson/topojson-specification/blob/master/README.md#213-arcs). Quantization is recommended to improve the quality of the topology if the input geometry is messy (*i.e.*, small floating point error means that adjacent boundaries do not have identical values); typical values are powers of ten, such as 1e4, 1e5 or 1e6. See also [topojson.quantize](https://github.com/topojson/topojson-client/blob/master/README.md#quantize) to quantize a topology after it has been constructed, without altering the topological relationships.

## Command-Line Reference

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "topojson-server",
"version": "2.0.0",
"version": "3.0.0",
"description": "Convert GeoJSON to TopoJSON for smaller files and the power of topology!",
"keywords": [
"topojson",
Expand Down Expand Up @@ -33,9 +33,9 @@
"devDependencies": {
"eslint": "3",
"package-preamble": "0.0",
"rollup": "0.36",
"rollup": "0.41",
"tape": "4",
"topojson-client": "2",
"topojson-client": "3",
"uglify-js": "2"
}
}
10 changes: 5 additions & 5 deletions src/bounds.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ export default function(objects) {
y1 = -Infinity;

function boundGeometry(geometry) {
if (geometry && boundGeometryType.hasOwnProperty(geometry.type)) boundGeometryType[geometry.type](geometry);
if (geometry != null && boundGeometryType.hasOwnProperty(geometry.type)) boundGeometryType[geometry.type](geometry);
}

var boundGeometryType = {
GeometryCollection: function(o) { o.geometries.forEach(boundGeometry); },
Point: function(o) { boundPoint(o.coordinates); },
MultiPoint: function(o) { o.coordinates.forEach(boundPoint); },
LineString: function(o) { boundLine(o.coordinates); },
MultiLineString: function(o) { o.coordinates.forEach(boundLine); },
Polygon: function(o) { o.coordinates.forEach(boundLine); },
MultiPolygon: function(o) { o.coordinates.forEach(boundMultiLine); }
LineString: function(o) { boundLine(o.arcs); },
MultiLineString: function(o) { o.arcs.forEach(boundLine); },
Polygon: function(o) { o.arcs.forEach(boundLine); },
MultiPolygon: function(o) { o.arcs.forEach(boundMultiLine); }
};

function boundPoint(coordinates) {
Expand Down
25 changes: 13 additions & 12 deletions src/delta.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
// Given a TopoJSON topology in absolute (quantized) coordinates,
// Given an array of arcs in absolute (but already quantized!) coordinates,
// converts to fixed-point delta encoding.
// This is a destructive operation that modifies the given topology!
export default function(topology) {
var arcs = topology.arcs,
i = -1,
// This is a destructive operation that modifies the given arcs!
export default function(arcs) {
var i = -1,
n = arcs.length;

while (++i < n) {
var arc = arcs[i],
j = 0,
k = 1,
m = arc.length,
point = arc[0],
x0 = point[0],
y0 = point[1],
x1,
y1;

while (++j < m) {
point = arc[j];
x1 = point[0];
y1 = point[1];
arc[j] = [x1 - x0, y1 - y0];
x0 = x1;
y0 = y1;
point = arc[j], x1 = point[0], y1 = point[1];
if (x1 !== x0 || y1 !== y0) arc[k++] = [x1 - x0, y1 - y0], x0 = x1, y0 = y1;
}

if (k === 1) arc[k++] = [0, 0]; // Each arc must be an array of two or more positions.

arc.length = k;
}

return topology;
return arcs;
}
8 changes: 4 additions & 4 deletions src/extract.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ export default function(objects) {

var extractGeometryType = {
GeometryCollection: function(o) { o.geometries.forEach(extractGeometry); },
LineString: function(o) { o.arcs = extractLine(o.coordinates); delete o.coordinates; },
MultiLineString: function(o) { o.arcs = o.coordinates.map(extractLine); delete o.coordinates; },
Polygon: function(o) { o.arcs = o.coordinates.map(extractRing); delete o.coordinates; },
MultiPolygon: function(o) { o.arcs = o.coordinates.map(extractMultiRing); delete o.coordinates; }
LineString: function(o) { o.arcs = extractLine(o.arcs); },
MultiLineString: function(o) { o.arcs = o.arcs.map(extractLine); },
Polygon: function(o) { o.arcs = o.arcs.map(extractRing); },
MultiPolygon: function(o) { o.arcs = o.arcs.map(extractMultiRing); }
};

function extractLine(line) {
Expand Down
138 changes: 31 additions & 107 deletions src/geometry.js
Original file line number Diff line number Diff line change
@@ -1,115 +1,39 @@
// Given a hash of GeoJSON objects, replaces Features with geometry objects.
// This is a destructive operation that modifies the input objects!
export default function(objects) {
var key;
for (key in objects) objects[key] = geomifyObject(objects[key]);
return objects;
// Given a hash of GeoJSON objects, returns a hash of GeoJSON geometry objects.
// Any null input geometry objects are represented as {type: null} in the output.
// Any feature.{id,properties,bbox} are transferred to the output geometry object.
// Each output geometry object is a shallow copy of the input (e.g., properties, coordinates)!
export default function(inputs) {
var outputs = {}, key;
for (key in inputs) outputs[key] = geomifyObject(inputs[key]);
return outputs;
}

function geomifyObject(object) {
return (object && geomifyObjectType.hasOwnProperty(object.type)
? geomifyObjectType[object.type]
: geomifyGeometry)(object);
function geomifyObject(input) {
return input == null ? {type: null}
: (input.type === "FeatureCollection" ? geomifyFeatureCollection
: input.type === "Feature" ? geomifyFeature
: geomifyGeometry)(input);
}

function geomifyFeature(feature) {
var geometry = feature.geometry;
if (geometry == null) {
feature.type = null;
} else {
geomifyGeometry(geometry);
feature.type = geometry.type;
if (geometry.geometries) feature.geometries = geometry.geometries;
else if (geometry.coordinates) feature.coordinates = geometry.coordinates;
if (geometry.bbox) feature.bbox = geometry.bbox;
}
delete feature.geometry;
return feature;
function geomifyFeatureCollection(input) {
var output = {type: "GeometryCollection", geometries: input.features.map(geomifyFeature)};
if (input.bbox != null) output.bbox = input.bbox;
return output;
}

function geomifyGeometry(geometry) {
if (!geometry) return {type: null};
if (geomifyGeometryType.hasOwnProperty(geometry.type)) geomifyGeometryType[geometry.type](geometry);
return geometry;
function geomifyFeature(input) {
var output = geomifyGeometry(input.geometry), key; // eslint-disable-line no-unused-vars
if (input.id != null) output.id = input.id;
if (input.bbox != null) output.bbox = input.bbox;
for (key in input.properties) { output.properties = input.properties; break; }
return output;
}

var geomifyObjectType = {
Feature: geomifyFeature,
FeatureCollection: function(collection) {
collection.type = "GeometryCollection";
collection.geometries = collection.features;
collection.features.forEach(geomifyFeature);
delete collection.features;
return collection;
}
};

var geomifyGeometryType = {
GeometryCollection: function(o) {
var geometries = o.geometries, i = -1, n = geometries.length;
while (++i < n) geometries[i] = geomifyGeometry(geometries[i]);
},
MultiPoint: function(o) {
if (!o.coordinates.length) {
o.type = null;
delete o.coordinates;
} else if (o.coordinates.length < 2) {
o.type = "Point";
o.coordinates = o.coordinates[0];
}
},
LineString: function(o) {
if (!o.coordinates.length) {
o.type = null;
delete o.coordinates;
}
},
MultiLineString: function(o) {
for (var lines = o.coordinates, i = 0, N = 0, n = lines.length; i < n; ++i) {
var line = lines[i];
if (line.length) lines[N++] = line;
}
if (!N) {
o.type = null;
delete o.coordinates;
} else if (N < 2) {
o.type = "LineString";
o.coordinates = lines[0];
} else {
o.coordinates.length = N;
}
},
Polygon: function(o) {
for (var rings = o.coordinates, i = 0, N = 0, n = rings.length; i < n; ++i) {
var ring = rings[i];
if (ring.length) rings[N++] = ring;
}
if (!N) {
o.type = null;
delete o.coordinates;
} else {
o.coordinates.length = N;
}
},
MultiPolygon: function(o) {
for (var polygons = o.coordinates, j = 0, M = 0, m = polygons.length; j < m; ++j) {
for (var rings = polygons[j], i = 0, N = 0, n = rings.length; i < n; ++i) {
var ring = rings[i];
if (ring.length) rings[N++] = ring;
}
if (N) {
rings.length = N;
polygons[M++] = rings;
}
}
if (!M) {
o.type = null;
delete o.coordinates;
} else if (M < 2) {
o.type = "Polygon";
o.coordinates = polygons[0];
} else {
polygons.length = M;
}
}
};
function geomifyGeometry(input) {
if (input == null) return {type: null};
var output = input.type === "GeometryCollection" ? {type: "GeometryCollection", geometries: input.geometries.map(geomifyGeometry)}
: input.type === "Point" || input.type === "MultiPoint" ? {type: input.type, coordinates: input.coordinates}
: {type: input.type, arcs: input.coordinates}; // TODO Check for unknown types?
if (input.bbox != null) output.bbox = input.bbox;
return output;
}
96 changes: 37 additions & 59 deletions src/prequantize.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,79 +6,57 @@ export default function(objects, bbox, n) {
kx = x1 - x0 ? (n - 1) / (x1 - x0) : 1,
ky = y1 - y0 ? (n - 1) / (y1 - y0) : 1;

function quantizePoint(coordinates) {
coordinates[0] = Math.round((coordinates[0] - x0) * kx);
coordinates[1] = Math.round((coordinates[1] - y0) * ky);
return coordinates;
function quantizePoint(input) {
return [Math.round((input[0] - x0) * kx), Math.round((input[1] - y0) * ky)];
}

function quantizeLine(coordinates) {
var i = 0,
j = 1,
n = coordinates.length,
pi = quantizePoint(coordinates[0]),
pj,
px = pi[0],
py = pi[1],
function quantizePoints(input, m) {
var i = -1,
j = 0,
n = input.length,
output = new Array(n), // pessimistic
pi,
px,
py,
x,
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;
}
pi = input[i];
x = Math.round((pi[0] - x0) * kx);
y = Math.round((pi[1] - y0) * ky);
if (x !== px || y !== py) output[j++] = [px = x, py = y]; // non-coincident points
}

coordinates.length = j;
output.length = j;
while (j < m) j = output.push([output[0][0], output[0][1]]);
return output;
}

function quantizeLine(input) {
return quantizePoints(input, 2);
}

function quantizeRing(input) {
return quantizePoints(input, 4);
}

function quantizePolygon(input) {
return input.map(quantizeRing);
}

function quantizeGeometry(o) {
if (o && quantizeGeometryType.hasOwnProperty(o.type)) quantizeGeometryType[o.type](o);
if (o != null && quantizeGeometryType.hasOwnProperty(o.type)) quantizeGeometryType[o.type](o);
}

var quantizeGeometryType = {
GeometryCollection: function(o) {
o.geometries.forEach(quantizeGeometry);
},
Point: function(o) {
quantizePoint(o.coordinates);
},
MultiPoint: function(o) {
o.coordinates.forEach(quantizePoint);
},
LineString: function(o) {
var line = o.coordinates;
quantizeLine(line);
if (line.length < 2) line[1] = line[0]; // must have 2+
},
MultiLineString: function(o) {
for (var lines = o.coordinates, i = 0, n = lines.length; i < n; ++i) {
var line = lines[i];
quantizeLine(line);
if (line.length < 2) line[1] = line[0]; // must have 2+
}
},
Polygon: function(o) {
for (var rings = o.coordinates, i = 0, n = rings.length; i < n; ++i) {
var ring = rings[i];
quantizeLine(ring);
while (ring.length < 4) ring.push(ring[0]); // must have 4+
}
},
MultiPolygon: function(o) {
for (var polygons = o.coordinates, i = 0, n = polygons.length; i < n; ++i) {
for (var rings = polygons[i], j = 0, m = rings.length; j < m; ++j) {
var ring = rings[j];
quantizeLine(ring);
while (ring.length < 4) ring.push(ring[0]); // must have 4+
}
}
}
GeometryCollection: function(o) { o.geometries.forEach(quantizeGeometry); },
Point: function(o) { o.coordinates = quantizePoint(o.coordinates); },
MultiPoint: function(o) { o.coordinates = o.coordinates.map(quantizePoint); },
LineString: function(o) { o.arcs = quantizeLine(o.arcs); },
MultiLineString: function(o) { o.arcs = o.arcs.map(quantizeLine); },
Polygon: function(o) { o.arcs = quantizePolygon(o.arcs); },
MultiPolygon: function(o) { o.arcs = o.arcs.map(quantizePolygon); }
};

for (var key in objects) {
Expand Down
Loading