|
- import * as d3geo from 'd3-geo';
- import * as d3geoProjection from 'd3-geo-projection';
- import * as d3geoPolygon from 'd3-geo-polygon';
- import { PNG } from 'pngjs';
- import { readFile } from 'fs/promises';
-
- type Coords = [number, number]
- type Bounds = [Coords, Coords]
-
- type ProjectionFunction = (...args: unknown[]) => (
- d3geo.GeoProjection | d3geoProjection.GeoProjection
- )
-
- const referenceWidth = 960 as const;
- const referenceHeight = 480 as const;
-
- type ProjectOptions = {
- inputDimensions?: { width: number, height: number },
- bounds?: Bounds,
- }
-
- type Projection = [string, ...unknown[]]
-
- const isValidLongitude = (lng: number) => lng >= -180 && lng <= 180;
- const isValidLatitude = (lat: number) => lat >= -90 && lat <= 90;
- const getCenter = (bounds: Bounds): Coords => {
- const [nw, se] = bounds;
- const [nwLng, nwLat] = nw;
- const [seLng, seLat] = se;
- return [
- // TODO: better center checking that wraps longitudes
- (nwLng + seLng) / 2,
- (nwLat + seLat) / 2,
- ];
- };
-
- export const projectPoint = (
- point: [number, number],
- projection: Projection,
- options: ProjectOptions = {},
- ): [number, number] | null => {
- const [lng, lat] = point;
- if (!(isValidLongitude(lng) && isValidLatitude(lat))) {
- throw new RangeError('Invalid value for point. Only values from [-180, -90] to [180, 90] are allowed');
- }
-
- const [projectionName, ...projectionArgs] = projection;
- if (projectionName === 'Equirectangular') {
- return point;
- }
-
- const projectionFunctionName = `geo${projectionName}`;
- let geoProjectionSource: Record<string, unknown> | undefined;
- if (projectionFunctionName in d3geoPolygon) {
- geoProjectionSource = d3geoPolygon as Record<string, unknown>;
- } else if (projectionFunctionName in d3geoProjection) {
- geoProjectionSource = d3geoProjection;
- } else if (projectionFunctionName in d3geo) {
- geoProjectionSource = d3geo;
- }
-
- if (!geoProjectionSource) {
- throw new Error(`Unknown projection: ${projectionName}`);
- }
-
- const projectionFn = geoProjectionSource[projectionFunctionName] as ProjectionFunction;
- const baseProjection = projectionFn(...projectionArgs);
- const {
- bounds = [[-180, 90], [180, -90]],
- inputDimensions = { width: referenceWidth, height: referenceHeight },
- } = options;
-
- const { invert } = baseProjection.center(getCenter(bounds));
- if (!invert) {
- return null;
- }
-
- const { width: sx, height: sy } = inputDimensions;
- return invert([
- (lng / sx) * referenceWidth,
- (lat / sy) * referenceHeight,
- ]) as [number, number];
- };
-
- const retrievePngData = async (pngInput: string | Buffer | PNG) => {
- if (typeof pngInput === 'string') {
- const pngFile = await readFile(pngInput);
- return PNG.sync.read(pngFile);
- }
-
- if (typeof pngInput === 'object') {
- return pngInput instanceof PNG ? pngInput : PNG.sync.read(pngInput);
- }
-
- throw new TypeError('Invalid input argument.');
- };
-
- const prepareOutputData = (
- _projection: Projection,
- sourceWidth: number,
- sourceHeight: number,
- ): PNG => {
- const [tx, ty] = [sourceWidth, sourceHeight];
- return new PNG({
- width: tx,
- height: ty,
- });
- };
-
- export const project = async (
- pngInput: string | Buffer | PNG,
- projection: Projection,
- options?: ProjectOptions,
- ) => {
- const sourcePngData = await retrievePngData(pngInput);
- const { width: sx, height: sy, data: sourceData } = sourcePngData;
- // const [tx, ty] = currentProjectionData.bounds(bounds)([sx, sy]);
- // TODO correctly compute result bounds
- const outputPngData = prepareOutputData(projection, sx, sy);
- const { width: tx, height: ty, data: targetData } = outputPngData;
-
- let i = 0;
- for (let y = (sy - ty) / 2; y < (ty + ((sy - ty) / 2)); y += 1) {
- for (let x = 0; x < (tx + ((sx - tx) / 2)); x += 1) {
- const projected = projectPoint([x, y], projection, {
- bounds: options?.bounds,
- });
- if (projected) {
- const [lambda, phi] = projected;
- if (!(lambda > 180 || lambda < -180 || phi > 90 || phi < -90)) {
- // eslint-disable-next-line no-bitwise
- const q = (((90 - phi) / 180) * sy | 0) * sx + (((180 + lambda) / 360) * sx | 0) << 2;
- targetData[i] = sourceData[q];
- targetData[i + 1] = sourceData[q + 1];
- targetData[i + 2] = sourceData[q + 2];
- targetData[i + 3] = sourceData[q + 3];
- }
- }
- i += 4;
- }
- }
-
- return outputPngData;
- };
|