Published
Edited
Sep 15, 2021
Importers
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require("d3@5", "d3-geo-projection@2")
Insert cell
import { range } from '@nuuuwan/list-utils'
Insert cell
import { addDefaults } from '@nuuuwan/option-utils'
Insert cell
import { slider } from "@jashkenas/inputs"
Insert cell
import {
getSVG,
drawText,
drawCircle,
drawImage,
drawRect
} from '@nuuuwan/svg-utils'
Insert cell
DEFAULT_ZOOM = 12
Insert cell
M_IN_KM = 1_000
Insert cell
RADIUS_OF_EARTH_M = 6_371_000
Insert cell
function degToRad(deg) {
const Q = 1_000_000;
const degRound = Math.round(deg * Q) / Q;
return (deg * Math.PI) / 180;
}
Insert cell
function getDistance([lat1Deg, lng1Deg], [lat2Deg, lng2Deg]) {
const [lat1, lng1] = [degToRad(lat1Deg), degToRad(lng1Deg)];
const [lat2, lng2] = [degToRad(lat2Deg), degToRad(lng2Deg)];

const dlat = lat2 - lat1;
const dlng = lng2 - lng1;

const a =
Math.sin(dlat / 2) ** 2 +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dlng / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return (c * RADIUS_OF_EARTH_M) / M_IN_KM;
}
Insert cell
function latLngToXY([lat, lng], zoom) {
const x = Math.floor(((lng + 180) / 360) * Math.pow(2, zoom));
const y = Math.floor(
((1 -
Math.log(
Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)
) /
Math.PI) /
2) *
Math.pow(2, zoom)
);
return [x, y];
}
Insert cell
function xyToLatLng([x, y], zoom) {
const lng = (x / Math.pow(2, zoom)) * 360 - 180;
var n = Math.PI - (2 * Math.PI * y) / Math.pow(2, zoom);
const lat = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
return [lat, lng];
}
Insert cell
function getTileArrayInfo(latLngList, options) {
const [minTileX, minTileY, maxTileX, maxTileY] = latLngList.reduce(
function([minTileX, minTileY, maxTileX, maxTileY], [lat, lng], i) {
const [xTile, yTile] = latLngToXY([lat, lng], options.zoom);
if (i === 0) {
minTileX = maxTileX = xTile;
minTileY = maxTileY = yTile;
} else {
minTileX = Math.min(minTileX, xTile);
minTileY = Math.min(minTileY, yTile);
maxTileX = Math.max(maxTileX, xTile);
maxTileY = Math.max(maxTileY, yTile);
}
return [minTileX, minTileY, maxTileX, maxTileY];
},
[undefined, undefined, undefined, undefined]
);

const [tileXSpan, tileYSpan] = [
maxTileX - minTileX + 1,
maxTileY - minTileY + 1
];

const tileDim = Math.min(
options.maxWidth / tileXSpan,
options.maxHeight / tileYSpan
);

const [minLat, minLng] = xyToLatLng([minTileX, minTileY], options.zoom);
const [maxLat, maxLng] = xyToLatLng(
[maxTileX + 1, maxTileY + 1],
options.zoom
);

const [latSpan, lngSpan] = [maxLat - minLat, maxLng - minLng];
const [tileArrayOriginX, tileArrayOriginY] = options.tileArrayOrigin;
function transform([lat, lng]) {
const [pLat, pLng] = [(lat - minLat) / latSpan, (lng - minLng) / lngSpan];
return [
parseInt(tileArrayOriginX + pLng * tileXSpan * tileDim),
parseInt(tileArrayOriginY + pLat * tileYSpan * tileDim)
];
}

return {
origin: [minTileX, minTileY],
dimensions: [tileXSpan, tileYSpan],
tileDim,
transform
};
}
Insert cell
function getMapURL([x, y], zoom) {
return `https://tile.openstreetmap.org/${zoom}/${x}/${y}.png`;
}
Insert cell
Insert cell
DEFAULT_OPTIONS = Object({
zoom: DEFAULT_ZOOM,
maxWidth: 800,
maxHeight: 450,
fontSize: 24,
tileArrayOrigin: [0, 0]
})
Insert cell
function drawTileArray(
svg,
[tileMinX, tileMinY],
[tileXSpan, tileYSpan],
options
) {
const [tileArrayOriginX, tileArrayOriginY] = options.tileArrayOrigin;
range(0, tileXSpan).forEach(function(iX) {
const xTile = tileMinX + iX;
range(0, tileYSpan).forEach(function(iY) {
const yTile = tileMinY + iY;
drawTile(
svg,
[
tileArrayOriginX + iX * options.tileDim,
tileArrayOriginY + iY * options.tileDim
],
options.tileDim,
[xTile, yTile],
options
);
});
});
}
Insert cell
function renderPoints(pointInfoList, options = {}) {
options = addDefaults(options, DEFAULT_OPTIONS);

const tileArrayinfo = getTileArrayInfo(
pointInfoList.map(pointInfo => pointInfo.latLng),
options
);
options.tileDim = tileArrayinfo.tileDim;

const svg = getSVG(options);
drawTileArray(svg, tileArrayinfo.origin, tileArrayinfo.dimensions, options);

pointInfoList.forEach(function(pointInfo) {
const [xLocation, yLocation] = tileArrayinfo.transform(pointInfo.latLng);

const r = options.fontSize / 2;
drawCircle(svg, [xLocation, yLocation], r, {
fill: pointInfo.color ||= 'white',
stroke: 'black',
strokeWidth: r / 2.5
});
drawText(svg, [xLocation + r * 1.5, yLocation], pointInfo.label, {
textAnchor: 'start',
fill: 'black',
fontSize: options.fontSize
});
});

return svg.node();
}
Insert cell
renderPoints(
[
{
latLng: [6.91568366128759, 79.86354070923832],
label: 'Town Hall',
color: 'blue'
},
{
latLng: [6.914772992482462, 79.87754136567636],
label: 'Borella Junction',
color: 'red'
},
{
latLng: [6.898880743934121, 79.86046492040172],
label: 'Thummulla',
color: 'green'
}
],
{ zoom: ZOOM }
)
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more