Skip to content

Commit

Permalink
Distance and Ramer–Douglas–Peucker functions.
Browse files Browse the repository at this point in the history
Move distance functions from the lineFeature to the util module, making
the function names more explicit.  Add a `rdpLineSimply` function for
reducing the complexity of a line or polygon.  Add a `rdpSimplifyData`
method to the line and polygon features for doing this while setting
data.
  • Loading branch information
manthey committed Mar 15, 2018
1 parent 673b2bd commit 8423f99
Show file tree
Hide file tree
Showing 8 changed files with 548 additions and 37 deletions.
1 change: 1 addition & 0 deletions examples/polygons/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ $(function () {
var hoverColor = query.hover || 'blue';
var polyColor = query.color ? geo.util.convertColor(query.color) : undefined;
$.getJSON(query.url || '../../data/land_polygons.json').done(function (data) {
polygonDebug.data = data;
polygons
/* This is the default accessor, so we don't have to define it ourselves.
.polygon(function (d) {
Expand Down
6 changes: 6 additions & 0 deletions src/gl/polygonFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ var gl_polygonFeature = function (arg) {
return;
}
outer = polygon.outer || (Array.isArray(polygon) ? polygon : []);
if (outer.length < 3) {
return;
}

/* expand to an earcut polygon geometry. We had been using a map call,
* but using loops is much faster in Chrome (4 versus 33 ms for one
Expand All @@ -144,6 +147,9 @@ var gl_polygonFeature = function (arg) {

if (polygon.inner) {
polygon.inner.forEach(function (hole) {
if (hole.length < 3) {
return;
}
original = original.concat(hole);
geometry.holes.push(d3 / 3);
for (i = 0; i < hole.length; i += 1, d3 += 3) {
Expand Down
82 changes: 58 additions & 24 deletions src/lineFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var inherit = require('./inherit');
var feature = require('./feature');
var timestamp = require('./timestamp');
var transform = require('./transform');
var util = require('./util');

/**
* Line feature specification.
Expand Down Expand Up @@ -199,33 +200,10 @@ var lineFeature = function (arg) {
pt = transform.transformCoordinates(map.ingcs(), map.gcs(), p),
i, j, record;

// minimum l2 distance squared from
// q -> line(u, v)
function lineDist2(q, u, v) {
var t, vux = v.x - u.x, vuy = v.y - u.y, l2 = vux * vux + vuy * vuy;

if (l2 < 1) {
// u, v are within 1 pixel
return dist2(q, u);
}

t = ((q.x - u.x) * vux + (q.y - u.y) * vuy) / l2;
if (t < 0) { return dist2(q, u); }
if (t > 1) { return dist2(q, v); }
return dist2(q, {x: u.x + t * vux, y: u.y + t * vuy});
}

// l2 distance squared from u to v
function dist2(u, v) {
var dx = u.x - v.x,
dy = u.y - v.y;
return dx * dx + dy * dy;
}

for (i = 0; i < m_pointSearchInfo.length; i += 1) {
record = m_pointSearchInfo[i];
for (j = 0; j < record.length; j += 1) {
if (lineDist2(pt, record[j].u, record[j].v) <= record[j].r2 * scale2) {
if (util.distance2dToLineSquared(pt, record[j].u, record[j].v) <= record[j].r2 * scale2) {
found.push(data[i]);
indices.push(i);
break;
Expand Down Expand Up @@ -280,6 +258,62 @@ var lineFeature = function (arg) {
return idx;
};

/**
* Take a set of data, reduce the number of vertices per linen using the
* Ramer–Douglas–Peucker algorithm, and use the result as the new data.
* This changes the instance's data, the position accessor, and the line
* accessor.
*
* @param {array} data A new data array.
* @param {number} [tolerance] The maximum variation allowed in map.gcs
* units. A value of zero will only remove perfectly colinear points. If
* not specified, this is set to a half display pixel at the map's current
* zoom level.
* @param {function} [posFunc=this.style.get('position')] The function to
* get the position of each vertex.
* @param {function} [lineFunc=this.style.get('line')] The function to get
* each line.
* @returns {this}
*/
this.rdpSimplifyData = function (data, tolerance, posFunc, lineFunc) {
data = data || m_this.data();
posFunc = posFunc || m_this.style.get('position');
lineFunc = lineFunc || m_this.style.get('line');
var map = m_this.layer().map(),
mapgcs = map.gcs(),
featuregcs = m_this.gcs(),
closedFunc = m_this.style.get('closed');
if (tolerance === undefined) {
tolerance = map.unitsPerPixel(map.zoom()) * 0.5;
}

/* transform the coordinates to the map gcs */
data = data.map(function (d, idx) {
var lineItem = lineFunc(d, idx),
pts = transform.transformCoordinates(featuregcs, mapgcs, lineItem.map(function (ld, lidx) {
return posFunc(ld, lidx, d, idx);
})),
elem = util.rdpLineSimplify(pts, tolerance, closedFunc(d, idx), []);
if (elem.length < 2 || (elem.length === 2 && util.distance2dSquared(elem[0], elem[1]) < tolerance * tolerance)) {
elem = [];
}
elem = transform.transformCoordinates(mapgcs, featuregcs, elem);
/* Copy element properties, as they might be used by styles */
for (var key in d) {
if (d.hasOwnProperty(key) && !(Array.isArray(d) && key >= 0 && key < d.length)) {
elem[key] = d[key];
}
}
return elem;
});

/* Set the reduced lines as the data and use simple accessors. */
m_this.style('position', function (d) { return d; });
m_this.style('line', function (d) { return d; });
m_this.data(data);
return m_this;
};

/**
* Initialize.
*
Expand Down
116 changes: 104 additions & 12 deletions src/polygonFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,24 +90,32 @@ var polygonFeature = function (arg) {
this.data = function (arg) {
var ret = s_data(arg);
if (arg !== undefined) {
getCoordinates();
m_coordinates = getCoordinates();
this._checkForStroke();
}
return ret;
};

/**
* Get the internal coordinates whenever the data changes. For now, we do
* the computation in world coordinates, but we will need to work in GCS
* for other projections. Also compute the extents of the outside of each
* polygon for faster checking if points are in the polygon.
* Get the internal coordinates whenever the data changes. Also compute the
* extents of the outside of each polygon for faster checking if points are
* in the polygon.
*
* @private
* @param {object[]} [data=this.data()] The data to process.
* @param {function} [posFunc=this.style.get('position')] The function to
* get the position of each vertex.
* @param {function} [polyFunc=this.style.get('polygon')] The function to
* get each polygon.
* @returns {object[]} An array of polygon positions. Each has `outer` and
* `inner` if it has any coordinates, or is undefined.
*/
function getCoordinates() {
var posFunc = m_this.style.get('position'),
polyFunc = m_this.style.get('polygon');
m_coordinates = m_this.data().map(function (d, i) {
var poly = polyFunc(d);
function getCoordinates(data, posFunc, polyFunc) {
data = data || m_this.data();
posFunc = posFunc || m_this.style.get('position');
polyFunc = polyFunc || m_this.style.get('polygon');
var coordinates = data.map(function (d, i) {
var poly = polyFunc(d, i);
if (!poly) {
return;
}
Expand Down Expand Up @@ -142,6 +150,7 @@ var polygonFeature = function (arg) {
range: range
};
});
return coordinates;
}

/**
Expand Down Expand Up @@ -182,7 +191,7 @@ var polygonFeature = function (arg) {
m_this.style('polygon', val);
m_this.dataTime().modified();
m_this.modified();
getCoordinates();
m_coordinates = getCoordinates();
}
return m_this;
};
Expand All @@ -202,7 +211,7 @@ var polygonFeature = function (arg) {
m_this.style('position', val);
m_this.dataTime().modified();
m_this.modified();
getCoordinates();
m_coordinates = getCoordinates();
}
return m_this;
};
Expand Down Expand Up @@ -392,6 +401,89 @@ var polygonFeature = function (arg) {
return result;
};

/**
* Take a set of data, reduce the number of vertices per polygon using the
* Ramer–Douglas–Peucker algorithm, and use the result as the new data.
* This changes the instance's data, the position accessor, and the polygon
* accessor.
*
* @param {array} data A new data array.
* @param {number} [tolerance] The maximum variation allowed in map.gcs
* units. A value of zero will only remove perfectly colinear points. If
* not specified, this is set to a half display pixel at the map's current
* zoom level.
* @param {function} [posFunc=this.style.get('position')] The function to
* get the position of each vertex.
* @param {function} [polyFunc=this.style.get('polygon')] The function to
* get each polygon.
* @returns {this}
*/
this.rdpSimplifyData = function (data, tolerance, posFunc, polyFunc) {
var map = m_this.layer().map(),
mapgcs = map.gcs(),
featuregcs = m_this.gcs(),
coordinates = getCoordinates(data, posFunc, polyFunc);
if (tolerance === undefined) {
tolerance = map.unitsPerPixel(map.zoom()) * 0.5;
}

/* transform the coordinates to the map gcs */
coordinates = coordinates.map(function (poly) {
return {
outer: transform.transformCoordinates(featuregcs, mapgcs, poly.outer),
inner: poly.inner.map(function (hole) {
return transform.transformCoordinates(featuregcs, mapgcs, hole);
})
};
});
data = data.map(function (d, idx) {
var poly = coordinates[idx],
elem = {};
/* Copy element properties, as they might be used by styles */
for (var key in d) {
if (d.hasOwnProperty(key) && !(Array.isArray(d) && key >= 0 && key < d.length)) {
elem[key] = d[key];
}
}
if (poly && poly.outer.length >= 3) {
// discard degenerate holes before anything else
elem.inner = poly.inner.filter(function (hole) {
return hole.length >= 3;
});
// simplify the outside of the polygon without letting it cross holes
elem.outer = util.rdpLineSimplify(poly.outer, tolerance, true, elem.inner);
if (elem.outer.length >= 3) {
var allButSelf = elem.inner.slice();
// simplify holes without crossing other holes or the outside
elem.inner.map(function (hole, idx) {
allButSelf[idx] = elem.outer;
var result = util.rdpLineSimplify(hole, tolerance, true, allButSelf);
allButSelf[idx] = result;
return result;
}).filter(function (hole) {
return hole.length >= 3;
});
// transform coordinates back to the feature gcs
elem.outer = transform.transformCoordinates(mapgcs, featuregcs, elem.outer);
elem.inner = elem.inner.map(function (hole) {
return transform.transformCoordinates(mapgcs, featuregcs, hole);
});
} else {
elem.outer = elem.inner = [];
}
} else {
elem.outer = [];
}
return elem;
});

/* Set the reduced polgons as the data and use simple accessors. */
m_this.style('position', function (d) { return d; });
m_this.style('polygon', function (d) { return d; });
m_this.data(data);
return m_this;
};

/**
* Destroy.
*/
Expand Down
Loading

0 comments on commit 8423f99

Please sign in to comment.