Skip to content

Instantly share code, notes, and snippets.

@Alex-Devoid
Last active January 23, 2020 07:28
Show Gist options
  • Save Alex-Devoid/5f6665782677129909bfc76569cf118d to your computer and use it in GitHub Desktop.
Save Alex-Devoid/5f6665782677129909bfc76569cf118d to your computer and use it in GitHub Desktop.
Different centers: path.centroid to leaflet | turf.centroid | turf.centerOfMass | polylabel
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
'use strict';
var Queue = require('tinyqueue');
module.exports = polylabel;
module.exports.default = polylabel;
function polylabel(polygon, precision, debug) {
precision = precision || 1.0;
// find the bounding box of the outer ring
var minX, minY, maxX, maxY;
for (var i = 0; i < polygon[0].length; i++) {
var p = polygon[0][i];
if (!i || p[0] < minX) minX = p[0];
if (!i || p[1] < minY) minY = p[1];
if (!i || p[0] > maxX) maxX = p[0];
if (!i || p[1] > maxY) maxY = p[1];
}
var width = maxX - minX;
var height = maxY - minY;
var cellSize = Math.min(width, height);
var h = cellSize / 2;
// a priority queue of cells in order of their "potential" (max distance to polygon)
var cellQueue = new Queue(null, compareMax);
if (cellSize === 0) return [minX, minY];
// cover polygon with initial cells
for (var x = minX; x < maxX; x += cellSize) {
for (var y = minY; y < maxY; y += cellSize) {
cellQueue.push(new Cell(x + h, y + h, h, polygon));
}
}
// take centroid as the first best guess
var bestCell = getCentroidCell(polygon);
// special case for rectangular polygons
var bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, polygon);
if (bboxCell.d > bestCell.d) bestCell = bboxCell;
var numProbes = cellQueue.length;
while (cellQueue.length) {
// pick the most promising cell from the queue
var cell = cellQueue.pop();
// update the best cell if we found a better one
if (cell.d > bestCell.d) {
bestCell = cell;
if (debug) console.log('found best %d after %d probes', Math.round(1e4 * cell.d) / 1e4, numProbes);
}
// do not drill down further if there's no chance of a better solution
if (cell.max - bestCell.d <= precision) continue;
// split the cell into four cells
h = cell.h / 2;
cellQueue.push(new Cell(cell.x - h, cell.y - h, h, polygon));
cellQueue.push(new Cell(cell.x + h, cell.y - h, h, polygon));
cellQueue.push(new Cell(cell.x - h, cell.y + h, h, polygon));
cellQueue.push(new Cell(cell.x + h, cell.y + h, h, polygon));
numProbes += 4;
}
if (debug) {
console.log('num probes: ' + numProbes);
console.log('best distance: ' + bestCell.d);
}
return [bestCell.x, bestCell.y];
}
function compareMax(a, b) {
return b.max - a.max;
}
function Cell(x, y, h, polygon) {
this.x = x; // cell center x
this.y = y; // cell center y
this.h = h; // half the cell size
this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon
this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell
}
// signed distance from point to polygon outline (negative if point is outside)
function pointToPolygonDist(x, y, polygon) {
var inside = false;
var minDistSq = Infinity;
for (var k = 0; k < polygon.length; k++) {
var ring = polygon[k];
for (var i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
var a = ring[i];
var b = ring[j];
if ((a[1] > y !== b[1] > y) &&
(x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0])) inside = !inside;
minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b));
}
}
return (inside ? 1 : -1) * Math.sqrt(minDistSq);
}
// get polygon centroid
function getCentroidCell(polygon) {
var area = 0;
var x = 0;
var y = 0;
var points = polygon[0];
for (var i = 0, len = points.length, j = len - 1; i < len; j = i++) {
var a = points[i];
var b = points[j];
var f = a[0] * b[1] - b[0] * a[1];
x += (a[0] + b[0]) * f;
y += (a[1] + b[1]) * f;
area += f * 3;
}
if (area === 0) return new Cell(points[0][0], points[0][1], 0, polygon);
return new Cell(x / area, y / area, 0, polygon);
}
// get squared distance from a point to a segment
function getSegDistSq(px, py, a, b) {
var x = a[0];
var y = a[1];
var dx = b[0] - x;
var dy = b[1] - y;
if (dx !== 0 || dy !== 0) {
var t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
if (t > 1) {
x = b[0];
y = b[1];
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}
dx = px - x;
dy = py - y;
return dx * dx + dy * dy;
}
},{"tinyqueue":2}],2:[function(require,module,exports){
'use strict';
module.exports = TinyQueue;
module.exports.default = TinyQueue;
function TinyQueue(data, compare) {
if (!(this instanceof TinyQueue)) return new TinyQueue(data, compare);
this.data = data || [];
this.length = this.data.length;
this.compare = compare || defaultCompare;
if (this.length > 0) {
for (var i = (this.length >> 1) - 1; i >= 0; i--) this._down(i);
}
}
function defaultCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
TinyQueue.prototype = {
push: function (item) {
this.data.push(item);
this.length++;
this._up(this.length - 1);
},
pop: function () {
if (this.length === 0) return undefined;
var top = this.data[0];
this.length--;
if (this.length > 0) {
this.data[0] = this.data[this.length];
this._down(0);
}
this.data.pop();
return top;
},
peek: function () {
return this.data[0];
},
_up: function (pos) {
var data = this.data;
var compare = this.compare;
var item = data[pos];
while (pos > 0) {
var parent = (pos - 1) >> 1;
var current = data[parent];
if (compare(item, current) >= 0) break;
data[pos] = current;
pos = parent;
}
data[pos] = item;
},
_down: function (pos) {
var data = this.data;
var compare = this.compare;
var halfLength = this.length >> 1;
var item = data[pos];
while (pos < halfLength) {
var left = (pos << 1) + 1;
var right = left + 1;
var best = data[left];
if (right < this.length && compare(data[right], best) < 0) {
left = right;
best = data[right];
}
if (compare(best, item) >= 0) break;
data[pos] = best;
pos = left;
}
data[pos] = item;
}
};
},{}],3:[function(require,module,exports){
window.polylabel = require('polylabel');
// var p = polylabel(geojsonFeatureCol, 1.0);
// var weightedCentroid = require('turf-weighted-centroid');
//
//
},{"polylabel":1}]},{},[3]);
<!-- First, I found the centoid with d3.path and converted it from pixels to lat lng coordinates with leaflet (https://github.com/d3/d3-geo#path_centroid)
I also used:
turf.centroid: https://turfjs.org/docs/#centroid
turf.centerOfMass: https://turfjs.org/docs/#centerOfMass
polylabel (pole of inaccessibility): https://github.com/mapbox/polylabel
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title></title>
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
<script src="https://d3js.org/queue.v1.min.js"></script>
<script src='bundle.js'></script>
<script src="https://cdn.jsdelivr.net/npm/geolib@3.0.4/lib/index.min.js"></script>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css" />
<style>
html, body {
padding: 0px;
margin: 0px;
}
html,body,#map {
width: 100%;
height: 100%;
}
.tick line {
stroke-dasharray: 2 2 ;
stroke: #ccc;
}
.dot {
height: .5em;
width: .5em;
background-color: #bbb;
border-radius: 50%;
display: inline-block;
}
.info {
padding: 6px 8px;
font: 14px/16px Arial, Helvetica, sans-serif;
background: white;
background: rgba(255,255,255,0.8);
box-shadow: 0 0 15px rgba(0,0,0,0.2);
border-radius: 5px;
}
.legend {
line-height: 18px;
color: #555;
}
.legend i {
width: 18px;
height: 18px;
float: left;
margin-right: 8px;
opacity: 0.7;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
<script>
var map;
var url = 'https://gist.githubusercontent.com/Alex-Devoid/5f6665782677129909bfc76569cf118d/raw/ce4e5f6ab65b58ed9380f286ec9f6c83dd5dda45/Annexations.json';
var geo = 'tucsonAnexations.geojson';
queue()
.defer(d3.json, url)
.await(main);
function main(error, data) {
addLmaps();
drawFeatures(data);
}
function addLmaps() {
map = L.map('map').setView([32.216191, -110.924758], 11);
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
L.svg().addTo(map);
var legend = L.control({position: 'bottomleft'});
legend.onAdd = function (map) {
var div = L.DomUtil.create('div', 'info legend');
// loop through our density intervals and generate a label with a colored square for each interval
div.innerHTML =
'<i style="background:'+"red" + '"></i> ' + "d3 + leaflet" + '</p>'+
'<i style="background:'+"blue" + '"></i> ' + "Turf Centroid" + '</p>'+
'<i style="background:'+"green" + '"></i> ' + "Turf Center of Mass" + '</p>'+
'<i style="background:'+"purple" + '"></i> ' + "Polylable" ;
return div;
};
legend.addTo(map);
}
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
function drawFeatures(az, geo) {
var svg = d3.select("#map").select("svg");
var g = svg.append("g")
var transform = d3.geoTransform({point: projectPoint});
var path = d3.geoPath().projection(transform);
var selected = d3.set([
'TUC'
]);
var mergeTucson = topojson.merge(az, az.objects.Annexations.geometries.filter(function(d) {
return selected.has(d.properties.CITY_CD)
}));
var mergeCentroid = path.centroid(mergeTucson);
var latLngCentroid = map.layerPointToLatLng(mergeCentroid);
///////////
var turfCentroid = turf.centroid(mergeTucson);
////////////
var turfCenterOfMass = turf.centerOfMass(mergeTucson);
///////////
var pp = polylabel(mergeTucson.coordinates[0], 1.0);
console.log('d3 + leaflet');
console.log(latLngCentroid);
console.log('Turf');
console.log("turfCentroid_GEO: ");
console.log(turfCentroid);
console.log('centerOfMass: ');
console.log(turfCenterOfMass);
console.log('polylabel');
console.log(pp);
var latLngCentroid1 = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": []
},
"properties": {
"name": "D3 to Leaflet Centroid"
}
}
var turfCentroid1 = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": []
},
"properties": {
"name": "Turf Centroid"
}
}
var turfCenterOfMass1 = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": []
},
"properties": {
"name": "Turf Center Of Mass"
}
}
var pp1 = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": []
},
"properties": {
"name": "Turf Weighted Centroid"
}
}
latLngCentroid1.geometry.coordinates.push(latLngCentroid.lat)
latLngCentroid1.geometry.coordinates.push(latLngCentroid.lng)
turfCentroid1.geometry.coordinates.push(turfCentroid.geometry.coordinates[1])
turfCentroid1.geometry.coordinates.push(turfCentroid.geometry.coordinates[0])
turfCenterOfMass1.geometry.coordinates.push(turfCenterOfMass.geometry.coordinates[1])
turfCenterOfMass1.geometry.coordinates.push(turfCenterOfMass.geometry.coordinates[0])
pp1.geometry.coordinates.push(pp[1])
pp1.geometry.coordinates.push(pp[0])
var d3LeafletCircle = L.circle(latLngCentroid1.geometry.coordinates, {
color: 'red',
fillColor: 'red',
fillOpacity: 0.5,
radius: 100
}).addTo(map);
d3LeafletCircle.bindPopup("d3 + leaflet").openPopup();
var turfCentroidCircle = L.circle(turfCentroid1.geometry.coordinates, {
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.5,
radius: 100
}).addTo(map);
turfCentroidCircle.bindPopup("Turf Centroid").openPopup();
var turfCenterOfMassCircle = L.circle(turfCenterOfMass1.geometry.coordinates, {
color: 'green',
fillColor: 'green',
fillOpacity: 0.5,
radius: 100
}).addTo(map);
turfCenterOfMassCircle.bindPopup("Turf Center of Mass").openPopup();
var polylableCircle = L.circle(pp1.geometry.coordinates, {
color: 'purple',
fillColor: 'purple',
fillOpacity: 0.5,
radius: 100
}).addTo(map);
polylableCircle.bindPopup('Polylable "pole of inaccessibility"').openPopup();
var featureElement = svg.selectAll("path")
.data([mergeTucson]).enter().append("path")
.attr("class", "state selected")
.attr("fill", "grey")
.attr("fill-opacity", 0.6)
.attr("stroke", "#000ffd");
map.on("moveend", update);
update();
function update() {
featureElement.attr("d", path);
}
}
</script>
</body>
</html>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment