Last active
January 23, 2020 07:28
-
-
Save Alex-Devoid/5f6665782677129909bfc76569cf118d to your computer and use it in GitHub Desktop.
Different centers: path.centroid to leaflet | turf.centroid | turf.centerOfMass | polylabel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- 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/[email protected]/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/[email protected]/dist/leaflet.css" /> | |
<script src="https://unpkg.com/[email protected]/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: '© <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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment