geojson-map-fit-mercator
Version:
Finds the optimal bearing, zoom and center point for fitting a set of GeoJSON features in a Mapbox GL or LibreMap Mercator map.
168 lines (160 loc) • 8.51 kB
JavaScript
/*!
* geojson-map-fit-mercator v1.0.1
* (c) undefined
* Released under the MIT License.
*/
;
var sphericalmercator = require('@mapbox/sphericalmercator');
var turf = require('@turf/turf');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var turf__namespace = /*#__PURE__*/_interopNamespaceDefault(turf);
function findScreenZoom(paddedScreenDimensions, paddedScreenRatio, boundingRectangleOrientation, maxZoom, floatZoom, merc) {
const { shortSide, longSide } = boundingRectangleOrientation;
const longSideCoords = turf__namespace.getCoords(longSide);
const shortSideCoords = turf__namespace.getCoords(shortSide);
// We need to determine the ratio required for the zoom level. To do this we are going to approximate the length
// of the longest and shortest sides of the polygon in pixels (This doesn't account for projection distortion but is
// a good estimation)
const longPx = [merc.px(longSideCoords[0], maxZoom), merc.px(longSideCoords[1], maxZoom)];
const shortPx = [merc.px(shortSideCoords[0], maxZoom), merc.px(shortSideCoords[1], maxZoom)];
// Because these points aren't aligned to the axis, we use the Pythagorean theorem to calculate the distance
const longPxX = longPx[0][0] - longPx[1][0];
const longPxY = longPx[0][1] - longPx[1][1];
const shortPxX = shortPx[0][0] - shortPx[1][0];
const shortPxY = shortPx[0][1] - shortPx[1][1];
const longPxDistance = Math.sqrt(Math.pow(longPxX, 2) + Math.pow(longPxY, 2));
const shortPxDistance = Math.sqrt(Math.pow(shortPxX, 2) + Math.pow(shortPxY, 2));
let xPx = longPxDistance;
let yPx = shortPxDistance;
// If the screen is taller than it is wide, swap the x and y values
if (paddedScreenRatio < 1) {
xPx = shortPxDistance;
yPx = longPxDistance;
}
const ratios = [Math.abs(xPx / paddedScreenDimensions[0]), Math.abs(yPx / paddedScreenDimensions[1])];
const zoom = Math.min(maxZoom - Math.log(ratios[0]) / Math.log(2), maxZoom - Math.log(ratios[1]) / Math.log(2));
return floatZoom ? zoom : Math.floor(zoom);
}
function findScreenBearing(boundingRectangleBearing, preferredBearing, screenRatio) {
let bearing = boundingRectangleBearing;
// Rotate the bearing by 90 degrees if the screen is wider than it is tall
if (screenRatio > 1) {
bearing = bearing + (90 % 360);
}
// Rotate the bearing 180 degrees if the preferred bearing is on the opposite side of the screen
if (bearing < (preferredBearing - 90) % 360 || bearing > (preferredBearing + 90) % 360) {
bearing = (bearing + 180) % 360;
}
return bearing;
}
function findScreenCenter(boundingRectangle, bearing, zoom, padding, merc) {
const { left = 0, right = 0, top = 0, bottom = 0 } = padding;
// Use the bounding rectangle's pixel location to calculate the centre of the
// map. This allows us to account for mercator projection distortion.
const coords = turf__namespace.getCoords(boundingRectangle);
const uniqCoords = coords[0].reduce((uniq, coord) => {
if (!uniq.find((c) => c[0] === coord[0] && c[1] === coord[1])) {
uniq.push(coord);
}
return uniq;
}, []);
const sumCoords = uniqCoords.reduce((acc, coord) => {
const [x, y] = merc.px(coord, zoom);
acc[0] = acc[0] + x;
acc[1] = acc[1] + y;
return acc;
}, [0, 0]);
const midX = sumCoords[0] / uniqCoords.length;
const midY = sumCoords[1] / uniqCoords.length;
const xPaddingOffset = right - left;
const yPaddingOffset = bottom - top;
const bearingRadians = bearing * (Math.PI / 180);
const centerXOffset = xPaddingOffset * Math.cos(bearingRadians) - yPaddingOffset * Math.sin(bearingRadians);
const centerYOffset = xPaddingOffset * Math.sin(bearingRadians) + yPaddingOffset * Math.cos(bearingRadians);
return merc.ll([midX + centerXOffset, midY + centerYOffset], zoom);
}
function mapFitFeatures(features, screenDimensions, options = {}) {
// Set default options
const { tileSize = 512, preferredBearing = 0, padding = {}, maxZoom = 23, floatZoom = true, } = options;
// Create a mercator projection. SphericalMercator caches its calculations so it's safe to create a new instance each run
const merc = new sphericalmercator.SphericalMercator({ size: tileSize, antimeridian: true });
const [screenWidth, screenHeight] = screenDimensions;
const { left = 0, right = 0, top = 0, bottom = 0 } = padding;
const paddedScreenWidth = screenWidth - left - right;
const paddedScreenHeight = screenHeight - top - bottom;
const paddedScreenRatio = paddedScreenWidth / paddedScreenHeight;
// Calculate the bounding rectangle of the features
const { boundsOrientation: { orientation, bearing: baseBearing }, boundingRectangle, } = minimumBoundingRectangle(features);
if (!boundingRectangle) {
throw new Error('Unable to calculate bounding rectangle');
}
// Determine how to fit the bounding rectangle to the screen
const zoom = findScreenZoom([paddedScreenWidth, paddedScreenHeight], paddedScreenRatio, orientation, maxZoom, floatZoom, merc);
const bearing = findScreenBearing(baseBearing, preferredBearing, paddedScreenRatio);
const center = findScreenCenter(boundingRectangle, bearing, zoom, padding, merc);
return { bearing, zoom, center };
}
function minimumBoundingRectangle(geoJsonInput) {
// Create a convex hull around the input geometry
const convexHull = turf__namespace.convex(geoJsonInput);
if (!convexHull)
throw new Error("Can't determine minimumBoundingRectangle for given geometry");
// Break the hull into its constituent edges and find the smallest
const hullLines = turf__namespace.polygonToLine(convexHull);
const smallestHullBoundsOrientation = turf__namespace.segmentReduce(hullLines, (smallestEnvelope, segment) => {
return smallestHullEnvelopeReducer(smallestEnvelope, segment, convexHull);
}, { bearing: undefined, orientation: { shortSide: undefined, longSide: undefined }, envelope: undefined });
const boundingRectangle = turf__namespace.transformRotate(turf__namespace.envelope(smallestHullBoundsOrientation.envelope), smallestHullBoundsOrientation.bearing, {
pivot: turf__namespace.centroid(convexHull),
});
return {
boundsOrientation: smallestHullBoundsOrientation,
boundingRectangle,
};
}
function smallestHullEnvelopeReducer(smallestEnvelope, segment, hull) {
const segmentCoords = turf__namespace.getCoords(segment);
const bearing = turf__namespace.bearing(segmentCoords[0], segmentCoords[1]);
const rotatedHull = turf__namespace.transformRotate(hull, -1 * bearing, {
pivot: turf__namespace.centroid(hull),
});
const envelopeOfHull = turf__namespace.envelope(rotatedHull);
const rectangleOrientation = findRectangleOrientation(envelopeOfHull);
const shortSideLength = turf__namespace.length(rectangleOrientation.shortSide);
if (smallestEnvelope.orientation.shortSide == undefined ||
shortSideLength < turf__namespace.length(smallestEnvelope.orientation.shortSide)) {
return { bearing, orientation: rectangleOrientation, envelope: envelopeOfHull };
}
return smallestEnvelope;
}
function findRectangleOrientation(rectangle) {
const rectangleSides = turf__namespace.polygonToLine(rectangle);
return turf__namespace.segmentReduce(rectangleSides, (sideOrientation, segment) => {
const segmentLength = turf__namespace.length(segment);
if (sideOrientation.shortSide == undefined || turf__namespace.length(sideOrientation.shortSide) > segmentLength) {
sideOrientation.shortSide = segment;
}
if (sideOrientation.longSide == undefined || turf__namespace.length(sideOrientation.longSide) < segmentLength) {
sideOrientation.longSide = segment;
}
return sideOrientation;
}, { shortSide: undefined, longSide: undefined });
}
exports.mapFitFeatures = mapFitFeatures;
exports.minimumBoundingRectangle = minimumBoundingRectangle;
//# sourceMappingURL=index.cjs.map