placemark/togeojson

View on GitHub
lib/kml/ground_overlay.ts

Summary

Maintainability
A
35 mins
Test Coverage
import { Feature, Polygon } from "geojson";
import { StyleMap, get1, num1, getMulti } from "../shared";
import {
  extractCascadedStyle,
  extractExtendedData,
  extractTimeSpan,
  extractTimeStamp,
  getMaybeHTMLDescription,
  Schema,
} from "./shared";
import { extractIconHref, extractStyle } from "./extractStyle";
import { coord, fixRing, getCoordinates } from "./geometry";
import { KMLOptions } from "lib/kml";

interface BoxGeometry {
  bbox?: BBox;
  geometry: Polygon;
}

function getGroundOverlayBox(node: Element): BoxGeometry | null {
  const latLonQuad = get1(node, "gx:LatLonQuad");

  if (latLonQuad) {
    const ring = fixRing(coord(getCoordinates(node)));
    return {
      geometry: {
        type: "Polygon",
        coordinates: [ring],
      },
    };
  }

  return getLatLonBox(node);
}

type BBox = [number, number, number, number];

const DEGREES_TO_RADIANS = Math.PI / 180;

function rotateBox(
  bbox: BBox,
  coordinates: Polygon["coordinates"],
  rotation: number
): Polygon["coordinates"] {
  const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];

  return [
    coordinates[0].map((coordinate) => {
      const dy = coordinate[1] - center[1];
      const dx = coordinate[0] - center[0];
      const distance = Math.sqrt(Math.pow(dy, 2) + Math.pow(dx, 2));
      const angle = Math.atan2(dy, dx) + rotation * DEGREES_TO_RADIANS;

      return [
        center[0] + Math.cos(angle) * distance,
        center[1] + Math.sin(angle) * distance,
      ];
    }),
  ];
}

function getLatLonBox(node: Element): BoxGeometry | null {
  const latLonBox = get1(node, "LatLonBox");

  if (latLonBox) {
    const north = num1(latLonBox, "north");
    const west = num1(latLonBox, "west");
    const east = num1(latLonBox, "east");
    const south = num1(latLonBox, "south");
    const rotation = num1(latLonBox, "rotation");

    if (
      typeof north === "number" &&
      typeof south === "number" &&
      typeof west === "number" &&
      typeof east === "number"
    ) {
      const bbox: BBox = [west, south, east, north];
      let coordinates = [
        [
          [west, north], // top left
          [east, north], // top right
          [east, south], // top right
          [west, south], // bottom left
          [west, north], // top left (again)
        ],
      ];
      if (typeof rotation === "number") {
        coordinates = rotateBox(bbox, coordinates, rotation);
      }
      return {
        bbox,
        geometry: {
          type: "Polygon",
          coordinates,
        },
      };
    }
  }

  return null;
}

export function getGroundOverlay(
  node: Element,
  styleMap: StyleMap,
  schema: Schema,
  options: KMLOptions
): Feature<Polygon | null> | null {
  const box = getGroundOverlayBox(node);

  const geometry = box?.geometry || null;

  if (!geometry && options.skipNullGeometry) {
    return null;
  }

  const feature: Feature<Polygon | null> = {
    type: "Feature",
    geometry,
    properties: Object.assign(
      /**
       * Related to
       * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46
       */
      { "@geometry-type": "groundoverlay" },
      getMulti(node, [
        "name",
        "address",
        "visibility",
        "open",
        "phoneNumber",
        "description",
      ]),
      getMaybeHTMLDescription(node),
      extractCascadedStyle(node, styleMap),
      extractStyle(node),
      extractIconHref(node),
      extractExtendedData(node, schema),
      extractTimeSpan(node),
      extractTimeStamp(node)
    ),
  };

  if (box?.bbox) {
    feature.bbox = box.bbox;
  }

  if (feature.properties?.visibility !== undefined) {
    feature.properties.visibility = feature.properties.visibility !== "0";
  }

  const id = node.getAttribute("id");
  if (id !== null && id !== "") feature.id = id;
  return feature;
}