|
- import * as d3geo from 'd3-geo';
- import * as d3geoProjection from 'd3-geo-projection';
- import * as d3geoPolygon from 'd3-geo-polygon';
-
- 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 project = (
- 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];
- };
|