/**
 * JSTS. See https://github.com/bjornharrtell/jsts
 * https://github.com/bjornharrtell/jsts/blob/master/LICENSE_EDLv1.txt
 * https://github.com/bjornharrtell/jsts/blob/master/LICENSE_EPLv1.txt
 * @license
 */

class NumberUtil {
  static equalsWithTolerance(x1, x2, tolerance) {
    return Math.abs(x1 - x2) <= tolerance;
  }

}

class Exception extends Error {
  constructor(message) {
    super(message);
    this.name = Object.keys({
      Exception
    })[0];
  }

  toString() {
    return this.message;
  }

}

class IllegalArgumentException extends Exception {
  constructor(message) {
    super(message);
    this.name = Object.keys({
      IllegalArgumentException
    })[0];
  }

}

class Long {
  constructor(high, low) {
    this.low = low || 0;
    this.high = high || 0;
  }

  static toBinaryString(i) {
    let mask;
    let result = '';

    for (mask = 0x80000000; mask > 0; mask >>>= 1) result += (i.high & mask) === mask ? '1' : '0';

    for (mask = 0x80000000; mask > 0; mask >>>= 1) result += (i.low & mask) === mask ? '1' : '0';

    return result;
  }

}

function Double() {}
Double.NaN = NaN;

Double.isNaN = n => Number.isNaN(n);

Double.isInfinite = n => !Number.isFinite(n);

Double.MAX_VALUE = Number.MAX_VALUE;
Double.POSITIVE_INFINITY = Number.POSITIVE_INFINITY;
Double.NEGATIVE_INFINITY = Number.NEGATIVE_INFINITY;
if (typeof Float64Array === 'function' && typeof Int32Array === 'function') // Simple and fast conversion between double and long bits
  // using TypedArrays and ArrayViewBuffers.
  (function () {
    const EXP_BIT_MASK = 0x7ff00000;
    const SIGNIF_BIT_MASK = 0xFFFFF;
    const f64buf = new Float64Array(1);
    const i32buf = new Int32Array(f64buf.buffer);

    Double.doubleToLongBits = function (value) {
      f64buf[0] = value;
      let low = i32buf[0] | 0;
      let high = i32buf[1] | 0; // Check for NaN based on values of bit fields, maximum
      // exponent and nonzero significand.

      if ((high & EXP_BIT_MASK) === EXP_BIT_MASK && (high & SIGNIF_BIT_MASK) !== 0 && low !== 0) {
        low = 0 | 0;
        high = 0x7ff80000 | 0;
      }

      return new Long(high, low);
    };

    Double.longBitsToDouble = function (bits) {
      i32buf[0] = bits.low;
      i32buf[1] = bits.high;
      return f64buf[0];
    };
  })();else // More complex and slower fallback implementation using
  // math and the divide-by-two and multiply-by-two algorithms.
  (function () {
    const BIAS = 1023;
    const log2 = Math.log2;
    const floor = Math.floor;
    const pow = Math.pow;

    const MAX_REL_BITS_INTEGER = function () {
      for (let i = 53; i > 0; i--) {
        const bits = pow(2, i) - 1;
        if (floor(log2(bits)) + 1 === i) return bits;
      }

      return 0;
    }();

    Double.doubleToLongBits = function (value) {
      let x, y, f, bits, skip;
      let sign, exp, high, low; // Get the sign bit and absolute value.

      if (value < 0 || 1 / value === Number.NEGATIVE_INFINITY) {
        sign = 1 << 31;
        value = -value;
      } else {
        sign = 0;
      } // Handle some special values.


      if (value === 0) {
        // Handle zeros (+/-0).
        low = 0 | 0;
        high = sign; // exponent: 00..00, significand: 00..00

        return new Long(high, low);
      }

      if (value === Infinity) {
        // Handle infinity (only positive values for value possible).
        low = 0 | 0;
        high = sign | 0x7ff00000; // exponent: 11..11, significand: 00..00

        return new Long(high, low);
      }

      if (value !== value) {
        // eslint-disable-line
        // Handle NaNs (boiled down to only one distinct NaN).
        low = 0 | 0;
        high = 0x7ff80000; // exponent: 11..11, significand: 10..00

        return new Long(high, low);
      } // Preinitialize variables, that are not neccessarily set by
      // the algorithm.


      bits = 0;
      low = 0 | 0; // Get the (always positive) integer part of value.

      x = floor(value); // Process the integer part if it's greater than 1. Zero requires
      // no bits at all, 1 represents the implicit (hidden) leading bit,
      // which must not be written as well.

      if (x > 1) // If we can reliably determine the number of bits required for
        // the integer part,
        if (x <= MAX_REL_BITS_INTEGER) {
          // get the number of bits required to represent it minus 1
          bits = floor(log2(x));
          /* + 1 - 1 */
          // and simply copy/shift the integer bits into low and high.
          // That's much faster than the divide-by-two algorithm (saves
          // up to ~60%).
          // We always need to mask out the most significant bit, which
          // is the implicit (aka hidden) bit.

          if (bits <= 20) {
            // The simple case in which the integer fits into the
            // lower 20 bits of the high word is worth to be handled
            // separately (saves ~25%).
            low = 0 | 0;
            high = x << 20 - bits & 0xfffff;
          } else {
            // Here, the integer part is split into low and high.
            // Since its value may require more than 32 bits, we
            // cannot use bitwise operators (which implicitly cast
            // to Int32), but use arithmetic operators % and / to
            // get low and high parts. The uppper 20 bits go to high,
            // the remaining bits (in f) to low.
            f = bits - 20; // Like (1 << f) but safe with even more than 32 bits.

            y = pow(2, f);
            low = x % y << 32 - f;
            high = x / y & 0xfffff;
          }
        } else {
          // For greater values, we must use the much slower divide-by-two
          // algorithm. Bits are generated from right to left, that is from
          // least to most significant bit. For each bit, we left-shift both
          // low and high by one and carry bit #0 from high to #31 in low.
          // The next bit is then copied into bit #19 in high, the leftmost
          // bit of the double's significand.
          // Preserve x for later user, so work with f.
          f = x;
          low = 0 | 0;

          for (;;) {
            y = f / 2;
            f = floor(y);
            if (f === 0) // We just found the most signigicant (1-)bit, which
              // is the implicit bit and so, not stored in the double
              // value. So, it's time to leave the loop.
              break; // Count this bit, shift low and carry bit #0 from high.

            bits++;
            low >>>= 1;
            low |= (high & 0x1) << 31; // Shift high.

            high >>>= 1;
            if (y !== f) // Copy the new bit into bit #19 in high (only required if 1).
              high |= 0x80000;
          }
        } // Bias the exponent.

      exp = bits + BIAS; // If the integer part is zero, we've not yet seen the implicit
      // leading bit. Variable skip is later used while processing the
      // fractional part (if any).

      skip = x === 0; // Get fraction only into x.

      x = value - x; // If some significand bits are still left to be filled and
      // the fractional part is not zero, convert the fraction using
      // the multiply-by-2 algorithm.

      if (bits < 52 && x !== 0) {
        // Initialize 'buffer' f, into which newly created bits get
        // shifted from right to left.
        f = 0;

        for (;;) {
          y = x * 2;

          if (y >= 1) {
            // This is a new 1-bit. Add and count this bit, if not
            // prohibited by skip.
            x = y - 1;

            if (!skip) {
              f <<= 1;
              f |= 1;
              bits++;
            } else {
              // Otherwise, decrement the exponent and unset
              // skip, so that all following bits get written.
              exp--;
              skip = false;
            }
          } else {
            // This is a new 0-bit. Add and count this bit, if not
            // prohibited by skip.
            x = y;

            if (!skip) {
              f <<= 1;
              bits++;
            } else if (--exp === 0) {
              // Otherwise we've just decremented the exponent. If the
              // biased exponent is zero now (-1023), we process a
              // subnormal number, which has no impled leading 1-bit.
              // So, count this 0-bit and unset skip to write out
              // all the following bits.
              bits++;
              skip = false;
            }
          }

          if (bits === 20) {
            // When 20 bits have been created in total, we're done with
            // the high word. Copy the bits from 'buffer' f into high
            // and reset 'buffer' f. Following bits will end up in the
            // low word.
            high |= f;
            f = 0;
          } else if (bits === 52) {
            // When 52 bits have been created in total, we're done with
            // low word as well. Copy the bits from 'buffer' f into low
            // and exit the loop.
            low |= f;
            break;
          }

          if (y === 1) {
            // When y is exactly 1, there is no remainder and the process
            // is complete (the number is finite). Copy the bits from
            // 'buffer' f into either low or high and exit the loop.
            if (bits < 20) high |= f << 20 - bits;else if (bits < 52) low |= f << 52 - bits;
            break;
          }
        }
      } // Copy/shift the exponent and sign bits into the high word.


      high |= exp << 20;
      high |= sign;
      return new Long(high, low);
    };

    Double.longBitsToDouble = function (bits) {
      let i;
      let x, exp, fract;
      const high = bits.high;
      const low = bits.low; // Extract the sign.

      const sign = high & 1 << 31 ? -1 : 1; // Extract the unbiased exponent.

      exp = ((high & 0x7ff00000) >> 20) - BIAS; // Calculate the fraction from left to right. Start
      // off with the 20 lower bits from the high word.

      fract = 0;
      x = 1 << 19;

      for (i = 1; i <= 20; i++) {
        if (high & x) fract += pow(2, -i);
        x >>>= 1;
      } // Continue with all 32 bits from the low word.


      x = 1 << 31;

      for (i = 21; i <= 52; i++) {
        if (low & x) fract += pow(2, -i);
        x >>>= 1;
      } // Handle special values.
      // Check for zero and subnormal values.


      if (exp === -BIAS) {
        if (fract === 0) // +/-1.0 * 0.0 => +/-0.0
          return sign * 0;
        exp = -1022;
      } else if (exp === BIAS + 1) {
        // Check for +/-Infinity or NaN.
        if (fract === 0) // +/-1.0 / 0.0 => +/-Infinity
          return sign / 0;
        return NaN;
      } else {
        // Nothing special? Seems to be a normal number.
        // Add the implicit leading bit (1*2^0).
        fract += 1;
      }

      return sign * fract * pow(2, exp);
    };
  })();

function Comparable() {}

function Clonable() {}

function Comparator() {}

function Serializable() {}

class RuntimeException extends Exception {
  constructor(message) {
    super(message);
    this.name = Object.keys({
      RuntimeException
    })[0];
  }

}

class AssertionFailedException extends RuntimeException {
  constructor() {
    super();
    AssertionFailedException.constructor_.apply(this, arguments);
  }

  static constructor_() {
    if (arguments.length === 0) {
      RuntimeException.constructor_.call(this);
    } else if (arguments.length === 1) {
      const message = arguments[0];
      RuntimeException.constructor_.call(this, message);
    }
  }

}

class Assert {
  static shouldNeverReachHere() {
    if (arguments.length === 0) {
      Assert.shouldNeverReachHere(null);
    } else if (arguments.length === 1) {
      const message = arguments[0];
      throw new AssertionFailedException('Should never reach here' + (message !== null ? ': ' + message : ''));
    }
  }

  static isTrue() {
    if (arguments.length === 1) {
      const assertion = arguments[0];
      Assert.isTrue(assertion, null);
    } else if (arguments.length === 2) {
      const assertion = arguments[0],
            message = arguments[1];
      if (!assertion) if (message === null) throw new AssertionFailedException();else throw new AssertionFailedException(message);
    }
  }

  static equals() {
    if (arguments.length === 2) {
      const expectedValue = arguments[0],
            actualValue = arguments[1];
      Assert.equals(expectedValue, actualValue, null);
    } else if (arguments.length === 3) {
      const expectedValue = arguments[0],
            actualValue = arguments[1],
            message = arguments[2];
      if (!actualValue.equals(expectedValue)) throw new AssertionFailedException('Expected ' + expectedValue + ' but encountered ' + actualValue + (message !== null ? ': ' + message : ''));
    }
  }

}

const kBuf = new ArrayBuffer(8);
const kBufAsF64 = new Float64Array(kBuf);
const kBufAsI32 = new Int32Array(kBuf);
class Coordinate {
  constructor() {
    Coordinate.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.x = null;
    this.y = null;
    this.z = null;

    if (arguments.length === 0) {
      Coordinate.constructor_.call(this, 0.0, 0.0);
    } else if (arguments.length === 1) {
      const c = arguments[0];
      Coordinate.constructor_.call(this, c.x, c.y, c.getZ());
    } else if (arguments.length === 2) {
      const x = arguments[0],
            y = arguments[1];
      Coordinate.constructor_.call(this, x, y, Coordinate.NULL_ORDINATE);
    } else if (arguments.length === 3) {
      const x = arguments[0],
            y = arguments[1],
            z = arguments[2];
      this.x = x;
      this.y = y;
      this.z = z;
    }
  }

  static hashCode(n) {
    kBufAsF64[0] = n;
    return kBufAsI32[0] ^ kBufAsI32[1];
  }

  getM() {
    return Double.NaN;
  }

  setOrdinate(ordinateIndex, value) {
    switch (ordinateIndex) {
      case Coordinate.X:
        this.x = value;
        break;

      case Coordinate.Y:
        this.y = value;
        break;

      case Coordinate.Z:
        this.setZ(value);
        break;

      default:
        throw new IllegalArgumentException('Invalid ordinate index: ' + ordinateIndex);
    }
  }

  equals2D() {
    if (arguments.length === 1) {
      const other = arguments[0];
      if (this.x !== other.x) return false;
      if (this.y !== other.y) return false;
      return true;
    } else if (arguments.length === 2) {
      const c = arguments[0],
            tolerance = arguments[1];
      if (!NumberUtil.equalsWithTolerance(this.x, c.x, tolerance)) return false;
      if (!NumberUtil.equalsWithTolerance(this.y, c.y, tolerance)) return false;
      return true;
    }
  }

  setM(m) {
    throw new IllegalArgumentException('Invalid ordinate index: ' + Coordinate.M);
  }

  getZ() {
    return this.z;
  }

  getOrdinate(ordinateIndex) {
    switch (ordinateIndex) {
      case Coordinate.X:
        return this.x;

      case Coordinate.Y:
        return this.y;

      case Coordinate.Z:
        return this.getZ();
    }

    throw new IllegalArgumentException('Invalid ordinate index: ' + ordinateIndex);
  }

  equals3D(other) {
    return this.x === other.x && this.y === other.y && (this.getZ() === other.getZ() || Double.isNaN(this.getZ()) && Double.isNaN(other.getZ()));
  }

  equals(other) {
    if (!(other instanceof Coordinate)) return false;
    return this.equals2D(other);
  }

  equalInZ(c, tolerance) {
    return NumberUtil.equalsWithTolerance(this.getZ(), c.getZ(), tolerance);
  }

  setX(x) {
    this.x = x;
  }

  compareTo(o) {
    const other = o;
    if (this.x < other.x) return -1;
    if (this.x > other.x) return 1;
    if (this.y < other.y) return -1;
    if (this.y > other.y) return 1;
    return 0;
  }

  getX() {
    return this.x;
  }

  setZ(z) {
    this.z = z;
  }

  clone() {
    try {
      const coord = null;
      return coord;
    } catch (e) {
      if (e instanceof CloneNotSupportedException) {
        Assert.shouldNeverReachHere('this shouldn\'t happen because this class is Cloneable');
        return null;
      } else {
        throw e;
      }
    } finally {}
  }

  copy() {
    return new Coordinate(this);
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ', ' + this.getZ() + ')';
  }

  distance3D(c) {
    const dx = this.x - c.x;
    const dy = this.y - c.y;
    const dz = this.getZ() - c.getZ();
    return Math.sqrt(dx * dx + dy * dy + dz * dz);
  }

  getY() {
    return this.y;
  }

  setY(y) {
    this.y = y;
  }

  distance(c) {
    const dx = this.x - c.x;
    const dy = this.y - c.y;
    return Math.sqrt(dx * dx + dy * dy);
  }

  hashCode() {
    let result = 17;
    result = 37 * result + Coordinate.hashCode(this.x);
    result = 37 * result + Coordinate.hashCode(this.y);
    return result;
  }

  setCoordinate(other) {
    this.x = other.x;
    this.y = other.y;
    this.z = other.getZ();
  }

  get interfaces_() {
    return [Comparable, Clonable, Serializable];
  }

}

class DimensionalComparator {
  constructor() {
    DimensionalComparator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._dimensionsToTest = 2;

    if (arguments.length === 0) {
      DimensionalComparator.constructor_.call(this, 2);
    } else if (arguments.length === 1) {
      const dimensionsToTest = arguments[0];
      if (dimensionsToTest !== 2 && dimensionsToTest !== 3) throw new IllegalArgumentException('only 2 or 3 dimensions may be specified');
      this._dimensionsToTest = dimensionsToTest;
    }
  }

  static compare(a, b) {
    if (a < b) return -1;
    if (a > b) return 1;

    if (Double.isNaN(a)) {
      if (Double.isNaN(b)) return 0;
      return -1;
    }

    if (Double.isNaN(b)) return 1;
    return 0;
  }

  compare(c1, c2) {
    const compX = DimensionalComparator.compare(c1.x, c2.x);
    if (compX !== 0) return compX;
    const compY = DimensionalComparator.compare(c1.y, c2.y);
    if (compY !== 0) return compY;
    if (this._dimensionsToTest <= 2) return 0;
    const compZ = DimensionalComparator.compare(c1.getZ(), c2.getZ());
    return compZ;
  }

  get interfaces_() {
    return [Comparator];
  }

}

Coordinate.DimensionalComparator = DimensionalComparator;
Coordinate.NULL_ORDINATE = Double.NaN;
Coordinate.X = 0;
Coordinate.Y = 1;
Coordinate.Z = 2;
Coordinate.M = 3;

class CoordinateXY extends Coordinate {
  constructor() {
    super();
    CoordinateXY.constructor_.apply(this, arguments);
  }

  static constructor_() {
    if (arguments.length === 0) {
      Coordinate.constructor_.call(this);
    } else if (arguments.length === 1) {
      if (arguments[0] instanceof CoordinateXY) {
        const coord = arguments[0];
        Coordinate.constructor_.call(this, coord.x, coord.y);
      } else if (arguments[0] instanceof Coordinate) {
        const coord = arguments[0];
        Coordinate.constructor_.call(this, coord.x, coord.y);
      }
    } else if (arguments.length === 2) {
      const x = arguments[0],
            y = arguments[1];
      Coordinate.constructor_.call(this, x, y, Coordinate.NULL_ORDINATE);
    }
  }

  setOrdinate(ordinateIndex, value) {
    switch (ordinateIndex) {
      case CoordinateXY.X:
        this.x = value;
        break;

      case CoordinateXY.Y:
        this.y = value;
        break;

      default:
        throw new IllegalArgumentException('Invalid ordinate index: ' + ordinateIndex);
    }
  }

  getZ() {
    return Coordinate.NULL_ORDINATE;
  }

  getOrdinate(ordinateIndex) {
    switch (ordinateIndex) {
      case CoordinateXY.X:
        return this.x;

      case CoordinateXY.Y:
        return this.y;
    }

    throw new IllegalArgumentException('Invalid ordinate index: ' + ordinateIndex);
  }

  setZ(z) {
    throw new IllegalArgumentException('CoordinateXY dimension 2 does not support z-ordinate');
  }

  copy() {
    return new CoordinateXY(this);
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }

  setCoordinate(other) {
    this.x = other.x;
    this.y = other.y;
    this.z = other.getZ();
  }

}
CoordinateXY.X = 0;
CoordinateXY.Y = 1;
CoordinateXY.Z = -1;
CoordinateXY.M = -1;

class CoordinateXYM extends Coordinate {
  constructor() {
    super();
    CoordinateXYM.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._m = null;

    if (arguments.length === 0) {
      Coordinate.constructor_.call(this);
      this._m = 0.0;
    } else if (arguments.length === 1) {
      if (arguments[0] instanceof CoordinateXYM) {
        const coord = arguments[0];
        Coordinate.constructor_.call(this, coord.x, coord.y);
        this._m = coord._m;
      } else if (arguments[0] instanceof Coordinate) {
        const coord = arguments[0];
        Coordinate.constructor_.call(this, coord.x, coord.y);
        this._m = this.getM();
      }
    } else if (arguments.length === 3) {
      const x = arguments[0],
            y = arguments[1],
            m = arguments[2];
      Coordinate.constructor_.call(this, x, y, Coordinate.NULL_ORDINATE);
      this._m = m;
    }
  }

  getM() {
    return this._m;
  }

  setOrdinate(ordinateIndex, value) {
    switch (ordinateIndex) {
      case CoordinateXYM.X:
        this.x = value;
        break;

      case CoordinateXYM.Y:
        this.y = value;
        break;

      case CoordinateXYM.M:
        this._m = value;
        break;

      default:
        throw new IllegalArgumentException('Invalid ordinate index: ' + ordinateIndex);
    }
  }

  setM(m) {
    this._m = m;
  }

  getZ() {
    return Coordinate.NULL_ORDINATE;
  }

  getOrdinate(ordinateIndex) {
    switch (ordinateIndex) {
      case CoordinateXYM.X:
        return this.x;

      case CoordinateXYM.Y:
        return this.y;

      case CoordinateXYM.M:
        return this._m;
    }

    throw new IllegalArgumentException('Invalid ordinate index: ' + ordinateIndex);
  }

  setZ(z) {
    throw new IllegalArgumentException('CoordinateXY dimension 2 does not support z-ordinate');
  }

  copy() {
    return new CoordinateXYM(this);
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ' m=' + this.getM() + ')';
  }

  setCoordinate(other) {
    this.x = other.x;
    this.y = other.y;
    this.z = other.getZ();
    this._m = other.getM();
  }

}
CoordinateXYM.X = 0;
CoordinateXYM.Y = 1;
CoordinateXYM.Z = -1;
CoordinateXYM.M = 2;

class CoordinateXYZM extends Coordinate {
  constructor() {
    super();
    CoordinateXYZM.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._m = null;

    if (arguments.length === 0) {
      Coordinate.constructor_.call(this);
      this._m = 0.0;
    } else if (arguments.length === 1) {
      if (arguments[0] instanceof CoordinateXYZM) {
        const coord = arguments[0];
        Coordinate.constructor_.call(this, coord);
        this._m = coord._m;
      } else if (arguments[0] instanceof Coordinate) {
        const coord = arguments[0];
        Coordinate.constructor_.call(this, coord);
        this._m = this.getM();
      }
    } else if (arguments.length === 4) {
      const x = arguments[0],
            y = arguments[1],
            z = arguments[2],
            m = arguments[3];
      Coordinate.constructor_.call(this, x, y, z);
      this._m = m;
    }
  }

  getM() {
    return this._m;
  }

  setOrdinate(ordinateIndex, value) {
    switch (ordinateIndex) {
      case Coordinate.X:
        this.x = value;
        break;

      case Coordinate.Y:
        this.y = value;
        break;

      case Coordinate.Z:
        this.z = value;
        break;

      case Coordinate.M:
        this._m = value;
        break;

      default:
        throw new IllegalArgumentException('Invalid ordinate index: ' + ordinateIndex);
    }
  }

  setM(m) {
    this._m = m;
  }

  getOrdinate(ordinateIndex) {
    switch (ordinateIndex) {
      case Coordinate.X:
        return this.x;

      case Coordinate.Y:
        return this.y;

      case Coordinate.Z:
        return this.getZ();

      case Coordinate.M:
        return this.getM();
    }

    throw new IllegalArgumentException('Invalid ordinate index: ' + ordinateIndex);
  }

  copy() {
    return new CoordinateXYZM(this);
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ', ' + this.getZ() + ' m=' + this.getM() + ')';
  }

  setCoordinate(other) {
    this.x = other.x;
    this.y = other.y;
    this.z = other.getZ();
    this._m = other.getM();
  }

}

function hasInterface (o, i) {
  return o.interfaces_ && o.interfaces_.indexOf(i) > -1;
}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/Collection.html
 */
class Collection {
  /**
     * Ensures that this collection contains the specified element (optional
     * operation).
     * @param {Object} e
     * @return {boolean}
     */
  add() {}
  /**
     * Appends all of the elements in the specified collection to the end of this
     * list, in the order that they are returned by the specified collection's
     * iterator (optional operation).
     * @param {javascript.util.Collection} c
     * @return {boolean}
     */


  addAll() {}
  /**
     * Returns true if this collection contains no elements.
     * @return {boolean}
     */


  isEmpty() {}
  /**
     * Returns an iterator over the elements in this collection.
     * @return {javascript.util.Iterator}
     */


  iterator() {}
  /**
     * Returns an iterator over the elements in this collection.
     * @return {number}
     */


  size() {}
  /**
     * Returns an array containing all of the elements in this collection.
     * @return {Array}
     */


  toArray() {}
  /**
     * Removes a single instance of the specified element from this collection if it
     * is present. (optional)
     * @param {Object} e
     * @return {boolean}
     */


  remove() {}

}

class IndexOutOfBoundsException extends Exception {
  constructor(message) {
    super(message);
    this.name = Object.keys({
      IndexOutOfBoundsException
    })[0];
  }

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/List.html
 */

class List extends Collection {
  /**
     * Returns the element at the specified position in this list.
     * @param {number} index
     * @return {Object}
     */
  get() {}
  /**
     * Replaces the element at the specified position in this list with the
     * specified element (optional operation).
     * @param {number} index
     * @param {Object} e
     * @return {Object}
     */


  set() {}
  /**
     * Returns true if this collection contains no elements.
     * @return {boolean}
     */


  isEmpty() {}

}

class NoSuchElementException extends Exception {
  constructor(message) {
    super(message);
    this.name = Object.keys({
      NoSuchElementException
    })[0];
  }

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/ArrayList.html
 */

class ArrayList extends List {
  constructor(o) {
    super();
    this.array = [];
    if (o instanceof Collection) this.addAll(o);
  }

  get interfaces_() {
    return [List, Collection];
  }

  ensureCapacity() {}

  add(e) {
    if (arguments.length === 1) this.array.push(e);else this.array.splice(arguments[0], 0, arguments[1]);
    return true;
  }

  clear() {
    this.array = [];
  }

  addAll(c) {
    for (const e of c) this.array.push(e);
  }

  set(index, element) {
    const oldElement = this.array[index];
    this.array[index] = element;
    return oldElement;
  }

  iterator() {
    return new Iterator$3(this);
  }

  get(index) {
    if (index < 0 || index >= this.size()) throw new IndexOutOfBoundsException();
    return this.array[index];
  }

  isEmpty() {
    return this.array.length === 0;
  }

  sort(comparator) {
    if (comparator) this.array.sort((a, b) => comparator.compare(a, b));else this.array.sort();
  }

  size() {
    return this.array.length;
  }

  toArray() {
    return this.array.slice();
  }

  remove(o) {
    for (let i = 0, len = this.array.length; i < len; i++) if (this.array[i] === o) return !!this.array.splice(i, 1);

    return false;
  }

  [Symbol.iterator]() {
    return this.array.values();
  }

}

class Iterator$3 {
  constructor(arrayList) {
    this.arrayList = arrayList;
    this.position = 0;
  }

  next() {
    if (this.position === this.arrayList.size()) throw new NoSuchElementException();
    return this.arrayList.get(this.position++);
  }

  hasNext() {
    return this.position < this.arrayList.size();
  }

  set(element) {
    return this.arrayList.set(this.position - 1, element);
  }

  remove() {
    this.arrayList.remove(this.arrayList.get(this.position));
  }

}

class CoordinateList extends ArrayList {
  constructor() {
    super();
    CoordinateList.constructor_.apply(this, arguments);
  }

  static constructor_() {
    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const coord = arguments[0];
      this.ensureCapacity(coord.length);
      this.add(coord, true);
    } else if (arguments.length === 2) {
      const coord = arguments[0],
            allowRepeated = arguments[1];
      this.ensureCapacity(coord.length);
      this.add(coord, allowRepeated);
    }
  }

  getCoordinate(i) {
    return this.get(i);
  }

  addAll() {
    if (arguments.length === 2 && typeof arguments[1] === 'boolean' && hasInterface(arguments[0], Collection)) {
      const coll = arguments[0],
            allowRepeated = arguments[1];
      let isChanged = false;

      for (let i = coll.iterator(); i.hasNext();) {
        this.add(i.next(), allowRepeated);
        isChanged = true;
      }

      return isChanged;
    } else {
      return super.addAll.apply(this, arguments);
    }
  }

  clone() {
    const clone = super.clone.call(this);

    for (let i = 0; i < this.size(); i++) clone.add(i, this.get(i).clone());

    return clone;
  }

  toCoordinateArray() {
    if (arguments.length === 0) {
      return this.toArray(CoordinateList.coordArrayType);
    } else if (arguments.length === 1) {
      const isForward = arguments[0];
      if (isForward) return this.toArray(CoordinateList.coordArrayType);
      const size = this.size();
      const pts = new Array(size).fill(null);

      for (let i = 0; i < size; i++) pts[i] = this.get(size - i - 1);

      return pts;
    }
  }

  add() {
    if (arguments.length === 1) {
      const coord = arguments[0];
      return super.add.call(this, coord);
    } else if (arguments.length === 2) {
      if (arguments[0] instanceof Array && typeof arguments[1] === 'boolean') {
        const coord = arguments[0],
              allowRepeated = arguments[1];
        this.add(coord, allowRepeated, true);
        return true;
      } else if (arguments[0] instanceof Coordinate && typeof arguments[1] === 'boolean') {
        const coord = arguments[0],
              allowRepeated = arguments[1];
        if (!allowRepeated) if (this.size() >= 1) {
          const last = this.get(this.size() - 1);
          if (last.equals2D(coord)) return null;
        }
        super.add.call(this, coord);
      } else if (arguments[0] instanceof Object && typeof arguments[1] === 'boolean') {
        const obj = arguments[0],
              allowRepeated = arguments[1];
        this.add(obj, allowRepeated);
        return true;
      }
    } else if (arguments.length === 3) {
      if (typeof arguments[2] === 'boolean' && arguments[0] instanceof Array && typeof arguments[1] === 'boolean') {
        const coord = arguments[0],
              allowRepeated = arguments[1],
              direction = arguments[2];
        if (direction) for (let i = 0; i < coord.length; i++) this.add(coord[i], allowRepeated);else for (let i = coord.length - 1; i >= 0; i--) this.add(coord[i], allowRepeated);
        return true;
      } else if (typeof arguments[2] === 'boolean' && Number.isInteger(arguments[0]) && arguments[1] instanceof Coordinate) {
        const i = arguments[0],
              coord = arguments[1],
              allowRepeated = arguments[2];

        if (!allowRepeated) {
          const size = this.size();

          if (size > 0) {
            if (i > 0) {
              const prev = this.get(i - 1);
              if (prev.equals2D(coord)) return null;
            }

            if (i < size) {
              const next = this.get(i);
              if (next.equals2D(coord)) return null;
            }
          }
        }

        super.add.call(this, i, coord);
      }
    } else if (arguments.length === 4) {
      const coord = arguments[0],
            allowRepeated = arguments[1],
            start = arguments[2],
            end = arguments[3];
      let inc = 1;
      if (start > end) inc = -1;

      for (let i = start; i !== end; i += inc) this.add(coord[i], allowRepeated);

      return true;
    }
  }

  closeRing() {
    if (this.size() > 0) {
      const duplicate = this.get(0).copy();
      this.add(duplicate, false);
    }
  }

}
CoordinateList.coordArrayType = new Array(0).fill(null);

class CoordinateSequenceFilter {
  filter(seq, i) {}

  isDone() {}

  isGeometryChanged() {}

}

class Envelope {
  constructor() {
    Envelope.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._minx = null;
    this._maxx = null;
    this._miny = null;
    this._maxy = null;

    if (arguments.length === 0) {
      this.init();
    } else if (arguments.length === 1) {
      if (arguments[0] instanceof Coordinate) {
        const p = arguments[0];
        this.init(p.x, p.x, p.y, p.y);
      } else if (arguments[0] instanceof Envelope) {
        const env = arguments[0];
        this.init(env);
      }
    } else if (arguments.length === 2) {
      const p1 = arguments[0],
            p2 = arguments[1];
      this.init(p1.x, p2.x, p1.y, p2.y);
    } else if (arguments.length === 4) {
      const x1 = arguments[0],
            x2 = arguments[1],
            y1 = arguments[2],
            y2 = arguments[3];
      this.init(x1, x2, y1, y2);
    }
  }

  static intersects() {
    if (arguments.length === 3) {
      const p1 = arguments[0],
            p2 = arguments[1],
            q = arguments[2];
      if (q.x >= (p1.x < p2.x ? p1.x : p2.x) && q.x <= (p1.x > p2.x ? p1.x : p2.x) && q.y >= (p1.y < p2.y ? p1.y : p2.y) && q.y <= (p1.y > p2.y ? p1.y : p2.y)) return true;
      return false;
    } else if (arguments.length === 4) {
      const p1 = arguments[0],
            p2 = arguments[1],
            q1 = arguments[2],
            q2 = arguments[3];
      let minq = Math.min(q1.x, q2.x);
      let maxq = Math.max(q1.x, q2.x);
      let minp = Math.min(p1.x, p2.x);
      let maxp = Math.max(p1.x, p2.x);
      if (minp > maxq) return false;
      if (maxp < minq) return false;
      minq = Math.min(q1.y, q2.y);
      maxq = Math.max(q1.y, q2.y);
      minp = Math.min(p1.y, p2.y);
      maxp = Math.max(p1.y, p2.y);
      if (minp > maxq) return false;
      if (maxp < minq) return false;
      return true;
    }
  }

  getArea() {
    return this.getWidth() * this.getHeight();
  }

  equals(other) {
    if (!(other instanceof Envelope)) return false;
    const otherEnvelope = other;
    if (this.isNull()) return otherEnvelope.isNull();
    return this._maxx === otherEnvelope.getMaxX() && this._maxy === otherEnvelope.getMaxY() && this._minx === otherEnvelope.getMinX() && this._miny === otherEnvelope.getMinY();
  }

  intersection(env) {
    if (this.isNull() || env.isNull() || !this.intersects(env)) return new Envelope();
    const intMinX = this._minx > env._minx ? this._minx : env._minx;
    const intMinY = this._miny > env._miny ? this._miny : env._miny;
    const intMaxX = this._maxx < env._maxx ? this._maxx : env._maxx;
    const intMaxY = this._maxy < env._maxy ? this._maxy : env._maxy;
    return new Envelope(intMinX, intMaxX, intMinY, intMaxY);
  }

  isNull() {
    return this._maxx < this._minx;
  }

  getMaxX() {
    return this._maxx;
  }

  covers() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof Coordinate) {
        const p = arguments[0];
        return this.covers(p.x, p.y);
      } else if (arguments[0] instanceof Envelope) {
        const other = arguments[0];
        if (this.isNull() || other.isNull()) return false;
        return other.getMinX() >= this._minx && other.getMaxX() <= this._maxx && other.getMinY() >= this._miny && other.getMaxY() <= this._maxy;
      }
    } else if (arguments.length === 2) {
      const x = arguments[0],
            y = arguments[1];
      if (this.isNull()) return false;
      return x >= this._minx && x <= this._maxx && y >= this._miny && y <= this._maxy;
    }
  }

  intersects() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof Envelope) {
        const other = arguments[0];
        if (this.isNull() || other.isNull()) return false;
        return !(other._minx > this._maxx || other._maxx < this._minx || other._miny > this._maxy || other._maxy < this._miny);
      } else if (arguments[0] instanceof Coordinate) {
        const p = arguments[0];
        return this.intersects(p.x, p.y);
      }
    } else if (arguments.length === 2) {
      if (arguments[0] instanceof Coordinate && arguments[1] instanceof Coordinate) {
        const a = arguments[0],
              b = arguments[1];
        if (this.isNull()) return false;
        const envminx = a.x < b.x ? a.x : b.x;
        if (envminx > this._maxx) return false;
        const envmaxx = a.x > b.x ? a.x : b.x;
        if (envmaxx < this._minx) return false;
        const envminy = a.y < b.y ? a.y : b.y;
        if (envminy > this._maxy) return false;
        const envmaxy = a.y > b.y ? a.y : b.y;
        if (envmaxy < this._miny) return false;
        return true;
      } else if (typeof arguments[0] === 'number' && typeof arguments[1] === 'number') {
        const x = arguments[0],
              y = arguments[1];
        if (this.isNull()) return false;
        return !(x > this._maxx || x < this._minx || y > this._maxy || y < this._miny);
      }
    }
  }

  getMinY() {
    return this._miny;
  }

  getDiameter() {
    if (this.isNull()) return 0;
    const w = this.getWidth();
    const h = this.getHeight();
    return Math.sqrt(w * w + h * h);
  }

  getMinX() {
    return this._minx;
  }

  expandToInclude() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof Coordinate) {
        const p = arguments[0];
        this.expandToInclude(p.x, p.y);
      } else if (arguments[0] instanceof Envelope) {
        const other = arguments[0];
        if (other.isNull()) return null;

        if (this.isNull()) {
          this._minx = other.getMinX();
          this._maxx = other.getMaxX();
          this._miny = other.getMinY();
          this._maxy = other.getMaxY();
        } else {
          if (other._minx < this._minx) this._minx = other._minx;
          if (other._maxx > this._maxx) this._maxx = other._maxx;
          if (other._miny < this._miny) this._miny = other._miny;
          if (other._maxy > this._maxy) this._maxy = other._maxy;
        }
      }
    } else if (arguments.length === 2) {
      const x = arguments[0],
            y = arguments[1];

      if (this.isNull()) {
        this._minx = x;
        this._maxx = x;
        this._miny = y;
        this._maxy = y;
      } else {
        if (x < this._minx) this._minx = x;
        if (x > this._maxx) this._maxx = x;
        if (y < this._miny) this._miny = y;
        if (y > this._maxy) this._maxy = y;
      }
    }
  }

  minExtent() {
    if (this.isNull()) return 0.0;
    const w = this.getWidth();
    const h = this.getHeight();
    if (w < h) return w;
    return h;
  }

  getWidth() {
    if (this.isNull()) return 0;
    return this._maxx - this._minx;
  }

  compareTo(o) {
    const env = o;

    if (this.isNull()) {
      if (env.isNull()) return 0;
      return -1;
    } else {
      if (env.isNull()) return 1;
    }

    if (this._minx < env._minx) return -1;
    if (this._minx > env._minx) return 1;
    if (this._miny < env._miny) return -1;
    if (this._miny > env._miny) return 1;
    if (this._maxx < env._maxx) return -1;
    if (this._maxx > env._maxx) return 1;
    if (this._maxy < env._maxy) return -1;
    if (this._maxy > env._maxy) return 1;
    return 0;
  }

  translate(transX, transY) {
    if (this.isNull()) return null;
    this.init(this.getMinX() + transX, this.getMaxX() + transX, this.getMinY() + transY, this.getMaxY() + transY);
  }

  copy() {
    return new Envelope(this);
  }

  toString() {
    return 'Env[' + this._minx + ' : ' + this._maxx + ', ' + this._miny + ' : ' + this._maxy + ']';
  }

  setToNull() {
    this._minx = 0;
    this._maxx = -1;
    this._miny = 0;
    this._maxy = -1;
  }

  disjoint(other) {
    if (this.isNull() || other.isNull()) return true;
    return other._minx > this._maxx || other._maxx < this._minx || other._miny > this._maxy || other._maxy < this._miny;
  }

  getHeight() {
    if (this.isNull()) return 0;
    return this._maxy - this._miny;
  }

  maxExtent() {
    if (this.isNull()) return 0.0;
    const w = this.getWidth();
    const h = this.getHeight();
    if (w > h) return w;
    return h;
  }

  expandBy() {
    if (arguments.length === 1) {
      const distance = arguments[0];
      this.expandBy(distance, distance);
    } else if (arguments.length === 2) {
      const deltaX = arguments[0],
            deltaY = arguments[1];
      if (this.isNull()) return null;
      this._minx -= deltaX;
      this._maxx += deltaX;
      this._miny -= deltaY;
      this._maxy += deltaY;
      if (this._minx > this._maxx || this._miny > this._maxy) this.setToNull();
    }
  }

  contains() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof Envelope) {
        const other = arguments[0];
        return this.covers(other);
      } else if (arguments[0] instanceof Coordinate) {
        const p = arguments[0];
        return this.covers(p);
      }
    } else if (arguments.length === 2) {
      const x = arguments[0],
            y = arguments[1];
      return this.covers(x, y);
    }
  }

  centre() {
    if (this.isNull()) return null;
    return new Coordinate((this.getMinX() + this.getMaxX()) / 2.0, (this.getMinY() + this.getMaxY()) / 2.0);
  }

  init() {
    if (arguments.length === 0) {
      this.setToNull();
    } else if (arguments.length === 1) {
      if (arguments[0] instanceof Coordinate) {
        const p = arguments[0];
        this.init(p.x, p.x, p.y, p.y);
      } else if (arguments[0] instanceof Envelope) {
        const env = arguments[0];
        this._minx = env._minx;
        this._maxx = env._maxx;
        this._miny = env._miny;
        this._maxy = env._maxy;
      }
    } else if (arguments.length === 2) {
      const p1 = arguments[0],
            p2 = arguments[1];
      this.init(p1.x, p2.x, p1.y, p2.y);
    } else if (arguments.length === 4) {
      const x1 = arguments[0],
            x2 = arguments[1],
            y1 = arguments[2],
            y2 = arguments[3];

      if (x1 < x2) {
        this._minx = x1;
        this._maxx = x2;
      } else {
        this._minx = x2;
        this._maxx = x1;
      }

      if (y1 < y2) {
        this._miny = y1;
        this._maxy = y2;
      } else {
        this._miny = y2;
        this._maxy = y1;
      }
    }
  }

  getMaxY() {
    return this._maxy;
  }

  distance(env) {
    if (this.intersects(env)) return 0;
    let dx = 0.0;
    if (this._maxx < env._minx) dx = env._minx - this._maxx;else if (this._minx > env._maxx) dx = this._minx - env._maxx;
    let dy = 0.0;
    if (this._maxy < env._miny) dy = env._miny - this._maxy;else if (this._miny > env._maxy) dy = this._miny - env._maxy;
    if (dx === 0.0) return dy;
    if (dy === 0.0) return dx;
    return Math.sqrt(dx * dx + dy * dy);
  }

  hashCode() {
    let result = 17;
    result = 37 * result + Coordinate.hashCode(this._minx);
    result = 37 * result + Coordinate.hashCode(this._maxx);
    result = 37 * result + Coordinate.hashCode(this._miny);
    result = 37 * result + Coordinate.hashCode(this._maxy);
    return result;
  }

  get interfaces_() {
    return [Comparable, Serializable];
  }

}

class StringBuffer {
  constructor(str) {
    this.str = str;
  }

  append(e) {
    this.str += e;
  }

  setCharAt(i, c) {
    this.str = this.str.substr(0, i) + c + this.str.substr(i + 1);
  }

  toString() {
    return this.str;
  }

}

class Integer {
  constructor(value) {
    this.value = value;
  }

  intValue() {
    return this.value;
  }

  compareTo(o) {
    if (this.value < o) return -1;
    if (this.value > o) return 1;
    return 0;
  }

  static compare(x, y) {
    if (x < y) return -1;
    if (x > y) return 1;
    return 0;
  }

  static isNan(n) {
    return Number.isNaN(n);
  }

  static valueOf(value) {
    return new Integer(value);
  }

}

class Character {
  static isWhitespace(c) {
    return c <= 32 && c >= 0 || c === 127;
  }

  static toUpperCase(c) {
    return c.toUpperCase();
  }

}

class DD {
  constructor() {
    DD.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._hi = 0.0;
    this._lo = 0.0;

    if (arguments.length === 0) {
      this.init(0.0);
    } else if (arguments.length === 1) {
      if (typeof arguments[0] === 'number') {
        const x = arguments[0];
        this.init(x);
      } else if (arguments[0] instanceof DD) {
        const dd = arguments[0];
        this.init(dd);
      } else if (typeof arguments[0] === 'string') {
        const str = arguments[0];
        DD.constructor_.call(this, DD.parse(str));
      }
    } else if (arguments.length === 2) {
      const hi = arguments[0],
            lo = arguments[1];
      this.init(hi, lo);
    }
  }

  static determinant() {
    if (typeof arguments[3] === 'number' && typeof arguments[2] === 'number' && typeof arguments[0] === 'number' && typeof arguments[1] === 'number') {
      const x1 = arguments[0],
            y1 = arguments[1],
            x2 = arguments[2],
            y2 = arguments[3];
      return DD.determinant(DD.valueOf(x1), DD.valueOf(y1), DD.valueOf(x2), DD.valueOf(y2));
    } else if (arguments[3] instanceof DD && arguments[2] instanceof DD && arguments[0] instanceof DD && arguments[1] instanceof DD) {
      const x1 = arguments[0],
            y1 = arguments[1],
            x2 = arguments[2],
            y2 = arguments[3];
      const det = x1.multiply(y2).selfSubtract(y1.multiply(x2));
      return det;
    }
  }

  static sqr(x) {
    return DD.valueOf(x).selfMultiply(x);
  }

  static valueOf() {
    if (typeof arguments[0] === 'string') {
      const str = arguments[0];
      return DD.parse(str);
    } else if (typeof arguments[0] === 'number') {
      const x = arguments[0];
      return new DD(x);
    }
  }

  static sqrt(x) {
    return DD.valueOf(x).sqrt();
  }

  static parse(str) {
    let i = 0;
    const strlen = str.length;

    while (Character.isWhitespace(str.charAt(i))) i++;

    let isNegative = false;

    if (i < strlen) {
      const signCh = str.charAt(i);

      if (signCh === '-' || signCh === '+') {
        i++;
        if (signCh === '-') isNegative = true;
      }
    }

    const val = new DD();
    let numDigits = 0;
    let numBeforeDec = 0;
    let exp = 0;
    let hasDecimalChar = false;

    while (true) {
      if (i >= strlen) break;
      const ch = str.charAt(i);
      i++;

      if (Character.isDigit(ch)) {
        const d = ch - '0';
        val.selfMultiply(DD.TEN);
        val.selfAdd(d);
        numDigits++;
        continue;
      }

      if (ch === '.') {
        numBeforeDec = numDigits;
        hasDecimalChar = true;
        continue;
      }

      if (ch === 'e' || ch === 'E') {
        const expStr = str.substring(i);

        try {
          exp = Integer.parseInt(expStr);
        } catch (ex) {
          if (ex instanceof NumberFormatException) throw new NumberFormatException('Invalid exponent ' + expStr + ' in string ' + str);else throw ex;
        } finally {}

        break;
      }

      throw new NumberFormatException('Unexpected character \'' + ch + '\' at position ' + i + ' in string ' + str);
    }

    let val2 = val;
    if (!hasDecimalChar) numBeforeDec = numDigits;
    const numDecPlaces = numDigits - numBeforeDec - exp;

    if (numDecPlaces === 0) {
      val2 = val;
    } else if (numDecPlaces > 0) {
      const scale = DD.TEN.pow(numDecPlaces);
      val2 = val.divide(scale);
    } else if (numDecPlaces < 0) {
      const scale = DD.TEN.pow(-numDecPlaces);
      val2 = val.multiply(scale);
    }

    if (isNegative) return val2.negate();
    return val2;
  }

  static createNaN() {
    return new DD(Double.NaN, Double.NaN);
  }

  static copy(dd) {
    return new DD(dd);
  }

  static magnitude(x) {
    const xAbs = Math.abs(x);
    const xLog10 = Math.log(xAbs) / Math.log(10);
    let xMag = Math.trunc(Math.floor(xLog10));
    const xApprox = Math.pow(10, xMag);
    if (xApprox * 10 <= xAbs) xMag += 1;
    return xMag;
  }

  static stringOfChar(ch, len) {
    const buf = new StringBuffer();

    for (let i = 0; i < len; i++) buf.append(ch);

    return buf.toString();
  }

  le(y) {
    return this._hi < y._hi || this._hi === y._hi && this._lo <= y._lo;
  }

  extractSignificantDigits(insertDecimalPoint, magnitude) {
    let y = this.abs();
    let mag = DD.magnitude(y._hi);
    const scale = DD.TEN.pow(mag);
    y = y.divide(scale);

    if (y.gt(DD.TEN)) {
      y = y.divide(DD.TEN);
      mag += 1;
    } else if (y.lt(DD.ONE)) {
      y = y.multiply(DD.TEN);
      mag -= 1;
    }

    const decimalPointPos = mag + 1;
    const buf = new StringBuffer();
    const numDigits = DD.MAX_PRINT_DIGITS - 1;

    for (let i = 0; i <= numDigits; i++) {
      if (insertDecimalPoint && i === decimalPointPos) buf.append('.');
      const digit = Math.trunc(y._hi);

      if (digit < 0) break;
      let rebiasBy10 = false;
      let digitChar = 0;

      if (digit > 9) {
        rebiasBy10 = true;
        digitChar = '9';
      } else {
        digitChar = '0' + digit;
      }

      buf.append(digitChar);
      y = y.subtract(DD.valueOf(digit)).multiply(DD.TEN);
      if (rebiasBy10) y.selfAdd(DD.TEN);
      let continueExtractingDigits = true;
      const remMag = DD.magnitude(y._hi);
      if (remMag < 0 && Math.abs(remMag) >= numDigits - i) continueExtractingDigits = false;
      if (!continueExtractingDigits) break;
    }

    magnitude[0] = mag;
    return buf.toString();
  }

  sqr() {
    return this.multiply(this);
  }

  doubleValue() {
    return this._hi + this._lo;
  }

  subtract() {
    if (arguments[0] instanceof DD) {
      const y = arguments[0];
      return this.add(y.negate());
    } else if (typeof arguments[0] === 'number') {
      const y = arguments[0];
      return this.add(-y);
    }
  }

  equals() {
    if (arguments.length === 1 && arguments[0] instanceof DD) {
      const y = arguments[0];
      return this._hi === y._hi && this._lo === y._lo;
    }
  }

  isZero() {
    return this._hi === 0.0 && this._lo === 0.0;
  }

  selfSubtract() {
    if (arguments[0] instanceof DD) {
      const y = arguments[0];
      if (this.isNaN()) return this;
      return this.selfAdd(-y._hi, -y._lo);
    } else if (typeof arguments[0] === 'number') {
      const y = arguments[0];
      if (this.isNaN()) return this;
      return this.selfAdd(-y, 0.0);
    }
  }

  getSpecialNumberString() {
    if (this.isZero()) return '0.0';
    if (this.isNaN()) return 'NaN ';
    return null;
  }

  min(x) {
    if (this.le(x)) return this;else return x;
  }

  selfDivide() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof DD) {
        const y = arguments[0];
        return this.selfDivide(y._hi, y._lo);
      } else if (typeof arguments[0] === 'number') {
        const y = arguments[0];
        return this.selfDivide(y, 0.0);
      }
    } else if (arguments.length === 2) {
      const yhi = arguments[0],
            ylo = arguments[1];
      let hc = null,
          tc = null,
          hy = null,
          ty = null,
          C = null,
          c = null,
          U = null,
          u = null;
      C = this._hi / yhi;
      c = DD.SPLIT * C;
      hc = c - C;
      u = DD.SPLIT * yhi;
      hc = c - hc;
      tc = C - hc;
      hy = u - yhi;
      U = C * yhi;
      hy = u - hy;
      ty = yhi - hy;
      u = hc * hy - U + hc * ty + tc * hy + tc * ty;
      c = (this._hi - U - u + this._lo - C * ylo) / yhi;
      u = C + c;
      this._hi = u;
      this._lo = C - u + c;
      return this;
    }
  }

  dump() {
    return 'DD<' + this._hi + ', ' + this._lo + '>';
  }

  divide() {
    if (arguments[0] instanceof DD) {
      const y = arguments[0];
      let hc = null,
          tc = null,
          hy = null,
          ty = null,
          C = null,
          c = null,
          U = null,
          u = null;
      C = this._hi / y._hi;
      c = DD.SPLIT * C;
      hc = c - C;
      u = DD.SPLIT * y._hi;
      hc = c - hc;
      tc = C - hc;
      hy = u - y._hi;
      U = C * y._hi;
      hy = u - hy;
      ty = y._hi - hy;
      u = hc * hy - U + hc * ty + tc * hy + tc * ty;
      c = (this._hi - U - u + this._lo - C * y._lo) / y._hi;
      u = C + c;
      const zhi = u;
      const zlo = C - u + c;
      return new DD(zhi, zlo);
    } else if (typeof arguments[0] === 'number') {
      const y = arguments[0];
      if (Double.isNaN(y)) return DD.createNaN();
      return DD.copy(this).selfDivide(y, 0.0);
    }
  }

  ge(y) {
    return this._hi > y._hi || this._hi === y._hi && this._lo >= y._lo;
  }

  pow(exp) {
    if (exp === 0.0) return DD.valueOf(1.0);
    let r = new DD(this);
    let s = DD.valueOf(1.0);
    let n = Math.abs(exp);
    if (n > 1) while (n > 0) {
      if (n % 2 === 1) s.selfMultiply(r);
      n /= 2;
      if (n > 0) r = r.sqr();
    } else s = r;
    if (exp < 0) return s.reciprocal();
    return s;
  }

  ceil() {
    if (this.isNaN()) return DD.NaN;
    const fhi = Math.ceil(this._hi);
    let flo = 0.0;
    if (fhi === this._hi) flo = Math.ceil(this._lo);
    return new DD(fhi, flo);
  }

  compareTo(o) {
    const other = o;
    if (this._hi < other._hi) return -1;
    if (this._hi > other._hi) return 1;
    if (this._lo < other._lo) return -1;
    if (this._lo > other._lo) return 1;
    return 0;
  }

  rint() {
    if (this.isNaN()) return this;
    const plus5 = this.add(0.5);
    return plus5.floor();
  }

  setValue() {
    if (arguments[0] instanceof DD) {
      const value = arguments[0];
      this.init(value);
      return this;
    } else if (typeof arguments[0] === 'number') {
      const value = arguments[0];
      this.init(value);
      return this;
    }
  }

  max(x) {
    if (this.ge(x)) return this;else return x;
  }

  sqrt() {
    if (this.isZero()) return DD.valueOf(0.0);
    if (this.isNegative()) return DD.NaN;
    const x = 1.0 / Math.sqrt(this._hi);
    const ax = this._hi * x;
    const axdd = DD.valueOf(ax);
    const diffSq = this.subtract(axdd.sqr());
    const d2 = diffSq._hi * (x * 0.5);
    return axdd.add(d2);
  }

  selfAdd() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof DD) {
        const y = arguments[0];
        return this.selfAdd(y._hi, y._lo);
      } else if (typeof arguments[0] === 'number') {
        const y = arguments[0];
        let H = null,
            h = null,
            S = null,
            s = null,
            e = null,
            f = null;
        S = this._hi + y;
        e = S - this._hi;
        s = S - e;
        s = y - e + (this._hi - s);
        f = s + this._lo;
        H = S + f;
        h = f + (S - H);
        this._hi = H + h;
        this._lo = h + (H - this._hi);
        return this;
      }
    } else if (arguments.length === 2) {
      const yhi = arguments[0],
            ylo = arguments[1];
      let H = null,
          h = null,
          T = null,
          t = null,
          S = null,
          s = null,
          e = null,
          f = null;
      S = this._hi + yhi;
      T = this._lo + ylo;
      e = S - this._hi;
      f = T - this._lo;
      s = S - e;
      t = T - f;
      s = yhi - e + (this._hi - s);
      t = ylo - f + (this._lo - t);
      e = s + T;
      H = S + e;
      h = e + (S - H);
      e = t + h;
      const zhi = H + e;
      const zlo = e + (H - zhi);
      this._hi = zhi;
      this._lo = zlo;
      return this;
    }
  }

  selfMultiply() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof DD) {
        const y = arguments[0];
        return this.selfMultiply(y._hi, y._lo);
      } else if (typeof arguments[0] === 'number') {
        const y = arguments[0];
        return this.selfMultiply(y, 0.0);
      }
    } else if (arguments.length === 2) {
      const yhi = arguments[0],
            ylo = arguments[1];
      let hx = null,
          tx = null,
          hy = null,
          ty = null,
          C = null,
          c = null;
      C = DD.SPLIT * this._hi;
      hx = C - this._hi;
      c = DD.SPLIT * yhi;
      hx = C - hx;
      tx = this._hi - hx;
      hy = c - yhi;
      C = this._hi * yhi;
      hy = c - hy;
      ty = yhi - hy;
      c = hx * hy - C + hx * ty + tx * hy + tx * ty + (this._hi * ylo + this._lo * yhi);
      const zhi = C + c;
      hx = C - zhi;
      const zlo = c + hx;
      this._hi = zhi;
      this._lo = zlo;
      return this;
    }
  }

  selfSqr() {
    return this.selfMultiply(this);
  }

  floor() {
    if (this.isNaN()) return DD.NaN;
    const fhi = Math.floor(this._hi);
    let flo = 0.0;
    if (fhi === this._hi) flo = Math.floor(this._lo);
    return new DD(fhi, flo);
  }

  negate() {
    if (this.isNaN()) return this;
    return new DD(-this._hi, -this._lo);
  }

  clone() {
    try {
      return null;
    } catch (ex) {
      if (ex instanceof CloneNotSupportedException) return null;else throw ex;
    } finally {}
  }

  multiply() {
    if (arguments[0] instanceof DD) {
      const y = arguments[0];
      if (y.isNaN()) return DD.createNaN();
      return DD.copy(this).selfMultiply(y);
    } else if (typeof arguments[0] === 'number') {
      const y = arguments[0];
      if (Double.isNaN(y)) return DD.createNaN();
      return DD.copy(this).selfMultiply(y, 0.0);
    }
  }

  isNaN() {
    return Double.isNaN(this._hi);
  }

  intValue() {
    return Math.trunc(this._hi);
  }

  toString() {
    const mag = DD.magnitude(this._hi);
    if (mag >= -3 && mag <= 20) return this.toStandardNotation();
    return this.toSciNotation();
  }

  toStandardNotation() {
    const specialStr = this.getSpecialNumberString();
    if (specialStr !== null) return specialStr;
    const magnitude = new Array(1).fill(null);
    const sigDigits = this.extractSignificantDigits(true, magnitude);
    const decimalPointPos = magnitude[0] + 1;
    let num = sigDigits;

    if (sigDigits.charAt(0) === '.') {
      num = '0' + sigDigits;
    } else if (decimalPointPos < 0) {
      num = '0.' + DD.stringOfChar('0', -decimalPointPos) + sigDigits;
    } else if (sigDigits.indexOf('.') === -1) {
      const numZeroes = decimalPointPos - sigDigits.length;
      const zeroes = DD.stringOfChar('0', numZeroes);
      num = sigDigits + zeroes + '.0';
    }

    if (this.isNegative()) return '-' + num;
    return num;
  }

  reciprocal() {
    let hc = null,
        tc = null,
        hy = null,
        ty = null,
        C = null,
        c = null,
        U = null,
        u = null;
    C = 1.0 / this._hi;
    c = DD.SPLIT * C;
    hc = c - C;
    u = DD.SPLIT * this._hi;
    hc = c - hc;
    tc = C - hc;
    hy = u - this._hi;
    U = C * this._hi;
    hy = u - hy;
    ty = this._hi - hy;
    u = hc * hy - U + hc * ty + tc * hy + tc * ty;
    c = (1.0 - U - u - C * this._lo) / this._hi;
    const zhi = C + c;
    const zlo = C - zhi + c;
    return new DD(zhi, zlo);
  }

  toSciNotation() {
    if (this.isZero()) return DD.SCI_NOT_ZERO;
    const specialStr = this.getSpecialNumberString();
    if (specialStr !== null) return specialStr;
    const magnitude = new Array(1).fill(null);
    const digits = this.extractSignificantDigits(false, magnitude);
    const expStr = DD.SCI_NOT_EXPONENT_CHAR + magnitude[0];
    if (digits.charAt(0) === '0') throw new IllegalStateException('Found leading zero: ' + digits);
    let trailingDigits = '';
    if (digits.length > 1) trailingDigits = digits.substring(1);
    const digitsWithDecimal = digits.charAt(0) + '.' + trailingDigits;
    if (this.isNegative()) return '-' + digitsWithDecimal + expStr;
    return digitsWithDecimal + expStr;
  }

  abs() {
    if (this.isNaN()) return DD.NaN;
    if (this.isNegative()) return this.negate();
    return new DD(this);
  }

  isPositive() {
    return this._hi > 0.0 || this._hi === 0.0 && this._lo > 0.0;
  }

  lt(y) {
    return this._hi < y._hi || this._hi === y._hi && this._lo < y._lo;
  }

  add() {
    if (arguments[0] instanceof DD) {
      const y = arguments[0];
      return DD.copy(this).selfAdd(y);
    } else if (typeof arguments[0] === 'number') {
      const y = arguments[0];
      return DD.copy(this).selfAdd(y);
    }
  }

  init() {
    if (arguments.length === 1) {
      if (typeof arguments[0] === 'number') {
        const x = arguments[0];
        this._hi = x;
        this._lo = 0.0;
      } else if (arguments[0] instanceof DD) {
        const dd = arguments[0];
        this._hi = dd._hi;
        this._lo = dd._lo;
      }
    } else if (arguments.length === 2) {
      const hi = arguments[0],
            lo = arguments[1];
      this._hi = hi;
      this._lo = lo;
    }
  }

  gt(y) {
    return this._hi > y._hi || this._hi === y._hi && this._lo > y._lo;
  }

  isNegative() {
    return this._hi < 0.0 || this._hi === 0.0 && this._lo < 0.0;
  }

  trunc() {
    if (this.isNaN()) return DD.NaN;
    if (this.isPositive()) return this.floor();else return this.ceil();
  }

  signum() {
    if (this._hi > 0) return 1;
    if (this._hi < 0) return -1;
    if (this._lo > 0) return 1;
    if (this._lo < 0) return -1;
    return 0;
  }

  get interfaces_() {
    return [Serializable, Comparable, Clonable];
  }

}
DD.PI = new DD(3.141592653589793116e+00, 1.224646799147353207e-16);
DD.TWO_PI = new DD(6.283185307179586232e+00, 2.449293598294706414e-16);
DD.PI_2 = new DD(1.570796326794896558e+00, 6.123233995736766036e-17);
DD.E = new DD(2.718281828459045091e+00, 1.445646891729250158e-16);
DD.NaN = new DD(Double.NaN, Double.NaN);
DD.EPS = 1.23259516440783e-32;
DD.SPLIT = 134217729.0;
DD.MAX_PRINT_DIGITS = 32;
DD.TEN = DD.valueOf(10.0);
DD.ONE = DD.valueOf(1.0);
DD.SCI_NOT_EXPONENT_CHAR = 'E';
DD.SCI_NOT_ZERO = '0.0E0';

class CGAlgorithmsDD {
  static orientationIndex(p1, p2, q) {
    const index = CGAlgorithmsDD.orientationIndexFilter(p1, p2, q);
    if (index <= 1) return index;
    const dx1 = DD.valueOf(p2.x).selfAdd(-p1.x);
    const dy1 = DD.valueOf(p2.y).selfAdd(-p1.y);
    const dx2 = DD.valueOf(q.x).selfAdd(-p2.x);
    const dy2 = DD.valueOf(q.y).selfAdd(-p2.y);
    return dx1.selfMultiply(dy2).selfSubtract(dy1.selfMultiply(dx2)).signum();
  }

  static signOfDet2x2() {
    if (arguments[3] instanceof DD && arguments[2] instanceof DD && arguments[0] instanceof DD && arguments[1] instanceof DD) {
      const x1 = arguments[0],
            y1 = arguments[1],
            x2 = arguments[2],
            y2 = arguments[3];
      const det = x1.multiply(y2).selfSubtract(y1.multiply(x2));
      return det.signum();
    } else if (typeof arguments[3] === 'number' && typeof arguments[2] === 'number' && typeof arguments[0] === 'number' && typeof arguments[1] === 'number') {
      const dx1 = arguments[0],
            dy1 = arguments[1],
            dx2 = arguments[2],
            dy2 = arguments[3];
      const x1 = DD.valueOf(dx1);
      const y1 = DD.valueOf(dy1);
      const x2 = DD.valueOf(dx2);
      const y2 = DD.valueOf(dy2);
      const det = x1.multiply(y2).selfSubtract(y1.multiply(x2));
      return det.signum();
    }
  }

  static intersection(p1, p2, q1, q2) {
    const px = new DD(p1.y).selfSubtract(p2.y);
    const py = new DD(p2.x).selfSubtract(p1.x);
    const pw = new DD(p1.x).selfMultiply(p2.y).selfSubtract(new DD(p2.x).selfMultiply(p1.y));
    const qx = new DD(q1.y).selfSubtract(q2.y);
    const qy = new DD(q2.x).selfSubtract(q1.x);
    const qw = new DD(q1.x).selfMultiply(q2.y).selfSubtract(new DD(q2.x).selfMultiply(q1.y));
    const x = py.multiply(qw).selfSubtract(qy.multiply(pw));
    const y = qx.multiply(pw).selfSubtract(px.multiply(qw));
    const w = px.multiply(qy).selfSubtract(qx.multiply(py));
    const xInt = x.selfDivide(w).doubleValue();
    const yInt = y.selfDivide(w).doubleValue();
    if (Double.isNaN(xInt) || Double.isInfinite(xInt) || Double.isNaN(yInt) || Double.isInfinite(yInt)) return null;
    return new Coordinate(xInt, yInt);
  }

  static orientationIndexFilter(pa, pb, pc) {
    let detsum = null;
    const detleft = (pa.x - pc.x) * (pb.y - pc.y);
    const detright = (pa.y - pc.y) * (pb.x - pc.x);
    const det = detleft - detright;
    if (detleft > 0.0) {
      if (detright <= 0.0) return CGAlgorithmsDD.signum(det);else detsum = detleft + detright;
    } else if (detleft < 0.0) {
      if (detright >= 0.0) return CGAlgorithmsDD.signum(det);else detsum = -detleft - detright;
    } else return CGAlgorithmsDD.signum(det);
    const errbound = CGAlgorithmsDD.DP_SAFE_EPSILON * detsum;
    if (det >= errbound || -det >= errbound) return CGAlgorithmsDD.signum(det);
    return 2;
  }

  static signum(x) {
    if (x > 0) return 1;
    if (x < 0) return -1;
    return 0;
  }

}
CGAlgorithmsDD.DP_SAFE_EPSILON = 1e-15;

class CoordinateSequence {
  getM(index) {
    if (this.hasM()) {
      const mIndex = this.getDimension() - this.getMeasures();
      return this.getOrdinate(index, mIndex);
    } else {
      return Double.NaN;
    }
  }

  setOrdinate(index, ordinateIndex, value) {}

  getZ(index) {
    if (this.hasZ()) return this.getOrdinate(index, 2);else return Double.NaN;
  }

  size() {}

  getOrdinate(index, ordinateIndex) {}

  getCoordinate() {
  }

  getCoordinateCopy(i) {}

  createCoordinate() {}

  getDimension() {}

  hasM() {
    return this.getMeasures() > 0;
  }

  getX(index) {}

  hasZ() {
    return this.getDimension() - this.getMeasures() > 2;
  }

  getMeasures() {
    return 0;
  }

  expandEnvelope(env) {}

  copy() {}

  getY(index) {}

  toCoordinateArray() {}

  get interfaces_() {
    return [Clonable];
  }

}
CoordinateSequence.X = 0;
CoordinateSequence.Y = 1;
CoordinateSequence.Z = 2;
CoordinateSequence.M = 3;

class Orientation {
  static index(p1, p2, q) {
    return CGAlgorithmsDD.orientationIndex(p1, p2, q);
  }

  static isCCW() {
    if (arguments[0] instanceof Array) {
      const ring = arguments[0];
      const nPts = ring.length - 1;
      if (nPts < 3) throw new IllegalArgumentException('Ring has fewer than 4 points, so orientation cannot be determined');
      let hiPt = ring[0];
      let hiIndex = 0;

      for (let i = 1; i <= nPts; i++) {
        const p = ring[i];

        if (p.y > hiPt.y) {
          hiPt = p;
          hiIndex = i;
        }
      }

      let iPrev = hiIndex;

      do {
        iPrev = iPrev - 1;
        if (iPrev < 0) iPrev = nPts;
      } while (ring[iPrev].equals2D(hiPt) && iPrev !== hiIndex);

      let iNext = hiIndex;

      do iNext = (iNext + 1) % nPts; while (ring[iNext].equals2D(hiPt) && iNext !== hiIndex);

      const prev = ring[iPrev];
      const next = ring[iNext];
      if (prev.equals2D(hiPt) || next.equals2D(hiPt) || prev.equals2D(next)) return false;
      const disc = Orientation.index(prev, hiPt, next);
      let isCCW = null;
      if (disc === 0) isCCW = prev.x > next.x;else isCCW = disc > 0;
      return isCCW;
    } else if (hasInterface(arguments[0], CoordinateSequence)) {
      const ring = arguments[0];
      const nPts = ring.size() - 1;
      if (nPts < 3) throw new IllegalArgumentException('Ring has fewer than 4 points, so orientation cannot be determined');
      let hiPt = ring.getCoordinate(0);
      let hiIndex = 0;

      for (let i = 1; i <= nPts; i++) {
        const p = ring.getCoordinate(i);

        if (p.y > hiPt.y) {
          hiPt = p;
          hiIndex = i;
        }
      }

      let prev = null;
      let iPrev = hiIndex;

      do {
        iPrev = iPrev - 1;
        if (iPrev < 0) iPrev = nPts;
        prev = ring.getCoordinate(iPrev);
      } while (prev.equals2D(hiPt) && iPrev !== hiIndex);

      let next = null;
      let iNext = hiIndex;

      do {
        iNext = (iNext + 1) % nPts;
        next = ring.getCoordinate(iNext);
      } while (next.equals2D(hiPt) && iNext !== hiIndex);

      if (prev.equals2D(hiPt) || next.equals2D(hiPt) || prev.equals2D(next)) return false;
      const disc = Orientation.index(prev, hiPt, next);
      let isCCW = null;
      if (disc === 0) isCCW = prev.x > next.x;else isCCW = disc > 0;
      return isCCW;
    }
  }

}
Orientation.CLOCKWISE = -1;
Orientation.RIGHT = Orientation.CLOCKWISE;
Orientation.COUNTERCLOCKWISE = 1;
Orientation.LEFT = Orientation.COUNTERCLOCKWISE;
Orientation.COLLINEAR = 0;
Orientation.STRAIGHT = Orientation.COLLINEAR;

class Intersection {
  static intersection(p1, p2, q1, q2) {
    const minX0 = p1.x < p2.x ? p1.x : p2.x;
    const minY0 = p1.y < p2.y ? p1.y : p2.y;
    const maxX0 = p1.x > p2.x ? p1.x : p2.x;
    const maxY0 = p1.y > p2.y ? p1.y : p2.y;
    const minX1 = q1.x < q2.x ? q1.x : q2.x;
    const minY1 = q1.y < q2.y ? q1.y : q2.y;
    const maxX1 = q1.x > q2.x ? q1.x : q2.x;
    const maxY1 = q1.y > q2.y ? q1.y : q2.y;
    const intMinX = minX0 > minX1 ? minX0 : minX1;
    const intMaxX = maxX0 < maxX1 ? maxX0 : maxX1;
    const intMinY = minY0 > minY1 ? minY0 : minY1;
    const intMaxY = maxY0 < maxY1 ? maxY0 : maxY1;
    const midx = (intMinX + intMaxX) / 2.0;
    const midy = (intMinY + intMaxY) / 2.0;
    const p1x = p1.x - midx;
    const p1y = p1.y - midy;
    const p2x = p2.x - midx;
    const p2y = p2.y - midy;
    const q1x = q1.x - midx;
    const q1y = q1.y - midy;
    const q2x = q2.x - midx;
    const q2y = q2.y - midy;
    const px = p1y - p2y;
    const py = p2x - p1x;
    const pw = p1x * p2y - p2x * p1y;
    const qx = q1y - q2y;
    const qy = q2x - q1x;
    const qw = q1x * q2y - q2x * q1y;
    const x = py * qw - qy * pw;
    const y = qx * pw - px * qw;
    const w = px * qy - qx * py;
    const xInt = x / w;
    const yInt = y / w;
    if (Double.isNaN(xInt) || Double.isInfinite(xInt) || Double.isNaN(yInt) || Double.isInfinite(yInt)) return null;
    return new Coordinate(xInt + midx, yInt + midy);
  }

}

class System {
  static arraycopy(src, srcPos, dest, destPos, len) {
    let c = 0;

    for (let i = srcPos; i < srcPos + len; i++) {
      dest[destPos + c] = src[i];
      c++;
    }
  }

  static getProperty(name) {
    return {
      'line.separator': '\n'
    }[name];
  }

}

class MathUtil {
  static log10(x) {
    const ln = Math.log(x);
    if (Double.isInfinite(ln)) return ln;
    if (Double.isNaN(ln)) return ln;
    return ln / MathUtil.LOG_10;
  }

  static min(v1, v2, v3, v4) {
    let min = v1;
    if (v2 < min) min = v2;
    if (v3 < min) min = v3;
    if (v4 < min) min = v4;
    return min;
  }

  static clamp() {
    if (typeof arguments[2] === 'number' && typeof arguments[0] === 'number' && typeof arguments[1] === 'number') {
      const x = arguments[0],
            min = arguments[1],
            max = arguments[2];
      if (x < min) return min;
      if (x > max) return max;
      return x;
    } else if (Number.isInteger(arguments[2]) && Number.isInteger(arguments[0]) && Number.isInteger(arguments[1])) {
      const x = arguments[0],
            min = arguments[1],
            max = arguments[2];
      if (x < min) return min;
      if (x > max) return max;
      return x;
    }
  }

  static wrap(index, max) {
    if (index < 0) return max - -index % max;
    return index % max;
  }

  static max() {
    if (arguments.length === 3) {
      const v1 = arguments[0],
            v2 = arguments[1],
            v3 = arguments[2];
      let max = v1;
      if (v2 > max) max = v2;
      if (v3 > max) max = v3;
      return max;
    } else if (arguments.length === 4) {
      const v1 = arguments[0],
            v2 = arguments[1],
            v3 = arguments[2],
            v4 = arguments[3];
      let max = v1;
      if (v2 > max) max = v2;
      if (v3 > max) max = v3;
      if (v4 > max) max = v4;
      return max;
    }
  }

  static average(x1, x2) {
    return (x1 + x2) / 2.0;
  }

}
MathUtil.LOG_10 = Math.log(10);

class Distance {
  static segmentToSegment(A, B, C, D) {
    if (A.equals(B)) return Distance.pointToSegment(A, C, D);
    if (C.equals(D)) return Distance.pointToSegment(D, A, B);
    let noIntersection = false;

    if (!Envelope.intersects(A, B, C, D)) {
      noIntersection = true;
    } else {
      const denom = (B.x - A.x) * (D.y - C.y) - (B.y - A.y) * (D.x - C.x);

      if (denom === 0) {
        noIntersection = true;
      } else {
        const r_num = (A.y - C.y) * (D.x - C.x) - (A.x - C.x) * (D.y - C.y);
        const s_num = (A.y - C.y) * (B.x - A.x) - (A.x - C.x) * (B.y - A.y);
        const s = s_num / denom;
        const r = r_num / denom;
        if (r < 0 || r > 1 || s < 0 || s > 1) noIntersection = true;
      }
    }

    if (noIntersection) return MathUtil.min(Distance.pointToSegment(A, C, D), Distance.pointToSegment(B, C, D), Distance.pointToSegment(C, A, B), Distance.pointToSegment(D, A, B));
    return 0.0;
  }

  static pointToSegment(p, A, B) {
    if (A.x === B.x && A.y === B.y) return p.distance(A);
    const len2 = (B.x - A.x) * (B.x - A.x) + (B.y - A.y) * (B.y - A.y);
    const r = ((p.x - A.x) * (B.x - A.x) + (p.y - A.y) * (B.y - A.y)) / len2;
    if (r <= 0.0) return p.distance(A);
    if (r >= 1.0) return p.distance(B);
    const s = ((A.y - p.y) * (B.x - A.x) - (A.x - p.x) * (B.y - A.y)) / len2;
    return Math.abs(s) * Math.sqrt(len2);
  }

  static pointToLinePerpendicular(p, A, B) {
    const len2 = (B.x - A.x) * (B.x - A.x) + (B.y - A.y) * (B.y - A.y);
    const s = ((A.y - p.y) * (B.x - A.x) - (A.x - p.x) * (B.y - A.y)) / len2;
    return Math.abs(s) * Math.sqrt(len2);
  }

  static pointToSegmentString(p, line) {
    if (line.length === 0) throw new IllegalArgumentException('Line array must contain at least one vertex');
    let minDistance = p.distance(line[0]);

    for (let i = 0; i < line.length - 1; i++) {
      const dist = Distance.pointToSegment(p, line[i], line[i + 1]);
      if (dist < minDistance) minDistance = dist;
    }

    return minDistance;
  }

}

class CoordinateSequenceFactory {
  create() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof Array) ; else if (hasInterface(arguments[0], CoordinateSequence)) ;
    } else if (arguments.length === 2) ; else if (arguments.length === 3) {
      const size = arguments[0],
            dimension = arguments[1];
      return this.create(size, dimension);
    }
  }

}

class GeometryComponentFilter {
  filter(geom) {}

}

class Geometry {
  constructor() {
    Geometry.constructor_.apply(this, arguments);
  }

  isGeometryCollection() {
    return this.getTypeCode() === Geometry.TYPECODE_GEOMETRYCOLLECTION;
  }

  getFactory() {
    return this._factory;
  }

  getGeometryN(n) {
    return this;
  }

  getArea() {
    return 0.0;
  }

  isRectangle() {
    return false;
  }

  equalsExact(other) {
    return this === other || this.equalsExact(other, 0);
  }

  geometryChanged() {
    this.apply(Geometry.geometryChangedFilter);
  }

  geometryChangedAction() {
    this._envelope = null;
  }

  equalsNorm(g) {
    if (g === null) return false;
    return this.norm().equalsExact(g.norm());
  }

  getLength() {
    return 0.0;
  }

  getNumGeometries() {
    return 1;
  }

  compareTo() {
    let other;

    if (arguments.length === 1) {
      const o = arguments[0];
      other = o;
      if (this.getTypeCode() !== other.getTypeCode()) return this.getTypeCode() - other.getTypeCode();
      if (this.isEmpty() && other.isEmpty()) return 0;
      if (this.isEmpty()) return -1;
      if (other.isEmpty()) return 1;
      return this.compareToSameClass(o);
    } else if (arguments.length === 2) {
      const o = arguments[0];
      const comp = arguments[1];
      other = o;
      if (this.getTypeCode() !== other.getTypeCode()) return this.getTypeCode() - other.getTypeCode();
      if (this.isEmpty() && other.isEmpty()) return 0;
      if (this.isEmpty()) return -1;
      if (other.isEmpty()) return 1;
      return this.compareToSameClass(o, comp);
    }
  }

  getUserData() {
    return this._userData;
  }

  getSRID() {
    return this._SRID;
  }

  getEnvelope() {
    return this.getFactory().toGeometry(this.getEnvelopeInternal());
  }

  checkNotGeometryCollection(g) {
    if (g.getTypeCode() === Geometry.TYPECODE_GEOMETRYCOLLECTION) throw new IllegalArgumentException('This method does not support GeometryCollection arguments');
  }

  equal(a, b, tolerance) {
    if (tolerance === 0) return a.equals(b);
    return a.distance(b) <= tolerance;
  }

  norm() {
    const copy = this.copy();
    copy.normalize();
    return copy;
  }

  reverse() {
    const res = this.reverseInternal();
    if (this.envelope != null) res.envelope = this.envelope.copy();
    res.setSRID(this.getSRID());
    return res;
  }

  copy() {
    const copy = this.copyInternal();
    copy.envelope = this._envelope == null ? null : this._envelope.copy();
    copy._SRID = this._SRID;
    copy._userData = this._userData;
    return copy;
  }

  getPrecisionModel() {
    return this._factory.getPrecisionModel();
  }

  getEnvelopeInternal() {
    if (this._envelope === null) this._envelope = this.computeEnvelopeInternal();
    return new Envelope(this._envelope);
  }

  setSRID(SRID) {
    this._SRID = SRID;
  }

  setUserData(userData) {
    this._userData = userData;
  }

  compare(a, b) {
    const i = a.iterator();
    const j = b.iterator();

    while (i.hasNext() && j.hasNext()) {
      const aElement = i.next();
      const bElement = j.next();
      const comparison = aElement.compareTo(bElement);
      if (comparison !== 0) return comparison;
    }

    if (i.hasNext()) return 1;
    if (j.hasNext()) return -1;
    return 0;
  }

  hashCode() {
    return this.getEnvelopeInternal().hashCode();
  }

  isEquivalentClass(other) {
    return this.getClass() === other.getClass();
  }

  isGeometryCollectionOrDerived() {
    if (this.getTypeCode() === Geometry.TYPECODE_GEOMETRYCOLLECTION || this.getTypeCode() === Geometry.TYPECODE_MULTIPOINT || this.getTypeCode() === Geometry.TYPECODE_MULTILINESTRING || this.getTypeCode() === Geometry.TYPECODE_MULTIPOLYGON) return true;
    return false;
  }

  get interfaces_() {
    return [Clonable, Comparable, Serializable];
  }

  getClass() {
    return Geometry;
  }

  static hasNonEmptyElements(geometries) {
    for (let i = 0; i < geometries.length; i++) if (!geometries[i].isEmpty()) return true;

    return false;
  }

  static hasNullElements(array) {
    for (let i = 0; i < array.length; i++) if (array[i] === null) return true;

    return false;
  }

}

Geometry.constructor_ = function (factory) {
  if (!factory) return;
  this._envelope = null;
  this._userData = null;
  this._factory = factory;
  this._SRID = factory.getSRID();
};

Geometry.TYPECODE_POINT = 0;
Geometry.TYPECODE_MULTIPOINT = 1;
Geometry.TYPECODE_LINESTRING = 2;
Geometry.TYPECODE_LINEARRING = 3;
Geometry.TYPECODE_MULTILINESTRING = 4;
Geometry.TYPECODE_POLYGON = 5;
Geometry.TYPECODE_MULTIPOLYGON = 6;
Geometry.TYPECODE_GEOMETRYCOLLECTION = 7;
Geometry.TYPENAME_POINT = 'Point';
Geometry.TYPENAME_MULTIPOINT = 'MultiPoint';
Geometry.TYPENAME_LINESTRING = 'LineString';
Geometry.TYPENAME_LINEARRING = 'LinearRing';
Geometry.TYPENAME_MULTILINESTRING = 'MultiLineString';
Geometry.TYPENAME_POLYGON = 'Polygon';
Geometry.TYPENAME_MULTIPOLYGON = 'MultiPolygon';
Geometry.TYPENAME_GEOMETRYCOLLECTION = 'GeometryCollection';
Geometry.geometryChangedFilter = {
  get interfaces_() {
    return [GeometryComponentFilter];
  },

  filter(geom) {
    geom.geometryChangedAction();
  }

};

class CoordinateFilter {
  filter(coord) {}

}

class Length {
  static ofLine(pts) {
    const n = pts.size();
    if (n <= 1) return 0.0;
    let len = 0.0;
    const p = new Coordinate();
    pts.getCoordinate(0, p);
    let x0 = p.x;
    let y0 = p.y;

    for (let i = 1; i < n; i++) {
      pts.getCoordinate(i, p);
      const x1 = p.x;
      const y1 = p.y;
      const dx = x1 - x0;
      const dy = y1 - y0;
      len += Math.sqrt(dx * dx + dy * dy);
      x0 = x1;
      y0 = y1;
    }

    return len;
  }

}

class Lineal {}

class CoordinateSequences {
  static copyCoord(src, srcPos, dest, destPos) {
    const minDim = Math.min(src.getDimension(), dest.getDimension());

    for (let dim = 0; dim < minDim; dim++) dest.setOrdinate(destPos, dim, src.getOrdinate(srcPos, dim));
  }

  static isRing(seq) {
    const n = seq.size();
    if (n === 0) return true;
    if (n <= 3) return false;
    return seq.getOrdinate(0, CoordinateSequence.X) === seq.getOrdinate(n - 1, CoordinateSequence.X) && seq.getOrdinate(0, CoordinateSequence.Y) === seq.getOrdinate(n - 1, CoordinateSequence.Y);
  }

  static scroll() {
    if (arguments.length === 2) {
      if (hasInterface(arguments[0], CoordinateSequence) && Number.isInteger(arguments[1])) {
        const seq = arguments[0],
              indexOfFirstCoordinate = arguments[1];
        CoordinateSequences.scroll(seq, indexOfFirstCoordinate, CoordinateSequences.isRing(seq));
      } else if (hasInterface(arguments[0], CoordinateSequence) && arguments[1] instanceof Coordinate) {
        const seq = arguments[0],
              firstCoordinate = arguments[1];
        const i = CoordinateSequences.indexOf(firstCoordinate, seq);
        if (i <= 0) return null;
        CoordinateSequences.scroll(seq, i);
      }
    } else if (arguments.length === 3) {
      const seq = arguments[0],
            indexOfFirstCoordinate = arguments[1],
            ensureRing = arguments[2];
      const i = indexOfFirstCoordinate;
      if (i <= 0) return null;
      const copy = seq.copy();
      const last = ensureRing ? seq.size() - 1 : seq.size();

      for (let j = 0; j < last; j++) for (let k = 0; k < seq.getDimension(); k++) seq.setOrdinate(j, k, copy.getOrdinate((indexOfFirstCoordinate + j) % last, k));

      if (ensureRing) for (let k = 0; k < seq.getDimension(); k++) seq.setOrdinate(last, k, seq.getOrdinate(0, k));
    }
  }

  static isEqual(cs1, cs2) {
    const cs1Size = cs1.size();
    const cs2Size = cs2.size();
    if (cs1Size !== cs2Size) return false;
    const dim = Math.min(cs1.getDimension(), cs2.getDimension());

    for (let i = 0; i < cs1Size; i++) for (let d = 0; d < dim; d++) {
      const v1 = cs1.getOrdinate(i, d);
      const v2 = cs2.getOrdinate(i, d);
      if (cs1.getOrdinate(i, d) === cs2.getOrdinate(i, d)) continue;
      if (Double.isNaN(v1) && Double.isNaN(v2)) continue;
      return false;
    }

    return true;
  }

  static minCoordinateIndex() {
    if (arguments.length === 1) {
      const seq = arguments[0];
      return CoordinateSequences.minCoordinateIndex(seq, 0, seq.size() - 1);
    } else if (arguments.length === 3) {
      const seq = arguments[0],
            from = arguments[1],
            to = arguments[2];
      let minCoordIndex = -1;
      let minCoord = null;

      for (let i = from; i <= to; i++) {
        const testCoord = seq.getCoordinate(i);

        if (minCoord === null || minCoord.compareTo(testCoord) > 0) {
          minCoord = testCoord;
          minCoordIndex = i;
        }
      }

      return minCoordIndex;
    }
  }

  static extend(fact, seq, size) {
    const newseq = fact.create(size, seq.getDimension());
    const n = seq.size();
    CoordinateSequences.copy(seq, 0, newseq, 0, n);
    if (n > 0) for (let i = n; i < size; i++) CoordinateSequences.copy(seq, n - 1, newseq, i, 1);
    return newseq;
  }

  static reverse(seq) {
    const last = seq.size() - 1;
    const mid = Math.trunc(last / 2);

    for (let i = 0; i <= mid; i++) CoordinateSequences.swap(seq, i, last - i);
  }

  static swap(seq, i, j) {
    if (i === j) return null;

    for (let dim = 0; dim < seq.getDimension(); dim++) {
      const tmp = seq.getOrdinate(i, dim);
      seq.setOrdinate(i, dim, seq.getOrdinate(j, dim));
      seq.setOrdinate(j, dim, tmp);
    }
  }

  static copy(src, srcPos, dest, destPos, length) {
    for (let i = 0; i < length; i++) CoordinateSequences.copyCoord(src, srcPos + i, dest, destPos + i);
  }

  static ensureValidRing(fact, seq) {
    const n = seq.size();
    if (n === 0) return seq;
    if (n <= 3) return CoordinateSequences.createClosedRing(fact, seq, 4);
    const isClosed = seq.getOrdinate(0, CoordinateSequence.X) === seq.getOrdinate(n - 1, CoordinateSequence.X) && seq.getOrdinate(0, CoordinateSequence.Y) === seq.getOrdinate(n - 1, CoordinateSequence.Y);
    if (isClosed) return seq;
    return CoordinateSequences.createClosedRing(fact, seq, n + 1);
  }

  static indexOf(coordinate, seq) {
    for (let i = 0; i < seq.size(); i++) if (coordinate.x === seq.getOrdinate(i, CoordinateSequence.X) && coordinate.y === seq.getOrdinate(i, CoordinateSequence.Y)) return i;

    return -1;
  }

  static createClosedRing(fact, seq, size) {
    const newseq = fact.create(size, seq.getDimension());
    const n = seq.size();
    CoordinateSequences.copy(seq, 0, newseq, 0, n);

    for (let i = n; i < size; i++) CoordinateSequences.copy(seq, 0, newseq, i, 1);

    return newseq;
  }

  static minCoordinate(seq) {
    let minCoord = null;

    for (let i = 0; i < seq.size(); i++) {
      const testCoord = seq.getCoordinate(i);
      if (minCoord === null || minCoord.compareTo(testCoord) > 0) minCoord = testCoord;
    }

    return minCoord;
  }

}

class UnsupportedOperationException extends Exception {
  constructor(message) {
    super(message);
    this.name = Object.keys({
      UnsupportedOperationException
    })[0];
  }

}

class Dimension {
  static toDimensionSymbol(dimensionValue) {
    switch (dimensionValue) {
      case Dimension.FALSE:
        return Dimension.SYM_FALSE;

      case Dimension.TRUE:
        return Dimension.SYM_TRUE;

      case Dimension.DONTCARE:
        return Dimension.SYM_DONTCARE;

      case Dimension.P:
        return Dimension.SYM_P;

      case Dimension.L:
        return Dimension.SYM_L;

      case Dimension.A:
        return Dimension.SYM_A;
    }

    throw new IllegalArgumentException('Unknown dimension value: ' + dimensionValue);
  }

  static toDimensionValue(dimensionSymbol) {
    switch (Character.toUpperCase(dimensionSymbol)) {
      case Dimension.SYM_FALSE:
        return Dimension.FALSE;

      case Dimension.SYM_TRUE:
        return Dimension.TRUE;

      case Dimension.SYM_DONTCARE:
        return Dimension.DONTCARE;

      case Dimension.SYM_P:
        return Dimension.P;

      case Dimension.SYM_L:
        return Dimension.L;

      case Dimension.SYM_A:
        return Dimension.A;
    }

    throw new IllegalArgumentException('Unknown dimension symbol: ' + dimensionSymbol);
  }

}
Dimension.P = 0;
Dimension.L = 1;
Dimension.A = 2;
Dimension.FALSE = -1;
Dimension.TRUE = -2;
Dimension.DONTCARE = -3;
Dimension.SYM_FALSE = 'F';
Dimension.SYM_TRUE = 'T';
Dimension.SYM_DONTCARE = '*';
Dimension.SYM_P = '0';
Dimension.SYM_L = '1';
Dimension.SYM_A = '2';

class GeometryFilter {
  filter(geom) {}

}

class LineString extends Geometry {
  constructor() {
    super();
    LineString.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._points = null;

    if (arguments.length === 0) ; else if (arguments.length === 2) {
      const points = arguments[0],
            factory = arguments[1];
      Geometry.constructor_.call(this, factory);
      this.init(points);
    }
  }

  computeEnvelopeInternal() {
    if (this.isEmpty()) return new Envelope();
    return this._points.expandEnvelope(new Envelope());
  }

  isRing() {
    return this.isClosed() && this.isSimple();
  }

  getCoordinates() {
    return this._points.toCoordinateArray();
  }

  copyInternal() {
    return new LineString(this._points.copy(), this._factory);
  }

  equalsExact() {
    if (arguments.length === 2 && typeof arguments[1] === 'number' && arguments[0] instanceof Geometry) {
      const other = arguments[0],
            tolerance = arguments[1];
      if (!this.isEquivalentClass(other)) return false;
      const otherLineString = other;
      if (this._points.size() !== otherLineString._points.size()) return false;

      for (let i = 0; i < this._points.size(); i++) if (!this.equal(this._points.getCoordinate(i), otherLineString._points.getCoordinate(i), tolerance)) return false;

      return true;
    } else {
      return super.equalsExact.apply(this, arguments);
    }
  }

  normalize() {
    for (let i = 0; i < Math.trunc(this._points.size() / 2); i++) {
      const j = this._points.size() - 1 - i;

      if (!this._points.getCoordinate(i).equals(this._points.getCoordinate(j))) {
        if (this._points.getCoordinate(i).compareTo(this._points.getCoordinate(j)) > 0) {
          const copy = this._points.copy();

          CoordinateSequences.reverse(copy);
          this._points = copy;
        }

        return null;
      }
    }
  }

  getCoordinate() {
    if (this.isEmpty()) return null;
    return this._points.getCoordinate(0);
  }

  getBoundaryDimension() {
    if (this.isClosed()) return Dimension.FALSE;
    return 0;
  }

  isClosed() {
    if (this.isEmpty()) return false;
    return this.getCoordinateN(0).equals2D(this.getCoordinateN(this.getNumPoints() - 1));
  }

  reverseInternal() {
    const seq = this._points.copy();

    CoordinateSequences.reverse(seq);
    return this.getFactory().createLineString(seq);
  }

  getEndPoint() {
    if (this.isEmpty()) return null;
    return this.getPointN(this.getNumPoints() - 1);
  }

  getTypeCode() {
    return Geometry.TYPECODE_LINESTRING;
  }

  getDimension() {
    return 1;
  }

  getLength() {
    return Length.ofLine(this._points);
  }

  getNumPoints() {
    return this._points.size();
  }

  compareToSameClass() {
    if (arguments.length === 1) {
      const o = arguments[0];
      const line = o;
      let i = 0;
      let j = 0;

      while (i < this._points.size() && j < line._points.size()) {
        const comparison = this._points.getCoordinate(i).compareTo(line._points.getCoordinate(j));

        if (comparison !== 0) return comparison;
        i++;
        j++;
      }

      if (i < this._points.size()) return 1;
      if (j < line._points.size()) return -1;
      return 0;
    } else if (arguments.length === 2) {
      const o = arguments[0],
            comp = arguments[1];
      const line = o;
      return comp.compare(this._points, line._points);
    }
  }

  apply() {
    if (hasInterface(arguments[0], CoordinateFilter)) {
      const filter = arguments[0];

      for (let i = 0; i < this._points.size(); i++) filter.filter(this._points.getCoordinate(i));
    } else if (hasInterface(arguments[0], CoordinateSequenceFilter)) {
      const filter = arguments[0];
      if (this._points.size() === 0) return null;

      for (let i = 0; i < this._points.size(); i++) {
        filter.filter(this._points, i);
        if (filter.isDone()) break;
      }

      if (filter.isGeometryChanged()) this.geometryChanged();
    } else if (hasInterface(arguments[0], GeometryFilter)) {
      const filter = arguments[0];
      filter.filter(this);
    } else if (hasInterface(arguments[0], GeometryComponentFilter)) {
      const filter = arguments[0];
      filter.filter(this);
    }
  }

  getBoundary() {
    throw new UnsupportedOperationException();
  }

  isEquivalentClass(other) {
    return other instanceof LineString;
  }

  getCoordinateN(n) {
    return this._points.getCoordinate(n);
  }

  getGeometryType() {
    return Geometry.TYPENAME_LINESTRING;
  }

  getCoordinateSequence() {
    return this._points;
  }

  isEmpty() {
    return this._points.size() === 0;
  }

  init(points) {
    if (points === null) points = this.getFactory().getCoordinateSequenceFactory().create([]);
    if (points.size() === 1) throw new IllegalArgumentException('Invalid number of points in LineString (found ' + points.size() + ' - must be 0 or >= 2)');
    this._points = points;
  }

  isCoordinate(pt) {
    for (let i = 0; i < this._points.size(); i++) if (this._points.getCoordinate(i).equals(pt)) return true;

    return false;
  }

  getStartPoint() {
    if (this.isEmpty()) return null;
    return this.getPointN(0);
  }

  getPointN(n) {
    return this.getFactory().createPoint(this._points.getCoordinate(n));
  }

  get interfaces_() {
    return [Lineal];
  }

}

class Puntal {}

class Point extends Geometry {
  constructor() {
    super();
    Point.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._coordinates = null;
    const coordinates = arguments[0],
          factory = arguments[1];
    Geometry.constructor_.call(this, factory);
    this.init(coordinates);
  }

  computeEnvelopeInternal() {
    if (this.isEmpty()) return new Envelope();
    const env = new Envelope();
    env.expandToInclude(this._coordinates.getX(0), this._coordinates.getY(0));
    return env;
  }

  getCoordinates() {
    return this.isEmpty() ? [] : [this.getCoordinate()];
  }

  copyInternal() {
    return new Point(this._coordinates.copy(), this._factory);
  }

  equalsExact() {
    if (arguments.length === 2 && typeof arguments[1] === 'number' && arguments[0] instanceof Geometry) {
      const other = arguments[0],
            tolerance = arguments[1];
      if (!this.isEquivalentClass(other)) return false;
      if (this.isEmpty() && other.isEmpty()) return true;
      if (this.isEmpty() !== other.isEmpty()) return false;
      return this.equal(other.getCoordinate(), this.getCoordinate(), tolerance);
    } else {
      return super.equalsExact.apply(this, arguments);
    }
  }

  normalize() {}

  getCoordinate() {
    return this._coordinates.size() !== 0 ? this._coordinates.getCoordinate(0) : null;
  }

  getBoundaryDimension() {
    return Dimension.FALSE;
  }

  reverseInternal() {
    return this.getFactory().createPoint(this._coordinates.copy());
  }

  getTypeCode() {
    return Geometry.TYPECODE_POINT;
  }

  getDimension() {
    return 0;
  }

  getNumPoints() {
    return this.isEmpty() ? 0 : 1;
  }

  getX() {
    if (this.getCoordinate() === null) throw new IllegalStateException('getX called on empty Point');
    return this.getCoordinate().x;
  }

  compareToSameClass() {
    if (arguments.length === 1) {
      const other = arguments[0];
      const point = other;
      return this.getCoordinate().compareTo(point.getCoordinate());
    } else if (arguments.length === 2) {
      const other = arguments[0],
            comp = arguments[1];
      const point = other;
      return comp.compare(this._coordinates, point._coordinates);
    }
  }

  apply() {
    if (hasInterface(arguments[0], CoordinateFilter)) {
      const filter = arguments[0];
      if (this.isEmpty()) return null;
      filter.filter(this.getCoordinate());
    } else if (hasInterface(arguments[0], CoordinateSequenceFilter)) {
      const filter = arguments[0];
      if (this.isEmpty()) return null;
      filter.filter(this._coordinates, 0);
      if (filter.isGeometryChanged()) this.geometryChanged();
    } else if (hasInterface(arguments[0], GeometryFilter)) {
      const filter = arguments[0];
      filter.filter(this);
    } else if (hasInterface(arguments[0], GeometryComponentFilter)) {
      const filter = arguments[0];
      filter.filter(this);
    }
  }

  getBoundary() {
    return this.getFactory().createGeometryCollection();
  }

  getGeometryType() {
    return Geometry.TYPENAME_POINT;
  }

  getCoordinateSequence() {
    return this._coordinates;
  }

  getY() {
    if (this.getCoordinate() === null) throw new IllegalStateException('getY called on empty Point');
    return this.getCoordinate().y;
  }

  isEmpty() {
    return this._coordinates.size() === 0;
  }

  init(coordinates) {
    if (coordinates === null) coordinates = this.getFactory().getCoordinateSequenceFactory().create([]);
    Assert.isTrue(coordinates.size() <= 1);
    this._coordinates = coordinates;
  }

  isSimple() {
    return true;
  }

  get interfaces_() {
    return [Puntal];
  }

}

class Area {
  static ofRing() {
    if (arguments[0] instanceof Array) {
      const ring = arguments[0];
      return Math.abs(Area.ofRingSigned(ring));
    } else if (hasInterface(arguments[0], CoordinateSequence)) {
      const ring = arguments[0];
      return Math.abs(Area.ofRingSigned(ring));
    }
  }

  static ofRingSigned() {
    if (arguments[0] instanceof Array) {
      const ring = arguments[0];
      if (ring.length < 3) return 0.0;
      let sum = 0.0;
      const x0 = ring[0].x;

      for (let i = 1; i < ring.length - 1; i++) {
        const x = ring[i].x - x0;
        const y1 = ring[i + 1].y;
        const y2 = ring[i - 1].y;
        sum += x * (y2 - y1);
      }

      return sum / 2.0;
    } else if (hasInterface(arguments[0], CoordinateSequence)) {
      const ring = arguments[0];
      const n = ring.size();
      if (n < 3) return 0.0;
      const p0 = new Coordinate();
      const p1 = new Coordinate();
      const p2 = new Coordinate();
      ring.getCoordinate(0, p1);
      ring.getCoordinate(1, p2);
      const x0 = p1.x;
      p2.x -= x0;
      let sum = 0.0;

      for (let i = 1; i < n - 1; i++) {
        p0.y = p1.y;
        p1.x = p2.x;
        p1.y = p2.y;
        ring.getCoordinate(i + 1, p2);
        p2.x -= x0;
        sum += p1.x * (p0.y - p2.y);
      }

      return sum / 2.0;
    }
  }

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/Arrays.html
 */

class Arrays {
  static sort() {
    const a = arguments[0];

    if (arguments.length === 1) {
      a.sort((a, b) => a.compareTo(b));
    } else if (arguments.length === 2) {
      a.sort((a, b) => arguments[1].compare(a, b));
    } else if (arguments.length === 3) {
      const t = a.slice(arguments[1], arguments[2]);
      t.sort();
      const r = a.slice(0, arguments[1]).concat(t, a.slice(arguments[2], a.length));
      a.splice(0, a.length);

      for (const e of r) a.push(e);
    } else if (arguments.length === 4) {
      const t = a.slice(arguments[1], arguments[2]);
      t.sort((a, b) => arguments[3].compare(a, b));
      const r = a.slice(0, arguments[1]).concat(t, a.slice(arguments[2], a.length));
      a.splice(0, a.length);

      for (const e of r) a.push(e);
    }
  }
  /**
   * @param {Array} array
   * @return {ArrayList}
   */


  static asList(array) {
    const arrayList = new ArrayList();

    for (const e of array) arrayList.add(e);

    return arrayList;
  }

  static copyOf(original, newLength) {
    return original.slice(0, newLength);
  }

}

class Polygonal {}

class Polygon extends Geometry {
  constructor() {
    super();
    Polygon.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._shell = null;
    this._holes = null;
    let shell = arguments[0],
        holes = arguments[1],
        factory = arguments[2];
    Geometry.constructor_.call(this, factory);
    if (shell === null) shell = this.getFactory().createLinearRing();
    if (holes === null) holes = [];
    if (Geometry.hasNullElements(holes)) throw new IllegalArgumentException('holes must not contain null elements');
    if (shell.isEmpty() && Geometry.hasNonEmptyElements(holes)) throw new IllegalArgumentException('shell is empty but holes are not');
    this._shell = shell;
    this._holes = holes;
  }

  computeEnvelopeInternal() {
    return this._shell.getEnvelopeInternal();
  }

  getCoordinates() {
    if (this.isEmpty()) return [];
    const coordinates = new Array(this.getNumPoints()).fill(null);
    let k = -1;

    const shellCoordinates = this._shell.getCoordinates();

    for (let x = 0; x < shellCoordinates.length; x++) {
      k++;
      coordinates[k] = shellCoordinates[x];
    }

    for (let i = 0; i < this._holes.length; i++) {
      const childCoordinates = this._holes[i].getCoordinates();

      for (let j = 0; j < childCoordinates.length; j++) {
        k++;
        coordinates[k] = childCoordinates[j];
      }
    }

    return coordinates;
  }

  getArea() {
    let area = 0.0;
    area += Area.ofRing(this._shell.getCoordinateSequence());

    for (let i = 0; i < this._holes.length; i++) area -= Area.ofRing(this._holes[i].getCoordinateSequence());

    return area;
  }

  copyInternal() {
    const shellCopy = this._shell.copy();

    const holeCopies = new Array(this._holes.length).fill(null);

    for (let i = 0; i < this._holes.length; i++) holeCopies[i] = this._holes[i].copy();

    return new Polygon(shellCopy, holeCopies, this._factory);
  }

  isRectangle() {
    if (this.getNumInteriorRing() !== 0) return false;
    if (this._shell === null) return false;
    if (this._shell.getNumPoints() !== 5) return false;

    const seq = this._shell.getCoordinateSequence();

    const env = this.getEnvelopeInternal();

    for (let i = 0; i < 5; i++) {
      const x = seq.getX(i);
      if (!(x === env.getMinX() || x === env.getMaxX())) return false;
      const y = seq.getY(i);
      if (!(y === env.getMinY() || y === env.getMaxY())) return false;
    }

    let prevX = seq.getX(0);
    let prevY = seq.getY(0);

    for (let i = 1; i <= 4; i++) {
      const x = seq.getX(i);
      const y = seq.getY(i);
      const xChanged = x !== prevX;
      const yChanged = y !== prevY;
      if (xChanged === yChanged) return false;
      prevX = x;
      prevY = y;
    }

    return true;
  }

  equalsExact() {
    if (arguments.length === 2 && typeof arguments[1] === 'number' && arguments[0] instanceof Geometry) {
      const other = arguments[0],
            tolerance = arguments[1];
      if (!this.isEquivalentClass(other)) return false;
      const otherPolygon = other;
      const thisShell = this._shell;
      const otherPolygonShell = otherPolygon._shell;
      if (!thisShell.equalsExact(otherPolygonShell, tolerance)) return false;
      if (this._holes.length !== otherPolygon._holes.length) return false;

      for (let i = 0; i < this._holes.length; i++) if (!this._holes[i].equalsExact(otherPolygon._holes[i], tolerance)) return false;

      return true;
    } else {
      return super.equalsExact.apply(this, arguments);
    }
  }

  normalize() {
    if (arguments.length === 0) {
      this._shell = this.normalized(this._shell, true);

      for (let i = 0; i < this._holes.length; i++) this._holes[i] = this.normalized(this._holes[i], false);

      Arrays.sort(this._holes);
    } else if (arguments.length === 2) {
      const ring = arguments[0],
            clockwise = arguments[1];
      if (ring.isEmpty()) return null;
      const seq = ring.getCoordinateSequence();
      const minCoordinateIndex = CoordinateSequences.minCoordinateIndex(seq, 0, seq.size() - 2);
      CoordinateSequences.scroll(seq, minCoordinateIndex, true);
      if (Orientation.isCCW(seq) === clockwise) CoordinateSequences.reverse(seq);
    }
  }

  getCoordinate() {
    return this._shell.getCoordinate();
  }

  getNumInteriorRing() {
    return this._holes.length;
  }

  getBoundaryDimension() {
    return 1;
  }

  reverseInternal() {
    const shell = this.getExteriorRing().reverse();
    const holes = new Array(this.getNumInteriorRing()).fill(null);

    for (let i = 0; i < holes.length; i++) holes[i] = this.getInteriorRingN(i).reverse();

    return this.getFactory().createPolygon(shell, holes);
  }

  getTypeCode() {
    return Geometry.TYPECODE_POLYGON;
  }

  getDimension() {
    return 2;
  }

  getLength() {
    let len = 0.0;
    len += this._shell.getLength();

    for (let i = 0; i < this._holes.length; i++) len += this._holes[i].getLength();

    return len;
  }

  getNumPoints() {
    let numPoints = this._shell.getNumPoints();

    for (let i = 0; i < this._holes.length; i++) numPoints += this._holes[i].getNumPoints();

    return numPoints;
  }

  convexHull() {
    return this.getExteriorRing().convexHull();
  }

  normalized(ring, clockwise) {
    const res = ring.copy();
    this.normalize(res, clockwise);
    return res;
  }

  compareToSameClass() {
    if (arguments.length === 1) {
      const o = arguments[0];
      const thisShell = this._shell;
      const otherShell = o._shell;
      return thisShell.compareToSameClass(otherShell);
    } else if (arguments.length === 2) {
      const o = arguments[0],
            comp = arguments[1];
      const poly = o;
      const thisShell = this._shell;
      const otherShell = poly._shell;
      const shellComp = thisShell.compareToSameClass(otherShell, comp);
      if (shellComp !== 0) return shellComp;
      const nHole1 = this.getNumInteriorRing();
      const nHole2 = poly.getNumInteriorRing();
      let i = 0;

      while (i < nHole1 && i < nHole2) {
        const thisHole = this.getInteriorRingN(i);
        const otherHole = poly.getInteriorRingN(i);
        const holeComp = thisHole.compareToSameClass(otherHole, comp);
        if (holeComp !== 0) return holeComp;
        i++;
      }

      if (i < nHole1) return 1;
      if (i < nHole2) return -1;
      return 0;
    }
  }

  apply() {
    if (hasInterface(arguments[0], CoordinateFilter)) {
      const filter = arguments[0];

      this._shell.apply(filter);

      for (let i = 0; i < this._holes.length; i++) this._holes[i].apply(filter);
    } else if (hasInterface(arguments[0], CoordinateSequenceFilter)) {
      const filter = arguments[0];

      this._shell.apply(filter);

      if (!filter.isDone()) for (let i = 0; i < this._holes.length; i++) {
        this._holes[i].apply(filter);

        if (filter.isDone()) break;
      }
      if (filter.isGeometryChanged()) this.geometryChanged();
    } else if (hasInterface(arguments[0], GeometryFilter)) {
      const filter = arguments[0];
      filter.filter(this);
    } else if (hasInterface(arguments[0], GeometryComponentFilter)) {
      const filter = arguments[0];
      filter.filter(this);

      this._shell.apply(filter);

      for (let i = 0; i < this._holes.length; i++) this._holes[i].apply(filter);
    }
  }

  getBoundary() {
    if (this.isEmpty()) return this.getFactory().createMultiLineString();
    const rings = new Array(this._holes.length + 1).fill(null);
    rings[0] = this._shell;

    for (let i = 0; i < this._holes.length; i++) rings[i + 1] = this._holes[i];

    if (rings.length <= 1) return this.getFactory().createLinearRing(rings[0].getCoordinateSequence());
    return this.getFactory().createMultiLineString(rings);
  }

  getGeometryType() {
    return Geometry.TYPENAME_POLYGON;
  }

  getExteriorRing() {
    return this._shell;
  }

  isEmpty() {
    return this._shell.isEmpty();
  }

  getInteriorRingN(n) {
    return this._holes[n];
  }

  get interfaces_() {
    return [Polygonal];
  }

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/Set.html
 *
 * @extends {Collection}
 * @constructor
 * @private
 */

class Set extends Collection {
  /**
   * Returns true if this set contains the specified element. More formally,
   * returns true if and only if this set contains an element e such that (o==null ?
   * e==null : o.equals(e)).
   * @param {Object} e
   * @return {boolean}
   */
  contains() {}

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/SortedSet.html
 */

class SortedSet extends Set {}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/TreeSet.html
 */

class TreeSet extends SortedSet {
  constructor(o) {
    super();
    this.array = [];
    if (o instanceof Collection) this.addAll(o);
  }

  contains(o) {
    for (const e of this.array) if (e.compareTo(o) === 0) return true;

    return false;
  }

  add(o) {
    if (this.contains(o)) return false;

    for (let i = 0, len = this.array.length; i < len; i++) {
      const e = this.array[i];
      if (e.compareTo(o) === 1) return !!this.array.splice(i, 0, o);
    }

    this.array.push(o);
    return true;
  }

  addAll(c) {
    for (const e of c) this.add(e);

    return true;
  }

  remove() {
    throw new UnsupportedOperationException();
  }

  size() {
    return this.array.length;
  }

  isEmpty() {
    return this.array.length === 0;
  }

  toArray() {
    return this.array.slice();
  }

  iterator() {
    return new Iterator$2(this.array);
  }

}

class Iterator$2 {
  constructor(array) {
    this.array = array;
    this.position = 0;
  }

  next() {
    if (this.position === this.array.length) throw new NoSuchElementException();
    return this.array[this.position++];
  }

  hasNext() {
    return this.position < this.array.length;
  }

  remove() {
    throw new UnsupportedOperationException();
  }

}

class GeometryCollection extends Geometry {
  constructor() {
    super();
    GeometryCollection.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geometries = null;

    if (arguments.length === 0) ; else if (arguments.length === 2) {
      let geometries = arguments[0],
          factory = arguments[1];
      Geometry.constructor_.call(this, factory);
      if (geometries === null) geometries = [];
      if (Geometry.hasNullElements(geometries)) throw new IllegalArgumentException('geometries must not contain null elements');
      this._geometries = geometries;
    }
  }

  computeEnvelopeInternal() {
    const envelope = new Envelope();

    for (let i = 0; i < this._geometries.length; i++) envelope.expandToInclude(this._geometries[i].getEnvelopeInternal());

    return envelope;
  }

  getGeometryN(n) {
    return this._geometries[n];
  }

  getCoordinates() {
    const coordinates = new Array(this.getNumPoints()).fill(null);
    let k = -1;

    for (let i = 0; i < this._geometries.length; i++) {
      const childCoordinates = this._geometries[i].getCoordinates();

      for (let j = 0; j < childCoordinates.length; j++) {
        k++;
        coordinates[k] = childCoordinates[j];
      }
    }

    return coordinates;
  }

  getArea() {
    let area = 0.0;

    for (let i = 0; i < this._geometries.length; i++) area += this._geometries[i].getArea();

    return area;
  }

  copyInternal() {
    const geometries = new Array(this._geometries.length).fill(null);

    for (let i = 0; i < geometries.length; i++) geometries[i] = this._geometries[i].copy();

    return new GeometryCollection(geometries, this._factory);
  }

  equalsExact() {
    if (arguments.length === 2 && typeof arguments[1] === 'number' && arguments[0] instanceof Geometry) {
      const other = arguments[0],
            tolerance = arguments[1];
      if (!this.isEquivalentClass(other)) return false;
      const otherCollection = other;
      if (this._geometries.length !== otherCollection._geometries.length) return false;

      for (let i = 0; i < this._geometries.length; i++) if (!this._geometries[i].equalsExact(otherCollection._geometries[i], tolerance)) return false;

      return true;
    } else {
      return super.equalsExact.apply(this, arguments);
    }
  }

  normalize() {
    for (let i = 0; i < this._geometries.length; i++) this._geometries[i].normalize();

    Arrays.sort(this._geometries);
  }

  getCoordinate() {
    if (this.isEmpty()) return null;
    return this._geometries[0].getCoordinate();
  }

  getBoundaryDimension() {
    let dimension = Dimension.FALSE;

    for (let i = 0; i < this._geometries.length; i++) dimension = Math.max(dimension, this._geometries[i].getBoundaryDimension());

    return dimension;
  }

  reverseInternal() {
    const numGeometries = this._geometries.length;
    const reversed = new ArrayList(numGeometries);

    for (let i = 0; i < numGeometries; i++) reversed.add(this._geometries[i].reverse());

    return this.getFactory().buildGeometry(reversed);
  }

  getTypeCode() {
    return Geometry.TYPECODE_GEOMETRYCOLLECTION;
  }

  getDimension() {
    let dimension = Dimension.FALSE;

    for (let i = 0; i < this._geometries.length; i++) dimension = Math.max(dimension, this._geometries[i].getDimension());

    return dimension;
  }

  getLength() {
    let sum = 0.0;

    for (let i = 0; i < this._geometries.length; i++) sum += this._geometries[i].getLength();

    return sum;
  }

  getNumPoints() {
    let numPoints = 0;

    for (let i = 0; i < this._geometries.length; i++) numPoints += this._geometries[i].getNumPoints();

    return numPoints;
  }

  getNumGeometries() {
    return this._geometries.length;
  }

  compareToSameClass() {
    if (arguments.length === 1) {
      const o = arguments[0];
      const theseElements = new TreeSet(Arrays.asList(this._geometries));
      const otherElements = new TreeSet(Arrays.asList(o._geometries));
      return this.compare(theseElements, otherElements);
    } else if (arguments.length === 2) {
      const o = arguments[0],
            comp = arguments[1];
      const gc = o;
      const n1 = this.getNumGeometries();
      const n2 = gc.getNumGeometries();
      let i = 0;

      while (i < n1 && i < n2) {
        const thisGeom = this.getGeometryN(i);
        const otherGeom = gc.getGeometryN(i);
        const holeComp = thisGeom.compareToSameClass(otherGeom, comp);
        if (holeComp !== 0) return holeComp;
        i++;
      }

      if (i < n1) return 1;
      if (i < n2) return -1;
      return 0;
    }
  }

  apply() {
    if (hasInterface(arguments[0], CoordinateFilter)) {
      const filter = arguments[0];

      for (let i = 0; i < this._geometries.length; i++) this._geometries[i].apply(filter);
    } else if (hasInterface(arguments[0], CoordinateSequenceFilter)) {
      const filter = arguments[0];
      if (this._geometries.length === 0) return null;

      for (let i = 0; i < this._geometries.length; i++) {
        this._geometries[i].apply(filter);

        if (filter.isDone()) break;
      }

      if (filter.isGeometryChanged()) this.geometryChanged();
    } else if (hasInterface(arguments[0], GeometryFilter)) {
      const filter = arguments[0];
      filter.filter(this);

      for (let i = 0; i < this._geometries.length; i++) this._geometries[i].apply(filter);
    } else if (hasInterface(arguments[0], GeometryComponentFilter)) {
      const filter = arguments[0];
      filter.filter(this);

      for (let i = 0; i < this._geometries.length; i++) this._geometries[i].apply(filter);
    }
  }

  getBoundary() {
    Geometry.checkNotGeometryCollection(this);
    Assert.shouldNeverReachHere();
    return null;
  }

  getGeometryType() {
    return Geometry.TYPENAME_GEOMETRYCOLLECTION;
  }

  isEmpty() {
    for (let i = 0; i < this._geometries.length; i++) if (!this._geometries[i].isEmpty()) return false;

    return true;
  }

}

class MultiPoint extends GeometryCollection {
  constructor() {
    super();
    MultiPoint.constructor_.apply(this, arguments);
  }

  static constructor_() {
    const points = arguments[0],
          factory = arguments[1];
    GeometryCollection.constructor_.call(this, points, factory);
  }

  copyInternal() {
    const points = new Array(this._geometries.length).fill(null);

    for (let i = 0; i < points.length; i++) points[i] = this._geometries[i].copy();

    return new MultiPoint(points, this._factory);
  }

  isValid() {
    return true;
  }

  equalsExact() {
    if (arguments.length === 2 && typeof arguments[1] === 'number' && arguments[0] instanceof Geometry) {
      const other = arguments[0],
            tolerance = arguments[1];
      if (!this.isEquivalentClass(other)) return false;
      return super.equalsExact.call(this, other, tolerance);
    } else {
      return super.equalsExact.apply(this, arguments);
    }
  }

  getCoordinate() {
    if (arguments.length === 1 && Number.isInteger(arguments[0])) {
      const n = arguments[0];
      return this._geometries[n].getCoordinate();
    } else {
      return super.getCoordinate.apply(this, arguments);
    }
  }

  getBoundaryDimension() {
    return Dimension.FALSE;
  }

  getTypeCode() {
    return Geometry.TYPECODE_MULTIPOINT;
  }

  getDimension() {
    return 0;
  }

  getBoundary() {
    return this.getFactory().createGeometryCollection();
  }

  getGeometryType() {
    return Geometry.TYPENAME_MULTIPOINT;
  }

  get interfaces_() {
    return [Puntal];
  }

}

class LinearRing extends LineString {
  constructor() {
    super();
    LinearRing.constructor_.apply(this, arguments);
  }

  static constructor_() {
    const points = arguments[0],
          factory = arguments[1];
    LineString.constructor_.call(this, points, factory);
    this.validateConstruction();
  }

  copyInternal() {
    return new LinearRing(this._points.copy(), this._factory);
  }

  getBoundaryDimension() {
    return Dimension.FALSE;
  }

  isClosed() {
    if (this.isEmpty()) return true;
    return super.isClosed.call(this);
  }

  reverseInternal() {
    const seq = this._points.copy();

    CoordinateSequences.reverse(seq);
    return this.getFactory().createLinearRing(seq);
  }

  getTypeCode() {
    return Geometry.TYPECODE_LINEARRING;
  }

  validateConstruction() {
    if (!this.isEmpty() && !super.isClosed.call(this)) throw new IllegalArgumentException('Points of LinearRing do not form a closed linestring');
    if (this.getCoordinateSequence().size() >= 1 && this.getCoordinateSequence().size() < LinearRing.MINIMUM_VALID_SIZE) throw new IllegalArgumentException('Invalid number of points in LinearRing (found ' + this.getCoordinateSequence().size() + ' - must be 0 or >= 4)');
  }

  getGeometryType() {
    return Geometry.TYPENAME_LINEARRING;
  }

}
LinearRing.MINIMUM_VALID_SIZE = 4;

class Coordinates {
  static measures(coordinate) {
    if (coordinate instanceof CoordinateXY) return 0;else if (coordinate instanceof CoordinateXYM) return 1;else if (coordinate instanceof CoordinateXYZM) return 1;else if (coordinate instanceof Coordinate) return 0;
    return 0;
  }

  static dimension(coordinate) {
    if (coordinate instanceof CoordinateXY) return 2;else if (coordinate instanceof CoordinateXYM) return 3;else if (coordinate instanceof CoordinateXYZM) return 4;else if (coordinate instanceof Coordinate) return 3;
    return 3;
  }

  static create() {
    if (arguments.length === 1) {
      const dimension = arguments[0];
      return Coordinates.create(dimension, 0);
    } else if (arguments.length === 2) {
      const dimension = arguments[0],
            measures = arguments[1];
      if (dimension === 2) return new CoordinateXY();else if (dimension === 3 && measures === 0) return new Coordinate();else if (dimension === 3 && measures === 1) return new CoordinateXYM();else if (dimension === 4 && measures === 1) return new CoordinateXYZM();
      return new Coordinate();
    }
  }

}

class CoordinateArrays {
  static isRing(pts) {
    if (pts.length < 4) return false;
    if (!pts[0].equals2D(pts[pts.length - 1])) return false;
    return true;
  }

  static ptNotInList(testPts, pts) {
    for (let i = 0; i < testPts.length; i++) {
      const testPt = testPts[i];
      if (CoordinateArrays.indexOf(testPt, pts) < 0) return testPt;
    }

    return null;
  }

  static scroll(coordinates, firstCoordinate) {
    const i = CoordinateArrays.indexOf(firstCoordinate, coordinates);
    if (i < 0) return null;
    const newCoordinates = new Array(coordinates.length).fill(null);
    System.arraycopy(coordinates, i, newCoordinates, 0, coordinates.length - i);
    System.arraycopy(coordinates, 0, newCoordinates, coordinates.length - i, i);
    System.arraycopy(newCoordinates, 0, coordinates, 0, coordinates.length);
  }

  static equals() {
    if (arguments.length === 2) {
      const coord1 = arguments[0],
            coord2 = arguments[1];
      if (coord1 === coord2) return true;
      if (coord1 === null || coord2 === null) return false;
      if (coord1.length !== coord2.length) return false;

      for (let i = 0; i < coord1.length; i++) if (!coord1[i].equals(coord2[i])) return false;

      return true;
    } else if (arguments.length === 3) {
      const coord1 = arguments[0],
            coord2 = arguments[1],
            coordinateComparator = arguments[2];
      if (coord1 === coord2) return true;
      if (coord1 === null || coord2 === null) return false;
      if (coord1.length !== coord2.length) return false;

      for (let i = 0; i < coord1.length; i++) if (coordinateComparator.compare(coord1[i], coord2[i]) !== 0) return false;

      return true;
    }
  }

  static intersection(coordinates, env) {
    const coordList = new CoordinateList();

    for (let i = 0; i < coordinates.length; i++) if (env.intersects(coordinates[i])) coordList.add(coordinates[i], true);

    return coordList.toCoordinateArray();
  }

  static measures(pts) {
    if (pts === null || pts.length === 0) return 0;
    let measures = 0;

    for (const coordinate of pts) measures = Math.max(measures, Coordinates.measures(coordinate));

    return measures;
  }

  static hasRepeatedPoints(coord) {
    for (let i = 1; i < coord.length; i++) if (coord[i - 1].equals(coord[i])) return true;

    return false;
  }

  static removeRepeatedPoints(coord) {
    if (!CoordinateArrays.hasRepeatedPoints(coord)) return coord;
    const coordList = new CoordinateList(coord, false);
    return coordList.toCoordinateArray();
  }

  static reverse(coord) {
    const last = coord.length - 1;
    const mid = Math.trunc(last / 2);

    for (let i = 0; i <= mid; i++) {
      const tmp = coord[i];
      coord[i] = coord[last - i];
      coord[last - i] = tmp;
    }
  }

  static removeNull(coord) {
    let nonNull = 0;

    for (let i = 0; i < coord.length; i++) if (coord[i] !== null) nonNull++;

    const newCoord = new Array(nonNull).fill(null);
    if (nonNull === 0) return newCoord;
    let j = 0;

    for (let i = 0; i < coord.length; i++) if (coord[i] !== null) newCoord[j++] = coord[i];

    return newCoord;
  }

  static copyDeep() {
    if (arguments.length === 1) {
      const coordinates = arguments[0];
      const copy = new Array(coordinates.length).fill(null);

      for (let i = 0; i < coordinates.length; i++) copy[i] = coordinates[i].copy();

      return copy;
    } else if (arguments.length === 5) {
      const src = arguments[0],
            srcStart = arguments[1],
            dest = arguments[2],
            destStart = arguments[3],
            length = arguments[4];

      for (let i = 0; i < length; i++) dest[destStart + i] = src[srcStart + i].copy();
    }
  }

  static isEqualReversed(pts1, pts2) {
    for (let i = 0; i < pts1.length; i++) {
      const p1 = pts1[i];
      const p2 = pts2[pts1.length - i - 1];
      if (p1.compareTo(p2) !== 0) return false;
    }

    return true;
  }

  static envelope(coordinates) {
    const env = new Envelope();

    for (let i = 0; i < coordinates.length; i++) env.expandToInclude(coordinates[i]);

    return env;
  }

  static toCoordinateArray(coordList) {
    return coordList.toArray(CoordinateArrays.coordArrayType);
  }

  static dimension(pts) {
    if (pts === null || pts.length === 0) return 3;
    let dimension = 0;

    for (const coordinate of pts) dimension = Math.max(dimension, Coordinates.dimension(coordinate));

    return dimension;
  }

  static atLeastNCoordinatesOrNothing(n, c) {
    return c.length >= n ? c : [];
  }

  static indexOf(coordinate, coordinates) {
    for (let i = 0; i < coordinates.length; i++) if (coordinate.equals(coordinates[i])) return i;

    return -1;
  }

  static increasingDirection(pts) {
    for (let i = 0; i < Math.trunc(pts.length / 2); i++) {
      const j = pts.length - 1 - i;
      const comp = pts[i].compareTo(pts[j]);
      if (comp !== 0) return comp;
    }

    return 1;
  }

  static compare(pts1, pts2) {
    let i = 0;

    while (i < pts1.length && i < pts2.length) {
      const compare = pts1[i].compareTo(pts2[i]);
      if (compare !== 0) return compare;
      i++;
    }

    if (i < pts2.length) return -1;
    if (i < pts1.length) return 1;
    return 0;
  }

  static minCoordinate(coordinates) {
    let minCoord = null;

    for (let i = 0; i < coordinates.length; i++) if (minCoord === null || minCoord.compareTo(coordinates[i]) > 0) minCoord = coordinates[i];

    return minCoord;
  }

  static extract(pts, start, end) {
    start = MathUtil.clamp(start, 0, pts.length);
    end = MathUtil.clamp(end, -1, pts.length);
    let npts = end - start + 1;
    if (end < 0) npts = 0;
    if (start >= pts.length) npts = 0;
    if (end < start) npts = 0;
    const extractPts = new Array(npts).fill(null);
    if (npts === 0) return extractPts;
    let iPts = 0;

    for (let i = start; i <= end; i++) extractPts[iPts++] = pts[i];

    return extractPts;
  }

}

class ForwardComparator {
  compare(o1, o2) {
    const pts1 = o1;
    const pts2 = o2;
    return CoordinateArrays.compare(pts1, pts2);
  }

  get interfaces_() {
    return [Comparator];
  }

}

class BidirectionalComparator {
  compare(o1, o2) {
    const pts1 = o1;
    const pts2 = o2;
    if (pts1.length < pts2.length) return -1;
    if (pts1.length > pts2.length) return 1;
    if (pts1.length === 0) return 0;
    const forwardComp = CoordinateArrays.compare(pts1, pts2);
    const isEqualRev = CoordinateArrays.isEqualReversed(pts1, pts2);
    if (isEqualRev) return 0;
    return forwardComp;
  }

  OLDcompare(o1, o2) {
    const pts1 = o1;
    const pts2 = o2;
    if (pts1.length < pts2.length) return -1;
    if (pts1.length > pts2.length) return 1;
    if (pts1.length === 0) return 0;
    const dir1 = CoordinateArrays.increasingDirection(pts1);
    const dir2 = CoordinateArrays.increasingDirection(pts2);
    let i1 = dir1 > 0 ? 0 : pts1.length - 1;
    let i2 = dir2 > 0 ? 0 : pts1.length - 1;

    for (let i = 0; i < pts1.length; i++) {
      const comparePt = pts1[i1].compareTo(pts2[i2]);
      if (comparePt !== 0) return comparePt;
      i1 += dir1;
      i2 += dir2;
    }

    return 0;
  }

  get interfaces_() {
    return [Comparator];
  }

}

CoordinateArrays.ForwardComparator = ForwardComparator;
CoordinateArrays.BidirectionalComparator = BidirectionalComparator;
CoordinateArrays.coordArrayType = new Array(0).fill(null);

class StringBuilder {
  constructor(str) {
    this.str = str;
  }

  append(e) {
    this.str += e;
  }

  setCharAt(i, c) {
    this.str = this.str.substr(0, i) + c + this.str.substr(i + 1);
  }

  toString() {
    return this.str;
  }

}

class CoordinateArraySequence {
  constructor() {
    CoordinateArraySequence.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._dimension = 3;
    this._measures = 0;
    this._coordinates = null;

    if (arguments.length === 1) {
      if (arguments[0] instanceof Array) {
        const coordinates = arguments[0];
        CoordinateArraySequence.constructor_.call(this, coordinates, CoordinateArrays.dimension(coordinates), CoordinateArrays.measures(coordinates));
      } else if (Number.isInteger(arguments[0])) {
        const size = arguments[0];
        this._coordinates = new Array(size).fill(null);

        for (let i = 0; i < size; i++) this._coordinates[i] = new Coordinate();
      } else if (hasInterface(arguments[0], CoordinateSequence)) {
        const coordSeq = arguments[0];

        if (coordSeq === null) {
          this._coordinates = new Array(0).fill(null);
          return null;
        }

        this._dimension = coordSeq.getDimension();
        this._measures = coordSeq.getMeasures();
        this._coordinates = new Array(coordSeq.size()).fill(null);

        for (let i = 0; i < this._coordinates.length; i++) this._coordinates[i] = coordSeq.getCoordinateCopy(i);
      }
    } else if (arguments.length === 2) {
      if (arguments[0] instanceof Array && Number.isInteger(arguments[1])) {
        const coordinates = arguments[0],
              dimension = arguments[1];
        CoordinateArraySequence.constructor_.call(this, coordinates, dimension, CoordinateArrays.measures(coordinates));
      } else if (Number.isInteger(arguments[0]) && Number.isInteger(arguments[1])) {
        const size = arguments[0],
              dimension = arguments[1];
        this._coordinates = new Array(size).fill(null);
        this._dimension = dimension;

        for (let i = 0; i < size; i++) this._coordinates[i] = Coordinates.create(dimension);
      }
    } else if (arguments.length === 3) {
      if (Number.isInteger(arguments[2]) && arguments[0] instanceof Array && Number.isInteger(arguments[1])) {
        const coordinates = arguments[0],
              dimension = arguments[1],
              measures = arguments[2];
        this._dimension = dimension;
        this._measures = measures;
        if (coordinates === null) this._coordinates = new Array(0).fill(null);else this._coordinates = coordinates;
      } else if (Number.isInteger(arguments[2]) && Number.isInteger(arguments[0]) && Number.isInteger(arguments[1])) {
        const size = arguments[0],
              dimension = arguments[1],
              measures = arguments[2];
        this._coordinates = new Array(size).fill(null);
        this._dimension = dimension;
        this._measures = measures;

        for (let i = 0; i < size; i++) this._coordinates[i] = this.createCoordinate();
      }
    }
  }

  getM(index) {
    if (this.hasM()) return this._coordinates[index].getM();else return Double.NaN;
  }

  setOrdinate(index, ordinateIndex, value) {
    switch (ordinateIndex) {
      case CoordinateSequence.X:
        this._coordinates[index].x = value;
        break;

      case CoordinateSequence.Y:
        this._coordinates[index].y = value;
        break;

      default:
        this._coordinates[index].setOrdinate(ordinateIndex, value);

    }
  }

  getZ(index) {
    if (this.hasZ()) return this._coordinates[index].getZ();else return Double.NaN;
  }

  size() {
    return this._coordinates.length;
  }

  getOrdinate(index, ordinateIndex) {
    switch (ordinateIndex) {
      case CoordinateSequence.X:
        return this._coordinates[index].x;

      case CoordinateSequence.Y:
        return this._coordinates[index].y;

      default:
        return this._coordinates[index].getOrdinate(ordinateIndex);
    }
  }

  getCoordinate() {
    if (arguments.length === 1) {
      const i = arguments[0];
      return this._coordinates[i];
    } else if (arguments.length === 2) {
      const index = arguments[0],
            coord = arguments[1];
      coord.setCoordinate(this._coordinates[index]);
    }
  }

  getCoordinateCopy(i) {
    const copy = this.createCoordinate();
    copy.setCoordinate(this._coordinates[i]);
    return copy;
  }

  createCoordinate() {
    return Coordinates.create(this.getDimension(), this.getMeasures());
  }

  getDimension() {
    return this._dimension;
  }

  getX(index) {
    return this._coordinates[index].x;
  }

  getMeasures() {
    return this._measures;
  }

  expandEnvelope(env) {
    for (let i = 0; i < this._coordinates.length; i++) env.expandToInclude(this._coordinates[i]);

    return env;
  }

  copy() {
    const cloneCoordinates = new Array(this.size()).fill(null);

    for (let i = 0; i < this._coordinates.length; i++) {
      const duplicate = this.createCoordinate();
      duplicate.setCoordinate(this._coordinates[i]);
      cloneCoordinates[i] = duplicate;
    }

    return new CoordinateArraySequence(cloneCoordinates, this._dimension, this._measures);
  }

  toString() {
    if (this._coordinates.length > 0) {
      const strBuilder = new StringBuilder(17 * this._coordinates.length);
      strBuilder.append('(');
      strBuilder.append(this._coordinates[0]);

      for (let i = 1; i < this._coordinates.length; i++) {
        strBuilder.append(', ');
        strBuilder.append(this._coordinates[i]);
      }

      strBuilder.append(')');
      return strBuilder.toString();
    } else {
      return '()';
    }
  }

  getY(index) {
    return this._coordinates[index].y;
  }

  toCoordinateArray() {
    return this._coordinates;
  }

  get interfaces_() {
    return [CoordinateSequence, Serializable];
  }

}

class CoordinateArraySequenceFactory {
  static instance() {
    return CoordinateArraySequenceFactory.instanceObject;
  }

  readResolve() {
    return CoordinateArraySequenceFactory.instance();
  }

  create() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof Array) {
        const coordinates = arguments[0];
        return new CoordinateArraySequence(coordinates);
      } else if (hasInterface(arguments[0], CoordinateSequence)) {
        const coordSeq = arguments[0];
        return new CoordinateArraySequence(coordSeq);
      }
    } else if (arguments.length === 2) {
      let size = arguments[0],
          dimension = arguments[1];
      if (dimension > 3) dimension = 3;
      if (dimension < 2) dimension = 2;
      return new CoordinateArraySequence(size, dimension);
    } else if (arguments.length === 3) {
      let size = arguments[0],
          dimension = arguments[1],
          measures = arguments[2];
      let spatial = dimension - measures;
      if (measures > 1) measures = 1;
      if (spatial > 3) spatial = 3;
      if (spatial < 2) spatial = 2;
      return new CoordinateArraySequence(size, spatial + measures, measures);
    }
  }

  get interfaces_() {
    return [CoordinateSequenceFactory, Serializable];
  }

}
CoordinateArraySequenceFactory.instanceObject = new CoordinateArraySequenceFactory();

class MultiPolygon extends GeometryCollection {
  constructor() {
    super();
    MultiPolygon.constructor_.apply(this, arguments);
  }

  static constructor_() {
    const polygons = arguments[0],
          factory = arguments[1];
    GeometryCollection.constructor_.call(this, polygons, factory);
  }

  copyInternal() {
    const polygons = new Array(this._geometries.length).fill(null);

    for (let i = 0; i < polygons.length; i++) polygons[i] = this._geometries[i].copy();

    return new MultiPolygon(polygons, this._factory);
  }

  equalsExact() {
    if (arguments.length === 2 && typeof arguments[1] === 'number' && arguments[0] instanceof Geometry) {
      const other = arguments[0],
            tolerance = arguments[1];
      if (!this.isEquivalentClass(other)) return false;
      return super.equalsExact.call(this, other, tolerance);
    } else {
      return super.equalsExact.apply(this, arguments);
    }
  }

  getBoundaryDimension() {
    return 1;
  }

  getTypeCode() {
    return Geometry.TYPECODE_MULTIPOLYGON;
  }

  getDimension() {
    return 2;
  }

  getBoundary() {
    if (this.isEmpty()) return this.getFactory().createMultiLineString();
    const allRings = new ArrayList();

    for (let i = 0; i < this._geometries.length; i++) {
      const polygon = this._geometries[i];
      const rings = polygon.getBoundary();

      for (let j = 0; j < rings.getNumGeometries(); j++) allRings.add(rings.getGeometryN(j));
    }

    const allRingsArray = new Array(allRings.size()).fill(null);
    return this.getFactory().createMultiLineString(allRings.toArray(allRingsArray));
  }

  getGeometryType() {
    return Geometry.TYPENAME_MULTIPOLYGON;
  }

  get interfaces_() {
    return [Polygonal];
  }

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/Map.html
 */
class Map$1 {
  /**
     * Returns the value to which the specified key is mapped, or null if this map
     * contains no mapping for the key.
     * @param {Object} key
     * @return {Object}
     */
  get() {}
  /**
     * Associates the specified value with the specified key in this map (optional
     * operation).
     * @param {Object} key
     * @param {Object} value
     * @return {Object}
     */


  put() {}
  /**
     * Returns the number of key-value mappings in this map.
     * @return {number}
     */


  size() {}
  /**
     * Returns a Collection view of the values contained in this map.
     * @return {javascript.util.Collection}
     */


  values() {}
  /**
     * Returns a {@link Set} view of the mappings contained in this map.
     * The set is backed by the map, so changes to the map are
     * reflected in the set, and vice-versa.  If the map is modified
     * while an iteration over the set is in progress (except through
     * the iterator's own <tt>remove</tt> operation, or through the
     * <tt>setValue</tt> operation on a map entry returned by the
     * iterator) the results of the iteration are undefined.  The set
     * supports element removal, which removes the corresponding
     * mapping from the map, via the <tt>Iterator.remove</tt>,
     * <tt>Set.remove</tt>, <tt>removeAll</tt>, <tt>retainAll</tt> and
     * <tt>clear</tt> operations.  It does not support the
     * <tt>add</tt> or <tt>addAll</tt> operations.
     *
     * @return {Set} a set view of the mappings contained in this map
     */


  entrySet() {}

}

/**
 * @see http://docs.oracle.com/javase/6/docs/api/java/util/HashSet.html
 */

class HashSet extends Set {
  constructor(o) {
    super();
    this.map = new Map();
    if (o instanceof Collection) this.addAll(o);
  }

  contains(o) {
    const hashCode = o.hashCode ? o.hashCode() : o;
    if (this.map.has(hashCode)) return true;
    return false;
  }

  add(o) {
    const hashCode = o.hashCode ? o.hashCode() : o;
    if (this.map.has(hashCode)) return false;
    return !!this.map.set(hashCode, o);
  }

  addAll(c) {
    for (const e of c) this.add(e);

    return true;
  }

  remove() {
    throw new UnsupportedOperationException();
  }

  size() {
    return this.map.size;
  }

  isEmpty() {
    return this.map.size === 0;
  }

  toArray() {
    return Array.from(this.map.values());
  }

  iterator() {
    return new Iterator$1(this.map);
  }

  [Symbol.iterator]() {
    return this.map;
  }

}

class Iterator$1 {
  constructor(map) {
    this.iterator = map.values();
    const {
      done,
      value
    } = this.iterator.next();
    this.done = done;
    this.value = value;
  }

  next() {
    if (this.done) throw new NoSuchElementException();
    const current = this.value;
    const {
      done,
      value
    } = this.iterator.next();
    this.done = done;
    this.value = value;
    return current;
  }

  hasNext() {
    return !this.done;
  }

  remove() {
    throw new UnsupportedOperationException();
  }

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/HashMap.html
 */

class HashMap extends Map$1 {
  constructor() {
    super();
    this.map = new Map();
  }

  get(key) {
    return this.map.get(key) || null;
  }

  put(key, value) {
    this.map.set(key, value);
    return value;
  }

  values() {
    const arrayList = new ArrayList();
    const it = this.map.values();
    let o = it.next();

    while (!o.done) {
      arrayList.add(o.value);
      o = it.next();
    }

    return arrayList;
  }

  entrySet() {
    const hashSet = new HashSet();
    this.map.entries().forEach(entry => hashSet.add(entry));
    return hashSet;
  }

  size() {
    return this.map.size();
  }

}

class PrecisionModel {
  constructor() {
    PrecisionModel.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._modelType = null;
    this._scale = null;
    if (arguments.length === 0) this._modelType = PrecisionModel.FLOATING;else if (arguments.length === 1) if (arguments[0] instanceof Type) {
      const modelType = arguments[0];
      this._modelType = modelType;
      if (modelType === PrecisionModel.FIXED) this.setScale(1.0);
    } else if (typeof arguments[0] === 'number') {
      const scale = arguments[0];
      this._modelType = PrecisionModel.FIXED;
      this.setScale(scale);
    } else if (arguments[0] instanceof PrecisionModel) {
      const pm = arguments[0];
      this._modelType = pm._modelType;
      this._scale = pm._scale;
    }
  }

  static mostPrecise(pm1, pm2) {
    if (pm1.compareTo(pm2) >= 0) return pm1;
    return pm2;
  }

  equals(other) {
    if (!(other instanceof PrecisionModel)) return false;
    const otherPrecisionModel = other;
    return this._modelType === otherPrecisionModel._modelType && this._scale === otherPrecisionModel._scale;
  }

  compareTo(o) {
    const other = o;
    const sigDigits = this.getMaximumSignificantDigits();
    const otherSigDigits = other.getMaximumSignificantDigits();
    return Integer.compare(sigDigits, otherSigDigits);
  }

  getScale() {
    return this._scale;
  }

  isFloating() {
    return this._modelType === PrecisionModel.FLOATING || this._modelType === PrecisionModel.FLOATING_SINGLE;
  }

  getType() {
    return this._modelType;
  }

  toString() {
    let description = 'UNKNOWN';
    if (this._modelType === PrecisionModel.FLOATING) description = 'Floating';else if (this._modelType === PrecisionModel.FLOATING_SINGLE) description = 'Floating-Single';else if (this._modelType === PrecisionModel.FIXED) description = 'Fixed (Scale=' + this.getScale() + ')';
    return description;
  }

  makePrecise() {
    if (typeof arguments[0] === 'number') {
      const val = arguments[0];
      if (Double.isNaN(val)) return val;

      if (this._modelType === PrecisionModel.FLOATING_SINGLE) {
        const floatSingleVal = val;
        return floatSingleVal;
      }

      if (this._modelType === PrecisionModel.FIXED) return Math.round(val * this._scale) / this._scale;
      return val;
    } else if (arguments[0] instanceof Coordinate) {
      const coord = arguments[0];
      if (this._modelType === PrecisionModel.FLOATING) return null;
      coord.x = this.makePrecise(coord.x);
      coord.y = this.makePrecise(coord.y);
    }
  }

  getMaximumSignificantDigits() {
    let maxSigDigits = 16;
    if (this._modelType === PrecisionModel.FLOATING) maxSigDigits = 16;else if (this._modelType === PrecisionModel.FLOATING_SINGLE) maxSigDigits = 6;else if (this._modelType === PrecisionModel.FIXED) maxSigDigits = 1 + Math.trunc(Math.ceil(Math.log(this.getScale()) / Math.log(10)));
    return maxSigDigits;
  }

  setScale(scale) {
    this._scale = Math.abs(scale);
  }

  get interfaces_() {
    return [Serializable, Comparable];
  }

}

class Type {
  constructor() {
    Type.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._name = null;
    const name = arguments[0];
    this._name = name;
    Type.nameToTypeMap.put(name, this);
  }

  readResolve() {
    return Type.nameToTypeMap.get(this._name);
  }

  toString() {
    return this._name;
  }

  get interfaces_() {
    return [Serializable];
  }

}

Type.nameToTypeMap = new HashMap();
PrecisionModel.Type = Type;
PrecisionModel.FIXED = new Type('FIXED');
PrecisionModel.FLOATING = new Type('FLOATING');
PrecisionModel.FLOATING_SINGLE = new Type('FLOATING SINGLE');
PrecisionModel.maximumPreciseValue = 9007199254740992.0;

class MultiLineString extends GeometryCollection {
  constructor() {
    super();
    MultiLineString.constructor_.apply(this, arguments);
  }

  static constructor_() {
    const lineStrings = arguments[0],
          factory = arguments[1];
    GeometryCollection.constructor_.call(this, lineStrings, factory);
  }

  copyInternal() {
    const lineStrings = new Array(this._geometries.length).fill(null);

    for (let i = 0; i < lineStrings.length; i++) lineStrings[i] = this._geometries[i].copy();

    return new MultiLineString(lineStrings, this._factory);
  }

  equalsExact() {
    if (arguments.length === 2 && typeof arguments[1] === 'number' && arguments[0] instanceof Geometry) {
      const other = arguments[0],
            tolerance = arguments[1];
      if (!this.isEquivalentClass(other)) return false;
      return super.equalsExact.call(this, other, tolerance);
    } else {
      return super.equalsExact.apply(this, arguments);
    }
  }

  getBoundaryDimension() {
    if (this.isClosed()) return Dimension.FALSE;
    return 0;
  }

  isClosed() {
    if (this.isEmpty()) return false;

    for (let i = 0; i < this._geometries.length; i++) if (!this._geometries[i].isClosed()) return false;

    return true;
  }

  getTypeCode() {
    return Geometry.TYPECODE_MULTILINESTRING;
  }

  getDimension() {
    return 1;
  }

  getBoundary() {
    throw new UnsupportedOperationException();
  }

  getGeometryType() {
    return Geometry.TYPENAME_MULTILINESTRING;
  }

  get interfaces_() {
    return [Lineal];
  }

}

class GeometryFactory {
  constructor() {
    GeometryFactory.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._precisionModel = null;
    this._coordinateSequenceFactory = null;
    this._SRID = null;

    if (arguments.length === 0) {
      GeometryFactory.constructor_.call(this, new PrecisionModel(), 0);
    } else if (arguments.length === 1) {
      if (hasInterface(arguments[0], CoordinateSequenceFactory)) {
        const coordinateSequenceFactory = arguments[0];
        GeometryFactory.constructor_.call(this, new PrecisionModel(), 0, coordinateSequenceFactory);
      } else if (arguments[0] instanceof PrecisionModel) {
        const precisionModel = arguments[0];
        GeometryFactory.constructor_.call(this, precisionModel, 0, GeometryFactory.getDefaultCoordinateSequenceFactory());
      }
    } else if (arguments.length === 2) {
      const precisionModel = arguments[0],
            SRID = arguments[1];
      GeometryFactory.constructor_.call(this, precisionModel, SRID, GeometryFactory.getDefaultCoordinateSequenceFactory());
    } else if (arguments.length === 3) {
      const precisionModel = arguments[0],
            SRID = arguments[1],
            coordinateSequenceFactory = arguments[2];
      this._precisionModel = precisionModel;
      this._coordinateSequenceFactory = coordinateSequenceFactory;
      this._SRID = SRID;
    }
  }

  static toMultiPolygonArray(multiPolygons) {
    const multiPolygonArray = new Array(multiPolygons.size()).fill(null);
    return multiPolygons.toArray(multiPolygonArray);
  }

  static toGeometryArray(geometries) {
    if (geometries === null) return null;
    const geometryArray = new Array(geometries.size()).fill(null);
    return geometries.toArray(geometryArray);
  }

  static getDefaultCoordinateSequenceFactory() {
    return CoordinateArraySequenceFactory.instance();
  }

  static toMultiLineStringArray(multiLineStrings) {
    const multiLineStringArray = new Array(multiLineStrings.size()).fill(null);
    return multiLineStrings.toArray(multiLineStringArray);
  }

  static toLineStringArray(lineStrings) {
    const lineStringArray = new Array(lineStrings.size()).fill(null);
    return lineStrings.toArray(lineStringArray);
  }

  static toMultiPointArray(multiPoints) {
    const multiPointArray = new Array(multiPoints.size()).fill(null);
    return multiPoints.toArray(multiPointArray);
  }

  static toLinearRingArray(linearRings) {
    const linearRingArray = new Array(linearRings.size()).fill(null);
    return linearRings.toArray(linearRingArray);
  }

  static toPointArray(points) {
    const pointArray = new Array(points.size()).fill(null);
    return points.toArray(pointArray);
  }

  static toPolygonArray(polygons) {
    const polygonArray = new Array(polygons.size()).fill(null);
    return polygons.toArray(polygonArray);
  }

  static createPointFromInternalCoord(coord, exemplar) {
    exemplar.getPrecisionModel().makePrecise(coord);
    return exemplar.getFactory().createPoint(coord);
  }

  createEmpty(dimension) {
    switch (dimension) {
      case -1:
        return this.createGeometryCollection();

      case 0:
        return this.createPoint();

      case 1:
        return this.createLineString();

      case 2:
        return this.createPolygon();

      default:
        throw new IllegalArgumentException('Invalid dimension: ' + dimension);
    }
  }

  toGeometry(envelope) {
    if (envelope.isNull()) return this.createPoint();
    if (envelope.getMinX() === envelope.getMaxX() && envelope.getMinY() === envelope.getMaxY()) return this.createPoint(new Coordinate(envelope.getMinX(), envelope.getMinY()));
    if (envelope.getMinX() === envelope.getMaxX() || envelope.getMinY() === envelope.getMaxY()) return this.createLineString([new Coordinate(envelope.getMinX(), envelope.getMinY()), new Coordinate(envelope.getMaxX(), envelope.getMaxY())]);
    return this.createPolygon(this.createLinearRing([new Coordinate(envelope.getMinX(), envelope.getMinY()), new Coordinate(envelope.getMinX(), envelope.getMaxY()), new Coordinate(envelope.getMaxX(), envelope.getMaxY()), new Coordinate(envelope.getMaxX(), envelope.getMinY()), new Coordinate(envelope.getMinX(), envelope.getMinY())]), null);
  }

  createLineString() {
    if (arguments.length === 0) return this.createLineString(this.getCoordinateSequenceFactory().create([]));else if (arguments.length === 1) if (arguments[0] instanceof Array) {
      const coordinates = arguments[0];
      return this.createLineString(coordinates !== null ? this.getCoordinateSequenceFactory().create(coordinates) : null);
    } else if (hasInterface(arguments[0], CoordinateSequence)) {
      const coordinates = arguments[0];
      return new LineString(coordinates, this);
    }
  }

  createMultiLineString() {
    if (arguments.length === 0) {
      return new MultiLineString(null, this);
    } else if (arguments.length === 1) {
      const lineStrings = arguments[0];
      return new MultiLineString(lineStrings, this);
    }
  }

  buildGeometry(geomList) {
    let geomType = null;
    let isHeterogeneous = false;
    let hasGeometryCollection = false;

    for (let i = geomList.iterator(); i.hasNext();) {
      const geom = i.next();
      const partType = geom.getTypeCode();
      if (geomType === null) geomType = partType;
      if (partType !== geomType) isHeterogeneous = true;
      if (geom instanceof GeometryCollection) hasGeometryCollection = true;
    }

    if (geomType === null) return this.createGeometryCollection();
    if (isHeterogeneous || hasGeometryCollection) return this.createGeometryCollection(GeometryFactory.toGeometryArray(geomList));
    const geom0 = geomList.iterator().next();
    const isCollection = geomList.size() > 1;

    if (isCollection) {
      if (geom0 instanceof Polygon) return this.createMultiPolygon(GeometryFactory.toPolygonArray(geomList));else if (geom0 instanceof LineString) return this.createMultiLineString(GeometryFactory.toLineStringArray(geomList));else if (geom0 instanceof Point) return this.createMultiPoint(GeometryFactory.toPointArray(geomList));
      Assert.shouldNeverReachHere('Unhandled geometry type: ' + geom0.getGeometryType());
    }

    return geom0;
  }

  createMultiPointFromCoords(coordinates) {
    return this.createMultiPoint(coordinates !== null ? this.getCoordinateSequenceFactory().create(coordinates) : null);
  }

  createPoint() {
    if (arguments.length === 0) return this.createPoint(this.getCoordinateSequenceFactory().create([]));else if (arguments.length === 1) if (arguments[0] instanceof Coordinate) {
      const coordinate = arguments[0];
      return this.createPoint(coordinate !== null ? this.getCoordinateSequenceFactory().create([coordinate]) : null);
    } else if (hasInterface(arguments[0], CoordinateSequence)) {
      const coordinates = arguments[0];
      return new Point(coordinates, this);
    }
  }

  getCoordinateSequenceFactory() {
    return this._coordinateSequenceFactory;
  }

  createPolygon() {
    if (arguments.length === 0) {
      return this.createPolygon(null, null);
    } else if (arguments.length === 1) {
      if (hasInterface(arguments[0], CoordinateSequence)) {
        const shell = arguments[0];
        return this.createPolygon(this.createLinearRing(shell));
      } else if (arguments[0] instanceof Array) {
        const shell = arguments[0];
        return this.createPolygon(this.createLinearRing(shell));
      } else if (arguments[0] instanceof LinearRing) {
        const shell = arguments[0];
        return this.createPolygon(shell, null);
      }
    } else if (arguments.length === 2) {
      const shell = arguments[0],
            holes = arguments[1];
      return new Polygon(shell, holes, this);
    }
  }

  getSRID() {
    return this._SRID;
  }

  createGeometryCollection() {
    if (arguments.length === 0) {
      return new GeometryCollection(null, this);
    } else if (arguments.length === 1) {
      const geometries = arguments[0];
      return new GeometryCollection(geometries, this);
    }
  }

  getPrecisionModel() {
    return this._precisionModel;
  }

  createLinearRing() {
    if (arguments.length === 0) return this.createLinearRing(this.getCoordinateSequenceFactory().create([]));else if (arguments.length === 1) if (arguments[0] instanceof Array) {
      const coordinates = arguments[0];
      return this.createLinearRing(coordinates !== null ? this.getCoordinateSequenceFactory().create(coordinates) : null);
    } else if (hasInterface(arguments[0], CoordinateSequence)) {
      const coordinates = arguments[0];
      return new LinearRing(coordinates, this);
    }
  }

  createMultiPolygon() {
    if (arguments.length === 0) {
      return new MultiPolygon(null, this);
    } else if (arguments.length === 1) {
      const polygons = arguments[0];
      return new MultiPolygon(polygons, this);
    }
  }

  createMultiPoint() {
    if (arguments.length === 0) return new MultiPoint(null, this);else if (arguments.length === 1) if (arguments[0] instanceof Array) {
      const point = arguments[0];
      return new MultiPoint(point, this);
    } else if (hasInterface(arguments[0], CoordinateSequence)) {
      const coordinates = arguments[0];
      if (coordinates === null) return this.createMultiPoint(new Array(0).fill(null));
      const points = new Array(coordinates.size()).fill(null);

      for (let i = 0; i < coordinates.size(); i++) {
        const ptSeq = this.getCoordinateSequenceFactory().create(1, coordinates.getDimension(), coordinates.getMeasures());
        CoordinateSequences.copy(coordinates, i, ptSeq, 0, 1);
        points[i] = this.createPoint(ptSeq);
      }

      return this.createMultiPoint(points);
    }
  }

  get interfaces_() {
    return [Serializable];
  }

}

/**
 * The coordinate layout for geometries, indicating whether a 3rd or 4th z ('Z')
 * or measure ('M') coordinate is available. Supported values are `'XY'`,
 * `'XYZ'`, `'XYM'`, `'XYZM'`.
 * @enum {string}
 */

const GeometryLayout = {
  XY: 'XY',
  XYZ: 'XYZ',
  XYM: 'XYM',
  XYZM: 'XYZM'
};
/**
 * The geometry type. One of `'Point'`, `'LineString'`, `'LinearRing'`,
 * `'Polygon'`, `'MultiPoint'`, `'MultiLineString'`, `'MultiPolygon'`,
 * `'GeometryCollection'`, `'Circle'`.
 * @enum {string}
 */

const GeometryType = {
  POINT: 'Point',
  LINE_STRING: 'LineString',
  LINEAR_RING: 'LinearRing',
  POLYGON: 'Polygon',
  MULTI_POINT: 'MultiPoint',
  MULTI_LINE_STRING: 'MultiLineString',
  MULTI_POLYGON: 'MultiPolygon',
  GEOMETRY_COLLECTION: 'GeometryCollection',
  CIRCLE: 'Circle'
};
/**
 * @typedef {Object} Options
 * @property {boolean} [splitCollection=false] Whether to split GeometryCollections into
 * multiple features on reading.
 */

/**
 * @typedef {Object} Token
 * @property {number} type
 * @property {number|string} [value]
 * @property {number} position
 */

/**
 * @const
 * @type {string}
 */

const EMPTY = 'EMPTY';
/**
 * @const
 * @type {string}
 */

const Z = 'Z';
/**
 * @const
 * @type {string}
 */

const M = 'M';
/**
 * @const
 * @type {string}
 */

const ZM = 'ZM';
/**
 * @const
 * @enum {number}
 */

const TokenType = {
  TEXT: 1,
  LEFT_PAREN: 2,
  RIGHT_PAREN: 3,
  NUMBER: 4,
  COMMA: 5,
  EOF: 6
};
/**
 * @const
 * @type {Object<string, string>}
 */

const WKTGeometryType = {};

for (const type in GeometryType) WKTGeometryType[type] = GeometryType[type].toUpperCase();
/**
 * Class to tokenize a WKT string.
 */


class Lexer {
  /**
   * @param {string} wkt WKT string.
   */
  constructor(wkt) {
    /**
     * @type {string}
     */
    this.wkt = wkt;
    /**
     * @type {number}
     * @private
     */

    this.index_ = -1;
  }
  /**
   * @param {string} c Character.
   * @return {boolean} Whether the character is alphabetic.
   * @private
   */


  isAlpha_(c) {
    return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
  }
  /**
   * @param {string} c Character.
   * @param {boolean=} opt_decimal Whether the string number
   *     contains a dot, i.e. is a decimal number.
   * @return {boolean} Whether the character is numeric.
   * @private
   */


  isNumeric_(c, opt_decimal) {
    const decimal = opt_decimal !== undefined ? opt_decimal : false;
    return c >= '0' && c <= '9' || c == '.' && !decimal;
  }
  /**
   * @param {string} c Character.
   * @return {boolean} Whether the character is whitespace.
   * @private
   */


  isWhiteSpace_(c) {
    return c == ' ' || c == '\t' || c == '\r' || c == '\n';
  }
  /**
   * @return {string} Next string character.
   * @private
   */


  nextChar_() {
    return this.wkt.charAt(++this.index_);
  }
  /**
   * Fetch and return the next token.
   * @return {!Token} Next string token.
   */


  nextToken() {
    const c = this.nextChar_();
    const position = this.index_;
    /** @type {number|string} */

    let value = c;
    let type;

    if (c == '(') {
      type = TokenType.LEFT_PAREN;
    } else if (c == ',') {
      type = TokenType.COMMA;
    } else if (c == ')') {
      type = TokenType.RIGHT_PAREN;
    } else if (this.isNumeric_(c) || c == '-') {
      type = TokenType.NUMBER;
      value = this.readNumber_();
    } else if (this.isAlpha_(c)) {
      type = TokenType.TEXT;
      value = this.readText_();
    } else if (this.isWhiteSpace_(c)) {
      return this.nextToken();
    } else if (c === '') {
      type = TokenType.EOF;
    } else {
      throw new Error('Unexpected character: ' + c);
    }

    return {
      position: position,
      value: value,
      type: type
    };
  }
  /**
   * @return {number} Numeric token value.
   * @private
   */


  readNumber_() {
    let c;
    const index = this.index_;
    let decimal = false;
    let scientificNotation = false;

    do {
      if (c == '.') decimal = true;else if (c == 'e' || c == 'E') scientificNotation = true;
      c = this.nextChar_();
    } while (this.isNumeric_(c, decimal) || // if we haven't detected a scientific number before, 'e' or 'E'
    // hint that we should continue to read
    !scientificNotation && (c == 'e' || c == 'E') || // once we know that we have a scientific number, both '-' and '+'
    // are allowed
    scientificNotation && (c == '-' || c == '+'));

    return parseFloat(this.wkt.substring(index, this.index_--));
  }
  /**
   * @return {string} String token value.
   * @private
   */


  readText_() {
    let c;
    const index = this.index_;

    do c = this.nextChar_(); while (this.isAlpha_(c));

    return this.wkt.substring(index, this.index_--).toUpperCase();
  }

}
/**
 * Class to parse the tokens from the WKT string.
 */


class Parser {
  /**
   * @param {Lexer} lexer The lexer.
   */
  constructor(lexer, factory) {
    /**
     * @type {Lexer}
     * @private
     */
    this.lexer_ = lexer;
    /**
     * @type {Token}
     * @private
     */

    this.token_;
    /**
     * @type {import("../geom/GeometryLayout.js").default}
     * @private
     */

    this.layout_ = GeometryLayout.XY;
    this.factory = factory;
  }
  /**
   * Fetch the next token form the lexer and replace the active token.
   * @private
   */


  consume_() {
    this.token_ = this.lexer_.nextToken();
  }
  /**
   * Tests if the given type matches the type of the current token.
   * @param {TokenType} type Token type.
   * @return {boolean} Whether the token matches the given type.
   */


  isTokenType(type) {
    const isMatch = this.token_.type == type;
    return isMatch;
  }
  /**
   * If the given type matches the current token, consume it.
   * @param {TokenType} type Token type.
   * @return {boolean} Whether the token matches the given type.
   */


  match(type) {
    const isMatch = this.isTokenType(type);
    if (isMatch) this.consume_();
    return isMatch;
  }
  /**
   * Try to parse the tokens provided by the lexer.
   * @return {import("../geom/Geometry.js").default} The geometry.
   */


  parse() {
    this.consume_();
    const geometry = this.parseGeometry_();
    return geometry;
  }
  /**
   * Try to parse the dimensional info.
   * @return {import("../geom/GeometryLayout.js").default} The layout.
   * @private
   */


  parseGeometryLayout_() {
    let layout = GeometryLayout.XY;
    const dimToken = this.token_;

    if (this.isTokenType(TokenType.TEXT)) {
      const dimInfo = dimToken.value;
      if (dimInfo === Z) layout = GeometryLayout.XYZ;else if (dimInfo === M) layout = GeometryLayout.XYM;else if (dimInfo === ZM) layout = GeometryLayout.XYZM;
      if (layout !== GeometryLayout.XY) this.consume_();
    }

    return layout;
  }
  /**
   * @return {!Array<import("../geom/Geometry.js").default>} A collection of geometries.
   * @private
   */


  parseGeometryCollectionText_() {
    if (this.match(TokenType.LEFT_PAREN)) {
      const geometries = [];

      do geometries.push(this.parseGeometry_()); while (this.match(TokenType.COMMA));

      if (this.match(TokenType.RIGHT_PAREN)) return geometries;
    } else if (this.isEmptyGeometry_()) {
      return [];
    }

    throw new Error(this.formatErrorMessage_());
  }
  /**
   * @return {Array<number>} All values in a point.
   * @private
   */


  parsePointText_() {
    if (this.match(TokenType.LEFT_PAREN)) {
      const coordinates = this.parsePoint_();
      if (this.match(TokenType.RIGHT_PAREN)) return coordinates;
    } else if (this.isEmptyGeometry_()) {
      return null;
    }

    throw new Error(this.formatErrorMessage_());
  }
  /**
   * @return {!Array<!Array<number>>} All points in a linestring.
   * @private
   */


  parseLineStringText_() {
    if (this.match(TokenType.LEFT_PAREN)) {
      const coordinates = this.parsePointList_();
      if (this.match(TokenType.RIGHT_PAREN)) return coordinates;
    } else if (this.isEmptyGeometry_()) {
      return [];
    }

    throw new Error(this.formatErrorMessage_());
  }
  /**
   * @return {!Array<!Array<!Array<number>>>} All points in a polygon.
   * @private
   */


  parsePolygonText_() {
    if (this.match(TokenType.LEFT_PAREN)) {
      const coordinates = this.parseLineStringTextList_();
      if (this.match(TokenType.RIGHT_PAREN)) return coordinates;
    } else if (this.isEmptyGeometry_()) {
      return [];
    }

    throw new Error(this.formatErrorMessage_());
  }
  /**
   * @return {!Array<!Array<number>>} All points in a multipoint.
   * @private
   */


  parseMultiPointText_() {
    if (this.match(TokenType.LEFT_PAREN)) {
      let coordinates;
      if (this.token_.type == TokenType.LEFT_PAREN) coordinates = this.parsePointTextList_();else coordinates = this.parsePointList_();
      if (this.match(TokenType.RIGHT_PAREN)) return coordinates;
    } else if (this.isEmptyGeometry_()) {
      return [];
    }

    throw new Error(this.formatErrorMessage_());
  }
  /**
   * @return {!Array<!Array<!Array<number>>>} All linestring points
   *                                          in a multilinestring.
   * @private
   */


  parseMultiLineStringText_() {
    if (this.match(TokenType.LEFT_PAREN)) {
      const coordinates = this.parseLineStringTextList_();
      if (this.match(TokenType.RIGHT_PAREN)) return coordinates;
    } else if (this.isEmptyGeometry_()) {
      return [];
    }

    throw new Error(this.formatErrorMessage_());
  }
  /**
   * @return {!Array<!Array<!Array<!Array<number>>>>} All polygon points in a multipolygon.
   * @private
   */


  parseMultiPolygonText_() {
    if (this.match(TokenType.LEFT_PAREN)) {
      const coordinates = this.parsePolygonTextList_();
      if (this.match(TokenType.RIGHT_PAREN)) return coordinates;
    } else if (this.isEmptyGeometry_()) {
      return [];
    }

    throw new Error(this.formatErrorMessage_());
  }
  /**
   * @return {!Array<number>} A point.
   * @private
   */


  parsePoint_() {
    const coordinates = [];
    const dimensions = this.layout_.length;

    for (let i = 0; i < dimensions; ++i) {
      const token = this.token_;
      if (this.match(TokenType.NUMBER)) coordinates.push(
      /** @type {number} */
      token.value);else break;
    }

    if (coordinates.length == dimensions) return coordinates;
    throw new Error(this.formatErrorMessage_());
  }
  /**
   * @return {!Array<!Array<number>>} An array of points.
   * @private
   */


  parsePointList_() {
    const coordinates = [this.parsePoint_()];

    while (this.match(TokenType.COMMA)) coordinates.push(this.parsePoint_());

    return coordinates;
  }
  /**
   * @return {!Array<!Array<number>>} An array of points.
   * @private
   */


  parsePointTextList_() {
    const coordinates = [this.parsePointText_()];

    while (this.match(TokenType.COMMA)) coordinates.push(this.parsePointText_());

    return coordinates;
  }
  /**
   * @return {!Array<!Array<!Array<number>>>} An array of points.
   * @private
   */


  parseLineStringTextList_() {
    const coordinates = [this.parseLineStringText_()];

    while (this.match(TokenType.COMMA)) coordinates.push(this.parseLineStringText_());

    return coordinates;
  }
  /**
   * @return {!Array<!Array<!Array<!Array<number>>>>} An array of points.
   * @private
   */


  parsePolygonTextList_() {
    const coordinates = [this.parsePolygonText_()];

    while (this.match(TokenType.COMMA)) coordinates.push(this.parsePolygonText_());

    return coordinates;
  }
  /**
   * @return {boolean} Whether the token implies an empty geometry.
   * @private
   */


  isEmptyGeometry_() {
    const isEmpty = this.isTokenType(TokenType.TEXT) && this.token_.value == EMPTY;
    if (isEmpty) this.consume_();
    return isEmpty;
  }
  /**
   * Create an error message for an unexpected token error.
   * @return {string} Error message.
   * @private
   */


  formatErrorMessage_() {
    return 'Unexpected `' + this.token_.value + '` at position ' + this.token_.position + ' in `' + this.lexer_.wkt + '`';
  }
  /**
   * @return {!import("../geom/Geometry.js").default} The geometry.
   * @private
   */


  parseGeometry_() {
    const factory = this.factory;

    const o2c = ordinates => new Coordinate(...ordinates);

    const ca2p = coordinates => {
      const rings = coordinates.map(a => factory.createLinearRing(a.map(o2c)));
      if (rings.length > 1) return factory.createPolygon(rings[0], rings.slice(1));else return factory.createPolygon(rings[0]);
    };

    const token = this.token_;

    if (this.match(TokenType.TEXT)) {
      const geomType = token.value;
      this.layout_ = this.parseGeometryLayout_();

      if (geomType == 'GEOMETRYCOLLECTION') {
        const geometries = this.parseGeometryCollectionText_();
        return factory.createGeometryCollection(geometries);
      } else {
        switch (geomType) {
          case 'POINT':
            {
              const ordinates = this.parsePointText_();
              if (!ordinates) return factory.createPoint();
              return factory.createPoint(new Coordinate(...ordinates));
            }

          case 'LINESTRING':
            {
              const coordinates = this.parseLineStringText_();
              const components = coordinates.map(o2c);
              return factory.createLineString(components);
            }

          case 'LINEARRING':
            {
              const coordinates = this.parseLineStringText_();
              const components = coordinates.map(o2c);
              return factory.createLinearRing(components);
            }

          case 'POLYGON':
            {
              const coordinates = this.parsePolygonText_();
              if (!coordinates || coordinates.length === 0) return factory.createPolygon();
              return ca2p(coordinates);
            }

          case 'MULTIPOINT':
            {
              const coordinates = this.parseMultiPointText_();
              if (!coordinates || coordinates.length === 0) return factory.createMultiPoint();
              const components = coordinates.map(o2c).map(c => factory.createPoint(c));
              return factory.createMultiPoint(components);
            }

          case 'MULTILINESTRING':
            {
              const coordinates = this.parseMultiLineStringText_();
              const components = coordinates.map(a => factory.createLineString(a.map(o2c)));
              return factory.createMultiLineString(components);
            }

          case 'MULTIPOLYGON':
            {
              const coordinates = this.parseMultiPolygonText_();
              if (!coordinates || coordinates.length === 0) return factory.createMultiPolygon();
              const polygons = coordinates.map(ca2p);
              return factory.createMultiPolygon(polygons);
            }

          default:
            {
              throw new Error('Invalid geometry type: ' + geomType);
            }
        }
      }
    }

    throw new Error(this.formatErrorMessage_());
  }

}
/**
 * @param {Point} geom Point geometry.
 * @return {string} Coordinates part of Point as WKT.
 */


function encodePointGeometry(geom) {
  if (geom.isEmpty()) return '';
  const c = geom.getCoordinate();
  const cs = [c.x, c.y];
  if (c.z !== undefined && !Number.isNaN(c.z)) cs.push(c.z);
  if (c.m !== undefined && !Number.isNaN(c.m)) cs.push(c.m);
  return cs.join(' ');
}
/**
 * @param {MultiPoint} geom MultiPoint geometry.
 * @return {string} Coordinates part of MultiPoint as WKT.
 */


function encodeMultiPointGeometry(geom) {
  const array = [];

  for (let i = 0, ii = geom.getNumGeometries(); i < ii; ++i) array.push('(' + encodePointGeometry(geom.getGeometryN(i)) + ')');

  return array.join(', ');
}
/**
 * @param {GeometryCollection} geom GeometryCollection geometry.
 * @return {string} Coordinates part of GeometryCollection as WKT.
 */


function encodeGeometryCollectionGeometry(geom) {
  const array = [];

  for (let i = 0, ii = geom.getNumGeometries(); i < ii; ++i) array.push(encode(geom.getGeometryN(i)));

  return array.join(', ');
}
/**
 * @param {LineString|import("../geom/LinearRing.js").default} geom LineString geometry.
 * @return {string} Coordinates part of LineString as WKT.
 */


function encodeLineStringGeometry(geom) {
  const coordinates = geom.getCoordinates().map(c => {
    const a = [c.x, c.y];
    if (c.z !== undefined && !Number.isNaN(c.z)) a.push(c.z);
    if (c.m !== undefined && !Number.isNaN(c.m)) a.push(c.m);
    return a;
  });
  const array = [];

  for (let i = 0, ii = coordinates.length; i < ii; ++i) array.push(coordinates[i].join(' '));

  return array.join(', ');
}
/**
 * @param {MultiLineString} geom MultiLineString geometry.
 * @return {string} Coordinates part of MultiLineString as WKT.
 */


function encodeMultiLineStringGeometry(geom) {
  const array = [];

  for (let i = 0, ii = geom.getNumGeometries(); i < ii; ++i) array.push('(' + encodeLineStringGeometry(geom.getGeometryN(i)) + ')');

  return array.join(', ');
}
/**
 * @param {Polygon} geom Polygon geometry.
 * @return {string} Coordinates part of Polygon as WKT.
 */


function encodePolygonGeometry(geom) {
  const array = [];
  array.push('(' + encodeLineStringGeometry(geom.getExteriorRing()) + ')');

  for (let i = 0, ii = geom.getNumInteriorRing(); i < ii; ++i) array.push('(' + encodeLineStringGeometry(geom.getInteriorRingN(i)) + ')');

  return array.join(', ');
}
/**
 * @param {MultiPolygon} geom MultiPolygon geometry.
 * @return {string} Coordinates part of MultiPolygon as WKT.
 */


function encodeMultiPolygonGeometry(geom) {
  const array = [];

  for (let i = 0, ii = geom.getNumGeometries(); i < ii; ++i) array.push('(' + encodePolygonGeometry(geom.getGeometryN(i)) + ')');

  return array.join(', ');
}
/**
 * @param {Geometry} geom Geometry geometry.
 * @return {string} Potential dimensional information for WKT type.
 */


function encodeGeometryLayout(geom) {
  let dimInfo = '';
  if (geom.isEmpty()) return dimInfo;
  const c = geom.getCoordinate();
  if (c.z !== undefined && !Number.isNaN(c.z)) dimInfo += Z;
  if (c.m !== undefined && !Number.isNaN(c.m)) dimInfo += M;
  return dimInfo;
}
/**
 * @const
 * @type {Object<string, function(import("../geom/Geometry.js").default): string>}
 */


const GeometryEncoder = {
  'Point': encodePointGeometry,
  'LineString': encodeLineStringGeometry,
  'LinearRing': encodeLineStringGeometry,
  'Polygon': encodePolygonGeometry,
  'MultiPoint': encodeMultiPointGeometry,
  'MultiLineString': encodeMultiLineStringGeometry,
  'MultiPolygon': encodeMultiPolygonGeometry,
  'GeometryCollection': encodeGeometryCollectionGeometry
};
/**
 * Encode a geometry as WKT.
 * @param {!import("../geom/Geometry.js").default} geom The geometry to encode.
 * @return {string} WKT string for the geometry.
 */

function encode(geom) {
  let type = geom.getGeometryType();
  const geometryEncoder = GeometryEncoder[type];
  type = type.toUpperCase();
  const dimInfo = encodeGeometryLayout(geom);
  if (dimInfo.length > 0) type += ' ' + dimInfo;
  if (geom.isEmpty()) return type + ' ' + EMPTY;
  const enc = geometryEncoder(geom);
  return type + ' (' + enc + ')';
}
/**
 * Class for reading and writing Well-Known Text.
 *
 * NOTE: Adapted from OpenLayers.
 */


class WKTParser {
  /** Create a new parser for WKT
   *
   * @param {GeometryFactory} geometryFactory
   * @return An instance of WKTParser.
   * @private
   */
  constructor(geometryFactory) {
    this.geometryFactory = geometryFactory || new GeometryFactory();
    this.precisionModel = this.geometryFactory.getPrecisionModel();
  }
  /**
   * Deserialize a WKT string and return a geometry. Supports WKT for POINT,
   * MULTIPOINT, LINESTRING, LINEARRING, MULTILINESTRING, POLYGON, MULTIPOLYGON,
   * and GEOMETRYCOLLECTION.
   *
   * @param {String} wkt A WKT string.
   * @return {Geometry} A geometry instance.
   * @private
   */


  read(wkt) {
    const lexer = new Lexer(wkt);
    const parser = new Parser(lexer, this.geometryFactory);
    const geometry = parser.parse();
    return geometry;
  }
  /**
   * Serialize a geometry into a WKT string.
   *
   * @param {Geometry} geometry A feature or array of features.
   * @return {String} The WKT string representation of the input geometries.
   * @private
   */


  write(geometry) {
    return encode(geometry);
  }

}

/**
 * @module org/locationtech/jts/io/WKTWriter
 */
/**
 * Writes the Well-Known Text representation of a {@link Geometry}. The
 * Well-Known Text format is defined in the <A
 * HREF="http://www.opengis.org/techno/specs.htm"> OGC Simple Features
 * Specification for SQL</A>.
 * <p>
 * The <code>WKTWriter</code> outputs coordinates rounded to the precision
 * model. Only the maximum number of decimal places necessary to represent the
 * ordinates to the required precision will be output.
 * <p>
 * The SFS WKT spec does not define a special tag for {@link LinearRing}s.
 * Under the spec, rings are output as <code>LINESTRING</code>s.
 */

class WKTWriter {
  /**
   * @param {GeometryFactory} geometryFactory
   */
  constructor(geometryFactory) {
    this.parser = new WKTParser(geometryFactory);
  }
  /**
   * Converts a <code>Geometry</code> to its Well-known Text representation.
   *
   * @param {Geometry} geometry a <code>Geometry</code> to process.
   * @return {string} a <Geometry Tagged Text> string (see the OpenGIS Simple
   *         Features Specification).
   * @memberof module:org/locationtech/jts/io/WKTWriter#
   */


  write(geometry) {
    return this.parser.write(geometry);
  }
  /**
   * Generates the WKT for a <tt>LINESTRING</tt> specified by two
   * {@link Coordinate}s.
   *
   * @param p0 the first coordinate.
   * @param p1 the second coordinate.
   *
   * @return the WKT.
   * @private
   */


  static toLineString(p0, p1) {
    if (arguments.length !== 2) throw new Error('Not implemented');
    return 'LINESTRING ( ' + p0.x + ' ' + p0.y + ', ' + p1.x + ' ' + p1.y + ' )';
  }

}

class LineIntersector {
  constructor() {
    LineIntersector.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._result = null;
    this._inputLines = Array(2).fill().map(() => Array(2));
    this._intPt = new Array(2).fill(null);
    this._intLineIndex = null;
    this._isProper = null;
    this._pa = null;
    this._pb = null;
    this._precisionModel = null;
    this._intPt[0] = new Coordinate();
    this._intPt[1] = new Coordinate();
    this._pa = this._intPt[0];
    this._pb = this._intPt[1];
    this._result = 0;
  }

  static computeEdgeDistance(p, p0, p1) {
    const dx = Math.abs(p1.x - p0.x);
    const dy = Math.abs(p1.y - p0.y);
    let dist = -1.0;

    if (p.equals(p0)) {
      dist = 0.0;
    } else if (p.equals(p1)) {
      if (dx > dy) dist = dx;else dist = dy;
    } else {
      const pdx = Math.abs(p.x - p0.x);
      const pdy = Math.abs(p.y - p0.y);
      if (dx > dy) dist = pdx;else dist = pdy;
      if (dist === 0.0 && !p.equals(p0)) dist = Math.max(pdx, pdy);
    }

    Assert.isTrue(!(dist === 0.0 && !p.equals(p0)), 'Bad distance calculation');
    return dist;
  }

  static nonRobustComputeEdgeDistance(p, p1, p2) {
    const dx = p.x - p1.x;
    const dy = p.y - p1.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    Assert.isTrue(!(dist === 0.0 && !p.equals(p1)), 'Invalid distance calculation');
    return dist;
  }

  getIndexAlongSegment(segmentIndex, intIndex) {
    this.computeIntLineIndex();
    return this._intLineIndex[segmentIndex][intIndex];
  }

  getTopologySummary() {
    const catBuilder = new StringBuilder();
    if (this.isEndPoint()) catBuilder.append(' endpoint');
    if (this._isProper) catBuilder.append(' proper');
    if (this.isCollinear()) catBuilder.append(' collinear');
    return catBuilder.toString();
  }

  computeIntersection(p1, p2, p3, p4) {
    this._inputLines[0][0] = p1;
    this._inputLines[0][1] = p2;
    this._inputLines[1][0] = p3;
    this._inputLines[1][1] = p4;
    this._result = this.computeIntersect(p1, p2, p3, p4);
  }

  getIntersectionNum() {
    return this._result;
  }

  computeIntLineIndex() {
    if (arguments.length === 0) {
      if (this._intLineIndex === null) {
        this._intLineIndex = Array(2).fill().map(() => Array(2));
        this.computeIntLineIndex(0);
        this.computeIntLineIndex(1);
      }
    } else if (arguments.length === 1) {
      const segmentIndex = arguments[0];
      const dist0 = this.getEdgeDistance(segmentIndex, 0);
      const dist1 = this.getEdgeDistance(segmentIndex, 1);

      if (dist0 > dist1) {
        this._intLineIndex[segmentIndex][0] = 0;
        this._intLineIndex[segmentIndex][1] = 1;
      } else {
        this._intLineIndex[segmentIndex][0] = 1;
        this._intLineIndex[segmentIndex][1] = 0;
      }
    }
  }

  isProper() {
    return this.hasIntersection() && this._isProper;
  }

  setPrecisionModel(precisionModel) {
    this._precisionModel = precisionModel;
  }

  isInteriorIntersection() {
    if (arguments.length === 0) {
      if (this.isInteriorIntersection(0)) return true;
      if (this.isInteriorIntersection(1)) return true;
      return false;
    } else if (arguments.length === 1) {
      const inputLineIndex = arguments[0];

      for (let i = 0; i < this._result; i++) if (!(this._intPt[i].equals2D(this._inputLines[inputLineIndex][0]) || this._intPt[i].equals2D(this._inputLines[inputLineIndex][1]))) return true;

      return false;
    }
  }

  getIntersection(intIndex) {
    return this._intPt[intIndex];
  }

  isEndPoint() {
    return this.hasIntersection() && !this._isProper;
  }

  hasIntersection() {
    return this._result !== LineIntersector.NO_INTERSECTION;
  }

  getEdgeDistance(segmentIndex, intIndex) {
    const dist = LineIntersector.computeEdgeDistance(this._intPt[intIndex], this._inputLines[segmentIndex][0], this._inputLines[segmentIndex][1]);
    return dist;
  }

  isCollinear() {
    return this._result === LineIntersector.COLLINEAR_INTERSECTION;
  }

  toString() {
    return WKTWriter.toLineString(this._inputLines[0][0], this._inputLines[0][1]) + ' - ' + WKTWriter.toLineString(this._inputLines[1][0], this._inputLines[1][1]) + this.getTopologySummary();
  }

  getEndpoint(segmentIndex, ptIndex) {
    return this._inputLines[segmentIndex][ptIndex];
  }

  isIntersection(pt) {
    for (let i = 0; i < this._result; i++) if (this._intPt[i].equals2D(pt)) return true;

    return false;
  }

  getIntersectionAlongSegment(segmentIndex, intIndex) {
    this.computeIntLineIndex();
    return this._intPt[this._intLineIndex[segmentIndex][intIndex]];
  }

}
LineIntersector.DONT_INTERSECT = 0;
LineIntersector.DO_INTERSECT = 1;
LineIntersector.COLLINEAR = 2;
LineIntersector.NO_INTERSECTION = 0;
LineIntersector.POINT_INTERSECTION = 1;
LineIntersector.COLLINEAR_INTERSECTION = 2;

class RobustLineIntersector extends LineIntersector {
  constructor() {
    super();
  }

  static nearestEndpoint(p1, p2, q1, q2) {
    let nearestPt = p1;
    let minDist = Distance.pointToSegment(p1, q1, q2);
    let dist = Distance.pointToSegment(p2, q1, q2);

    if (dist < minDist) {
      minDist = dist;
      nearestPt = p2;
    }

    dist = Distance.pointToSegment(q1, p1, p2);

    if (dist < minDist) {
      minDist = dist;
      nearestPt = q1;
    }

    dist = Distance.pointToSegment(q2, p1, p2);

    if (dist < minDist) {
      minDist = dist;
      nearestPt = q2;
    }

    return nearestPt;
  }

  isInSegmentEnvelopes(intPt) {
    const env0 = new Envelope(this._inputLines[0][0], this._inputLines[0][1]);
    const env1 = new Envelope(this._inputLines[1][0], this._inputLines[1][1]);
    return env0.contains(intPt) && env1.contains(intPt);
  }

  computeIntersection() {
    if (arguments.length === 3) {
      const p = arguments[0],
            p1 = arguments[1],
            p2 = arguments[2];
      this._isProper = false;
      if (Envelope.intersects(p1, p2, p)) if (Orientation.index(p1, p2, p) === 0 && Orientation.index(p2, p1, p) === 0) {
        this._isProper = true;
        if (p.equals(p1) || p.equals(p2)) this._isProper = false;
        this._result = LineIntersector.POINT_INTERSECTION;
        return null;
      }
      this._result = LineIntersector.NO_INTERSECTION;
    } else {
      return super.computeIntersection.apply(this, arguments);
    }
  }

  intersection(p1, p2, q1, q2) {
    let intPt = this.intersectionSafe(p1, p2, q1, q2);
    if (!this.isInSegmentEnvelopes(intPt)) intPt = new Coordinate(RobustLineIntersector.nearestEndpoint(p1, p2, q1, q2));
    if (this._precisionModel !== null) this._precisionModel.makePrecise(intPt);
    return intPt;
  }

  checkDD(p1, p2, q1, q2, intPt) {
    const intPtDD = CGAlgorithmsDD.intersection(p1, p2, q1, q2);
    const isIn = this.isInSegmentEnvelopes(intPtDD);
    System.out.println('DD in env = ' + isIn + '  --------------------- ' + intPtDD);
    if (intPt.distance(intPtDD) > 0.0001) System.out.println('Distance = ' + intPt.distance(intPtDD));
  }

  intersectionSafe(p1, p2, q1, q2) {
    let intPt = Intersection.intersection(p1, p2, q1, q2);
    if (intPt === null) intPt = RobustLineIntersector.nearestEndpoint(p1, p2, q1, q2);
    return intPt;
  }

  computeCollinearIntersection(p1, p2, q1, q2) {
    const p1q1p2 = Envelope.intersects(p1, p2, q1);
    const p1q2p2 = Envelope.intersects(p1, p2, q2);
    const q1p1q2 = Envelope.intersects(q1, q2, p1);
    const q1p2q2 = Envelope.intersects(q1, q2, p2);

    if (p1q1p2 && p1q2p2) {
      this._intPt[0] = q1;
      this._intPt[1] = q2;
      return LineIntersector.COLLINEAR_INTERSECTION;
    }

    if (q1p1q2 && q1p2q2) {
      this._intPt[0] = p1;
      this._intPt[1] = p2;
      return LineIntersector.COLLINEAR_INTERSECTION;
    }

    if (p1q1p2 && q1p1q2) {
      this._intPt[0] = q1;
      this._intPt[1] = p1;
      return q1.equals(p1) && !p1q2p2 && !q1p2q2 ? LineIntersector.POINT_INTERSECTION : LineIntersector.COLLINEAR_INTERSECTION;
    }

    if (p1q1p2 && q1p2q2) {
      this._intPt[0] = q1;
      this._intPt[1] = p2;
      return q1.equals(p2) && !p1q2p2 && !q1p1q2 ? LineIntersector.POINT_INTERSECTION : LineIntersector.COLLINEAR_INTERSECTION;
    }

    if (p1q2p2 && q1p1q2) {
      this._intPt[0] = q2;
      this._intPt[1] = p1;
      return q2.equals(p1) && !p1q1p2 && !q1p2q2 ? LineIntersector.POINT_INTERSECTION : LineIntersector.COLLINEAR_INTERSECTION;
    }

    if (p1q2p2 && q1p2q2) {
      this._intPt[0] = q2;
      this._intPt[1] = p2;
      return q2.equals(p2) && !p1q1p2 && !q1p1q2 ? LineIntersector.POINT_INTERSECTION : LineIntersector.COLLINEAR_INTERSECTION;
    }

    return LineIntersector.NO_INTERSECTION;
  }

  computeIntersect(p1, p2, q1, q2) {
    this._isProper = false;
    if (!Envelope.intersects(p1, p2, q1, q2)) return LineIntersector.NO_INTERSECTION;
    const Pq1 = Orientation.index(p1, p2, q1);
    const Pq2 = Orientation.index(p1, p2, q2);
    if (Pq1 > 0 && Pq2 > 0 || Pq1 < 0 && Pq2 < 0) return LineIntersector.NO_INTERSECTION;
    const Qp1 = Orientation.index(q1, q2, p1);
    const Qp2 = Orientation.index(q1, q2, p2);
    if (Qp1 > 0 && Qp2 > 0 || Qp1 < 0 && Qp2 < 0) return LineIntersector.NO_INTERSECTION;
    const collinear = Pq1 === 0 && Pq2 === 0 && Qp1 === 0 && Qp2 === 0;
    if (collinear) return this.computeCollinearIntersection(p1, p2, q1, q2);

    if (Pq1 === 0 || Pq2 === 0 || Qp1 === 0 || Qp2 === 0) {
      this._isProper = false;
      if (p1.equals2D(q1) || p1.equals2D(q2)) this._intPt[0] = p1;else if (p2.equals2D(q1) || p2.equals2D(q2)) this._intPt[0] = p2;else if (Pq1 === 0) this._intPt[0] = new Coordinate(q1);else if (Pq2 === 0) this._intPt[0] = new Coordinate(q2);else if (Qp1 === 0) this._intPt[0] = new Coordinate(p1);else if (Qp2 === 0) this._intPt[0] = new Coordinate(p2);
    } else {
      this._isProper = true;
      this._intPt[0] = this.intersection(p1, p2, q1, q2);
    }

    return LineIntersector.POINT_INTERSECTION;
  }

}

class LineSegment {
  constructor() {
    LineSegment.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.p0 = null;
    this.p1 = null;

    if (arguments.length === 0) {
      LineSegment.constructor_.call(this, new Coordinate(), new Coordinate());
    } else if (arguments.length === 1) {
      const ls = arguments[0];
      LineSegment.constructor_.call(this, ls.p0, ls.p1);
    } else if (arguments.length === 2) {
      const p0 = arguments[0],
            p1 = arguments[1];
      this.p0 = p0;
      this.p1 = p1;
    } else if (arguments.length === 4) {
      const x0 = arguments[0],
            y0 = arguments[1],
            x1 = arguments[2],
            y1 = arguments[3];
      LineSegment.constructor_.call(this, new Coordinate(x0, y0), new Coordinate(x1, y1));
    }
  }

  static midPoint(p0, p1) {
    return new Coordinate((p0.x + p1.x) / 2, (p0.y + p1.y) / 2);
  }

  minX() {
    return Math.min(this.p0.x, this.p1.x);
  }

  orientationIndex() {
    if (arguments[0] instanceof LineSegment) {
      const seg = arguments[0];
      const orient0 = Orientation.index(this.p0, this.p1, seg.p0);
      const orient1 = Orientation.index(this.p0, this.p1, seg.p1);
      if (orient0 >= 0 && orient1 >= 0) return Math.max(orient0, orient1);
      if (orient0 <= 0 && orient1 <= 0) return Math.max(orient0, orient1);
      return 0;
    } else if (arguments[0] instanceof Coordinate) {
      const p = arguments[0];
      return Orientation.index(this.p0, this.p1, p);
    }
  }

  toGeometry(geomFactory) {
    return geomFactory.createLineString([this.p0, this.p1]);
  }

  isVertical() {
    return this.p0.x === this.p1.x;
  }

  equals(o) {
    if (!(o instanceof LineSegment)) return false;
    const other = o;
    return this.p0.equals(other.p0) && this.p1.equals(other.p1);
  }

  intersection(line) {
    const li = new RobustLineIntersector();
    li.computeIntersection(this.p0, this.p1, line.p0, line.p1);
    if (li.hasIntersection()) return li.getIntersection(0);
    return null;
  }

  project() {
    if (arguments[0] instanceof Coordinate) {
      const p = arguments[0];
      if (p.equals(this.p0) || p.equals(this.p1)) return new Coordinate(p);
      const r = this.projectionFactor(p);
      const coord = new Coordinate();
      coord.x = this.p0.x + r * (this.p1.x - this.p0.x);
      coord.y = this.p0.y + r * (this.p1.y - this.p0.y);
      return coord;
    } else if (arguments[0] instanceof LineSegment) {
      const seg = arguments[0];
      const pf0 = this.projectionFactor(seg.p0);
      const pf1 = this.projectionFactor(seg.p1);
      if (pf0 >= 1.0 && pf1 >= 1.0) return null;
      if (pf0 <= 0.0 && pf1 <= 0.0) return null;
      let newp0 = this.project(seg.p0);
      if (pf0 < 0.0) newp0 = this.p0;
      if (pf0 > 1.0) newp0 = this.p1;
      let newp1 = this.project(seg.p1);
      if (pf1 < 0.0) newp1 = this.p0;
      if (pf1 > 1.0) newp1 = this.p1;
      return new LineSegment(newp0, newp1);
    }
  }

  normalize() {
    if (this.p1.compareTo(this.p0) < 0) this.reverse();
  }

  angle() {
    return Math.atan2(this.p1.y - this.p0.y, this.p1.x - this.p0.x);
  }

  getCoordinate(i) {
    if (i === 0) return this.p0;
    return this.p1;
  }

  distancePerpendicular(p) {
    return Distance.pointToLinePerpendicular(p, this.p0, this.p1);
  }

  minY() {
    return Math.min(this.p0.y, this.p1.y);
  }

  midPoint() {
    return LineSegment.midPoint(this.p0, this.p1);
  }

  projectionFactor(p) {
    if (p.equals(this.p0)) return 0.0;
    if (p.equals(this.p1)) return 1.0;
    const dx = this.p1.x - this.p0.x;
    const dy = this.p1.y - this.p0.y;
    const len = dx * dx + dy * dy;
    if (len <= 0.0) return Double.NaN;
    const r = ((p.x - this.p0.x) * dx + (p.y - this.p0.y) * dy) / len;
    return r;
  }

  closestPoints(line) {
    const intPt = this.intersection(line);
    if (intPt !== null) return [intPt, intPt];
    const closestPt = new Array(2).fill(null);
    let minDistance = Double.MAX_VALUE;
    let dist = null;
    const close00 = this.closestPoint(line.p0);
    minDistance = close00.distance(line.p0);
    closestPt[0] = close00;
    closestPt[1] = line.p0;
    const close01 = this.closestPoint(line.p1);
    dist = close01.distance(line.p1);

    if (dist < minDistance) {
      minDistance = dist;
      closestPt[0] = close01;
      closestPt[1] = line.p1;
    }

    const close10 = line.closestPoint(this.p0);
    dist = close10.distance(this.p0);

    if (dist < minDistance) {
      minDistance = dist;
      closestPt[0] = this.p0;
      closestPt[1] = close10;
    }

    const close11 = line.closestPoint(this.p1);
    dist = close11.distance(this.p1);

    if (dist < minDistance) {
      minDistance = dist;
      closestPt[0] = this.p1;
      closestPt[1] = close11;
    }

    return closestPt;
  }

  closestPoint(p) {
    const factor = this.projectionFactor(p);
    if (factor > 0 && factor < 1) return this.project(p);
    const dist0 = this.p0.distance(p);
    const dist1 = this.p1.distance(p);
    if (dist0 < dist1) return this.p0;
    return this.p1;
  }

  maxX() {
    return Math.max(this.p0.x, this.p1.x);
  }

  getLength() {
    return this.p0.distance(this.p1);
  }

  compareTo(o) {
    const other = o;
    const comp0 = this.p0.compareTo(other.p0);
    if (comp0 !== 0) return comp0;
    return this.p1.compareTo(other.p1);
  }

  reverse() {
    const temp = this.p0;
    this.p0 = this.p1;
    this.p1 = temp;
  }

  equalsTopo(other) {
    return this.p0.equals(other.p0) && this.p1.equals(other.p1) || this.p0.equals(other.p1) && this.p1.equals(other.p0);
  }

  lineIntersection(line) {
    const intPt = Intersection.intersection(this.p0, this.p1, line.p0, line.p1);
    return intPt;
  }

  maxY() {
    return Math.max(this.p0.y, this.p1.y);
  }

  pointAlongOffset(segmentLengthFraction, offsetDistance) {
    const segx = this.p0.x + segmentLengthFraction * (this.p1.x - this.p0.x);
    const segy = this.p0.y + segmentLengthFraction * (this.p1.y - this.p0.y);
    const dx = this.p1.x - this.p0.x;
    const dy = this.p1.y - this.p0.y;
    const len = Math.sqrt(dx * dx + dy * dy);
    let ux = 0.0;
    let uy = 0.0;

    if (offsetDistance !== 0.0) {
      if (len <= 0.0) throw new IllegalStateException('Cannot compute offset from zero-length line segment');
      ux = offsetDistance * dx / len;
      uy = offsetDistance * dy / len;
    }

    const offsetx = segx - uy;
    const offsety = segy + ux;
    const coord = new Coordinate(offsetx, offsety);
    return coord;
  }

  setCoordinates() {
    if (arguments.length === 1) {
      const ls = arguments[0];
      this.setCoordinates(ls.p0, ls.p1);
    } else if (arguments.length === 2) {
      const p0 = arguments[0],
            p1 = arguments[1];
      this.p0.x = p0.x;
      this.p0.y = p0.y;
      this.p1.x = p1.x;
      this.p1.y = p1.y;
    }
  }

  segmentFraction(inputPt) {
    let segFrac = this.projectionFactor(inputPt);
    if (segFrac < 0.0) segFrac = 0.0;else if (segFrac > 1.0 || Double.isNaN(segFrac)) segFrac = 1.0;
    return segFrac;
  }

  toString() {
    return 'LINESTRING( ' + this.p0.x + ' ' + this.p0.y + ', ' + this.p1.x + ' ' + this.p1.y + ')';
  }

  isHorizontal() {
    return this.p0.y === this.p1.y;
  }

  reflect(p) {
    const A = this.p1.getY() - this.p0.getY();
    const B = this.p0.getX() - this.p1.getX();
    const C = this.p0.getY() * (this.p1.getX() - this.p0.getX()) - this.p0.getX() * (this.p1.getY() - this.p0.getY());
    const A2plusB2 = A * A + B * B;
    const A2subB2 = A * A - B * B;
    const x = p.getX();
    const y = p.getY();
    const rx = (-A2subB2 * x - 2 * A * B * y - 2 * A * C) / A2plusB2;
    const ry = (A2subB2 * y - 2 * A * B * x - 2 * B * C) / A2plusB2;
    return new Coordinate(rx, ry);
  }

  distance() {
    if (arguments[0] instanceof LineSegment) {
      const ls = arguments[0];
      return Distance.segmentToSegment(this.p0, this.p1, ls.p0, ls.p1);
    } else if (arguments[0] instanceof Coordinate) {
      const p = arguments[0];
      return Distance.pointToSegment(p, this.p0, this.p1);
    }
  }

  pointAlong(segmentLengthFraction) {
    const coord = new Coordinate();
    coord.x = this.p0.x + segmentLengthFraction * (this.p1.x - this.p0.x);
    coord.y = this.p0.y + segmentLengthFraction * (this.p1.y - this.p0.y);
    return coord;
  }

  hashCode() {
    let bits0 = Double.doubleToLongBits(this.p0.x);
    bits0 ^= Double.doubleToLongBits(this.p0.y) * 31;
    const hash0 = Math.trunc(bits0) ^ Math.trunc(bits0 >> 32);
    let bits1 = Double.doubleToLongBits(this.p1.x);
    bits1 ^= Double.doubleToLongBits(this.p1.y) * 31;
    const hash1 = Math.trunc(bits1) ^ Math.trunc(bits1 >> 32);
    return hash0 ^ hash1;
  }

  get interfaces_() {
    return [Comparable, Serializable];
  }

}

class Location {
  static toLocationSymbol(locationValue) {
    switch (locationValue) {
      case Location.EXTERIOR:
        return 'e';

      case Location.BOUNDARY:
        return 'b';

      case Location.INTERIOR:
        return 'i';

      case Location.NONE:
        return '-';
    }

    throw new IllegalArgumentException('Unknown location value: ' + locationValue);
  }

}
Location.INTERIOR = 0;
Location.BOUNDARY = 1;
Location.EXTERIOR = 2;
Location.NONE = -1;

class IntersectionMatrix {
  constructor() {
    IntersectionMatrix.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._matrix = null;

    if (arguments.length === 0) {
      this._matrix = Array(3).fill().map(() => Array(3));
      this.setAll(Dimension.FALSE);
    } else if (arguments.length === 1) {
      if (typeof arguments[0] === 'string') {
        const elements = arguments[0];
        IntersectionMatrix.constructor_.call(this);
        this.set(elements);
      } else if (arguments[0] instanceof IntersectionMatrix) {
        const other = arguments[0];
        IntersectionMatrix.constructor_.call(this);
        this._matrix[Location.INTERIOR][Location.INTERIOR] = other._matrix[Location.INTERIOR][Location.INTERIOR];
        this._matrix[Location.INTERIOR][Location.BOUNDARY] = other._matrix[Location.INTERIOR][Location.BOUNDARY];
        this._matrix[Location.INTERIOR][Location.EXTERIOR] = other._matrix[Location.INTERIOR][Location.EXTERIOR];
        this._matrix[Location.BOUNDARY][Location.INTERIOR] = other._matrix[Location.BOUNDARY][Location.INTERIOR];
        this._matrix[Location.BOUNDARY][Location.BOUNDARY] = other._matrix[Location.BOUNDARY][Location.BOUNDARY];
        this._matrix[Location.BOUNDARY][Location.EXTERIOR] = other._matrix[Location.BOUNDARY][Location.EXTERIOR];
        this._matrix[Location.EXTERIOR][Location.INTERIOR] = other._matrix[Location.EXTERIOR][Location.INTERIOR];
        this._matrix[Location.EXTERIOR][Location.BOUNDARY] = other._matrix[Location.EXTERIOR][Location.BOUNDARY];
        this._matrix[Location.EXTERIOR][Location.EXTERIOR] = other._matrix[Location.EXTERIOR][Location.EXTERIOR];
      }
    }
  }

  static matches() {
    if (Number.isInteger(arguments[0]) && typeof arguments[1] === 'string') {
      const actualDimensionValue = arguments[0],
            requiredDimensionSymbol = arguments[1];
      if (requiredDimensionSymbol === Dimension.SYM_DONTCARE) return true;
      if (requiredDimensionSymbol === Dimension.SYM_TRUE && (actualDimensionValue >= 0 || actualDimensionValue === Dimension.TRUE)) return true;
      if (requiredDimensionSymbol === Dimension.SYM_FALSE && actualDimensionValue === Dimension.FALSE) return true;
      if (requiredDimensionSymbol === Dimension.SYM_P && actualDimensionValue === Dimension.P) return true;
      if (requiredDimensionSymbol === Dimension.SYM_L && actualDimensionValue === Dimension.L) return true;
      if (requiredDimensionSymbol === Dimension.SYM_A && actualDimensionValue === Dimension.A) return true;
      return false;
    } else if (typeof arguments[0] === 'string' && typeof arguments[1] === 'string') {
      const actualDimensionSymbols = arguments[0],
            requiredDimensionSymbols = arguments[1];
      const m = new IntersectionMatrix(actualDimensionSymbols);
      return m.matches(requiredDimensionSymbols);
    }
  }

  static isTrue(actualDimensionValue) {
    if (actualDimensionValue >= 0 || actualDimensionValue === Dimension.TRUE) return true;
    return false;
  }

  isIntersects() {
    return !this.isDisjoint();
  }

  isCovers() {
    const hasPointInCommon = IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.INTERIOR]) || IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.BOUNDARY]) || IntersectionMatrix.isTrue(this._matrix[Location.BOUNDARY][Location.INTERIOR]) || IntersectionMatrix.isTrue(this._matrix[Location.BOUNDARY][Location.BOUNDARY]);
    return hasPointInCommon && this._matrix[Location.EXTERIOR][Location.INTERIOR] === Dimension.FALSE && this._matrix[Location.EXTERIOR][Location.BOUNDARY] === Dimension.FALSE;
  }

  isCoveredBy() {
    const hasPointInCommon = IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.INTERIOR]) || IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.BOUNDARY]) || IntersectionMatrix.isTrue(this._matrix[Location.BOUNDARY][Location.INTERIOR]) || IntersectionMatrix.isTrue(this._matrix[Location.BOUNDARY][Location.BOUNDARY]);
    return hasPointInCommon && this._matrix[Location.INTERIOR][Location.EXTERIOR] === Dimension.FALSE && this._matrix[Location.BOUNDARY][Location.EXTERIOR] === Dimension.FALSE;
  }

  set() {
    if (arguments.length === 1) {
      const dimensionSymbols = arguments[0];

      for (let i = 0; i < dimensionSymbols.length; i++) {
        const row = Math.trunc(i / 3);
        const col = i % 3;
        this._matrix[row][col] = Dimension.toDimensionValue(dimensionSymbols.charAt(i));
      }
    } else if (arguments.length === 3) {
      const row = arguments[0],
            column = arguments[1],
            dimensionValue = arguments[2];
      this._matrix[row][column] = dimensionValue;
    }
  }

  isContains() {
    return IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.INTERIOR]) && this._matrix[Location.EXTERIOR][Location.INTERIOR] === Dimension.FALSE && this._matrix[Location.EXTERIOR][Location.BOUNDARY] === Dimension.FALSE;
  }

  setAtLeast() {
    if (arguments.length === 1) {
      const minimumDimensionSymbols = arguments[0];

      for (let i = 0; i < minimumDimensionSymbols.length; i++) {
        const row = Math.trunc(i / 3);
        const col = i % 3;
        this.setAtLeast(row, col, Dimension.toDimensionValue(minimumDimensionSymbols.charAt(i)));
      }
    } else if (arguments.length === 3) {
      const row = arguments[0],
            column = arguments[1],
            minimumDimensionValue = arguments[2];
      if (this._matrix[row][column] < minimumDimensionValue) this._matrix[row][column] = minimumDimensionValue;
    }
  }

  setAtLeastIfValid(row, column, minimumDimensionValue) {
    if (row >= 0 && column >= 0) this.setAtLeast(row, column, minimumDimensionValue);
  }

  isWithin() {
    return IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.INTERIOR]) && this._matrix[Location.INTERIOR][Location.EXTERIOR] === Dimension.FALSE && this._matrix[Location.BOUNDARY][Location.EXTERIOR] === Dimension.FALSE;
  }

  isTouches(dimensionOfGeometryA, dimensionOfGeometryB) {
    if (dimensionOfGeometryA > dimensionOfGeometryB) return this.isTouches(dimensionOfGeometryB, dimensionOfGeometryA);
    if (dimensionOfGeometryA === Dimension.A && dimensionOfGeometryB === Dimension.A || dimensionOfGeometryA === Dimension.L && dimensionOfGeometryB === Dimension.L || dimensionOfGeometryA === Dimension.L && dimensionOfGeometryB === Dimension.A || dimensionOfGeometryA === Dimension.P && dimensionOfGeometryB === Dimension.A || dimensionOfGeometryA === Dimension.P && dimensionOfGeometryB === Dimension.L) return this._matrix[Location.INTERIOR][Location.INTERIOR] === Dimension.FALSE && (IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.BOUNDARY]) || IntersectionMatrix.isTrue(this._matrix[Location.BOUNDARY][Location.INTERIOR]) || IntersectionMatrix.isTrue(this._matrix[Location.BOUNDARY][Location.BOUNDARY]));
    return false;
  }

  isOverlaps(dimensionOfGeometryA, dimensionOfGeometryB) {
    if (dimensionOfGeometryA === Dimension.P && dimensionOfGeometryB === Dimension.P || dimensionOfGeometryA === Dimension.A && dimensionOfGeometryB === Dimension.A) return IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.INTERIOR]) && IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.EXTERIOR]) && IntersectionMatrix.isTrue(this._matrix[Location.EXTERIOR][Location.INTERIOR]);
    if (dimensionOfGeometryA === Dimension.L && dimensionOfGeometryB === Dimension.L) return this._matrix[Location.INTERIOR][Location.INTERIOR] === 1 && IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.EXTERIOR]) && IntersectionMatrix.isTrue(this._matrix[Location.EXTERIOR][Location.INTERIOR]);
    return false;
  }

  isEquals(dimensionOfGeometryA, dimensionOfGeometryB) {
    if (dimensionOfGeometryA !== dimensionOfGeometryB) return false;
    return IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.INTERIOR]) && this._matrix[Location.INTERIOR][Location.EXTERIOR] === Dimension.FALSE && this._matrix[Location.BOUNDARY][Location.EXTERIOR] === Dimension.FALSE && this._matrix[Location.EXTERIOR][Location.INTERIOR] === Dimension.FALSE && this._matrix[Location.EXTERIOR][Location.BOUNDARY] === Dimension.FALSE;
  }

  toString() {
    const builder = new StringBuilder('123456789');

    for (let ai = 0; ai < 3; ai++) for (let bi = 0; bi < 3; bi++) builder.setCharAt(3 * ai + bi, Dimension.toDimensionSymbol(this._matrix[ai][bi]));

    return builder.toString();
  }

  setAll(dimensionValue) {
    for (let ai = 0; ai < 3; ai++) for (let bi = 0; bi < 3; bi++) this._matrix[ai][bi] = dimensionValue;
  }

  get(row, column) {
    return this._matrix[row][column];
  }

  transpose() {
    let temp = this._matrix[1][0];
    this._matrix[1][0] = this._matrix[0][1];
    this._matrix[0][1] = temp;
    temp = this._matrix[2][0];
    this._matrix[2][0] = this._matrix[0][2];
    this._matrix[0][2] = temp;
    temp = this._matrix[2][1];
    this._matrix[2][1] = this._matrix[1][2];
    this._matrix[1][2] = temp;
    return this;
  }

  matches(requiredDimensionSymbols) {
    if (requiredDimensionSymbols.length !== 9) throw new IllegalArgumentException('Should be length 9: ' + requiredDimensionSymbols);

    for (let ai = 0; ai < 3; ai++) for (let bi = 0; bi < 3; bi++) if (!IntersectionMatrix.matches(this._matrix[ai][bi], requiredDimensionSymbols.charAt(3 * ai + bi))) return false;

    return true;
  }

  add(im) {
    for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) this.setAtLeast(i, j, im.get(i, j));
  }

  isDisjoint() {
    return this._matrix[Location.INTERIOR][Location.INTERIOR] === Dimension.FALSE && this._matrix[Location.INTERIOR][Location.BOUNDARY] === Dimension.FALSE && this._matrix[Location.BOUNDARY][Location.INTERIOR] === Dimension.FALSE && this._matrix[Location.BOUNDARY][Location.BOUNDARY] === Dimension.FALSE;
  }

  isCrosses(dimensionOfGeometryA, dimensionOfGeometryB) {
    if (dimensionOfGeometryA === Dimension.P && dimensionOfGeometryB === Dimension.L || dimensionOfGeometryA === Dimension.P && dimensionOfGeometryB === Dimension.A || dimensionOfGeometryA === Dimension.L && dimensionOfGeometryB === Dimension.A) return IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.INTERIOR]) && IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.EXTERIOR]);
    if (dimensionOfGeometryA === Dimension.L && dimensionOfGeometryB === Dimension.P || dimensionOfGeometryA === Dimension.A && dimensionOfGeometryB === Dimension.P || dimensionOfGeometryA === Dimension.A && dimensionOfGeometryB === Dimension.L) return IntersectionMatrix.isTrue(this._matrix[Location.INTERIOR][Location.INTERIOR]) && IntersectionMatrix.isTrue(this._matrix[Location.EXTERIOR][Location.INTERIOR]);
    if (dimensionOfGeometryA === Dimension.L && dimensionOfGeometryB === Dimension.L) return this._matrix[Location.INTERIOR][Location.INTERIOR] === 0;
    return false;
  }

  get interfaces_() {
    return [Clonable];
  }

}

class Angle {
  static toDegrees(radians) {
    return radians * 180 / Math.PI;
  }

  static normalize(angle) {
    while (angle > Math.PI) angle -= Angle.PI_TIMES_2;

    while (angle <= -Math.PI) angle += Angle.PI_TIMES_2;

    return angle;
  }

  static angle() {
    if (arguments.length === 1) {
      const p = arguments[0];
      return Math.atan2(p.y, p.x);
    } else if (arguments.length === 2) {
      const p0 = arguments[0],
            p1 = arguments[1];
      const dx = p1.x - p0.x;
      const dy = p1.y - p0.y;
      return Math.atan2(dy, dx);
    }
  }

  static isAcute(p0, p1, p2) {
    const dx0 = p0.x - p1.x;
    const dy0 = p0.y - p1.y;
    const dx1 = p2.x - p1.x;
    const dy1 = p2.y - p1.y;
    const dotprod = dx0 * dx1 + dy0 * dy1;
    return dotprod > 0;
  }

  static isObtuse(p0, p1, p2) {
    const dx0 = p0.x - p1.x;
    const dy0 = p0.y - p1.y;
    const dx1 = p2.x - p1.x;
    const dy1 = p2.y - p1.y;
    const dotprod = dx0 * dx1 + dy0 * dy1;
    return dotprod < 0;
  }

  static interiorAngle(p0, p1, p2) {
    const anglePrev = Angle.angle(p1, p0);
    const angleNext = Angle.angle(p1, p2);
    return Math.abs(angleNext - anglePrev);
  }

  static normalizePositive(angle) {
    if (angle < 0.0) {
      while (angle < 0.0) angle += Angle.PI_TIMES_2;

      if (angle >= Angle.PI_TIMES_2) angle = 0.0;
    } else {
      while (angle >= Angle.PI_TIMES_2) angle -= Angle.PI_TIMES_2;

      if (angle < 0.0) angle = 0.0;
    }

    return angle;
  }

  static angleBetween(tip1, tail, tip2) {
    const a1 = Angle.angle(tail, tip1);
    const a2 = Angle.angle(tail, tip2);
    return Angle.diff(a1, a2);
  }

  static diff(ang1, ang2) {
    let delAngle = null;
    if (ang1 < ang2) delAngle = ang2 - ang1;else delAngle = ang1 - ang2;
    if (delAngle > Math.PI) delAngle = 2 * Math.PI - delAngle;
    return delAngle;
  }

  static toRadians(angleDegrees) {
    return angleDegrees * Math.PI / 180.0;
  }

  static getTurn(ang1, ang2) {
    const crossproduct = Math.sin(ang2 - ang1);
    if (crossproduct > 0) return Angle.COUNTERCLOCKWISE;
    if (crossproduct < 0) return Angle.CLOCKWISE;
    return Angle.NONE;
  }

  static angleBetweenOriented(tip1, tail, tip2) {
    const a1 = Angle.angle(tail, tip1);
    const a2 = Angle.angle(tail, tip2);
    const angDel = a2 - a1;
    if (angDel <= -Math.PI) return angDel + Angle.PI_TIMES_2;
    if (angDel > Math.PI) return angDel - Angle.PI_TIMES_2;
    return angDel;
  }

}
Angle.PI_TIMES_2 = 2.0 * Math.PI;
Angle.PI_OVER_2 = Math.PI / 2.0;
Angle.PI_OVER_4 = Math.PI / 4.0;
Angle.COUNTERCLOCKWISE = Orientation.COUNTERCLOCKWISE;
Angle.CLOCKWISE = Orientation.CLOCKWISE;
Angle.NONE = Orientation.COLLINEAR;

class NotRepresentableException extends Exception {
  constructor() {
    super();
    NotRepresentableException.constructor_.apply(this, arguments);
  }

  static constructor_() {
    Exception.constructor_.call(this, 'Projective point not representable on the Cartesian plane.');
  }

}

class HCoordinate {
  constructor() {
    HCoordinate.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.x = null;
    this.y = null;
    this.w = null;

    if (arguments.length === 0) {
      this.x = 0.0;
      this.y = 0.0;
      this.w = 1.0;
    } else if (arguments.length === 1) {
      const p = arguments[0];
      this.x = p.x;
      this.y = p.y;
      this.w = 1.0;
    } else if (arguments.length === 2) {
      if (typeof arguments[0] === 'number' && typeof arguments[1] === 'number') {
        const _x = arguments[0],
              _y = arguments[1];
        this.x = _x;
        this.y = _y;
        this.w = 1.0;
      } else if (arguments[0] instanceof HCoordinate && arguments[1] instanceof HCoordinate) {
        const p1 = arguments[0],
              p2 = arguments[1];
        this.x = p1.y * p2.w - p2.y * p1.w;
        this.y = p2.x * p1.w - p1.x * p2.w;
        this.w = p1.x * p2.y - p2.x * p1.y;
      } else if (arguments[0] instanceof Coordinate && arguments[1] instanceof Coordinate) {
        const p1 = arguments[0],
              p2 = arguments[1];
        this.x = p1.y - p2.y;
        this.y = p2.x - p1.x;
        this.w = p1.x * p2.y - p2.x * p1.y;
      }
    } else if (arguments.length === 3) {
      const _x = arguments[0],
            _y = arguments[1],
            _w = arguments[2];
      this.x = _x;
      this.y = _y;
      this.w = _w;
    } else if (arguments.length === 4) {
      const p1 = arguments[0],
            p2 = arguments[1],
            q1 = arguments[2],
            q2 = arguments[3];
      const px = p1.y - p2.y;
      const py = p2.x - p1.x;
      const pw = p1.x * p2.y - p2.x * p1.y;
      const qx = q1.y - q2.y;
      const qy = q2.x - q1.x;
      const qw = q1.x * q2.y - q2.x * q1.y;
      this.x = py * qw - qy * pw;
      this.y = qx * pw - px * qw;
      this.w = px * qy - qx * py;
    }
  }

  getY() {
    const a = this.y / this.w;
    if (Double.isNaN(a) || Double.isInfinite(a)) throw new NotRepresentableException();
    return a;
  }

  getX() {
    const a = this.x / this.w;
    if (Double.isNaN(a) || Double.isInfinite(a)) throw new NotRepresentableException();
    return a;
  }

  getCoordinate() {
    const p = new Coordinate();
    p.x = this.getX();
    p.y = this.getY();
    return p;
  }

}

class Triangle {
  constructor() {
    Triangle.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.p0 = null;
    this.p1 = null;
    this.p2 = null;
    const p0 = arguments[0],
          p1 = arguments[1],
          p2 = arguments[2];
    this.p0 = p0;
    this.p1 = p1;
    this.p2 = p2;
  }

  static area(a, b, c) {
    return Math.abs(((c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y)) / 2);
  }

  static signedArea(a, b, c) {
    return ((c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y)) / 2;
  }

  static det(m00, m01, m10, m11) {
    return m00 * m11 - m01 * m10;
  }

  static interpolateZ(p, v0, v1, v2) {
    const x0 = v0.x;
    const y0 = v0.y;
    const a = v1.x - x0;
    const b = v2.x - x0;
    const c = v1.y - y0;
    const d = v2.y - y0;
    const det = a * d - b * c;
    const dx = p.x - x0;
    const dy = p.y - y0;
    const t = (d * dx - b * dy) / det;
    const u = (-c * dx + a * dy) / det;
    const z = v0.getZ() + t * (v1.getZ() - v0.getZ()) + u * (v2.getZ() - v0.getZ());
    return z;
  }

  static longestSideLength(a, b, c) {
    const lenAB = a.distance(b);
    const lenBC = b.distance(c);
    const lenCA = c.distance(a);
    let maxLen = lenAB;
    if (lenBC > maxLen) maxLen = lenBC;
    if (lenCA > maxLen) maxLen = lenCA;
    return maxLen;
  }

  static circumcentreDD(a, b, c) {
    const ax = DD.valueOf(a.x).subtract(c.x);
    const ay = DD.valueOf(a.y).subtract(c.y);
    const bx = DD.valueOf(b.x).subtract(c.x);
    const by = DD.valueOf(b.y).subtract(c.y);
    const denom = DD.determinant(ax, ay, bx, by).multiply(2);
    const asqr = ax.sqr().add(ay.sqr());
    const bsqr = bx.sqr().add(by.sqr());
    const numx = DD.determinant(ay, asqr, by, bsqr);
    const numy = DD.determinant(ax, asqr, bx, bsqr);
    const ccx = DD.valueOf(c.x).subtract(numx.divide(denom)).doubleValue();
    const ccy = DD.valueOf(c.y).add(numy.divide(denom)).doubleValue();
    return new Coordinate(ccx, ccy);
  }

  static isAcute(a, b, c) {
    if (!Angle.isAcute(a, b, c)) return false;
    if (!Angle.isAcute(b, c, a)) return false;
    if (!Angle.isAcute(c, a, b)) return false;
    return true;
  }

  static circumcentre(a, b, c) {
    const cx = c.x;
    const cy = c.y;
    const ax = a.x - cx;
    const ay = a.y - cy;
    const bx = b.x - cx;
    const by = b.y - cy;
    const denom = 2 * Triangle.det(ax, ay, bx, by);
    const numx = Triangle.det(ay, ax * ax + ay * ay, by, bx * bx + by * by);
    const numy = Triangle.det(ax, ax * ax + ay * ay, bx, bx * bx + by * by);
    const ccx = cx - numx / denom;
    const ccy = cy + numy / denom;
    return new Coordinate(ccx, ccy);
  }

  static perpendicularBisector(a, b) {
    const dx = b.x - a.x;
    const dy = b.y - a.y;
    const l1 = new HCoordinate(a.x + dx / 2.0, a.y + dy / 2.0, 1.0);
    const l2 = new HCoordinate(a.x - dy + dx / 2.0, a.y + dx + dy / 2.0, 1.0);
    return new HCoordinate(l1, l2);
  }

  static angleBisector(a, b, c) {
    const len0 = b.distance(a);
    const len2 = b.distance(c);
    const frac = len0 / (len0 + len2);
    const dx = c.x - a.x;
    const dy = c.y - a.y;
    const splitPt = new Coordinate(a.x + frac * dx, a.y + frac * dy);
    return splitPt;
  }

  static area3D(a, b, c) {
    const ux = b.x - a.x;
    const uy = b.y - a.y;
    const uz = b.getZ() - a.getZ();
    const vx = c.x - a.x;
    const vy = c.y - a.y;
    const vz = c.getZ() - a.getZ();
    const crossx = uy * vz - uz * vy;
    const crossy = uz * vx - ux * vz;
    const crossz = ux * vy - uy * vx;
    const absSq = crossx * crossx + crossy * crossy + crossz * crossz;
    const area3D = Math.sqrt(absSq) / 2;
    return area3D;
  }

  static centroid(a, b, c) {
    const x = (a.x + b.x + c.x) / 3;
    const y = (a.y + b.y + c.y) / 3;
    return new Coordinate(x, y);
  }

  static inCentre(a, b, c) {
    const len0 = b.distance(c);
    const len1 = a.distance(c);
    const len2 = a.distance(b);
    const circum = len0 + len1 + len2;
    const inCentreX = (len0 * a.x + len1 * b.x + len2 * c.x) / circum;
    const inCentreY = (len0 * a.y + len1 * b.y + len2 * c.y) / circum;
    return new Coordinate(inCentreX, inCentreY);
  }

  area() {
    return Triangle.area(this.p0, this.p1, this.p2);
  }

  signedArea() {
    return Triangle.signedArea(this.p0, this.p1, this.p2);
  }

  interpolateZ(p) {
    if (p === null) throw new IllegalArgumentException('Supplied point is null.');
    return Triangle.interpolateZ(p, this.p0, this.p1, this.p2);
  }

  longestSideLength() {
    return Triangle.longestSideLength(this.p0, this.p1, this.p2);
  }

  isAcute() {
    return Triangle.isAcute(this.p0, this.p1, this.p2);
  }

  circumcentre() {
    return Triangle.circumcentre(this.p0, this.p1, this.p2);
  }

  area3D() {
    return Triangle.area3D(this.p0, this.p1, this.p2);
  }

  centroid() {
    return Triangle.centroid(this.p0, this.p1, this.p2);
  }

  inCentre() {
    return Triangle.inCentre(this.p0, this.p1, this.p2);
  }

}

class NoninvertibleTransformationException extends Exception {
  constructor() {
    super();
    NoninvertibleTransformationException.constructor_.apply(this, arguments);
  }

  static constructor_() {
    if (arguments.length === 0) {
      Exception.constructor_.call(this);
    } else if (arguments.length === 1) {
      const msg = arguments[0];
      Exception.constructor_.call(this, msg);
    }
  }

}

class AffineTransformation {
  constructor() {
    AffineTransformation.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._m00 = null;
    this._m01 = null;
    this._m02 = null;
    this._m10 = null;
    this._m11 = null;
    this._m12 = null;

    if (arguments.length === 0) {
      this.setToIdentity();
    } else if (arguments.length === 1) {
      if (arguments[0] instanceof Array) {
        const matrix = arguments[0];
        this._m00 = matrix[0];
        this._m01 = matrix[1];
        this._m02 = matrix[2];
        this._m10 = matrix[3];
        this._m11 = matrix[4];
        this._m12 = matrix[5];
      } else if (arguments[0] instanceof AffineTransformation) {
        const trans = arguments[0];
        this.setTransformation(trans);
      }
    } else if (arguments.length === 6) {
      if (typeof arguments[5] === 'number' && typeof arguments[4] === 'number' && typeof arguments[3] === 'number' && typeof arguments[2] === 'number' && typeof arguments[0] === 'number' && typeof arguments[1] === 'number') {
        const m00 = arguments[0],
              m01 = arguments[1],
              m02 = arguments[2],
              m10 = arguments[3],
              m11 = arguments[4],
              m12 = arguments[5];
        this.setTransformation(m00, m01, m02, m10, m11, m12);
      }
    }
  }

  static translationInstance(x, y) {
    const trans = new AffineTransformation();
    trans.setToTranslation(x, y);
    return trans;
  }

  static shearInstance(xShear, yShear) {
    const trans = new AffineTransformation();
    trans.setToShear(xShear, yShear);
    return trans;
  }

  static reflectionInstance() {
    if (arguments.length === 2) {
      const x = arguments[0],
            y = arguments[1];
      const trans = new AffineTransformation();
      trans.setToReflection(x, y);
      return trans;
    } else if (arguments.length === 4) {
      const x0 = arguments[0],
            y0 = arguments[1],
            x1 = arguments[2],
            y1 = arguments[3];
      const trans = new AffineTransformation();
      trans.setToReflection(x0, y0, x1, y1);
      return trans;
    }
  }

  static rotationInstance() {
    if (arguments.length === 1) {
      const theta = arguments[0];
      return AffineTransformation.rotationInstance(Math.sin(theta), Math.cos(theta));
    } else if (arguments.length === 2) {
      const sinTheta = arguments[0],
            cosTheta = arguments[1];
      const trans = new AffineTransformation();
      trans.setToRotation(sinTheta, cosTheta);
      return trans;
    } else if (arguments.length === 3) {
      const theta = arguments[0],
            x = arguments[1],
            y = arguments[2];
      return AffineTransformation.rotationInstance(Math.sin(theta), Math.cos(theta), x, y);
    } else if (arguments.length === 4) {
      const sinTheta = arguments[0],
            cosTheta = arguments[1],
            x = arguments[2],
            y = arguments[3];
      const trans = new AffineTransformation();
      trans.setToRotation(sinTheta, cosTheta, x, y);
      return trans;
    }
  }

  static scaleInstance() {
    if (arguments.length === 2) {
      const xScale = arguments[0],
            yScale = arguments[1];
      const trans = new AffineTransformation();
      trans.setToScale(xScale, yScale);
      return trans;
    } else if (arguments.length === 4) {
      const xScale = arguments[0],
            yScale = arguments[1],
            x = arguments[2],
            y = arguments[3];
      const trans = new AffineTransformation();
      trans.translate(-x, -y);
      trans.scale(xScale, yScale);
      trans.translate(x, y);
      return trans;
    }
  }

  setToReflectionBasic(x0, y0, x1, y1) {
    if (x0 === x1 && y0 === y1) throw new IllegalArgumentException('Reflection line points must be distinct');
    const dx = x1 - x0;
    const dy = y1 - y0;
    const d = Math.sqrt(dx * dx + dy * dy);
    const sin = dy / d;
    const cos = dx / d;
    const cs2 = 2 * sin * cos;
    const c2s2 = cos * cos - sin * sin;
    this._m00 = c2s2;
    this._m01 = cs2;
    this._m02 = 0.0;
    this._m10 = cs2;
    this._m11 = -c2s2;
    this._m12 = 0.0;
    return this;
  }

  getInverse() {
    const det = this.getDeterminant();
    if (det === 0) throw new NoninvertibleTransformationException('Transformation is non-invertible');
    const im00 = this._m11 / det;
    const im10 = -this._m10 / det;
    const im01 = -this._m01 / det;
    const im11 = this._m00 / det;
    const im02 = (this._m01 * this._m12 - this._m02 * this._m11) / det;
    const im12 = (-this._m00 * this._m12 + this._m10 * this._m02) / det;
    return new AffineTransformation(im00, im01, im02, im10, im11, im12);
  }

  compose(trans) {
    const mp00 = trans._m00 * this._m00 + trans._m01 * this._m10;
    const mp01 = trans._m00 * this._m01 + trans._m01 * this._m11;
    const mp02 = trans._m00 * this._m02 + trans._m01 * this._m12 + trans._m02;
    const mp10 = trans._m10 * this._m00 + trans._m11 * this._m10;
    const mp11 = trans._m10 * this._m01 + trans._m11 * this._m11;
    const mp12 = trans._m10 * this._m02 + trans._m11 * this._m12 + trans._m12;
    this._m00 = mp00;
    this._m01 = mp01;
    this._m02 = mp02;
    this._m10 = mp10;
    this._m11 = mp11;
    this._m12 = mp12;
    return this;
  }

  equals(obj) {
    if (obj === null) return false;
    if (!(obj instanceof AffineTransformation)) return false;
    const trans = obj;
    return this._m00 === trans._m00 && this._m01 === trans._m01 && this._m02 === trans._m02 && this._m10 === trans._m10 && this._m11 === trans._m11 && this._m12 === trans._m12;
  }

  setToScale(xScale, yScale) {
    this._m00 = xScale;
    this._m01 = 0.0;
    this._m02 = 0.0;
    this._m10 = 0.0;
    this._m11 = yScale;
    this._m12 = 0.0;
    return this;
  }

  isIdentity() {
    return this._m00 === 1 && this._m01 === 0 && this._m02 === 0 && this._m10 === 0 && this._m11 === 1 && this._m12 === 0;
  }

  scale(xScale, yScale) {
    this.compose(AffineTransformation.scaleInstance(xScale, yScale));
    return this;
  }

  setToIdentity() {
    this._m00 = 1.0;
    this._m01 = 0.0;
    this._m02 = 0.0;
    this._m10 = 0.0;
    this._m11 = 1.0;
    this._m12 = 0.0;
    return this;
  }

  isGeometryChanged() {
    return true;
  }

  setTransformation() {
    if (arguments.length === 1) {
      const trans = arguments[0];
      this._m00 = trans._m00;
      this._m01 = trans._m01;
      this._m02 = trans._m02;
      this._m10 = trans._m10;
      this._m11 = trans._m11;
      this._m12 = trans._m12;
      return this;
    } else if (arguments.length === 6) {
      const m00 = arguments[0],
            m01 = arguments[1],
            m02 = arguments[2],
            m10 = arguments[3],
            m11 = arguments[4],
            m12 = arguments[5];
      this._m00 = m00;
      this._m01 = m01;
      this._m02 = m02;
      this._m10 = m10;
      this._m11 = m11;
      this._m12 = m12;
      return this;
    }
  }

  setToRotation() {
    if (arguments.length === 1) {
      const theta = arguments[0];
      this.setToRotation(Math.sin(theta), Math.cos(theta));
      return this;
    } else if (arguments.length === 2) {
      const sinTheta = arguments[0],
            cosTheta = arguments[1];
      this._m00 = cosTheta;
      this._m01 = -sinTheta;
      this._m02 = 0.0;
      this._m10 = sinTheta;
      this._m11 = cosTheta;
      this._m12 = 0.0;
      return this;
    } else if (arguments.length === 3) {
      const theta = arguments[0],
            x = arguments[1],
            y = arguments[2];
      this.setToRotation(Math.sin(theta), Math.cos(theta), x, y);
      return this;
    } else if (arguments.length === 4) {
      const sinTheta = arguments[0],
            cosTheta = arguments[1],
            x = arguments[2],
            y = arguments[3];
      this._m00 = cosTheta;
      this._m01 = -sinTheta;
      this._m02 = x - x * cosTheta + y * sinTheta;
      this._m10 = sinTheta;
      this._m11 = cosTheta;
      this._m12 = y - x * sinTheta - y * cosTheta;
      return this;
    }
  }

  getMatrixEntries() {
    return [this._m00, this._m01, this._m02, this._m10, this._m11, this._m12];
  }

  filter(seq, i) {
    this.transform(seq, i);
  }

  rotate() {
    if (arguments.length === 1) {
      const theta = arguments[0];
      this.compose(AffineTransformation.rotationInstance(theta));
      return this;
    } else if (arguments.length === 2) {
      const sinTheta = arguments[0],
            cosTheta = arguments[1];
      this.compose(AffineTransformation.rotationInstance(sinTheta, cosTheta));
      return this;
    } else if (arguments.length === 3) {
      const theta = arguments[0],
            x = arguments[1],
            y = arguments[2];
      this.compose(AffineTransformation.rotationInstance(theta, x, y));
      return this;
    } else if (arguments.length === 4) {
      const sinTheta = arguments[0],
            cosTheta = arguments[1],
            x = arguments[2],
            y = arguments[3];
      this.compose(AffineTransformation.rotationInstance(sinTheta, cosTheta, x, y));
      return this;
    }
  }

  getDeterminant() {
    return this._m00 * this._m11 - this._m01 * this._m10;
  }

  composeBefore(trans) {
    const mp00 = this._m00 * trans._m00 + this._m01 * trans._m10;
    const mp01 = this._m00 * trans._m01 + this._m01 * trans._m11;
    const mp02 = this._m00 * trans._m02 + this._m01 * trans._m12 + this._m02;
    const mp10 = this._m10 * trans._m00 + this._m11 * trans._m10;
    const mp11 = this._m10 * trans._m01 + this._m11 * trans._m11;
    const mp12 = this._m10 * trans._m02 + this._m11 * trans._m12 + this._m12;
    this._m00 = mp00;
    this._m01 = mp01;
    this._m02 = mp02;
    this._m10 = mp10;
    this._m11 = mp11;
    this._m12 = mp12;
    return this;
  }

  setToShear(xShear, yShear) {
    this._m00 = 1.0;
    this._m01 = xShear;
    this._m02 = 0.0;
    this._m10 = yShear;
    this._m11 = 1.0;
    this._m12 = 0.0;
    return this;
  }

  isDone() {
    return false;
  }

  clone() {
    try {
      return null;
    } catch (ex) {
      if (ex instanceof Exception) Assert.shouldNeverReachHere();else throw ex;
    } finally {}

    return null;
  }

  translate(x, y) {
    this.compose(AffineTransformation.translationInstance(x, y));
    return this;
  }

  setToReflection() {
    if (arguments.length === 2) {
      const x = arguments[0],
            y = arguments[1];
      if (x === 0.0 && y === 0.0) throw new IllegalArgumentException('Reflection vector must be non-zero');

      if (x === y) {
        this._m00 = 0.0;
        this._m01 = 1.0;
        this._m02 = 0.0;
        this._m10 = 1.0;
        this._m11 = 0.0;
        this._m12 = 0.0;
        return this;
      }

      const d = Math.sqrt(x * x + y * y);
      const sin = y / d;
      const cos = x / d;
      this.rotate(-sin, cos);
      this.scale(1, -1);
      this.rotate(sin, cos);
      return this;
    } else if (arguments.length === 4) {
      const x0 = arguments[0],
            y0 = arguments[1],
            x1 = arguments[2],
            y1 = arguments[3];
      if (x0 === x1 && y0 === y1) throw new IllegalArgumentException('Reflection line points must be distinct');
      this.setToTranslation(-x0, -y0);
      const dx = x1 - x0;
      const dy = y1 - y0;
      const d = Math.sqrt(dx * dx + dy * dy);
      const sin = dy / d;
      const cos = dx / d;
      this.rotate(-sin, cos);
      this.scale(1, -1);
      this.rotate(sin, cos);
      this.translate(x0, y0);
      return this;
    }
  }

  toString() {
    return 'AffineTransformation[[' + this._m00 + ', ' + this._m01 + ', ' + this._m02 + '], [' + this._m10 + ', ' + this._m11 + ', ' + this._m12 + ']]';
  }

  setToTranslation(dx, dy) {
    this._m00 = 1.0;
    this._m01 = 0.0;
    this._m02 = dx;
    this._m10 = 0.0;
    this._m11 = 1.0;
    this._m12 = dy;
    return this;
  }

  shear(xShear, yShear) {
    this.compose(AffineTransformation.shearInstance(xShear, yShear));
    return this;
  }

  transform() {
    if (arguments.length === 1) {
      const g = arguments[0];
      const g2 = g.copy();
      g2.apply(this);
      return g2;
    } else if (arguments.length === 2) {
      if (arguments[0] instanceof Coordinate && arguments[1] instanceof Coordinate) {
        const src = arguments[0],
              dest = arguments[1];
        const xp = this._m00 * src.x + this._m01 * src.y + this._m02;
        const yp = this._m10 * src.x + this._m11 * src.y + this._m12;
        dest.x = xp;
        dest.y = yp;
        return dest;
      } else if (hasInterface(arguments[0], CoordinateSequence) && Number.isInteger(arguments[1])) {
        const seq = arguments[0],
              i = arguments[1];

        const xp = this._m00 * seq.getOrdinate(i, 0) + this._m01 * seq.getOrdinate(i, 1) + this._m02;

        const yp = this._m10 * seq.getOrdinate(i, 0) + this._m11 * seq.getOrdinate(i, 1) + this._m12;

        seq.setOrdinate(i, 0, xp);
        seq.setOrdinate(i, 1, yp);
      }
    }
  }

  reflect() {
    if (arguments.length === 2) {
      const x = arguments[0],
            y = arguments[1];
      this.compose(AffineTransformation.reflectionInstance(x, y));
      return this;
    } else if (arguments.length === 4) {
      const x0 = arguments[0],
            y0 = arguments[1],
            x1 = arguments[2],
            y1 = arguments[3];
      this.compose(AffineTransformation.reflectionInstance(x0, y0, x1, y1));
      return this;
    }
  }

  get interfaces_() {
    return [Clonable, CoordinateSequenceFilter];
  }

}

class Matrix {
  static solve(a, b) {
    const n = b.length;
    if (a.length !== n || a[0].length !== n) throw new IllegalArgumentException('Matrix A is incorrectly sized');

    for (let i = 0; i < n; i++) {
      let maxElementRow = i;

      for (let j = i + 1; j < n; j++) if (Math.abs(a[j][i]) > Math.abs(a[maxElementRow][i])) maxElementRow = j;

      if (a[maxElementRow][i] === 0.0) return null;
      Matrix.swapRows(a, i, maxElementRow);
      Matrix.swapRows(b, i, maxElementRow);

      for (let j = i + 1; j < n; j++) {
        const rowFactor = a[j][i] / a[i][i];

        for (let k = n - 1; k >= i; k--) a[j][k] -= a[i][k] * rowFactor;

        b[j] -= b[i] * rowFactor;
      }
    }

    const solution = new Array(n).fill(null);

    for (let j = n - 1; j >= 0; j--) {
      let t = 0.0;

      for (let k = j + 1; k < n; k++) t += a[j][k] * solution[k];

      solution[j] = (b[j] - t) / a[j][j];
    }

    return solution;
  }

  static swapRows() {
    if (Number.isInteger(arguments[2]) && arguments[0] instanceof Array && Number.isInteger(arguments[1])) {
      const m = arguments[0],
            i = arguments[1],
            j = arguments[2];
      if (i === j) return null;

      for (let col = 0; col < m[0].length; col++) {
        const temp = m[i][col];
        m[i][col] = m[j][col];
        m[j][col] = temp;
      }
    } else if (Number.isInteger(arguments[2]) && arguments[0] instanceof Array && Number.isInteger(arguments[1])) {
      const m = arguments[0],
            i = arguments[1],
            j = arguments[2];
      if (i === j) return null;
      const temp = m[i];
      m[i] = m[j];
      m[j] = temp;
    }
  }

}

class AffineTransformationBuilder {
  constructor() {
    AffineTransformationBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._src0 = null;
    this._src1 = null;
    this._src2 = null;
    this._dest0 = null;
    this._dest1 = null;
    this._dest2 = null;
    this._m00 = null;
    this._m01 = null;
    this._m02 = null;
    this._m10 = null;
    this._m11 = null;
    this._m12 = null;
    const src0 = arguments[0],
          src1 = arguments[1],
          src2 = arguments[2],
          dest0 = arguments[3],
          dest1 = arguments[4],
          dest2 = arguments[5];
    this._src0 = src0;
    this._src1 = src1;
    this._src2 = src2;
    this._dest0 = dest0;
    this._dest1 = dest1;
    this._dest2 = dest2;
  }

  solve(b) {
    const a = [[this._src0.x, this._src0.y, 1], [this._src1.x, this._src1.y, 1], [this._src2.x, this._src2.y, 1]];
    return Matrix.solve(a, b);
  }

  compute() {
    const bx = [this._dest0.x, this._dest1.x, this._dest2.x];
    const row0 = this.solve(bx);
    if (row0 === null) return false;
    this._m00 = row0[0];
    this._m01 = row0[1];
    this._m02 = row0[2];
    const by = [this._dest0.y, this._dest1.y, this._dest2.y];
    const row1 = this.solve(by);
    if (row1 === null) return false;
    this._m10 = row1[0];
    this._m11 = row1[1];
    this._m12 = row1[2];
    return true;
  }

  getTransformation() {
    const isSolvable = this.compute();
    if (isSolvable) return new AffineTransformation(this._m00, this._m01, this._m02, this._m10, this._m11, this._m12);
    return null;
  }

}

class AffineTransformationFactory {
  static createFromBaseLines(src0, src1, dest0, dest1) {
    const rotPt = new Coordinate(src0.x + dest1.x - dest0.x, src0.y + dest1.y - dest0.y);
    const ang = Angle.angleBetweenOriented(src1, src0, rotPt);
    const srcDist = src1.distance(src0);
    const destDist = dest1.distance(dest0);
    if (srcDist === 0.0) return new AffineTransformation();
    const scale = destDist / srcDist;
    const trans = AffineTransformation.translationInstance(-src0.x, -src0.y);
    trans.rotate(ang);
    trans.scale(scale, scale);
    trans.translate(dest0.x, dest0.y);
    return trans;
  }

  static createFromControlVectors() {
    if (arguments.length === 2) {
      if (arguments[0] instanceof Coordinate && arguments[1] instanceof Coordinate) {
        const src0 = arguments[0],
              dest0 = arguments[1];
        const dx = dest0.x - src0.x;
        const dy = dest0.y - src0.y;
        return AffineTransformation.translationInstance(dx, dy);
      } else if (arguments[0] instanceof Array && arguments[1] instanceof Array) {
        const src = arguments[0],
              dest = arguments[1];
        if (src.length !== dest.length) throw new IllegalArgumentException('Src and Dest arrays are not the same length');
        if (src.length <= 0) throw new IllegalArgumentException('Too few control points');
        if (src.length > 3) throw new IllegalArgumentException('Too many control points');
        if (src.length === 1) return AffineTransformationFactory.createFromControlVectors(src[0], dest[0]);
        if (src.length === 2) return AffineTransformationFactory.createFromControlVectors(src[0], src[1], dest[0], dest[1]);
        return AffineTransformationFactory.createFromControlVectors(src[0], src[1], src[2], dest[0], dest[1], dest[2]);
      }
    } else if (arguments.length === 4) {
      const src0 = arguments[0],
            src1 = arguments[1],
            dest0 = arguments[2],
            dest1 = arguments[3];
      const rotPt = new Coordinate(dest1.x - dest0.x, dest1.y - dest0.y);
      const ang = Angle.angleBetweenOriented(src1, src0, rotPt);
      const srcDist = src1.distance(src0);
      const destDist = dest1.distance(dest0);
      if (srcDist === 0.0) return null;
      const scale = destDist / srcDist;
      const trans = AffineTransformation.translationInstance(-src0.x, -src0.y);
      trans.rotate(ang);
      trans.scale(scale, scale);
      trans.translate(dest0.x, dest0.y);
      return trans;
    } else if (arguments.length === 6) {
      const src0 = arguments[0],
            src1 = arguments[1],
            src2 = arguments[2],
            dest0 = arguments[3],
            dest1 = arguments[4],
            dest2 = arguments[5];
      const builder = new AffineTransformationBuilder(src0, src1, src2, dest0, dest1, dest2);
      return builder.getTransformation();
    }
  }

}

class ComponentCoordinateExtracter {
  constructor() {
    ComponentCoordinateExtracter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._coords = null;
    const coords = arguments[0];
    this._coords = coords;
  }

  static getCoordinates(geom) {
    const coords = new ArrayList();
    geom.apply(new ComponentCoordinateExtracter(coords));
    return coords;
  }

  filter(geom) {
    if (geom instanceof LineString || geom instanceof Point) this._coords.add(geom.getCoordinate());
  }

  get interfaces_() {
    return [GeometryComponentFilter];
  }

}

class GeometryCollectionMapper {
  constructor() {
    GeometryCollectionMapper.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._mapOp = null;
    const mapOp = arguments[0];
    this._mapOp = mapOp;
  }

  static map(gc, op) {
    const mapper = new GeometryCollectionMapper(op);
    return mapper.map(gc);
  }

  map(gc) {
    const mapped = new ArrayList();

    for (let i = 0; i < gc.getNumGeometries(); i++) {
      const g = this._mapOp.map(gc.getGeometryN(i));

      if (!g.isEmpty()) mapped.add(g);
    }

    return gc.getFactory().createGeometryCollection(GeometryFactory.toGeometryArray(mapped));
  }

}

class GeometryCombiner {
  constructor() {
    GeometryCombiner.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geomFactory = null;
    this._skipEmpty = false;
    this._inputGeoms = null;
    const geoms = arguments[0];
    this._geomFactory = GeometryCombiner.extractFactory(geoms);
    this._inputGeoms = geoms;
  }

  static combine() {
    if (arguments.length === 1) {
      const geoms = arguments[0];
      const combiner = new GeometryCombiner(geoms);
      return combiner.combine();
    } else if (arguments.length === 2) {
      const g0 = arguments[0],
            g1 = arguments[1];
      const combiner = new GeometryCombiner(GeometryCombiner.createList(g0, g1));
      return combiner.combine();
    } else if (arguments.length === 3) {
      const g0 = arguments[0],
            g1 = arguments[1],
            g2 = arguments[2];
      const combiner = new GeometryCombiner(GeometryCombiner.createList(g0, g1, g2));
      return combiner.combine();
    }
  }

  static extractFactory(geoms) {
    if (geoms.isEmpty()) return null;
    return geoms.iterator().next().getFactory();
  }

  static createList() {
    if (arguments.length === 2) {
      const obj0 = arguments[0],
            obj1 = arguments[1];
      const list = new ArrayList();
      list.add(obj0);
      list.add(obj1);
      return list;
    } else if (arguments.length === 3) {
      const obj0 = arguments[0],
            obj1 = arguments[1],
            obj2 = arguments[2];
      const list = new ArrayList();
      list.add(obj0);
      list.add(obj1);
      list.add(obj2);
      return list;
    }
  }

  extractElements(geom, elems) {
    if (geom === null) return null;

    for (let i = 0; i < geom.getNumGeometries(); i++) {
      const elemGeom = geom.getGeometryN(i);
      if (this._skipEmpty && elemGeom.isEmpty()) continue;
      elems.add(elemGeom);
    }
  }

  combine() {
    const elems = new ArrayList();

    for (let i = this._inputGeoms.iterator(); i.hasNext();) {
      const g = i.next();
      this.extractElements(g, elems);
    }

    if (elems.size() === 0) {
      if (this._geomFactory !== null) return this._geomFactory.createGeometryCollection();
      return null;
    }

    return this._geomFactory.buildGeometry(elems);
  }

}

class GeometryEditor {
  constructor() {
    GeometryEditor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._factory = null;
    this._isUserDataCopied = false;

    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const factory = arguments[0];
      this._factory = factory;
    }
  }

  setCopyUserData(isUserDataCopied) {
    this._isUserDataCopied = isUserDataCopied;
  }

  edit(geometry, operation) {
    if (geometry === null) return null;
    const result = this.editInternal(geometry, operation);
    if (this._isUserDataCopied) result.setUserData(geometry.getUserData());
    return result;
  }

  editInternal(geometry, operation) {
    if (this._factory === null) this._factory = geometry.getFactory();
    if (geometry instanceof GeometryCollection) return this.editGeometryCollection(geometry, operation);
    if (geometry instanceof Polygon) return this.editPolygon(geometry, operation);
    if (geometry instanceof Point) return operation.edit(geometry, this._factory);
    if (geometry instanceof LineString) return operation.edit(geometry, this._factory);
    Assert.shouldNeverReachHere('Unsupported Geometry type: ' + geometry.getGeometryType());
    return null;
  }

  editGeometryCollection(collection, operation) {
    const collectionForType = operation.edit(collection, this._factory);
    const geometries = new ArrayList();

    for (let i = 0; i < collectionForType.getNumGeometries(); i++) {
      const geometry = this.edit(collectionForType.getGeometryN(i), operation);
      if (geometry === null || geometry.isEmpty()) continue;
      geometries.add(geometry);
    }

    if (collectionForType.getGeometryType() === Geometry.TYPENAME_MULTIPOINT) return this._factory.createMultiPoint(geometries.toArray([]));
    if (collectionForType.getGeometryType() === Geometry.TYPENAME_MULTILINESTRING) return this._factory.createMultiLineString(geometries.toArray([]));
    if (collectionForType.getGeometryType() === Geometry.TYPENAME_MULTIPOLYGON) return this._factory.createMultiPolygon(geometries.toArray([]));
    return this._factory.createGeometryCollection(geometries.toArray([]));
  }

  editPolygon(polygon, operation) {
    let newPolygon = operation.edit(polygon, this._factory);
    if (newPolygon === null) newPolygon = this._factory.createPolygon();
    if (newPolygon.isEmpty()) return newPolygon;
    const shell = this.edit(newPolygon.getExteriorRing(), operation);
    if (shell === null || shell.isEmpty()) return this._factory.createPolygon();
    const holes = new ArrayList();

    for (let i = 0; i < newPolygon.getNumInteriorRing(); i++) {
      const hole = this.edit(newPolygon.getInteriorRingN(i), operation);
      if (hole === null || hole.isEmpty()) continue;
      holes.add(hole);
    }

    return this._factory.createPolygon(shell, holes.toArray([]));
  }

}

function GeometryEditorOperation() {}

GeometryEditor.GeometryEditorOperation = GeometryEditorOperation;

class NoOpGeometryOperation {
  edit(geometry, factory) {
    return geometry;
  }

  get interfaces_() {
    return [GeometryEditorOperation];
  }

}

class CoordinateOperation {
  edit(geometry, factory) {
    const coordinates = this.edit(geometry.getCoordinates(), geometry);
    if (geometry instanceof LinearRing) if (coordinates === null) return factory.createLinearRing();else return factory.createLinearRing(coordinates);
    if (geometry instanceof LineString) if (coordinates === null) return factory.createLineString();else return factory.createLineString(coordinates);
    if (geometry instanceof Point) if (coordinates === null || coordinates.length === 0) return factory.createPoint();else return factory.createPoint(coordinates[0]);
    return geometry;
  }

  get interfaces_() {
    return [GeometryEditorOperation];
  }

}

class CoordinateSequenceOperation {
  edit(geometry, factory) {
    if (geometry instanceof LinearRing) return factory.createLinearRing(this.edit(geometry.getCoordinateSequence(), geometry));
    if (geometry instanceof LineString) return factory.createLineString(this.edit(geometry.getCoordinateSequence(), geometry));
    if (geometry instanceof Point) return factory.createPoint(this.edit(geometry.getCoordinateSequence(), geometry));
    return geometry;
  }

  get interfaces_() {
    return [GeometryEditorOperation];
  }

}

GeometryEditor.NoOpGeometryOperation = NoOpGeometryOperation;
GeometryEditor.CoordinateOperation = CoordinateOperation;
GeometryEditor.CoordinateSequenceOperation = CoordinateSequenceOperation;

class GeometryExtracter {
  constructor() {
    GeometryExtracter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geometryType = null;
    this._comps = null;
    const geometryType = arguments[0],
          comps = arguments[1];
    this._geometryType = geometryType;
    this._comps = comps;
  }

  static isOfType(geom, geometryType) {
    if (geom.getGeometryType() === geometryType) return true;
    if (geometryType === Geometry.TYPENAME_LINESTRING && geom.getGeometryType() === Geometry.TYPENAME_LINEARRING) return true;
    return false;
  }

  static extract() {
    if (arguments.length === 2) {
      const geom = arguments[0],
            geometryType = arguments[1];
      return GeometryExtracter.extract(geom, geometryType, new ArrayList());
    } else if (arguments.length === 3) {
      const geom = arguments[0],
            geometryType = arguments[1],
            list = arguments[2];
      if (geom.getGeometryType() === geometryType) list.add(geom);else if (geom instanceof GeometryCollection) geom.apply(new GeometryExtracter(geometryType, list));
      return list;
    }
  }

  filter(geom) {
    if (this._geometryType === null || GeometryExtracter.isOfType(geom, this._geometryType)) this._comps.add(geom);
  }

  get interfaces_() {
    return [GeometryFilter];
  }

}

class GeometryMapper {
  static map() {
    if (arguments[0] instanceof Geometry && hasInterface(arguments[1], MapOp$1)) {
      const geom = arguments[0],
            op = arguments[1];
      const mapped = new ArrayList();

      for (let i = 0; i < geom.getNumGeometries(); i++) {
        const g = op.map(geom.getGeometryN(i));
        if (g !== null) mapped.add(g);
      }

      return geom.getFactory().buildGeometry(mapped);
    } else if (hasInterface(arguments[0], Collection) && hasInterface(arguments[1], MapOp$1)) {
      const geoms = arguments[0],
            op = arguments[1];
      const mapped = new ArrayList();

      for (let i = geoms.iterator(); i.hasNext();) {
        const g = i.next();
        const gr = op.map(g);
        if (gr !== null) mapped.add(gr);
      }

      return mapped;
    }
  }

}

function MapOp$1() {}

GeometryMapper.MapOp = MapOp$1;

class GeometryTransformer {
  constructor() {
    GeometryTransformer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._factory = null;
    this._pruneEmptyGeometry = true;
    this._preserveGeometryCollectionType = true;
    this._preserveCollections = false;
    this._preserveType = false;
  }

  transformPoint(geom, parent) {
    return this._factory.createPoint(this.transformCoordinates(geom.getCoordinateSequence(), geom));
  }

  transformPolygon(geom, parent) {
    let isAllValidLinearRings = true;
    const shell = this.transformLinearRing(geom.getExteriorRing(), geom);
    if (shell === null || !(shell instanceof LinearRing) || shell.isEmpty()) isAllValidLinearRings = false;
    const holes = new ArrayList();

    for (let i = 0; i < geom.getNumInteriorRing(); i++) {
      const hole = this.transformLinearRing(geom.getInteriorRingN(i), geom);
      if (hole === null || hole.isEmpty()) continue;
      if (!(hole instanceof LinearRing)) isAllValidLinearRings = false;
      holes.add(hole);
    }

    if (isAllValidLinearRings) {
      return this._factory.createPolygon(shell, holes.toArray([]));
    } else {
      const components = new ArrayList();
      if (shell !== null) components.add(shell);
      components.addAll(holes);
      return this._factory.buildGeometry(components);
    }
  }

  createCoordinateSequence(coords) {
    return this._factory.getCoordinateSequenceFactory().create(coords);
  }

  getInputGeometry() {
    return this._inputGeom;
  }

  transformMultiLineString(geom, parent) {
    const transGeomList = new ArrayList();

    for (let i = 0; i < geom.getNumGeometries(); i++) {
      const transformGeom = this.transformLineString(geom.getGeometryN(i), geom);
      if (transformGeom === null) continue;
      if (transformGeom.isEmpty()) continue;
      transGeomList.add(transformGeom);
    }

    return this._factory.buildGeometry(transGeomList);
  }

  transformCoordinates(coords, parent) {
    return this.copy(coords);
  }

  transformLineString(geom, parent) {
    return this._factory.createLineString(this.transformCoordinates(geom.getCoordinateSequence(), geom));
  }

  transformMultiPoint(geom, parent) {
    const transGeomList = new ArrayList();

    for (let i = 0; i < geom.getNumGeometries(); i++) {
      const transformGeom = this.transformPoint(geom.getGeometryN(i), geom);
      if (transformGeom === null) continue;
      if (transformGeom.isEmpty()) continue;
      transGeomList.add(transformGeom);
    }

    return this._factory.buildGeometry(transGeomList);
  }

  transformMultiPolygon(geom, parent) {
    const transGeomList = new ArrayList();

    for (let i = 0; i < geom.getNumGeometries(); i++) {
      const transformGeom = this.transformPolygon(geom.getGeometryN(i), geom);
      if (transformGeom === null) continue;
      if (transformGeom.isEmpty()) continue;
      transGeomList.add(transformGeom);
    }

    return this._factory.buildGeometry(transGeomList);
  }

  copy(seq) {
    return seq.copy();
  }

  transformGeometryCollection(geom, parent) {
    const transGeomList = new ArrayList();

    for (let i = 0; i < geom.getNumGeometries(); i++) {
      const transformGeom = this.transform(geom.getGeometryN(i));
      if (transformGeom === null) continue;
      if (this._pruneEmptyGeometry && transformGeom.isEmpty()) continue;
      transGeomList.add(transformGeom);
    }

    if (this._preserveGeometryCollectionType) return this._factory.createGeometryCollection(GeometryFactory.toGeometryArray(transGeomList));
    return this._factory.buildGeometry(transGeomList);
  }

  transform(inputGeom) {
    this._inputGeom = inputGeom;
    this._factory = inputGeom.getFactory();
    if (inputGeom instanceof Point) return this.transformPoint(inputGeom, null);
    if (inputGeom instanceof MultiPoint) return this.transformMultiPoint(inputGeom, null);
    if (inputGeom instanceof LinearRing) return this.transformLinearRing(inputGeom, null);
    if (inputGeom instanceof LineString) return this.transformLineString(inputGeom, null);
    if (inputGeom instanceof MultiLineString) return this.transformMultiLineString(inputGeom, null);
    if (inputGeom instanceof Polygon) return this.transformPolygon(inputGeom, null);
    if (inputGeom instanceof MultiPolygon) return this.transformMultiPolygon(inputGeom, null);
    if (inputGeom instanceof GeometryCollection) return this.transformGeometryCollection(inputGeom, null);
    throw new IllegalArgumentException('Unknown Geometry subtype: ' + inputGeom.getGeometryType());
  }

  transformLinearRing(geom, parent) {
    const seq = this.transformCoordinates(geom.getCoordinateSequence(), geom);
    if (seq === null) return this._factory.createLinearRing(null);
    const seqSize = seq.size();
    if (seqSize > 0 && seqSize < 4 && !this._preserveType) return this._factory.createLineString(seq);
    return this._factory.createLinearRing(seq);
  }

}

class LineStringExtracter {
  constructor() {
    LineStringExtracter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._comps = null;
    const comps = arguments[0];
    this._comps = comps;
  }

  static getGeometry(geom) {
    return geom.getFactory().buildGeometry(LineStringExtracter.getLines(geom));
  }

  static getLines() {
    if (arguments.length === 1) {
      const geom = arguments[0];
      return LineStringExtracter.getLines(geom, new ArrayList());
    } else if (arguments.length === 2) {
      const geom = arguments[0],
            lines = arguments[1];
      if (geom instanceof LineString) lines.add(geom);else if (geom instanceof GeometryCollection) geom.apply(new LineStringExtracter(lines));
      return lines;
    }
  }

  filter(geom) {
    if (geom instanceof LineString) this._comps.add(geom);
  }

  get interfaces_() {
    return [GeometryFilter];
  }

}

class LinearComponentExtracter {
  constructor() {
    LinearComponentExtracter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._lines = null;
    this._isForcedToLineString = false;

    if (arguments.length === 1) {
      const lines = arguments[0];
      this._lines = lines;
    } else if (arguments.length === 2) {
      const lines = arguments[0],
            isForcedToLineString = arguments[1];
      this._lines = lines;
      this._isForcedToLineString = isForcedToLineString;
    }
  }

  static getGeometry() {
    if (arguments.length === 1) {
      const geom = arguments[0];
      return geom.getFactory().buildGeometry(LinearComponentExtracter.getLines(geom));
    } else if (arguments.length === 2) {
      const geom = arguments[0],
            forceToLineString = arguments[1];
      return geom.getFactory().buildGeometry(LinearComponentExtracter.getLines(geom, forceToLineString));
    }
  }

  static getLines() {
    if (arguments.length === 1) {
      const geom = arguments[0];
      return LinearComponentExtracter.getLines(geom, false);
    } else if (arguments.length === 2) {
      if (hasInterface(arguments[0], Collection) && hasInterface(arguments[1], Collection)) {
        const geoms = arguments[0],
              lines = arguments[1];

        for (let i = geoms.iterator(); i.hasNext();) {
          const g = i.next();
          LinearComponentExtracter.getLines(g, lines);
        }

        return lines;
      } else if (arguments[0] instanceof Geometry && typeof arguments[1] === 'boolean') {
        const geom = arguments[0],
              forceToLineString = arguments[1];
        const lines = new ArrayList();
        geom.apply(new LinearComponentExtracter(lines, forceToLineString));
        return lines;
      } else if (arguments[0] instanceof Geometry && hasInterface(arguments[1], Collection)) {
        const geom = arguments[0],
              lines = arguments[1];
        if (geom instanceof LineString) lines.add(geom);else geom.apply(new LinearComponentExtracter(lines));
        return lines;
      }
    } else if (arguments.length === 3) {
      if (typeof arguments[2] === 'boolean' && hasInterface(arguments[0], Collection) && hasInterface(arguments[1], Collection)) {
        const geoms = arguments[0],
              lines = arguments[1],
              forceToLineString = arguments[2];

        for (let i = geoms.iterator(); i.hasNext();) {
          const g = i.next();
          LinearComponentExtracter.getLines(g, lines, forceToLineString);
        }

        return lines;
      } else if (typeof arguments[2] === 'boolean' && arguments[0] instanceof Geometry && hasInterface(arguments[1], Collection)) {
        const geom = arguments[0],
              lines = arguments[1],
              forceToLineString = arguments[2];
        geom.apply(new LinearComponentExtracter(lines, forceToLineString));
        return lines;
      }
    }
  }

  filter(geom) {
    if (this._isForcedToLineString && geom instanceof LinearRing) {
      const line = geom.getFactory().createLineString(geom.getCoordinateSequence());

      this._lines.add(line);

      return null;
    }

    if (geom instanceof LineString) this._lines.add(geom);
  }

  setForceToLineString(isForcedToLineString) {
    this._isForcedToLineString = isForcedToLineString;
  }

  get interfaces_() {
    return [GeometryComponentFilter];
  }

}

const Collections = {
  reverseOrder: function () {
    return {
      compare(a, b) {
        return b.compareTo(a);
      }

    };
  },
  min: function (l) {
    Collections.sort(l);
    return l.get(0);
  },
  sort: function (l, c) {
    const a = l.toArray();
    if (c) Arrays.sort(a, c);else Arrays.sort(a);
    const i = l.iterator();

    for (let pos = 0, alen = a.length; pos < alen; pos++) {
      i.next();
      i.set(a[pos]);
    }
  },
  singletonList: function (o) {
    const arrayList = new ArrayList();
    arrayList.add(o);
    return arrayList;
  }
};

class PointExtracter {
  constructor() {
    PointExtracter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pts = null;
    const pts = arguments[0];
    this._pts = pts;
  }

  static getPoints() {
    if (arguments.length === 1) {
      const geom = arguments[0];
      if (geom instanceof Point) return Collections.singletonList(geom);
      return PointExtracter.getPoints(geom, new ArrayList());
    } else if (arguments.length === 2) {
      const geom = arguments[0],
            list = arguments[1];
      if (geom instanceof Point) list.add(geom);else if (geom instanceof GeometryCollection) geom.apply(new PointExtracter(list));
      return list;
    }
  }

  filter(geom) {
    if (geom instanceof Point) this._pts.add(geom);
  }

  get interfaces_() {
    return [GeometryFilter];
  }

}

class PolygonExtracter {
  constructor() {
    PolygonExtracter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._comps = null;
    const comps = arguments[0];
    this._comps = comps;
  }

  static getPolygons() {
    if (arguments.length === 1) {
      const geom = arguments[0];
      return PolygonExtracter.getPolygons(geom, new ArrayList());
    } else if (arguments.length === 2) {
      const geom = arguments[0],
            list = arguments[1];
      if (geom instanceof Polygon) list.add(geom);else if (geom instanceof GeometryCollection) geom.apply(new PolygonExtracter(list));
      return list;
    }
  }

  filter(geom) {
    if (geom instanceof Polygon) this._comps.add(geom);
  }

  get interfaces_() {
    return [GeometryFilter];
  }

}

class ShortCircuitedGeometryVisitor {
  constructor() {
    ShortCircuitedGeometryVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._isDone = false;
  }

  applyTo(geom) {
    for (let i = 0; i < geom.getNumGeometries() && !this._isDone; i++) {
      const element = geom.getGeometryN(i);

      if (!(element instanceof GeometryCollection)) {
        this.visit(element);

        if (this.isDone()) {
          this._isDone = true;
          return null;
        }
      } else {
        this.applyTo(element);
      }
    }
  }

}

class GeometricShapeFactory {
  constructor() {
    GeometricShapeFactory.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geomFact = null;
    this._precModel = null;
    this._dim = new Dimensions();
    this._nPts = 100;
    this._rotationAngle = 0.0;

    if (arguments.length === 0) {
      GeometricShapeFactory.constructor_.call(this, new GeometryFactory());
    } else if (arguments.length === 1) {
      const geomFact = arguments[0];
      this._geomFact = geomFact;
      this._precModel = geomFact.getPrecisionModel();
    }
  }

  createSupercircle(power) {
    const recipPow = 1.0 / power;
    const radius = this._dim.getMinSize() / 2;

    const centre = this._dim.getCentre();

    const r4 = Math.pow(radius, power);
    const y0 = radius;
    const xyInt = Math.pow(r4 / 2, recipPow);
    const nSegsInOct = Math.trunc(this._nPts / 8);
    const totPts = nSegsInOct * 8 + 1;
    const pts = new Array(totPts).fill(null);
    const xInc = xyInt / nSegsInOct;

    for (let i = 0; i <= nSegsInOct; i++) {
      let x = 0.0;
      let y = y0;

      if (i !== 0) {
        x = xInc * i;
        const x4 = Math.pow(x, power);
        y = Math.pow(r4 - x4, recipPow);
      }

      pts[i] = this.coordTrans(x, y, centre);
      pts[2 * nSegsInOct - i] = this.coordTrans(y, x, centre);
      pts[2 * nSegsInOct + i] = this.coordTrans(y, -x, centre);
      pts[4 * nSegsInOct - i] = this.coordTrans(x, -y, centre);
      pts[4 * nSegsInOct + i] = this.coordTrans(-x, -y, centre);
      pts[6 * nSegsInOct - i] = this.coordTrans(-y, -x, centre);
      pts[6 * nSegsInOct + i] = this.coordTrans(-y, x, centre);
      pts[8 * nSegsInOct - i] = this.coordTrans(-x, y, centre);
    }

    pts[pts.length - 1] = new Coordinate(pts[0]);

    const ring = this._geomFact.createLinearRing(pts);

    const poly = this._geomFact.createPolygon(ring);

    return this.rotate(poly);
  }

  setNumPoints(nPts) {
    this._nPts = nPts;
  }

  setBase(base) {
    this._dim.setBase(base);
  }

  setRotation(radians) {
    this._rotationAngle = radians;
  }

  setWidth(width) {
    this._dim.setWidth(width);
  }

  createEllipse() {
    const env = this._dim.getEnvelope();

    const xRadius = env.getWidth() / 2.0;
    const yRadius = env.getHeight() / 2.0;
    const centreX = env.getMinX() + xRadius;
    const centreY = env.getMinY() + yRadius;
    const pts = new Array(this._nPts + 1).fill(null);
    let iPt = 0;

    for (let i = 0; i < this._nPts; i++) {
      const ang = i * (2 * Math.PI / this._nPts);
      const x = xRadius * Math.cos(ang) + centreX;
      const y = yRadius * Math.sin(ang) + centreY;
      pts[iPt++] = this.coord(x, y);
    }

    pts[iPt] = new Coordinate(pts[0]);

    const ring = this._geomFact.createLinearRing(pts);

    const poly = this._geomFact.createPolygon(ring);

    return this.rotate(poly);
  }

  coordTrans(x, y, trans) {
    return this.coord(x + trans.x, y + trans.y);
  }

  createSquircle() {
    return this.createSupercircle(4);
  }

  setEnvelope(env) {
    this._dim.setEnvelope(env);
  }

  setCentre(centre) {
    this._dim.setCentre(centre);
  }

  createArc(startAng, angExtent) {
    const env = this._dim.getEnvelope();

    const xRadius = env.getWidth() / 2.0;
    const yRadius = env.getHeight() / 2.0;
    const centreX = env.getMinX() + xRadius;
    const centreY = env.getMinY() + yRadius;
    let angSize = angExtent;
    if (angSize <= 0.0 || angSize > 2 * Math.PI) angSize = 2 * Math.PI;
    const angInc = angSize / (this._nPts - 1);
    const pts = new Array(this._nPts).fill(null);
    let iPt = 0;

    for (let i = 0; i < this._nPts; i++) {
      const ang = startAng + i * angInc;
      const x = xRadius * Math.cos(ang) + centreX;
      const y = yRadius * Math.sin(ang) + centreY;
      pts[iPt++] = this.coord(x, y);
    }

    const line = this._geomFact.createLineString(pts);

    return this.rotate(line);
  }

  rotate(geom) {
    if (this._rotationAngle !== 0.0) {
      const trans = AffineTransformation.rotationInstance(this._rotationAngle, this._dim.getCentre().x, this._dim.getCentre().y);
      geom.apply(trans);
    }

    return geom;
  }

  coord(x, y) {
    const pt = new Coordinate(x, y);

    this._precModel.makePrecise(pt);

    return pt;
  }

  createArcPolygon(startAng, angExtent) {
    const env = this._dim.getEnvelope();

    const xRadius = env.getWidth() / 2.0;
    const yRadius = env.getHeight() / 2.0;
    const centreX = env.getMinX() + xRadius;
    const centreY = env.getMinY() + yRadius;
    let angSize = angExtent;
    if (angSize <= 0.0 || angSize > 2 * Math.PI) angSize = 2 * Math.PI;
    const angInc = angSize / (this._nPts - 1);
    const pts = new Array(this._nPts + 2).fill(null);
    let iPt = 0;
    pts[iPt++] = this.coord(centreX, centreY);

    for (let i = 0; i < this._nPts; i++) {
      const ang = startAng + angInc * i;
      const x = xRadius * Math.cos(ang) + centreX;
      const y = yRadius * Math.sin(ang) + centreY;
      pts[iPt++] = this.coord(x, y);
    }

    pts[iPt++] = this.coord(centreX, centreY);

    const ring = this._geomFact.createLinearRing(pts);

    const poly = this._geomFact.createPolygon(ring);

    return this.rotate(poly);
  }

  createRectangle() {
    let i = null;
    let ipt = 0;
    let nSide = Math.trunc(this._nPts / 4);
    if (nSide < 1) nSide = 1;
    const XsegLen = this._dim.getEnvelope().getWidth() / nSide;
    const YsegLen = this._dim.getEnvelope().getHeight() / nSide;
    const pts = new Array(4 * nSide + 1).fill(null);

    const env = this._dim.getEnvelope();

    for (i = 0; i < nSide; i++) {
      const x = env.getMinX() + i * XsegLen;
      const y = env.getMinY();
      pts[ipt++] = this.coord(x, y);
    }

    for (i = 0; i < nSide; i++) {
      const x = env.getMaxX();
      const y = env.getMinY() + i * YsegLen;
      pts[ipt++] = this.coord(x, y);
    }

    for (i = 0; i < nSide; i++) {
      const x = env.getMaxX() - i * XsegLen;
      const y = env.getMaxY();
      pts[ipt++] = this.coord(x, y);
    }

    for (i = 0; i < nSide; i++) {
      const x = env.getMinX();
      const y = env.getMaxY() - i * YsegLen;
      pts[ipt++] = this.coord(x, y);
    }

    pts[ipt++] = new Coordinate(pts[0]);

    const ring = this._geomFact.createLinearRing(pts);

    const poly = this._geomFact.createPolygon(ring);

    return this.rotate(poly);
  }

  createCircle() {
    return this.createEllipse();
  }

  setHeight(height) {
    this._dim.setHeight(height);
  }

  setSize(size) {
    this._dim.setSize(size);
  }

}

class Dimensions {
  constructor() {
    Dimensions.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.base = null;
    this.centre = null;
    this.width = null;
    this.height = null;
  }

  setBase(base) {
    this.base = base;
  }

  setWidth(width) {
    this.width = width;
  }

  getBase() {
    return this.base;
  }

  getWidth() {
    return this.width;
  }

  setEnvelope(env) {
    this.width = env.getWidth();
    this.height = env.getHeight();
    this.base = new Coordinate(env.getMinX(), env.getMinY());
    this.centre = new Coordinate(env.centre());
  }

  setCentre(centre) {
    this.centre = centre;
  }

  getMinSize() {
    return Math.min(this.width, this.height);
  }

  getEnvelope() {
    if (this.base !== null) return new Envelope(this.base.x, this.base.x + this.width, this.base.y, this.base.y + this.height);
    if (this.centre !== null) return new Envelope(this.centre.x - this.width / 2, this.centre.x + this.width / 2, this.centre.y - this.height / 2, this.centre.y + this.height / 2);
    return new Envelope(0, this.width, 0, this.height);
  }

  getCentre() {
    if (this.centre === null) this.centre = new Coordinate(this.base.x + this.width / 2, this.base.y + this.height / 2);
    return this.centre;
  }

  getHeight() {
    return this.height;
  }

  setHeight(height) {
    this.height = height;
  }

  setSize(size) {
    this.height = size;
    this.width = size;
  }

}

GeometricShapeFactory.Dimensions = Dimensions;

class SineStarFactory extends GeometricShapeFactory {
  constructor() {
    super();
    SineStarFactory.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._numArms = 8;
    this._armLengthRatio = 0.5;

    if (arguments.length === 0) {
      GeometricShapeFactory.constructor_.call(this);
    } else if (arguments.length === 1) {
      const geomFact = arguments[0];
      GeometricShapeFactory.constructor_.call(this, geomFact);
    }
  }

  static create(origin, size, nPts, nArms, armLengthRatio) {
    const gsf = new SineStarFactory();
    gsf.setCentre(origin);
    gsf.setSize(size);
    gsf.setNumPoints(nPts);
    gsf.setArmLengthRatio(armLengthRatio);
    gsf.setNumArms(nArms);
    const poly = gsf.createSineStar();
    return poly;
  }

  setNumArms(numArms) {
    this._numArms = numArms;
  }

  setArmLengthRatio(armLengthRatio) {
    this._armLengthRatio = armLengthRatio;
  }

  createSineStar() {
    const env = this._dim.getEnvelope();

    const radius = env.getWidth() / 2.0;
    let armRatio = this._armLengthRatio;
    if (armRatio < 0.0) armRatio = 0.0;
    if (armRatio > 1.0) armRatio = 1.0;
    const armMaxLen = armRatio * radius;
    const insideRadius = (1 - armRatio) * radius;
    const centreX = env.getMinX() + radius;
    const centreY = env.getMinY() + radius;
    const pts = new Array(this._nPts + 1).fill(null);
    let iPt = 0;

    for (let i = 0; i < this._nPts; i++) {
      const ptArcFrac = i / this._nPts * this._numArms;
      const armAngFrac = ptArcFrac - Math.floor(ptArcFrac);
      const armAng = 2 * Math.PI * armAngFrac;
      const armLenFrac = (Math.cos(armAng) + 1.0) / 2.0;
      const curveRadius = insideRadius + armMaxLen * armLenFrac;
      const ang = i * (2 * Math.PI / this._nPts);
      const x = curveRadius * Math.cos(ang) + centreX;
      const y = curveRadius * Math.sin(ang) + centreY;
      pts[iPt++] = this.coord(x, y);
    }

    pts[iPt] = new Coordinate(pts[0]);

    const ring = this._geomFact.createLinearRing(pts);

    const poly = this._geomFact.createPolygon(ring);

    return poly;
  }

}

var util$1 = /*#__PURE__*/Object.freeze({
  __proto__: null,
  AffineTransformation: AffineTransformation,
  AffineTransformationBuilder: AffineTransformationBuilder,
  AffineTransformationFactory: AffineTransformationFactory,
  ComponentCoordinateExtracter: ComponentCoordinateExtracter,
  GeometryCollectionMapper: GeometryCollectionMapper,
  GeometryCombiner: GeometryCombiner,
  GeometryEditor: GeometryEditor,
  GeometryExtracter: GeometryExtracter,
  GeometryMapper: GeometryMapper,
  GeometryTransformer: GeometryTransformer,
  LineStringExtracter: LineStringExtracter,
  LinearComponentExtracter: LinearComponentExtracter,
  PointExtracter: PointExtracter,
  PolygonExtracter: PolygonExtracter,
  ShortCircuitedGeometryVisitor: ShortCircuitedGeometryVisitor,
  SineStarFactory: SineStarFactory
});

var geom = /*#__PURE__*/Object.freeze({
  __proto__: null,
  Coordinate: Coordinate,
  CoordinateXY: CoordinateXY,
  CoordinateXYM: CoordinateXYM,
  CoordinateXYZM: CoordinateXYZM,
  CoordinateList: CoordinateList,
  CoordinateSequenceFilter: CoordinateSequenceFilter,
  Envelope: Envelope,
  LineSegment: LineSegment,
  GeometryFactory: GeometryFactory,
  Geometry: Geometry,
  Point: Point,
  LineString: LineString,
  LinearRing: LinearRing,
  Polygon: Polygon,
  GeometryCollection: GeometryCollection,
  MultiPoint: MultiPoint,
  MultiLineString: MultiLineString,
  MultiPolygon: MultiPolygon,
  Dimension: Dimension,
  IntersectionMatrix: IntersectionMatrix,
  PrecisionModel: PrecisionModel,
  Location: Location,
  Triangle: Triangle,
  util: util$1
});

class PointPairDistance {
  constructor() {
    PointPairDistance.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pt = [new Coordinate(), new Coordinate()];
    this._distance = Double.NaN;
    this._isNull = true;
  }

  getCoordinates() {
    return this._pt;
  }

  getCoordinate(i) {
    return this._pt[i];
  }

  setMinimum() {
    if (arguments.length === 1) {
      const ptDist = arguments[0];
      this.setMinimum(ptDist._pt[0], ptDist._pt[1]);
    } else if (arguments.length === 2) {
      const p0 = arguments[0],
            p1 = arguments[1];

      if (this._isNull) {
        this.initialize(p0, p1);
        return null;
      }

      const dist = p0.distance(p1);
      if (dist < this._distance) this.initialize(p0, p1, dist);
    }
  }

  initialize() {
    if (arguments.length === 0) {
      this._isNull = true;
    } else if (arguments.length === 2) {
      const p0 = arguments[0],
            p1 = arguments[1];

      this._pt[0].setCoordinate(p0);

      this._pt[1].setCoordinate(p1);

      this._distance = p0.distance(p1);
      this._isNull = false;
    } else if (arguments.length === 3) {
      const p0 = arguments[0],
            p1 = arguments[1],
            distance = arguments[2];

      this._pt[0].setCoordinate(p0);

      this._pt[1].setCoordinate(p1);

      this._distance = distance;
      this._isNull = false;
    }
  }

  toString() {
    return WKTWriter.toLineString(this._pt[0], this._pt[1]);
  }

  getDistance() {
    return this._distance;
  }

  setMaximum() {
    if (arguments.length === 1) {
      const ptDist = arguments[0];
      this.setMaximum(ptDist._pt[0], ptDist._pt[1]);
    } else if (arguments.length === 2) {
      const p0 = arguments[0],
            p1 = arguments[1];

      if (this._isNull) {
        this.initialize(p0, p1);
        return null;
      }

      const dist = p0.distance(p1);
      if (dist > this._distance) this.initialize(p0, p1, dist);
    }
  }

}

class DistanceToPoint {
  static computeDistance() {
    if (arguments[2] instanceof PointPairDistance && arguments[0] instanceof LineString && arguments[1] instanceof Coordinate) {
      const line = arguments[0],
            pt = arguments[1],
            ptDist = arguments[2];
      const tempSegment = new LineSegment();
      const coords = line.getCoordinates();

      for (let i = 0; i < coords.length - 1; i++) {
        tempSegment.setCoordinates(coords[i], coords[i + 1]);
        const closestPt = tempSegment.closestPoint(pt);
        ptDist.setMinimum(closestPt, pt);
      }
    } else if (arguments[2] instanceof PointPairDistance && arguments[0] instanceof Polygon && arguments[1] instanceof Coordinate) {
      const poly = arguments[0],
            pt = arguments[1],
            ptDist = arguments[2];
      DistanceToPoint.computeDistance(poly.getExteriorRing(), pt, ptDist);

      for (let i = 0; i < poly.getNumInteriorRing(); i++) DistanceToPoint.computeDistance(poly.getInteriorRingN(i), pt, ptDist);
    } else if (arguments[2] instanceof PointPairDistance && arguments[0] instanceof Geometry && arguments[1] instanceof Coordinate) {
      const geom = arguments[0],
            pt = arguments[1],
            ptDist = arguments[2];

      if (geom instanceof LineString) {
        DistanceToPoint.computeDistance(geom, pt, ptDist);
      } else if (geom instanceof Polygon) {
        DistanceToPoint.computeDistance(geom, pt, ptDist);
      } else if (geom instanceof GeometryCollection) {
        const gc = geom;

        for (let i = 0; i < gc.getNumGeometries(); i++) {
          const g = gc.getGeometryN(i);
          DistanceToPoint.computeDistance(g, pt, ptDist);
        }
      } else {
        ptDist.setMinimum(geom.getCoordinate(), pt);
      }
    } else if (arguments[2] instanceof PointPairDistance && arguments[0] instanceof LineSegment && arguments[1] instanceof Coordinate) {
      const segment = arguments[0],
            pt = arguments[1],
            ptDist = arguments[2];
      const closestPt = segment.closestPoint(pt);
      ptDist.setMinimum(closestPt, pt);
    }
  }

}

class DiscreteHausdorffDistance {
  constructor() {
    DiscreteHausdorffDistance.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._g0 = null;
    this._g1 = null;
    this._ptDist = new PointPairDistance();
    this._densifyFrac = 0.0;
    const g0 = arguments[0],
          g1 = arguments[1];
    this._g0 = g0;
    this._g1 = g1;
  }

  static distance() {
    if (arguments.length === 2) {
      const g0 = arguments[0],
            g1 = arguments[1];
      const dist = new DiscreteHausdorffDistance(g0, g1);
      return dist.distance();
    } else if (arguments.length === 3) {
      const g0 = arguments[0],
            g1 = arguments[1],
            densifyFrac = arguments[2];
      const dist = new DiscreteHausdorffDistance(g0, g1);
      dist.setDensifyFraction(densifyFrac);
      return dist.distance();
    }
  }

  getCoordinates() {
    return this._ptDist.getCoordinates();
  }

  setDensifyFraction(densifyFrac) {
    if (densifyFrac > 1.0 || densifyFrac <= 0.0) throw new IllegalArgumentException('Fraction is not in range (0.0 - 1.0]');
    this._densifyFrac = densifyFrac;
  }

  compute(g0, g1) {
    this.computeOrientedDistance(g0, g1, this._ptDist);
    this.computeOrientedDistance(g1, g0, this._ptDist);
  }

  distance() {
    this.compute(this._g0, this._g1);
    return this._ptDist.getDistance();
  }

  computeOrientedDistance(discreteGeom, geom, ptDist) {
    const distFilter = new MaxPointDistanceFilter(geom);
    discreteGeom.apply(distFilter);
    ptDist.setMaximum(distFilter.getMaxPointDistance());

    if (this._densifyFrac > 0) {
      const fracFilter = new MaxDensifiedByFractionDistanceFilter(geom, this._densifyFrac);
      discreteGeom.apply(fracFilter);
      ptDist.setMaximum(fracFilter.getMaxPointDistance());
    }
  }

  orientedDistance() {
    this.computeOrientedDistance(this._g0, this._g1, this._ptDist);
    return this._ptDist.getDistance();
  }

}

class MaxPointDistanceFilter {
  constructor() {
    MaxPointDistanceFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._maxPtDist = new PointPairDistance();
    this._minPtDist = new PointPairDistance();
    this._euclideanDist = new DistanceToPoint();
    this._geom = null;
    const geom = arguments[0];
    this._geom = geom;
  }

  filter(pt) {
    this._minPtDist.initialize();

    DistanceToPoint.computeDistance(this._geom, pt, this._minPtDist);

    this._maxPtDist.setMaximum(this._minPtDist);
  }

  getMaxPointDistance() {
    return this._maxPtDist;
  }

  get interfaces_() {
    return [CoordinateFilter];
  }

}

class MaxDensifiedByFractionDistanceFilter {
  constructor() {
    MaxDensifiedByFractionDistanceFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._maxPtDist = new PointPairDistance();
    this._minPtDist = new PointPairDistance();
    this._geom = null;
    this._numSubSegs = 0;
    const geom = arguments[0],
          fraction = arguments[1];
    this._geom = geom;
    this._numSubSegs = Math.trunc(Math.round(1.0 / fraction));
  }

  filter(seq, index) {
    if (index === 0) return null;
    const p0 = seq.getCoordinate(index - 1);
    const p1 = seq.getCoordinate(index);
    const delx = (p1.x - p0.x) / this._numSubSegs;
    const dely = (p1.y - p0.y) / this._numSubSegs;

    for (let i = 0; i < this._numSubSegs; i++) {
      const x = p0.x + i * delx;
      const y = p0.y + i * dely;
      const pt = new Coordinate(x, y);

      this._minPtDist.initialize();

      DistanceToPoint.computeDistance(this._geom, pt, this._minPtDist);

      this._maxPtDist.setMaximum(this._minPtDist);
    }
  }

  isDone() {
    return false;
  }

  isGeometryChanged() {
    return false;
  }

  getMaxPointDistance() {
    return this._maxPtDist;
  }

  get interfaces_() {
    return [CoordinateSequenceFilter];
  }

}

DiscreteHausdorffDistance.MaxPointDistanceFilter = MaxPointDistanceFilter;
DiscreteHausdorffDistance.MaxDensifiedByFractionDistanceFilter = MaxDensifiedByFractionDistanceFilter;

var distance_module = /*#__PURE__*/Object.freeze({
  __proto__: null,
  DiscreteHausdorffDistance: DiscreteHausdorffDistance,
  DistanceToPoint: DistanceToPoint,
  PointPairDistance: PointPairDistance
});

class ItemVisitor {
  visitItem(item) {}

}

class PointOnGeometryLocator {
  locate(p) {}

}

class IntervalRTreeNode {
  constructor() {
    IntervalRTreeNode.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._min = Double.POSITIVE_INFINITY;
    this._max = Double.NEGATIVE_INFINITY;
  }

  getMin() {
    return this._min;
  }

  intersects(queryMin, queryMax) {
    if (this._min > queryMax || this._max < queryMin) return false;
    return true;
  }

  getMax() {
    return this._max;
  }

  toString() {
    return WKTWriter.toLineString(new Coordinate(this._min, 0), new Coordinate(this._max, 0));
  }

}

class NodeComparator {
  compare(o1, o2) {
    const n1 = o1;
    const n2 = o2;
    const mid1 = (n1._min + n1._max) / 2;
    const mid2 = (n2._min + n2._max) / 2;
    if (mid1 < mid2) return -1;
    if (mid1 > mid2) return 1;
    return 0;
  }

  get interfaces_() {
    return [Comparator];
  }

}

IntervalRTreeNode.NodeComparator = NodeComparator;

class IntervalRTreeLeafNode extends IntervalRTreeNode {
  constructor() {
    super();
    IntervalRTreeLeafNode.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._item = null;
    const min = arguments[0],
          max = arguments[1],
          item = arguments[2];
    this._min = min;
    this._max = max;
    this._item = item;
  }

  query(queryMin, queryMax, visitor) {
    if (!this.intersects(queryMin, queryMax)) return null;
    visitor.visitItem(this._item);
  }

}

class IntervalRTreeBranchNode extends IntervalRTreeNode {
  constructor() {
    super();
    IntervalRTreeBranchNode.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._node1 = null;
    this._node2 = null;
    const n1 = arguments[0],
          n2 = arguments[1];
    this._node1 = n1;
    this._node2 = n2;
    this.buildExtent(this._node1, this._node2);
  }

  buildExtent(n1, n2) {
    this._min = Math.min(n1._min, n2._min);
    this._max = Math.max(n1._max, n2._max);
  }

  query(queryMin, queryMax, visitor) {
    if (!this.intersects(queryMin, queryMax)) return null;
    if (this._node1 !== null) this._node1.query(queryMin, queryMax, visitor);
    if (this._node2 !== null) this._node2.query(queryMin, queryMax, visitor);
  }

}

class SortedPackedIntervalRTree {
  constructor() {
    SortedPackedIntervalRTree.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._leaves = new ArrayList();
    this._root = null;
    this._level = 0;
  }

  buildTree() {
    Collections.sort(this._leaves, new IntervalRTreeNode.NodeComparator());
    let src = this._leaves;
    let temp = null;
    let dest = new ArrayList();

    while (true) {
      this.buildLevel(src, dest);
      if (dest.size() === 1) return dest.get(0);
      temp = src;
      src = dest;
      dest = temp;
    }
  }

  insert(min, max, item) {
    if (this._root !== null) throw new IllegalStateException('Index cannot be added to once it has been queried');

    this._leaves.add(new IntervalRTreeLeafNode(min, max, item));
  }

  query(min, max, visitor) {
    this.init();
    if (this._root === null) return null;

    this._root.query(min, max, visitor);
  }

  buildRoot() {
    if (this._root !== null) return null;
    this._root = this.buildTree();
  }

  printNode(node) {
    System.out.println(WKTWriter.toLineString(new Coordinate(node._min, this._level), new Coordinate(node._max, this._level)));
  }

  init() {
    if (this._root !== null) return null;
    if (this._leaves.size() === 0) return null;
    this.buildRoot();
  }

  buildLevel(src, dest) {
    this._level++;
    dest.clear();

    for (let i = 0; i < src.size(); i += 2) {
      const n1 = src.get(i);
      const n2 = i + 1 < src.size() ? src.get(i) : null;

      if (n2 === null) {
        dest.add(n1);
      } else {
        const node = new IntervalRTreeBranchNode(src.get(i), src.get(i + 1));
        dest.add(node);
      }
    }
  }

}

class ArrayListVisitor {
  constructor() {
    ArrayListVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._items = new ArrayList();
  }

  visitItem(item) {
    this._items.add(item);
  }

  getItems() {
    return this._items;
  }

  get interfaces_() {
    return [ItemVisitor];
  }

}

class RayCrossingCounter {
  constructor() {
    RayCrossingCounter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._p = null;
    this._crossingCount = 0;
    this._isPointOnSegment = false;
    const p = arguments[0];
    this._p = p;
  }

  static locatePointInRing() {
    if (arguments[0] instanceof Coordinate && hasInterface(arguments[1], CoordinateSequence)) {
      const p = arguments[0],
            ring = arguments[1];
      const counter = new RayCrossingCounter(p);
      const p1 = new Coordinate();
      const p2 = new Coordinate();

      for (let i = 1; i < ring.size(); i++) {
        ring.getCoordinate(i, p1);
        ring.getCoordinate(i - 1, p2);
        counter.countSegment(p1, p2);
        if (counter.isOnSegment()) return counter.getLocation();
      }

      return counter.getLocation();
    } else if (arguments[0] instanceof Coordinate && arguments[1] instanceof Array) {
      const p = arguments[0],
            ring = arguments[1];
      const counter = new RayCrossingCounter(p);

      for (let i = 1; i < ring.length; i++) {
        const p1 = ring[i];
        const p2 = ring[i - 1];
        counter.countSegment(p1, p2);
        if (counter.isOnSegment()) return counter.getLocation();
      }

      return counter.getLocation();
    }
  }

  countSegment(p1, p2) {
    if (p1.x < this._p.x && p2.x < this._p.x) return null;

    if (this._p.x === p2.x && this._p.y === p2.y) {
      this._isPointOnSegment = true;
      return null;
    }

    if (p1.y === this._p.y && p2.y === this._p.y) {
      let minx = p1.x;
      let maxx = p2.x;

      if (minx > maxx) {
        minx = p2.x;
        maxx = p1.x;
      }

      if (this._p.x >= minx && this._p.x <= maxx) this._isPointOnSegment = true;
      return null;
    }

    if (p1.y > this._p.y && p2.y <= this._p.y || p2.y > this._p.y && p1.y <= this._p.y) {
      let orient = Orientation.index(p1, p2, this._p);

      if (orient === Orientation.COLLINEAR) {
        this._isPointOnSegment = true;
        return null;
      }

      if (p2.y < p1.y) orient = -orient;
      if (orient === Orientation.LEFT) this._crossingCount++;
    }
  }

  isPointInPolygon() {
    return this.getLocation() !== Location.EXTERIOR;
  }

  getLocation() {
    if (this._isPointOnSegment) return Location.BOUNDARY;
    if (this._crossingCount % 2 === 1) return Location.INTERIOR;
    return Location.EXTERIOR;
  }

  isOnSegment() {
    return this._isPointOnSegment;
  }

}

class IndexedPointInAreaLocator {
  constructor() {
    IndexedPointInAreaLocator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geom = null;
    this._index = null;
    const g = arguments[0];
    if (!(hasInterface(g, Polygonal) || g instanceof LinearRing)) throw new IllegalArgumentException('Argument must be Polygonal or LinearRing');
    this._geom = g;
  }

  locate(p) {
    if (this._index === null) {
      this._index = new IntervalIndexedGeometry(this._geom);
      this._geom = null;
    }

    const rcc = new RayCrossingCounter(p);
    const visitor = new SegmentVisitor(rcc);

    this._index.query(p.y, p.y, visitor);

    return rcc.getLocation();
  }

  get interfaces_() {
    return [PointOnGeometryLocator];
  }

}

class SegmentVisitor {
  constructor() {
    SegmentVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._counter = null;
    const counter = arguments[0];
    this._counter = counter;
  }

  visitItem(item) {
    const seg = item;

    this._counter.countSegment(seg.getCoordinate(0), seg.getCoordinate(1));
  }

  get interfaces_() {
    return [ItemVisitor];
  }

}

class IntervalIndexedGeometry {
  constructor() {
    IntervalIndexedGeometry.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._isEmpty = false;
    this._index = new SortedPackedIntervalRTree();
    const geom = arguments[0];
    if (geom.isEmpty()) this._isEmpty = true;else this.init(geom);
  }

  init(geom) {
    const lines = LinearComponentExtracter.getLines(geom);

    for (let i = lines.iterator(); i.hasNext();) {
      const line = i.next();
      const pts = line.getCoordinates();
      this.addLine(pts);
    }
  }

  addLine(pts) {
    for (let i = 1; i < pts.length; i++) {
      const seg = new LineSegment(pts[i - 1], pts[i]);
      const min = Math.min(seg.p0.y, seg.p1.y);
      const max = Math.max(seg.p0.y, seg.p1.y);

      this._index.insert(min, max, seg);
    }
  }

  query() {
    if (arguments.length === 2) {
      const min = arguments[0],
            max = arguments[1];
      if (this._isEmpty) return new ArrayList();
      const visitor = new ArrayListVisitor();

      this._index.query(min, max, visitor);

      return visitor.getItems();
    } else if (arguments.length === 3) {
      const min = arguments[0],
            max = arguments[1],
            visitor = arguments[2];
      if (this._isEmpty) return null;

      this._index.query(min, max, visitor);
    }
  }

}

IndexedPointInAreaLocator.SegmentVisitor = SegmentVisitor;
IndexedPointInAreaLocator.IntervalIndexedGeometry = IntervalIndexedGeometry;

class PointLocation {
  static isOnLine() {
    if (arguments[0] instanceof Coordinate && hasInterface(arguments[1], CoordinateSequence)) {
      const p = arguments[0],
            line = arguments[1];
      const lineIntersector = new RobustLineIntersector();
      const p0 = new Coordinate();
      const p1 = new Coordinate();
      const n = line.size();

      for (let i = 1; i < n; i++) {
        line.getCoordinate(i - 1, p0);
        line.getCoordinate(i, p1);
        lineIntersector.computeIntersection(p, p0, p1);
        if (lineIntersector.hasIntersection()) return true;
      }

      return false;
    } else if (arguments[0] instanceof Coordinate && arguments[1] instanceof Array) {
      const p = arguments[0],
            line = arguments[1];
      const lineIntersector = new RobustLineIntersector();

      for (let i = 1; i < line.length; i++) {
        const p0 = line[i - 1];
        const p1 = line[i];
        lineIntersector.computeIntersection(p, p0, p1);
        if (lineIntersector.hasIntersection()) return true;
      }

      return false;
    }
  }

  static locateInRing(p, ring) {
    return RayCrossingCounter.locatePointInRing(p, ring);
  }

  static isInRing(p, ring) {
    return PointLocation.locateInRing(p, ring) !== Location.EXTERIOR;
  }

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/Iterator.html
 * @constructor
 * @private
 */
class Iterator {
  /**
     * Returns true if the iteration has more elements.
     * @return {boolean}
     */
  hasNext() {}
  /**
     * Returns the next element in the iteration.
     * @return {Object}
     */


  next() {}
  /**
     * Removes from the underlying collection the last element returned by the
     * iterator (optional operation).
     */


  remove() {}

}

class GeometryCollectionIterator {
  constructor() {
    GeometryCollectionIterator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._parent = null;
    this._atStart = null;
    this._max = null;
    this._index = null;
    this._subcollectionIterator = null;
    const parent = arguments[0];
    this._parent = parent;
    this._atStart = true;
    this._index = 0;
    this._max = parent.getNumGeometries();
  }

  static isAtomic(geom) {
    return !(geom instanceof GeometryCollection);
  }

  next() {
    if (this._atStart) {
      this._atStart = false;
      if (GeometryCollectionIterator.isAtomic(this._parent)) this._index++;
      return this._parent;
    }

    if (this._subcollectionIterator !== null) if (this._subcollectionIterator.hasNext()) return this._subcollectionIterator.next();else this._subcollectionIterator = null;
    if (this._index >= this._max) throw new NoSuchElementException();

    const obj = this._parent.getGeometryN(this._index++);

    if (obj instanceof GeometryCollection) {
      this._subcollectionIterator = new GeometryCollectionIterator(obj);
      return this._subcollectionIterator.next();
    }

    return obj;
  }

  remove() {
    throw new UnsupportedOperationException(this.getClass().getName());
  }

  hasNext() {
    if (this._atStart) return true;

    if (this._subcollectionIterator !== null) {
      if (this._subcollectionIterator.hasNext()) return true;
      this._subcollectionIterator = null;
    }

    if (this._index >= this._max) return false;
    return true;
  }

  get interfaces_() {
    return [Iterator];
  }

}

class SimplePointInAreaLocator {
  constructor() {
    SimplePointInAreaLocator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geom = null;
    const geom = arguments[0];
    this._geom = geom;
  }

  static locatePointInPolygon(p, poly) {
    if (poly.isEmpty()) return Location.EXTERIOR;
    const shell = poly.getExteriorRing();
    const shellLoc = SimplePointInAreaLocator.locatePointInRing(p, shell);
    if (shellLoc !== Location.INTERIOR) return shellLoc;

    for (let i = 0; i < poly.getNumInteriorRing(); i++) {
      const hole = poly.getInteriorRingN(i);
      const holeLoc = SimplePointInAreaLocator.locatePointInRing(p, hole);
      if (holeLoc === Location.BOUNDARY) return Location.BOUNDARY;
      if (holeLoc === Location.INTERIOR) return Location.EXTERIOR;
    }

    return Location.INTERIOR;
  }

  static locatePointInRing(p, ring) {
    if (!ring.getEnvelopeInternal().intersects(p)) return Location.EXTERIOR;
    return PointLocation.locateInRing(p, ring.getCoordinates());
  }

  static containsPointInPolygon(p, poly) {
    return Location.EXTERIOR !== SimplePointInAreaLocator.locatePointInPolygon(p, poly);
  }

  static locateInGeometry(p, geom) {
    if (geom instanceof Polygon) return SimplePointInAreaLocator.locatePointInPolygon(p, geom);

    if (geom instanceof GeometryCollection) {
      const geomi = new GeometryCollectionIterator(geom);

      while (geomi.hasNext()) {
        const g2 = geomi.next();

        if (g2 !== geom) {
          const loc = SimplePointInAreaLocator.locateInGeometry(p, g2);
          if (loc !== Location.EXTERIOR) return loc;
        }
      }
    }

    return Location.EXTERIOR;
  }

  static isContained(p, geom) {
    return Location.EXTERIOR !== SimplePointInAreaLocator.locate(p, geom);
  }

  static locate(p, geom) {
    if (geom.isEmpty()) return Location.EXTERIOR;
    if (!geom.getEnvelopeInternal().intersects(p)) return Location.EXTERIOR;
    return SimplePointInAreaLocator.locateInGeometry(p, geom);
  }

  locate(p) {
    return SimplePointInAreaLocator.locate(p, this._geom);
  }

  get interfaces_() {
    return [PointOnGeometryLocator];
  }

}

var locate = /*#__PURE__*/Object.freeze({
  __proto__: null,
  IndexedPointInAreaLocator: IndexedPointInAreaLocator,
  PointOnGeometryLocator: PointOnGeometryLocator,
  SimplePointInAreaLocator: SimplePointInAreaLocator
});

class SimilarityMeasure {
  measure(g1, g2) {}

}

class AreaSimilarityMeasure {
  measure(g1, g2) {
    const areaInt = g1.intersection(g2).getArea();
    const areaUnion = g1.union(g2).getArea();
    return areaInt / areaUnion;
  }

  get interfaces_() {
    return [SimilarityMeasure];
  }

}

class HausdorffSimilarityMeasure {
  static diagonalSize(env) {
    if (env.isNull()) return 0.0;
    const width = env.getWidth();
    const hgt = env.getHeight();
    return Math.sqrt(width * width + hgt * hgt);
  }

  measure(g1, g2) {
    const distance = DiscreteHausdorffDistance.distance(g1, g2, HausdorffSimilarityMeasure.DENSIFY_FRACTION);
    const env = new Envelope(g1.getEnvelopeInternal());
    env.expandToInclude(g2.getEnvelopeInternal());
    const envSize = HausdorffSimilarityMeasure.diagonalSize(env);
    const measure = 1 - distance / envSize;
    return measure;
  }

  get interfaces_() {
    return [SimilarityMeasure];
  }

}
HausdorffSimilarityMeasure.DENSIFY_FRACTION = 0.25;

class SimilarityMeasureCombiner {
  static combine(measure1, measure2) {
    return Math.min(measure1, measure2);
  }

}

var match = /*#__PURE__*/Object.freeze({
  __proto__: null,
  AreaSimilarityMeasure: AreaSimilarityMeasure,
  HausdorffSimilarityMeasure: HausdorffSimilarityMeasure,
  SimilarityMeasure: SimilarityMeasure,
  SimilarityMeasureCombiner: SimilarityMeasureCombiner
});

class Centroid {
  constructor() {
    Centroid.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._areaBasePt = null;
    this._triangleCent3 = new Coordinate();
    this._areasum2 = 0;
    this._cg3 = new Coordinate();
    this._lineCentSum = new Coordinate();
    this._totalLength = 0.0;
    this._ptCount = 0;
    this._ptCentSum = new Coordinate();
    const geom = arguments[0];
    this._areaBasePt = null;
    this.add(geom);
  }

  static area2(p1, p2, p3) {
    return (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
  }

  static centroid3(p1, p2, p3, c) {
    c.x = p1.x + p2.x + p3.x;
    c.y = p1.y + p2.y + p3.y;
    return null;
  }

  static getCentroid(geom) {
    const cent = new Centroid(geom);
    return cent.getCentroid();
  }

  setAreaBasePoint(basePt) {
    this._areaBasePt = basePt;
  }

  addPoint(pt) {
    this._ptCount += 1;
    this._ptCentSum.x += pt.x;
    this._ptCentSum.y += pt.y;
  }

  addLineSegments(pts) {
    let lineLen = 0.0;

    for (let i = 0; i < pts.length - 1; i++) {
      const segmentLen = pts[i].distance(pts[i + 1]);
      if (segmentLen === 0.0) continue;
      lineLen += segmentLen;
      const midx = (pts[i].x + pts[i + 1].x) / 2;
      this._lineCentSum.x += segmentLen * midx;
      const midy = (pts[i].y + pts[i + 1].y) / 2;
      this._lineCentSum.y += segmentLen * midy;
    }

    this._totalLength += lineLen;
    if (lineLen === 0.0 && pts.length > 0) this.addPoint(pts[0]);
  }

  addHole(pts) {
    const isPositiveArea = Orientation.isCCW(pts);

    for (let i = 0; i < pts.length - 1; i++) this.addTriangle(this._areaBasePt, pts[i], pts[i + 1], isPositiveArea);

    this.addLineSegments(pts);
  }

  getCentroid() {
    const cent = new Coordinate();

    if (Math.abs(this._areasum2) > 0.0) {
      cent.x = this._cg3.x / 3 / this._areasum2;
      cent.y = this._cg3.y / 3 / this._areasum2;
    } else if (this._totalLength > 0.0) {
      cent.x = this._lineCentSum.x / this._totalLength;
      cent.y = this._lineCentSum.y / this._totalLength;
    } else if (this._ptCount > 0) {
      cent.x = this._ptCentSum.x / this._ptCount;
      cent.y = this._ptCentSum.y / this._ptCount;
    } else {
      return null;
    }

    return cent;
  }

  addShell(pts) {
    if (pts.length > 0) this.setAreaBasePoint(pts[0]);
    const isPositiveArea = !Orientation.isCCW(pts);

    for (let i = 0; i < pts.length - 1; i++) this.addTriangle(this._areaBasePt, pts[i], pts[i + 1], isPositiveArea);

    this.addLineSegments(pts);
  }

  addTriangle(p0, p1, p2, isPositiveArea) {
    const sign = isPositiveArea ? 1.0 : -1.0;
    Centroid.centroid3(p0, p1, p2, this._triangleCent3);
    const area2 = Centroid.area2(p0, p1, p2);
    this._cg3.x += sign * area2 * this._triangleCent3.x;
    this._cg3.y += sign * area2 * this._triangleCent3.y;
    this._areasum2 += sign * area2;
  }

  add() {
    if (arguments[0] instanceof Polygon) {
      const poly = arguments[0];
      this.addShell(poly.getExteriorRing().getCoordinates());

      for (let i = 0; i < poly.getNumInteriorRing(); i++) this.addHole(poly.getInteriorRingN(i).getCoordinates());
    } else if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];
      if (geom.isEmpty()) return null;

      if (geom instanceof Point) {
        this.addPoint(geom.getCoordinate());
      } else if (geom instanceof LineString) {
        this.addLineSegments(geom.getCoordinates());
      } else if (geom instanceof Polygon) {
        const poly = geom;
        this.add(poly);
      } else if (geom instanceof GeometryCollection) {
        const gc = geom;

        for (let i = 0; i < gc.getNumGeometries(); i++) this.add(gc.getGeometryN(i));
      }
    }
  }

}

class EmptyStackException extends Exception {
  constructor(message) {
    super(message);
    this.name = Object.keys({
      EmptyStackException
    })[0];
  }

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/Stack.html
 */

class Stack extends List {
  constructor() {
    super();
    this.array = [];
  }

  add(e) {
    this.array.push(e);
    return true;
  }

  get(index) {
    if (index < 0 || index >= this.size()) throw new IndexOutOfBoundsException();
    return this.array[index];
  }
  /**
   * Pushes an item onto the top of this stack.
   * @param {Object} e
   * @return {Object}
   */


  push(e) {
    this.array.push(e);
    return e;
  }
  /**
   * Removes the object at the top of this stack and returns that object as the value of this function.
   * @return {Object}
   */


  pop() {
    if (this.array.length === 0) throw new EmptyStackException();
    return this.array.pop();
  }
  /**
   * Looks at the object at the top of this stack without removing it from the
   * stack.
   * @return {Object}
   */


  peek() {
    if (this.array.length === 0) throw new EmptyStackException();
    return this.array[this.array.length - 1];
  }
  /**
   * Tests if this stack is empty.
   * @return {boolean} true if and only if this stack contains no items; false
   *         otherwise.
   */


  empty() {
    return this.array.length === 0;
  }
  /**
   * @return {boolean}
   */


  isEmpty() {
    return this.empty();
  }
  /**
   * Returns the 1-based position where an object is on this stack. If the object
   * o occurs as an item in this stack, this method returns the distance from the
   * top of the stack of the occurrence nearest the top of the stack; the topmost
   * item on the stack is considered to be at distance 1. The equals method is
   * used to compare o to the items in this stack.
   *
   * NOTE: does not currently actually use equals. (=== is used)
   *
   * @param {Object} o
   * @return {number} the 1-based position from the top of the stack where the
   *         object is located; the return value -1 indicates that the object is
   *         not on the stack.
   */


  search(o) {
    return this.array.indexOf(o);
  }
  /**
   * @return {number}
   */


  size() {
    return this.array.length;
  }
  /**
   * @return {Array}
   */


  toArray() {
    return this.array.slice();
  }

}

class UniqueCoordinateArrayFilter {
  constructor() {
    UniqueCoordinateArrayFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._coordSet = new HashSet();
    this._list = new ArrayList();
  }

  static filterCoordinates(coords) {
    const filter = new UniqueCoordinateArrayFilter();

    for (let i = 0; i < coords.length; i++) filter.filter(coords[i]);

    return filter.getCoordinates();
  }

  filter(coord) {
    if (this._coordSet.add(coord)) this._list.add(coord);
  }

  getCoordinates() {
    const coordinates = new Array(this._list.size()).fill(null);
    return this._list.toArray(coordinates);
  }

  get interfaces_() {
    return [CoordinateFilter];
  }

}

class ConvexHull {
  constructor() {
    ConvexHull.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geomFactory = null;
    this._inputPts = null;

    if (arguments.length === 1) {
      const geometry = arguments[0];
      ConvexHull.constructor_.call(this, ConvexHull.extractCoordinates(geometry), geometry.getFactory());
    } else if (arguments.length === 2) {
      const pts = arguments[0],
            geomFactory = arguments[1];
      this._inputPts = UniqueCoordinateArrayFilter.filterCoordinates(pts);
      this._geomFactory = geomFactory;
    }
  }

  static extractCoordinates(geom) {
    const filter = new UniqueCoordinateArrayFilter();
    geom.apply(filter);
    return filter.getCoordinates();
  }

  preSort(pts) {
    let t = null;

    for (let i = 1; i < pts.length; i++) if (pts[i].y < pts[0].y || pts[i].y === pts[0].y && pts[i].x < pts[0].x) {
      t = pts[0];
      pts[0] = pts[i];
      pts[i] = t;
    }

    Arrays.sort(pts, 1, pts.length, new RadialComparator(pts[0]));
    return pts;
  }

  computeOctRing(inputPts) {
    const octPts = this.computeOctPts(inputPts);
    const coordList = new CoordinateList();
    coordList.add(octPts, false);
    if (coordList.size() < 3) return null;
    coordList.closeRing();
    return coordList.toCoordinateArray();
  }

  lineOrPolygon(coordinates) {
    coordinates = this.cleanRing(coordinates);
    if (coordinates.length === 3) return this._geomFactory.createLineString([coordinates[0], coordinates[1]]);

    const linearRing = this._geomFactory.createLinearRing(coordinates);

    return this._geomFactory.createPolygon(linearRing);
  }

  cleanRing(original) {
    Assert.equals(original[0], original[original.length - 1]);
    const cleanedRing = new ArrayList();
    let previousDistinctCoordinate = null;

    for (let i = 0; i <= original.length - 2; i++) {
      const currentCoordinate = original[i];
      const nextCoordinate = original[i + 1];
      if (currentCoordinate.equals(nextCoordinate)) continue;
      if (previousDistinctCoordinate !== null && this.isBetween(previousDistinctCoordinate, currentCoordinate, nextCoordinate)) continue;
      cleanedRing.add(currentCoordinate);
      previousDistinctCoordinate = currentCoordinate;
    }

    cleanedRing.add(original[original.length - 1]);
    const cleanedRingCoordinates = new Array(cleanedRing.size()).fill(null);
    return cleanedRing.toArray(cleanedRingCoordinates);
  }

  isBetween(c1, c2, c3) {
    if (Orientation.index(c1, c2, c3) !== 0) return false;

    if (c1.x !== c3.x) {
      if (c1.x <= c2.x && c2.x <= c3.x) return true;
      if (c3.x <= c2.x && c2.x <= c1.x) return true;
    }

    if (c1.y !== c3.y) {
      if (c1.y <= c2.y && c2.y <= c3.y) return true;
      if (c3.y <= c2.y && c2.y <= c1.y) return true;
    }

    return false;
  }

  reduce(inputPts) {
    const polyPts = this.computeOctRing(inputPts);
    if (polyPts === null) return inputPts;
    const reducedSet = new TreeSet();

    for (let i = 0; i < polyPts.length; i++) reducedSet.add(polyPts[i]);

    for (let i = 0; i < inputPts.length; i++) if (!PointLocation.isInRing(inputPts[i], polyPts)) reducedSet.add(inputPts[i]);

    const reducedPts = CoordinateArrays.toCoordinateArray(reducedSet);
    if (reducedPts.length < 3) return this.padArray3(reducedPts);
    return reducedPts;
  }

  getConvexHull() {
    if (this._inputPts.length === 0) return this._geomFactory.createGeometryCollection();
    if (this._inputPts.length === 1) return this._geomFactory.createPoint(this._inputPts[0]);
    if (this._inputPts.length === 2) return this._geomFactory.createLineString(this._inputPts);
    let reducedPts = this._inputPts;
    if (this._inputPts.length > 50) reducedPts = this.reduce(this._inputPts);
    const sortedPts = this.preSort(reducedPts);
    const cHS = this.grahamScan(sortedPts);
    const cH = this.toCoordinateArray(cHS);
    return this.lineOrPolygon(cH);
  }

  padArray3(pts) {
    const pad = new Array(3).fill(null);

    for (let i = 0; i < pad.length; i++) if (i < pts.length) pad[i] = pts[i];else pad[i] = pts[0];

    return pad;
  }

  computeOctPts(inputPts) {
    const pts = new Array(8).fill(null);

    for (let j = 0; j < pts.length; j++) pts[j] = inputPts[0];

    for (let i = 1; i < inputPts.length; i++) {
      if (inputPts[i].x < pts[0].x) pts[0] = inputPts[i];
      if (inputPts[i].x - inputPts[i].y < pts[1].x - pts[1].y) pts[1] = inputPts[i];
      if (inputPts[i].y > pts[2].y) pts[2] = inputPts[i];
      if (inputPts[i].x + inputPts[i].y > pts[3].x + pts[3].y) pts[3] = inputPts[i];
      if (inputPts[i].x > pts[4].x) pts[4] = inputPts[i];
      if (inputPts[i].x - inputPts[i].y > pts[5].x - pts[5].y) pts[5] = inputPts[i];
      if (inputPts[i].y < pts[6].y) pts[6] = inputPts[i];
      if (inputPts[i].x + inputPts[i].y < pts[7].x + pts[7].y) pts[7] = inputPts[i];
    }

    return pts;
  }

  toCoordinateArray(stack) {
    const coordinates = new Array(stack.size()).fill(null);

    for (let i = 0; i < stack.size(); i++) {
      const coordinate = stack.get(i);
      coordinates[i] = coordinate;
    }

    return coordinates;
  }

  grahamScan(c) {
    let p = null;
    const ps = new Stack();
    ps.push(c[0]);
    ps.push(c[1]);
    ps.push(c[2]);

    for (let i = 3; i < c.length; i++) {
      p = ps.pop();

      while (!ps.empty() && Orientation.index(ps.peek(), p, c[i]) > 0) p = ps.pop();

      ps.push(p);
      ps.push(c[i]);
    }

    ps.push(c[0]);
    return ps;
  }

}

class RadialComparator {
  constructor() {
    RadialComparator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._origin = null;
    const origin = arguments[0];
    this._origin = origin;
  }

  static polarCompare(o, p, q) {
    const dxp = p.x - o.x;
    const dyp = p.y - o.y;
    const dxq = q.x - o.x;
    const dyq = q.y - o.y;
    const orient = Orientation.index(o, p, q);
    if (orient === Orientation.COUNTERCLOCKWISE) return 1;
    if (orient === Orientation.CLOCKWISE) return -1;
    const op = dxp * dxp + dyp * dyp;
    const oq = dxq * dxq + dyq * dyq;
    if (op < oq) return -1;
    if (op > oq) return 1;
    return 0;
  }

  compare(o1, o2) {
    const p1 = o1;
    const p2 = o2;
    return RadialComparator.polarCompare(this._origin, p1, p2);
  }

  get interfaces_() {
    return [Comparator];
  }

}

ConvexHull.RadialComparator = RadialComparator;

class InteriorPointArea {
  constructor() {
    InteriorPointArea.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._interiorPoint = null;
    this._maxWidth = -1;
    const g = arguments[0];
    this.process(g);
  }

  static getInteriorPoint(geom) {
    const intPt = new InteriorPointArea(geom);
    return intPt.getInteriorPoint();
  }

  static avg(a, b) {
    return (a + b) / 2.0;
  }

  getInteriorPoint() {
    return this._interiorPoint;
  }

  process(geom) {
    if (geom.isEmpty()) return null;

    if (geom instanceof Polygon) {
      this.processPolygon(geom);
    } else if (geom instanceof GeometryCollection) {
      const gc = geom;

      for (let i = 0; i < gc.getNumGeometries(); i++) this.process(gc.getGeometryN(i));
    }
  }

  processPolygon(polygon) {
    const intPtPoly = new InteriorPointPolygon(polygon);
    intPtPoly.process();
    const width = intPtPoly.getWidth();

    if (width > this._maxWidth) {
      this._maxWidth = width;
      this._interiorPoint = intPtPoly.getInteriorPoint();
    }
  }

}

class InteriorPointPolygon {
  constructor() {
    InteriorPointPolygon.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._polygon = null;
    this._interiorPointY = null;
    this._interiorSectionWidth = 0.0;
    this._interiorPoint = null;
    const polygon = arguments[0];
    this._polygon = polygon;
    this._interiorPointY = ScanLineYOrdinateFinder.getScanLineY(polygon);
  }

  static isEdgeCrossingCounted(p0, p1, scanY) {
    const y0 = p0.getY();
    const y1 = p1.getY();
    if (y0 === y1) return false;
    if (y0 === scanY && y1 < scanY) return false;
    if (y1 === scanY && y0 < scanY) return false;
    return true;
  }

  static intersectsHorizontalLine() {
    if (arguments.length === 2) {
      const env = arguments[0],
            y = arguments[1];
      if (y < env.getMinY()) return false;
      if (y > env.getMaxY()) return false;
      return true;
    } else if (arguments.length === 3) {
      const p0 = arguments[0],
            p1 = arguments[1],
            y = arguments[2];
      if (p0.getY() > y && p1.getY() > y) return false;
      if (p0.getY() < y && p1.getY() < y) return false;
      return true;
    }
  }

  static intersection(p0, p1, Y) {
    const x0 = p0.getX();
    const x1 = p1.getX();
    if (x0 === x1) return x0;
    const segDX = x1 - x0;
    const segDY = p1.getY() - p0.getY();
    const m = segDY / segDX;
    const x = x0 + (Y - p0.getY()) / m;
    return x;
  }

  findBestMidpoint(crossings) {
    if (crossings.size() === 0) return null;
    Assert.isTrue(0 === crossings.size() % 2, 'Interior Point robustness failure: odd number of scanline crossings');
    crossings.sort(new DoubleComparator());

    for (let i = 0; i < crossings.size(); i += 2) {
      const x1 = crossings.get(i);
      const x2 = crossings.get(i + 1);
      const width = x2 - x1;

      if (width > this._interiorSectionWidth) {
        this._interiorSectionWidth = width;
        const interiorPointX = InteriorPointArea.avg(x1, x2);
        this._interiorPoint = new Coordinate(interiorPointX, this._interiorPointY);
      }
    }
  }

  process() {
    if (this._polygon.isEmpty()) return null;
    this._interiorPoint = new Coordinate(this._polygon.getCoordinate());
    const crossings = new ArrayList();
    this.scanRing(this._polygon.getExteriorRing(), crossings);

    for (let i = 0; i < this._polygon.getNumInteriorRing(); i++) this.scanRing(this._polygon.getInteriorRingN(i), crossings);

    this.findBestMidpoint(crossings);
  }

  scanRing(ring, crossings) {
    if (!InteriorPointPolygon.intersectsHorizontalLine(ring.getEnvelopeInternal(), this._interiorPointY)) return null;
    const seq = ring.getCoordinateSequence();

    for (let i = 1; i < seq.size(); i++) {
      const ptPrev = seq.getCoordinate(i - 1);
      const pt = seq.getCoordinate(i);
      this.addEdgeCrossing(ptPrev, pt, this._interiorPointY, crossings);
    }
  }

  getWidth() {
    return this._interiorSectionWidth;
  }

  getInteriorPoint() {
    return this._interiorPoint;
  }

  addEdgeCrossing(p0, p1, scanY, crossings) {
    if (!InteriorPointPolygon.intersectsHorizontalLine(p0, p1, scanY)) return null;
    if (!InteriorPointPolygon.isEdgeCrossingCounted(p0, p1, scanY)) return null;
    const xInt = InteriorPointPolygon.intersection(p0, p1, scanY);
    crossings.add(xInt);
  }

}

class DoubleComparator {
  compare(v1, v2) {
    return v1 < v2 ? -1 : v1 > v2 ? +1 : 0;
  }

  get interfaces_() {
    return [Comparator];
  }

}

InteriorPointPolygon.DoubleComparator = DoubleComparator;

class ScanLineYOrdinateFinder {
  constructor() {
    ScanLineYOrdinateFinder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._poly = null;
    this._centreY = null;
    this._hiY = Double.MAX_VALUE;
    this._loY = -Double.MAX_VALUE;
    const poly = arguments[0];
    this._poly = poly;
    this._hiY = poly.getEnvelopeInternal().getMaxY();
    this._loY = poly.getEnvelopeInternal().getMinY();
    this._centreY = InteriorPointArea.avg(this._loY, this._hiY);
  }

  static getScanLineY(poly) {
    const finder = new ScanLineYOrdinateFinder(poly);
    return finder.getScanLineY();
  }

  updateInterval(y) {
    if (y <= this._centreY) {
      if (y > this._loY) this._loY = y;
    } else if (y > this._centreY) {
      if (y < this._hiY) this._hiY = y;
    }
  }

  getScanLineY() {
    this.process(this._poly.getExteriorRing());

    for (let i = 0; i < this._poly.getNumInteriorRing(); i++) this.process(this._poly.getInteriorRingN(i));

    const scanLineY = InteriorPointArea.avg(this._hiY, this._loY);
    return scanLineY;
  }

  process(line) {
    const seq = line.getCoordinateSequence();

    for (let i = 0; i < seq.size(); i++) {
      const y = seq.getY(i);
      this.updateInterval(y);
    }
  }

}

InteriorPointArea.InteriorPointPolygon = InteriorPointPolygon;
InteriorPointArea.ScanLineYOrdinateFinder = ScanLineYOrdinateFinder;

class InteriorPointLine {
  constructor() {
    InteriorPointLine.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._centroid = null;
    this._minDistance = Double.MAX_VALUE;
    this._interiorPoint = null;
    const g = arguments[0];

    if (g.isEmpty()) {
      this._centroid = null;
    } else {
      this._centroid = Centroid.getCentroid(g);
      g.getPrecisionModel().makePrecise(this._centroid);
    }

    this.addInterior(g);
    if (this._interiorPoint === null) this.addEndpoints(g);
  }

  static getInteriorPoint(geom) {
    const intPt = new InteriorPointLine(geom);
    return intPt.getInteriorPoint();
  }

  addEndpoints() {
    if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];

      if (geom instanceof LineString) {
        this.addEndpoints(geom.getCoordinates());
      } else if (geom instanceof GeometryCollection) {
        const gc = geom;

        for (let i = 0; i < gc.getNumGeometries(); i++) this.addEndpoints(gc.getGeometryN(i));
      }
    } else if (arguments[0] instanceof Array) {
      const pts = arguments[0];
      this.add(pts[0]);
      this.add(pts[pts.length - 1]);
    }
  }

  getInteriorPoint() {
    return this._interiorPoint;
  }

  addInterior() {
    if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];

      if (geom instanceof LineString) {
        this.addInterior(geom.getCoordinates());
      } else if (geom instanceof GeometryCollection) {
        const gc = geom;

        for (let i = 0; i < gc.getNumGeometries(); i++) this.addInterior(gc.getGeometryN(i));
      }
    } else if (arguments[0] instanceof Array) {
      const pts = arguments[0];

      for (let i = 1; i < pts.length - 1; i++) this.add(pts[i]);
    }
  }

  add(point) {
    const dist = point.distance(this._centroid);

    if (dist < this._minDistance) {
      this._interiorPoint = new Coordinate(point);
      this._minDistance = dist;
    }
  }

}

class InteriorPointPoint {
  constructor() {
    InteriorPointPoint.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._centroid = null;
    this._minDistance = Double.MAX_VALUE;
    this._interiorPoint = null;
    const g = arguments[0];
    this._centroid = Centroid.getCentroid(g);
    this.add(g);
  }

  static getInteriorPoint(geom) {
    const intPt = new InteriorPointPoint(geom);
    return intPt.getInteriorPoint();
  }

  getInteriorPoint() {
    return this._interiorPoint;
  }

  add() {
    if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];

      if (geom instanceof Point) {
        this.add(geom.getCoordinate());
      } else if (geom instanceof GeometryCollection) {
        const gc = geom;

        for (let i = 0; i < gc.getNumGeometries(); i++) this.add(gc.getGeometryN(i));
      }
    } else if (arguments[0] instanceof Coordinate) {
      const point = arguments[0];
      const dist = point.distance(this._centroid);

      if (dist < this._minDistance) {
        this._interiorPoint = new Coordinate(point);
        this._minDistance = dist;
      }
    }
  }

}

class BoundaryNodeRule {
  isInBoundary(boundaryCount) {}

}

class Mod2BoundaryNodeRule {
  isInBoundary(boundaryCount) {
    return boundaryCount % 2 === 1;
  }

  get interfaces_() {
    return [BoundaryNodeRule];
  }

}

class EndPointBoundaryNodeRule {
  isInBoundary(boundaryCount) {
    return boundaryCount > 0;
  }

  get interfaces_() {
    return [BoundaryNodeRule];
  }

}

class MultiValentEndPointBoundaryNodeRule {
  isInBoundary(boundaryCount) {
    return boundaryCount > 1;
  }

  get interfaces_() {
    return [BoundaryNodeRule];
  }

}

class MonoValentEndPointBoundaryNodeRule {
  isInBoundary(boundaryCount) {
    return boundaryCount === 1;
  }

  get interfaces_() {
    return [BoundaryNodeRule];
  }

}

BoundaryNodeRule.Mod2BoundaryNodeRule = Mod2BoundaryNodeRule;
BoundaryNodeRule.EndPointBoundaryNodeRule = EndPointBoundaryNodeRule;
BoundaryNodeRule.MultiValentEndPointBoundaryNodeRule = MultiValentEndPointBoundaryNodeRule;
BoundaryNodeRule.MonoValentEndPointBoundaryNodeRule = MonoValentEndPointBoundaryNodeRule;
BoundaryNodeRule.MOD2_BOUNDARY_RULE = new Mod2BoundaryNodeRule();
BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE = new EndPointBoundaryNodeRule();
BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE = new MultiValentEndPointBoundaryNodeRule();
BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE = new MonoValentEndPointBoundaryNodeRule();
BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE = BoundaryNodeRule.MOD2_BOUNDARY_RULE;

class PointLocator {
  constructor() {
    PointLocator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._boundaryRule = BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE;
    this._isIn = null;
    this._numBoundaries = null;

    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const boundaryRule = arguments[0];
      if (boundaryRule === null) throw new IllegalArgumentException('Rule must be non-null');
      this._boundaryRule = boundaryRule;
    }
  }

  locateInPolygonRing(p, ring) {
    if (!ring.getEnvelopeInternal().intersects(p)) return Location.EXTERIOR;
    return PointLocation.locateInRing(p, ring.getCoordinates());
  }

  intersects(p, geom) {
    return this.locate(p, geom) !== Location.EXTERIOR;
  }

  updateLocationInfo(loc) {
    if (loc === Location.INTERIOR) this._isIn = true;
    if (loc === Location.BOUNDARY) this._numBoundaries++;
  }

  computeLocation(p, geom) {
    if (geom instanceof Point) this.updateLocationInfo(this.locateOnPoint(p, geom));

    if (geom instanceof LineString) {
      this.updateLocationInfo(this.locateOnLineString(p, geom));
    } else if (geom instanceof Polygon) {
      this.updateLocationInfo(this.locateInPolygon(p, geom));
    } else if (geom instanceof MultiLineString) {
      const ml = geom;

      for (let i = 0; i < ml.getNumGeometries(); i++) {
        const l = ml.getGeometryN(i);
        this.updateLocationInfo(this.locateOnLineString(p, l));
      }
    } else if (geom instanceof MultiPolygon) {
      const mpoly = geom;

      for (let i = 0; i < mpoly.getNumGeometries(); i++) {
        const poly = mpoly.getGeometryN(i);
        this.updateLocationInfo(this.locateInPolygon(p, poly));
      }
    } else if (geom instanceof GeometryCollection) {
      const geomi = new GeometryCollectionIterator(geom);

      while (geomi.hasNext()) {
        const g2 = geomi.next();
        if (g2 !== geom) this.computeLocation(p, g2);
      }
    }
  }

  locateOnPoint(p, pt) {
    const ptCoord = pt.getCoordinate();
    if (ptCoord.equals2D(p)) return Location.INTERIOR;
    return Location.EXTERIOR;
  }

  locateOnLineString(p, l) {
    if (!l.getEnvelopeInternal().intersects(p)) return Location.EXTERIOR;
    const seq = l.getCoordinateSequence();
    if (!l.isClosed()) if (p.equals(seq.getCoordinate(0)) || p.equals(seq.getCoordinate(seq.size() - 1))) return Location.BOUNDARY;
    if (PointLocation.isOnLine(p, seq)) return Location.INTERIOR;
    return Location.EXTERIOR;
  }

  locateInPolygon(p, poly) {
    if (poly.isEmpty()) return Location.EXTERIOR;
    const shell = poly.getExteriorRing();
    const shellLoc = this.locateInPolygonRing(p, shell);
    if (shellLoc === Location.EXTERIOR) return Location.EXTERIOR;
    if (shellLoc === Location.BOUNDARY) return Location.BOUNDARY;

    for (let i = 0; i < poly.getNumInteriorRing(); i++) {
      const hole = poly.getInteriorRingN(i);
      const holeLoc = this.locateInPolygonRing(p, hole);
      if (holeLoc === Location.INTERIOR) return Location.EXTERIOR;
      if (holeLoc === Location.BOUNDARY) return Location.BOUNDARY;
    }

    return Location.INTERIOR;
  }

  locate(p, geom) {
    if (geom.isEmpty()) return Location.EXTERIOR;
    if (geom instanceof LineString) return this.locateOnLineString(p, geom);else if (geom instanceof Polygon) return this.locateInPolygon(p, geom);
    this._isIn = false;
    this._numBoundaries = 0;
    this.computeLocation(p, geom);
    if (this._boundaryRule.isInBoundary(this._numBoundaries)) return Location.BOUNDARY;
    if (this._numBoundaries > 0 || this._isIn) return Location.INTERIOR;
    return Location.EXTERIOR;
  }

}

class MinimumBoundingCircle {
  constructor() {
    MinimumBoundingCircle.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._input = null;
    this._extremalPts = null;
    this._centre = null;
    this._radius = 0.0;
    const geom = arguments[0];
    this._input = geom;
  }

  static farthestPoints(pts) {
    const dist01 = pts[0].distance(pts[1]);
    const dist12 = pts[1].distance(pts[2]);
    const dist20 = pts[2].distance(pts[0]);
    if (dist01 >= dist12 && dist01 >= dist20) return [pts[0], pts[1]];
    if (dist12 >= dist01 && dist12 >= dist20) return [pts[1], pts[2]];
    return [pts[2], pts[0]];
  }

  static pointWitMinAngleWithX(pts, P) {
    let minSin = Double.MAX_VALUE;
    let minAngPt = null;

    for (let i = 0; i < pts.length; i++) {
      const p = pts[i];
      if (p === P) continue;
      const dx = p.x - P.x;
      let dy = p.y - P.y;
      if (dy < 0) dy = -dy;
      const len = Math.sqrt(dx * dx + dy * dy);
      const sin = dy / len;

      if (sin < minSin) {
        minSin = sin;
        minAngPt = p;
      }
    }

    return minAngPt;
  }

  static lowestPoint(pts) {
    let min = pts[0];

    for (let i = 1; i < pts.length; i++) if (pts[i].y < min.y) min = pts[i];

    return min;
  }

  static pointWithMinAngleWithSegment(pts, P, Q) {
    let minAng = Double.MAX_VALUE;
    let minAngPt = null;

    for (let i = 0; i < pts.length; i++) {
      const p = pts[i];
      if (p === P) continue;
      if (p === Q) continue;
      const ang = Angle.angleBetween(P, p, Q);

      if (ang < minAng) {
        minAng = ang;
        minAngPt = p;
      }
    }

    return minAngPt;
  }

  getRadius() {
    this.compute();
    return this._radius;
  }

  getDiameter() {
    this.compute();

    switch (this._extremalPts.length) {
      case 0:
        return this._input.getFactory().createLineString();

      case 1:
        return this._input.getFactory().createPoint(this._centre);
    }

    const p0 = this._extremalPts[0];
    const p1 = this._extremalPts[1];
    return this._input.getFactory().createLineString([p0, p1]);
  }

  getExtremalPoints() {
    this.compute();
    return this._extremalPts;
  }

  computeCirclePoints() {
    if (this._input.isEmpty()) {
      this._extremalPts = new Array(0).fill(null);
      return null;
    }

    if (this._input.getNumPoints() === 1) {
      const pts = this._input.getCoordinates();

      this._extremalPts = [new Coordinate(pts[0])];
      return null;
    }

    const convexHull = this._input.convexHull();

    const hullPts = convexHull.getCoordinates();
    let pts = hullPts;

    if (hullPts[0].equals2D(hullPts[hullPts.length - 1])) {
      pts = new Array(hullPts.length - 1).fill(null);
      CoordinateArrays.copyDeep(hullPts, 0, pts, 0, hullPts.length - 1);
    }

    if (pts.length <= 2) {
      this._extremalPts = CoordinateArrays.copyDeep(pts);
      return null;
    }

    let P = MinimumBoundingCircle.lowestPoint(pts);
    let Q = MinimumBoundingCircle.pointWitMinAngleWithX(pts, P);

    for (let i = 0; i < pts.length; i++) {
      const R = MinimumBoundingCircle.pointWithMinAngleWithSegment(pts, P, Q);

      if (Angle.isObtuse(P, R, Q)) {
        this._extremalPts = [new Coordinate(P), new Coordinate(Q)];
        return null;
      }

      if (Angle.isObtuse(R, P, Q)) {
        P = R;
        continue;
      }

      if (Angle.isObtuse(R, Q, P)) {
        Q = R;
        continue;
      }

      this._extremalPts = [new Coordinate(P), new Coordinate(Q), new Coordinate(R)];
      return null;
    }

    Assert.shouldNeverReachHere('Logic failure in Minimum Bounding Circle algorithm!');
  }

  compute() {
    if (this._extremalPts !== null) return null;
    this.computeCirclePoints();
    this.computeCentre();
    if (this._centre !== null) this._radius = this._centre.distance(this._extremalPts[0]);
  }

  getCircle() {
    this.compute();
    if (this._centre === null) return this._input.getFactory().createPolygon();

    const centrePoint = this._input.getFactory().createPoint(this._centre);

    if (this._radius === 0.0) return centrePoint;
    return centrePoint.buffer(this._radius);
  }

  getCentre() {
    this.compute();
    return this._centre;
  }

  getMaximumDiameter() {
    this.compute();

    switch (this._extremalPts.length) {
      case 0:
        return this._input.getFactory().createLineString();

      case 1:
        return this._input.getFactory().createPoint(this._centre);

      case 2:
        return this._input.getFactory().createLineString([this._extremalPts[0], this._extremalPts[1]]);

      default:
        const maxDiameter = MinimumBoundingCircle.farthestPoints(this._extremalPts);
        return this._input.getFactory().createLineString(maxDiameter);
    }
  }

  computeCentre() {
    switch (this._extremalPts.length) {
      case 0:
        this._centre = null;
        break;

      case 1:
        this._centre = this._extremalPts[0];
        break;

      case 2:
        this._centre = new Coordinate((this._extremalPts[0].x + this._extremalPts[1].x) / 2.0, (this._extremalPts[0].y + this._extremalPts[1].y) / 2.0);
        break;

      case 3:
        this._centre = Triangle.circumcentre(this._extremalPts[0], this._extremalPts[1], this._extremalPts[2]);
        break;
    }
  }

}

class MinimumDiameter {
  constructor() {
    MinimumDiameter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._isConvex = null;
    this._convexHullPts = null;
    this._minBaseSeg = new LineSegment();
    this._minWidthPt = null;
    this._minPtIndex = null;
    this._minWidth = 0.0;

    if (arguments.length === 1) {
      const inputGeom = arguments[0];
      MinimumDiameter.constructor_.call(this, inputGeom, false);
    } else if (arguments.length === 2) {
      const inputGeom = arguments[0],
            isConvex = arguments[1];
      this._inputGeom = inputGeom;
      this._isConvex = isConvex;
    }
  }

  static nextIndex(pts, index) {
    index++;
    if (index >= pts.length) index = 0;
    return index;
  }

  static computeC(a, b, p) {
    return a * p.y - b * p.x;
  }

  static getMinimumDiameter(geom) {
    return new MinimumDiameter(geom).getDiameter();
  }

  static getMinimumRectangle(geom) {
    return new MinimumDiameter(geom).getMinimumRectangle();
  }

  static computeSegmentForLine(a, b, c) {
    let p0 = null;
    let p1 = null;

    if (Math.abs(b) > Math.abs(a)) {
      p0 = new Coordinate(0.0, c / b);
      p1 = new Coordinate(1.0, c / b - a / b);
    } else {
      p0 = new Coordinate(c / a, 0.0);
      p1 = new Coordinate(c / a - b / a, 1.0);
    }

    return new LineSegment(p0, p1);
  }

  getWidthCoordinate() {
    this.computeMinimumDiameter();
    return this._minWidthPt;
  }

  getSupportingSegment() {
    this.computeMinimumDiameter();
    return this._inputGeom.getFactory().createLineString([this._minBaseSeg.p0, this._minBaseSeg.p1]);
  }

  getDiameter() {
    this.computeMinimumDiameter();
    if (this._minWidthPt === null) return this._inputGeom.getFactory().createLineString();

    const basePt = this._minBaseSeg.project(this._minWidthPt);

    return this._inputGeom.getFactory().createLineString([basePt, this._minWidthPt]);
  }

  computeWidthConvex(convexGeom) {
    if (convexGeom instanceof Polygon) this._convexHullPts = convexGeom.getExteriorRing().getCoordinates();else this._convexHullPts = convexGeom.getCoordinates();

    if (this._convexHullPts.length === 0) {
      this._minWidth = 0.0;
      this._minWidthPt = null;
      this._minBaseSeg = null;
    } else if (this._convexHullPts.length === 1) {
      this._minWidth = 0.0;
      this._minWidthPt = this._convexHullPts[0];
      this._minBaseSeg.p0 = this._convexHullPts[0];
      this._minBaseSeg.p1 = this._convexHullPts[0];
    } else if (this._convexHullPts.length === 2 || this._convexHullPts.length === 3) {
      this._minWidth = 0.0;
      this._minWidthPt = this._convexHullPts[0];
      this._minBaseSeg.p0 = this._convexHullPts[0];
      this._minBaseSeg.p1 = this._convexHullPts[1];
    } else {
      this.computeConvexRingMinDiameter(this._convexHullPts);
    }
  }

  computeConvexRingMinDiameter(pts) {
    this._minWidth = Double.MAX_VALUE;
    let currMaxIndex = 1;
    const seg = new LineSegment();

    for (let i = 0; i < pts.length - 1; i++) {
      seg.p0 = pts[i];
      seg.p1 = pts[i + 1];
      currMaxIndex = this.findMaxPerpDistance(pts, seg, currMaxIndex);
    }
  }

  computeMinimumDiameter() {
    if (this._minWidthPt !== null) return null;

    if (this._isConvex) {
      this.computeWidthConvex(this._inputGeom);
    } else {
      const convexGeom = new ConvexHull(this._inputGeom).getConvexHull();
      this.computeWidthConvex(convexGeom);
    }
  }

  getLength() {
    this.computeMinimumDiameter();
    return this._minWidth;
  }

  findMaxPerpDistance(pts, seg, startIndex) {
    let maxPerpDistance = seg.distancePerpendicular(pts[startIndex]);
    let nextPerpDistance = maxPerpDistance;
    let maxIndex = startIndex;
    let nextIndex = maxIndex;

    while (nextPerpDistance >= maxPerpDistance) {
      maxPerpDistance = nextPerpDistance;
      maxIndex = nextIndex;
      nextIndex = MinimumDiameter.nextIndex(pts, maxIndex);
      nextPerpDistance = seg.distancePerpendicular(pts[nextIndex]);
    }

    if (maxPerpDistance < this._minWidth) {
      this._minPtIndex = maxIndex;
      this._minWidth = maxPerpDistance;
      this._minWidthPt = pts[this._minPtIndex];
      this._minBaseSeg = new LineSegment(seg);
    }

    return maxIndex;
  }

  getMinimumRectangle() {
    this.computeMinimumDiameter();

    if (this._minWidth === 0.0) {
      if (this._minBaseSeg.p0.equals2D(this._minBaseSeg.p1)) return this._inputGeom.getFactory().createPoint(this._minBaseSeg.p0);
      return this._minBaseSeg.toGeometry(this._inputGeom.getFactory());
    }

    const dx = this._minBaseSeg.p1.x - this._minBaseSeg.p0.x;
    const dy = this._minBaseSeg.p1.y - this._minBaseSeg.p0.y;
    let minPara = Double.MAX_VALUE;
    let maxPara = -Double.MAX_VALUE;
    let minPerp = Double.MAX_VALUE;
    let maxPerp = -Double.MAX_VALUE;

    for (let i = 0; i < this._convexHullPts.length; i++) {
      const paraC = MinimumDiameter.computeC(dx, dy, this._convexHullPts[i]);
      if (paraC > maxPara) maxPara = paraC;
      if (paraC < minPara) minPara = paraC;
      const perpC = MinimumDiameter.computeC(-dy, dx, this._convexHullPts[i]);
      if (perpC > maxPerp) maxPerp = perpC;
      if (perpC < minPerp) minPerp = perpC;
    }

    const maxPerpLine = MinimumDiameter.computeSegmentForLine(-dx, -dy, maxPerp);
    const minPerpLine = MinimumDiameter.computeSegmentForLine(-dx, -dy, minPerp);
    const maxParaLine = MinimumDiameter.computeSegmentForLine(-dy, dx, maxPara);
    const minParaLine = MinimumDiameter.computeSegmentForLine(-dy, dx, minPara);
    const p0 = maxParaLine.lineIntersection(maxPerpLine);
    const p1 = minParaLine.lineIntersection(maxPerpLine);
    const p2 = minParaLine.lineIntersection(minPerpLine);
    const p3 = maxParaLine.lineIntersection(minPerpLine);

    const shell = this._inputGeom.getFactory().createLinearRing([p0, p1, p2, p3, p0]);

    return this._inputGeom.getFactory().createPolygon(shell);
  }

}

var algorithm = /*#__PURE__*/Object.freeze({
  __proto__: null,
  distance: distance_module,
  locate: locate,
  match: match,
  Angle: Angle,
  Area: Area,
  Centroid: Centroid,
  ConvexHull: ConvexHull,
  Distance: Distance,
  InteriorPointArea: InteriorPointArea,
  InteriorPointLine: InteriorPointLine,
  InteriorPointPoint: InteriorPointPoint,
  Length: Length,
  Orientation: Orientation,
  PointLocation: PointLocation,
  PointLocator: PointLocator,
  RobustLineIntersector: RobustLineIntersector,
  MinimumBoundingCircle: MinimumBoundingCircle,
  MinimumDiameter: MinimumDiameter
});

class Densifier {
  constructor() {
    Densifier.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._distanceTolerance = null;
    const inputGeom = arguments[0];
    this._inputGeom = inputGeom;
  }

  static densifyPoints(pts, distanceTolerance, precModel) {
    const seg = new LineSegment();
    const coordList = new CoordinateList();

    for (let i = 0; i < pts.length - 1; i++) {
      seg.p0 = pts[i];
      seg.p1 = pts[i + 1];
      coordList.add(seg.p0, false);
      const len = seg.getLength();
      const densifiedSegCount = Math.trunc(len / distanceTolerance) + 1;

      if (densifiedSegCount > 1) {
        const densifiedSegLen = len / densifiedSegCount;

        for (let j = 1; j < densifiedSegCount; j++) {
          const segFract = j * densifiedSegLen / len;
          const p = seg.pointAlong(segFract);
          precModel.makePrecise(p);
          coordList.add(p, false);
        }
      }
    }

    coordList.add(pts[pts.length - 1], false);
    return coordList.toCoordinateArray();
  }

  static densify(geom, distanceTolerance) {
    const densifier = new Densifier(geom);
    densifier.setDistanceTolerance(distanceTolerance);
    return densifier.getResultGeometry();
  }

  getResultGeometry() {
    return new DensifyTransformer(this._distanceTolerance).transform(this._inputGeom);
  }

  setDistanceTolerance(distanceTolerance) {
    if (distanceTolerance <= 0.0) throw new IllegalArgumentException('Tolerance must be positive');
    this._distanceTolerance = distanceTolerance;
  }

}

class DensifyTransformer extends GeometryTransformer {
  constructor() {
    super();
    DensifyTransformer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.distanceTolerance = null;
    const distanceTolerance = arguments[0];
    this.distanceTolerance = distanceTolerance;
  }

  transformMultiPolygon(geom, parent) {
    const roughGeom = super.transformMultiPolygon.call(this, geom, parent);
    return this.createValidArea(roughGeom);
  }

  transformPolygon(geom, parent) {
    const roughGeom = super.transformPolygon.call(this, geom, parent);
    if (parent instanceof MultiPolygon) return roughGeom;
    return this.createValidArea(roughGeom);
  }

  transformCoordinates(coords, parent) {
    const inputPts = coords.toCoordinateArray();
    let newPts = Densifier.densifyPoints(inputPts, this.distanceTolerance, parent.getPrecisionModel());
    if (parent instanceof LineString && newPts.length === 1) newPts = new Array(0).fill(null);
    return this._factory.getCoordinateSequenceFactory().create(newPts);
  }

  createValidArea(roughAreaGeom) {
    return roughAreaGeom.buffer(0.0);
  }

}

Densifier.DensifyTransformer = DensifyTransformer;

var densify = /*#__PURE__*/Object.freeze({
  __proto__: null,
  Densifier: Densifier
});

class Quadrant {
  static isNorthern(quad) {
    return quad === Quadrant.NE || quad === Quadrant.NW;
  }

  static isOpposite(quad1, quad2) {
    if (quad1 === quad2) return false;
    const diff = (quad1 - quad2 + 4) % 4;
    if (diff === 2) return true;
    return false;
  }

  static commonHalfPlane(quad1, quad2) {
    if (quad1 === quad2) return quad1;
    const diff = (quad1 - quad2 + 4) % 4;
    if (diff === 2) return -1;
    const min = quad1 < quad2 ? quad1 : quad2;
    const max = quad1 > quad2 ? quad1 : quad2;
    if (min === 0 && max === 3) return 3;
    return min;
  }

  static isInHalfPlane(quad, halfPlane) {
    if (halfPlane === Quadrant.SE) return quad === Quadrant.SE || quad === Quadrant.SW;
    return quad === halfPlane || quad === halfPlane + 1;
  }

  static quadrant() {
    if (typeof arguments[0] === 'number' && typeof arguments[1] === 'number') {
      const dx = arguments[0],
            dy = arguments[1];
      if (dx === 0.0 && dy === 0.0) throw new IllegalArgumentException('Cannot compute the quadrant for point ( ' + dx + ', ' + dy + ' )');
      if (dx >= 0.0) {
        if (dy >= 0.0) return Quadrant.NE;else return Quadrant.SE;
      } else if (dy >= 0.0) return Quadrant.NW;else return Quadrant.SW;
    } else if (arguments[0] instanceof Coordinate && arguments[1] instanceof Coordinate) {
      const p0 = arguments[0],
            p1 = arguments[1];
      if (p1.x === p0.x && p1.y === p0.y) throw new IllegalArgumentException('Cannot compute the quadrant for two identical points ' + p0);
      if (p1.x >= p0.x) {
        if (p1.y >= p0.y) return Quadrant.NE;else return Quadrant.SE;
      } else if (p1.y >= p0.y) return Quadrant.NW;else return Quadrant.SW;
    }
  }

}
Quadrant.NE = 0;
Quadrant.NW = 1;
Quadrant.SW = 2;
Quadrant.SE = 3;

class HalfEdge {
  constructor() {
    HalfEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._orig = null;
    this._sym = null;
    this._next = null;
    const orig = arguments[0];
    this._orig = orig;
  }

  static create(p0, p1) {
    const e0 = new HalfEdge(p0);
    const e1 = new HalfEdge(p1);
    e0.link(e1);
    return e0;
  }

  find(dest) {
    let oNext = this;

    do {
      if (oNext === null) return null;
      if (oNext.dest().equals2D(dest)) return oNext;
      oNext = oNext.oNext();
    } while (oNext !== this);

    return null;
  }

  dest() {
    return this._sym._orig;
  }

  isEdgesSorted() {
    const lowest = this.findLowest();
    let e = lowest;

    do {
      const eNext = e.oNext();
      if (eNext === lowest) break;
      const isSorted = eNext.compareTo(e) > 0;
      if (!isSorted) return false;
      e = eNext;
    } while (e !== lowest);

    return true;
  }

  oNext() {
    return this._sym._next;
  }

  directionY() {
    return this.directionPt().getY() - this._orig.getY();
  }

  insert(eAdd) {
    if (this.oNext() === this) {
      this.insertAfter(eAdd);
      return null;
    }

    const ePrev = this.insertionEdge(eAdd);
    ePrev.insertAfter(eAdd);
  }

  insertAfter(e) {
    Assert.equals(this._orig, e.orig());
    const save = this.oNext();

    this._sym.setNext(e);

    e.sym().setNext(save);
  }

  degree() {
    let degree = 0;
    let e = this;

    do {
      degree++;
      e = e.oNext();
    } while (e !== this);

    return degree;
  }

  equals() {
    if (arguments.length === 2 && arguments[1] instanceof Coordinate && arguments[0] instanceof Coordinate) {
      const p0 = arguments[0],
            p1 = arguments[1];
      return this._orig.equals2D(p0) && this._sym._orig.equals(p1);
    }
  }

  findLowest() {
    let lowest = this;
    let e = this.oNext();

    do {
      if (e.compareTo(lowest) < 0) lowest = e;
      e = e.oNext();
    } while (e !== this);

    return lowest;
  }

  directionPt() {
    return this.dest();
  }

  sym() {
    return this._sym;
  }

  prev() {
    return this._sym.next()._sym;
  }

  compareAngularDirection(e) {
    const dx = this.directionX();
    const dy = this.directionY();
    const dx2 = e.directionX();
    const dy2 = e.directionY();
    if (dx === dx2 && dy === dy2) return 0;
    const quadrant = Quadrant.quadrant(dx, dy);
    const quadrant2 = Quadrant.quadrant(dx2, dy2);
    if (quadrant > quadrant2) return 1;
    if (quadrant < quadrant2) return -1;
    const dir1 = this.directionPt();
    const dir2 = e.directionPt();
    return Orientation.index(e._orig, dir2, dir1);
  }

  prevNode() {
    let e = this;

    while (e.degree() === 2) {
      e = e.prev();
      if (e === this) return null;
    }

    return e;
  }

  directionX() {
    return this.directionPt().getX() - this._orig.getX();
  }

  insertionEdge(eAdd) {
    let ePrev = this;

    do {
      const eNext = ePrev.oNext();
      if (eNext.compareTo(ePrev) > 0 && eAdd.compareTo(ePrev) >= 0 && eAdd.compareTo(eNext) <= 0) return ePrev;
      if (eNext.compareTo(ePrev) <= 0 && (eAdd.compareTo(eNext) <= 0 || eAdd.compareTo(ePrev) >= 0)) return ePrev;
      ePrev = eNext;
    } while (ePrev !== this);

    Assert.shouldNeverReachHere();
    return null;
  }

  compareTo(obj) {
    const e = obj;
    const comp = this.compareAngularDirection(e);
    return comp;
  }

  toStringNode() {
    const orig = this.orig();
    this.dest();
    const sb = new StringBuilder();
    sb.append('Node( ' + WKTWriter.format(orig) + ' )' + '\n');
    let e = this;

    do {
      sb.append('  -> ' + e);
      sb.append('\n');
      e = e.oNext();
    } while (e !== this);

    return sb.toString();
  }

  link(sym) {
    this.setSym(sym);
    sym.setSym(this);
    this.setNext(sym);
    sym.setNext(this);
  }

  next() {
    return this._next;
  }

  setSym(e) {
    this._sym = e;
  }

  orig() {
    return this._orig;
  }

  toString() {
    return 'HE(' + this._orig.x + ' ' + this._orig.y + ', ' + this._sym._orig.x + ' ' + this._sym._orig.y + ')';
  }

  toStringNodeEdge() {
    return '  -> (' + WKTWriter.format(this.dest());
  }

  setNext(e) {
    this._next = e;
  }

}

class MarkHalfEdge extends HalfEdge {
  constructor() {
    super();
    MarkHalfEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._isMarked = false;
    const orig = arguments[0];
    HalfEdge.constructor_.call(this, orig);
  }

  static setMarkBoth(e, isMarked) {
    e.setMark(isMarked);
    e.sym().setMark(isMarked);
  }

  static isMarked(e) {
    return e.isMarked();
  }

  static setMark(e, isMarked) {
    e.setMark(isMarked);
  }

  static markBoth(e) {
    e.mark();
    e.sym().mark();
  }

  static mark(e) {
    e.mark();
  }

  mark() {
    this._isMarked = true;
  }

  setMark(isMarked) {
    this._isMarked = isMarked;
  }

  isMarked() {
    return this._isMarked;
  }

}

class EdgeGraph {
  constructor() {
    EdgeGraph.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._vertexMap = new HashMap();
  }

  static isValidEdge(orig, dest) {
    const cmp = dest.compareTo(orig);
    return cmp !== 0;
  }

  insert(orig, dest, eAdj) {
    const e = this.create(orig, dest);
    if (eAdj !== null) eAdj.insert(e);else this._vertexMap.put(orig, e);

    const eAdjDest = this._vertexMap.get(dest);

    if (eAdjDest !== null) eAdjDest.insert(e.sym());else this._vertexMap.put(dest, e.sym());
    return e;
  }

  create(p0, p1) {
    const e0 = this.createEdge(p0);
    const e1 = this.createEdge(p1);
    e0.link(e1);
    return e0;
  }

  createEdge(orig) {
    return new HalfEdge(orig);
  }

  addEdge(orig, dest) {
    if (!EdgeGraph.isValidEdge(orig, dest)) return null;

    const eAdj = this._vertexMap.get(orig);

    let eSame = null;
    if (eAdj !== null) eSame = eAdj.find(dest);
    if (eSame !== null) return eSame;
    const e = this.insert(orig, dest, eAdj);
    return e;
  }

  getVertexEdges() {
    return this._vertexMap.values();
  }

  findEdge(orig, dest) {
    const e = this._vertexMap.get(orig);

    if (e === null) return null;
    return e.find(dest);
  }

}

class DissolveHalfEdge extends MarkHalfEdge {
  constructor() {
    super();
    DissolveHalfEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._isStart = false;
    const orig = arguments[0];
    MarkHalfEdge.constructor_.call(this, orig);
  }

  setStart() {
    this._isStart = true;
  }

  isStart() {
    return this._isStart;
  }

}

class DissolveEdgeGraph extends EdgeGraph {
  constructor() {
    super();
  }

  createEdge(p0) {
    return new DissolveHalfEdge(p0);
  }

}

class LineDissolver {
  constructor() {
    LineDissolver.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._result = null;
    this._factory = null;
    this._graph = null;
    this._lines = new ArrayList();
    this._nodeEdgeStack = new Stack();
    this._ringStartEdge = null;
    this._graph = new DissolveEdgeGraph();
  }

  static dissolve(g) {
    const d = new LineDissolver();
    d.add(g);
    return d.getResult();
  }

  addLine(line) {
    this._lines.add(this._factory.createLineString(line.toCoordinateArray()));
  }

  updateRingStartEdge(e) {
    if (!e.isStart()) {
      e = e.sym();
      if (!e.isStart()) return null;
    }

    if (this._ringStartEdge === null) {
      this._ringStartEdge = e;
      return null;
    }

    if (e.orig().compareTo(this._ringStartEdge.orig()) < 0) this._ringStartEdge = e;
  }

  getResult() {
    if (this._result === null) this.computeResult();
    return this._result;
  }

  process(e) {
    let eNode = e.prevNode();
    if (eNode === null) eNode = e;
    this.stackEdges(eNode);
    this.buildLines();
  }

  buildRing(eStartRing) {
    const line = new CoordinateList();
    let e = eStartRing;
    line.add(e.orig().copy(), false);

    while (e.sym().degree() === 2) {
      const eNext = e.next();
      if (eNext === eStartRing) break;
      line.add(eNext.orig().copy(), false);
      e = eNext;
    }

    line.add(e.dest().copy(), false);
    this.addLine(line);
  }

  buildLine(eStart) {
    const line = new CoordinateList();
    let e = eStart;
    this._ringStartEdge = null;
    MarkHalfEdge.markBoth(e);
    line.add(e.orig().copy(), false);

    while (e.sym().degree() === 2) {
      this.updateRingStartEdge(e);
      const eNext = e.next();

      if (eNext === eStart) {
        this.buildRing(this._ringStartEdge);
        return null;
      }

      line.add(eNext.orig().copy(), false);
      e = eNext;
      MarkHalfEdge.markBoth(e);
    }

    line.add(e.dest().clone(), false);
    this.stackEdges(e.sym());
    this.addLine(line);
  }

  stackEdges(node) {
    let e = node;

    do {
      if (!MarkHalfEdge.isMarked(e)) this._nodeEdgeStack.add(e);
      e = e.oNext();
    } while (e !== node);
  }

  computeResult() {
    const edges = this._graph.getVertexEdges();

    for (let i = edges.iterator(); i.hasNext();) {
      const e = i.next();
      if (MarkHalfEdge.isMarked(e)) continue;
      this.process(e);
    }

    this._result = this._factory.buildGeometry(this._lines);
  }

  buildLines() {
    while (!this._nodeEdgeStack.empty()) {
      const e = this._nodeEdgeStack.pop();

      if (MarkHalfEdge.isMarked(e)) continue;
      this.buildLine(e);
    }
  }

  add() {
    if (arguments[0] instanceof Geometry) {
      const geometry = arguments[0];
      geometry.apply(new class {
        get interfaces_() {
          return [GeometryComponentFilter];
        }

        filter(component) {
          if (component instanceof LineString) this.add(component);
        }

      }());
    } else if (hasInterface(arguments[0], Collection)) {
      const geometries = arguments[0];

      for (let i = geometries.iterator(); i.hasNext();) {
        const geometry = i.next();
        this.add(geometry);
      }
    } else if (arguments[0] instanceof LineString) {
      const lineString = arguments[0];
      if (this._factory === null) this._factory = lineString.getFactory();
      const seq = lineString.getCoordinateSequence();
      let doneStart = false;

      for (let i = 1; i < seq.size(); i++) {
        const e = this._graph.addEdge(seq.getCoordinate(i - 1), seq.getCoordinate(i));

        if (e === null) continue;

        if (!doneStart) {
          e.setStart();
          doneStart = true;
        }
      }
    }
  }

}

var dissolve = /*#__PURE__*/Object.freeze({
  __proto__: null,
  LineDissolver: LineDissolver
});

class Position {
  static opposite(position) {
    if (position === Position.LEFT) return Position.RIGHT;
    if (position === Position.RIGHT) return Position.LEFT;
    return position;
  }

}
Position.ON = 0;
Position.LEFT = 1;
Position.RIGHT = 2;

class MonotoneChain$1 {
  constructor() {
    MonotoneChain$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.mce = null;
    this.chainIndex = null;
    const mce = arguments[0],
          chainIndex = arguments[1];
    this.mce = mce;
    this.chainIndex = chainIndex;
  }

  computeIntersections(mc, si) {
    this.mce.computeIntersectsForChain(this.chainIndex, mc.mce, mc.chainIndex, si);
  }

}

class SweepLineEvent {
  constructor() {
    SweepLineEvent.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._label = null;
    this._xValue = null;
    this._eventType = null;
    this._insertEvent = null;
    this._deleteEventIndex = null;
    this._obj = null;

    if (arguments.length === 2) {
      const x = arguments[0],
            insertEvent = arguments[1];
      this._eventType = SweepLineEvent.DELETE;
      this._xValue = x;
      this._insertEvent = insertEvent;
    } else if (arguments.length === 3) {
      const label = arguments[0],
            x = arguments[1],
            obj = arguments[2];
      this._eventType = SweepLineEvent.INSERT;
      this._label = label;
      this._xValue = x;
      this._obj = obj;
    }
  }

  isDelete() {
    return this._eventType === SweepLineEvent.DELETE;
  }

  setDeleteEventIndex(deleteEventIndex) {
    this._deleteEventIndex = deleteEventIndex;
  }

  getObject() {
    return this._obj;
  }

  compareTo(o) {
    const pe = o;
    if (this._xValue < pe._xValue) return -1;
    if (this._xValue > pe._xValue) return 1;
    if (this._eventType < pe._eventType) return -1;
    if (this._eventType > pe._eventType) return 1;
    return 0;
  }

  getInsertEvent() {
    return this._insertEvent;
  }

  isInsert() {
    return this._eventType === SweepLineEvent.INSERT;
  }

  isSameLabel(ev) {
    if (this._label === null) return false;
    return this._label === ev._label;
  }

  getDeleteEventIndex() {
    return this._deleteEventIndex;
  }

  get interfaces_() {
    return [Comparable];
  }

}
SweepLineEvent.INSERT = 1;
SweepLineEvent.DELETE = 2;

class EdgeSetIntersector {}

class SegmentIntersector$1 {
  constructor() {
    SegmentIntersector$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._hasIntersection = false;
    this._hasProper = false;
    this._hasProperInterior = false;
    this._properIntersectionPoint = null;
    this._li = null;
    this._includeProper = null;
    this._recordIsolated = null;
    this._isSelfIntersection = null;
    this._numIntersections = 0;
    this.numTests = 0;
    this._bdyNodes = null;
    this._isDone = false;
    this._isDoneWhenProperInt = false;
    const li = arguments[0],
          includeProper = arguments[1],
          recordIsolated = arguments[2];
    this._li = li;
    this._includeProper = includeProper;
    this._recordIsolated = recordIsolated;
  }

  static isAdjacentSegments(i1, i2) {
    return Math.abs(i1 - i2) === 1;
  }

  isTrivialIntersection(e0, segIndex0, e1, segIndex1) {
    if (e0 === e1) if (this._li.getIntersectionNum() === 1) {
      if (SegmentIntersector$1.isAdjacentSegments(segIndex0, segIndex1)) return true;

      if (e0.isClosed()) {
        const maxSegIndex = e0.getNumPoints() - 1;
        if (segIndex0 === 0 && segIndex1 === maxSegIndex || segIndex1 === 0 && segIndex0 === maxSegIndex) return true;
      }
    }
    return false;
  }

  getProperIntersectionPoint() {
    return this._properIntersectionPoint;
  }

  setIsDoneIfProperInt(isDoneWhenProperInt) {
    this._isDoneWhenProperInt = isDoneWhenProperInt;
  }

  hasProperInteriorIntersection() {
    return this._hasProperInterior;
  }

  isBoundaryPointInternal(li, bdyNodes) {
    for (let i = bdyNodes.iterator(); i.hasNext();) {
      const node = i.next();
      const pt = node.getCoordinate();
      if (li.isIntersection(pt)) return true;
    }

    return false;
  }

  hasProperIntersection() {
    return this._hasProper;
  }

  hasIntersection() {
    return this._hasIntersection;
  }

  isDone() {
    return this._isDone;
  }

  isBoundaryPoint(li, bdyNodes) {
    if (bdyNodes === null) return false;
    if (this.isBoundaryPointInternal(li, bdyNodes[0])) return true;
    if (this.isBoundaryPointInternal(li, bdyNodes[1])) return true;
    return false;
  }

  setBoundaryNodes(bdyNodes0, bdyNodes1) {
    this._bdyNodes = new Array(2).fill(null);
    this._bdyNodes[0] = bdyNodes0;
    this._bdyNodes[1] = bdyNodes1;
  }

  addIntersections(e0, segIndex0, e1, segIndex1) {
    if (e0 === e1 && segIndex0 === segIndex1) return null;
    this.numTests++;
    const p00 = e0.getCoordinates()[segIndex0];
    const p01 = e0.getCoordinates()[segIndex0 + 1];
    const p10 = e1.getCoordinates()[segIndex1];
    const p11 = e1.getCoordinates()[segIndex1 + 1];

    this._li.computeIntersection(p00, p01, p10, p11);

    if (this._li.hasIntersection()) {
      if (this._recordIsolated) {
        e0.setIsolated(false);
        e1.setIsolated(false);
      }

      this._numIntersections++;

      if (!this.isTrivialIntersection(e0, segIndex0, e1, segIndex1)) {
        this._hasIntersection = true;

        if (this._includeProper || !this._li.isProper()) {
          e0.addIntersections(this._li, segIndex0, 0);
          e1.addIntersections(this._li, segIndex1, 1);
        }

        if (this._li.isProper()) {
          this._properIntersectionPoint = this._li.getIntersection(0).copy();
          this._hasProper = true;
          if (this._isDoneWhenProperInt) this._isDone = true;
          if (!this.isBoundaryPoint(this._li, this._bdyNodes)) this._hasProperInterior = true;
        }
      }
    }
  }

}

class SimpleMCSweepLineIntersector extends EdgeSetIntersector {
  constructor() {
    super();
    SimpleMCSweepLineIntersector.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.events = new ArrayList();
    this.nOverlaps = null;
  }

  prepareEvents() {
    Collections.sort(this.events);

    for (let i = 0; i < this.events.size(); i++) {
      const ev = this.events.get(i);
      if (ev.isDelete()) ev.getInsertEvent().setDeleteEventIndex(i);
    }
  }

  computeIntersections() {
    if (arguments.length === 1) {
      const si = arguments[0];
      this.nOverlaps = 0;
      this.prepareEvents();

      for (let i = 0; i < this.events.size(); i++) {
        const ev = this.events.get(i);
        if (ev.isInsert()) this.processOverlaps(i, ev.getDeleteEventIndex(), ev, si);
        if (si.isDone()) break;
      }
    } else if (arguments.length === 3) {
      if (arguments[2] instanceof SegmentIntersector$1 && hasInterface(arguments[0], List) && hasInterface(arguments[1], List)) {
        const edges0 = arguments[0],
              edges1 = arguments[1],
              si = arguments[2];
        this.addEdges(edges0, edges0);
        this.addEdges(edges1, edges1);
        this.computeIntersections(si);
      } else if (typeof arguments[2] === 'boolean' && hasInterface(arguments[0], List) && arguments[1] instanceof SegmentIntersector$1) {
        const edges = arguments[0],
              si = arguments[1],
              testAllSegments = arguments[2];
        if (testAllSegments) this.addEdges(edges, null);else this.addEdges(edges);
        this.computeIntersections(si);
      }
    }
  }

  addEdge(edge, edgeSet) {
    const mce = edge.getMonotoneChainEdge();
    const startIndex = mce.getStartIndexes();

    for (let i = 0; i < startIndex.length - 1; i++) {
      const mc = new MonotoneChain$1(mce, i);
      const insertEvent = new SweepLineEvent(edgeSet, mce.getMinX(i), mc);
      this.events.add(insertEvent);
      this.events.add(new SweepLineEvent(mce.getMaxX(i), insertEvent));
    }
  }

  processOverlaps(start, end, ev0, si) {
    const mc0 = ev0.getObject();

    for (let i = start; i < end; i++) {
      const ev1 = this.events.get(i);

      if (ev1.isInsert()) {
        const mc1 = ev1.getObject();

        if (!ev0.isSameLabel(ev1)) {
          mc0.computeIntersections(mc1, si);
          this.nOverlaps++;
        }
      }
    }
  }

  addEdges() {
    if (arguments.length === 1) {
      const edges = arguments[0];

      for (let i = edges.iterator(); i.hasNext();) {
        const edge = i.next();
        this.addEdge(edge, edge);
      }
    } else if (arguments.length === 2) {
      const edges = arguments[0],
            edgeSet = arguments[1];

      for (let i = edges.iterator(); i.hasNext();) {
        const edge = i.next();
        this.addEdge(edge, edgeSet);
      }
    }
  }

}

class TopologyLocation {
  constructor() {
    TopologyLocation.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.location = null;

    if (arguments.length === 1) {
      if (arguments[0] instanceof Array) {
        const location = arguments[0];
        this.init(location.length);
      } else if (Number.isInteger(arguments[0])) {
        const on = arguments[0];
        this.init(1);
        this.location[Position.ON] = on;
      } else if (arguments[0] instanceof TopologyLocation) {
        const gl = arguments[0];
        this.init(gl.location.length);
        if (gl !== null) for (let i = 0; i < this.location.length; i++) this.location[i] = gl.location[i];
      }
    } else if (arguments.length === 3) {
      const on = arguments[0],
            left = arguments[1],
            right = arguments[2];
      this.init(3);
      this.location[Position.ON] = on;
      this.location[Position.LEFT] = left;
      this.location[Position.RIGHT] = right;
    }
  }

  setAllLocations(locValue) {
    for (let i = 0; i < this.location.length; i++) this.location[i] = locValue;
  }

  isNull() {
    for (let i = 0; i < this.location.length; i++) if (this.location[i] !== Location.NONE) return false;

    return true;
  }

  setAllLocationsIfNull(locValue) {
    for (let i = 0; i < this.location.length; i++) if (this.location[i] === Location.NONE) this.location[i] = locValue;
  }

  isLine() {
    return this.location.length === 1;
  }

  merge(gl) {
    if (gl.location.length > this.location.length) {
      const newLoc = new Array(3).fill(null);
      newLoc[Position.ON] = this.location[Position.ON];
      newLoc[Position.LEFT] = Location.NONE;
      newLoc[Position.RIGHT] = Location.NONE;
      this.location = newLoc;
    }

    for (let i = 0; i < this.location.length; i++) if (this.location[i] === Location.NONE && i < gl.location.length) this.location[i] = gl.location[i];
  }

  getLocations() {
    return this.location;
  }

  flip() {
    if (this.location.length <= 1) return null;
    const temp = this.location[Position.LEFT];
    this.location[Position.LEFT] = this.location[Position.RIGHT];
    this.location[Position.RIGHT] = temp;
  }

  toString() {
    const buf = new StringBuffer();
    if (this.location.length > 1) buf.append(Location.toLocationSymbol(this.location[Position.LEFT]));
    buf.append(Location.toLocationSymbol(this.location[Position.ON]));
    if (this.location.length > 1) buf.append(Location.toLocationSymbol(this.location[Position.RIGHT]));
    return buf.toString();
  }

  setLocations(on, left, right) {
    this.location[Position.ON] = on;
    this.location[Position.LEFT] = left;
    this.location[Position.RIGHT] = right;
  }

  get(posIndex) {
    if (posIndex < this.location.length) return this.location[posIndex];
    return Location.NONE;
  }

  isArea() {
    return this.location.length > 1;
  }

  isAnyNull() {
    for (let i = 0; i < this.location.length; i++) if (this.location[i] === Location.NONE) return true;

    return false;
  }

  setLocation() {
    if (arguments.length === 1) {
      const locValue = arguments[0];
      this.setLocation(Position.ON, locValue);
    } else if (arguments.length === 2) {
      const locIndex = arguments[0],
            locValue = arguments[1];
      this.location[locIndex] = locValue;
    }
  }

  init(size) {
    this.location = new Array(size).fill(null);
    this.setAllLocations(Location.NONE);
  }

  isEqualOnSide(le, locIndex) {
    return this.location[locIndex] === le.location[locIndex];
  }

  allPositionsEqual(loc) {
    for (let i = 0; i < this.location.length; i++) if (this.location[i] !== loc) return false;

    return true;
  }

}

class Label {
  constructor() {
    Label.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.elt = new Array(2).fill(null);

    if (arguments.length === 1) {
      if (Number.isInteger(arguments[0])) {
        const onLoc = arguments[0];
        this.elt[0] = new TopologyLocation(onLoc);
        this.elt[1] = new TopologyLocation(onLoc);
      } else if (arguments[0] instanceof Label) {
        const lbl = arguments[0];
        this.elt[0] = new TopologyLocation(lbl.elt[0]);
        this.elt[1] = new TopologyLocation(lbl.elt[1]);
      }
    } else if (arguments.length === 2) {
      const geomIndex = arguments[0],
            onLoc = arguments[1];
      this.elt[0] = new TopologyLocation(Location.NONE);
      this.elt[1] = new TopologyLocation(Location.NONE);
      this.elt[geomIndex].setLocation(onLoc);
    } else if (arguments.length === 3) {
      const onLoc = arguments[0],
            leftLoc = arguments[1],
            rightLoc = arguments[2];
      this.elt[0] = new TopologyLocation(onLoc, leftLoc, rightLoc);
      this.elt[1] = new TopologyLocation(onLoc, leftLoc, rightLoc);
    } else if (arguments.length === 4) {
      const geomIndex = arguments[0],
            onLoc = arguments[1],
            leftLoc = arguments[2],
            rightLoc = arguments[3];
      this.elt[0] = new TopologyLocation(Location.NONE, Location.NONE, Location.NONE);
      this.elt[1] = new TopologyLocation(Location.NONE, Location.NONE, Location.NONE);
      this.elt[geomIndex].setLocations(onLoc, leftLoc, rightLoc);
    }
  }

  static toLineLabel(label) {
    const lineLabel = new Label(Location.NONE);

    for (let i = 0; i < 2; i++) lineLabel.setLocation(i, label.getLocation(i));

    return lineLabel;
  }

  getGeometryCount() {
    let count = 0;
    if (!this.elt[0].isNull()) count++;
    if (!this.elt[1].isNull()) count++;
    return count;
  }

  setAllLocations(geomIndex, location) {
    this.elt[geomIndex].setAllLocations(location);
  }

  isNull(geomIndex) {
    return this.elt[geomIndex].isNull();
  }

  setAllLocationsIfNull() {
    if (arguments.length === 1) {
      const location = arguments[0];
      this.setAllLocationsIfNull(0, location);
      this.setAllLocationsIfNull(1, location);
    } else if (arguments.length === 2) {
      const geomIndex = arguments[0],
            location = arguments[1];
      this.elt[geomIndex].setAllLocationsIfNull(location);
    }
  }

  isLine(geomIndex) {
    return this.elt[geomIndex].isLine();
  }

  merge(lbl) {
    for (let i = 0; i < 2; i++) if (this.elt[i] === null && lbl.elt[i] !== null) this.elt[i] = new TopologyLocation(lbl.elt[i]);else this.elt[i].merge(lbl.elt[i]);
  }

  flip() {
    this.elt[0].flip();
    this.elt[1].flip();
  }

  getLocation() {
    if (arguments.length === 1) {
      const geomIndex = arguments[0];
      return this.elt[geomIndex].get(Position.ON);
    } else if (arguments.length === 2) {
      const geomIndex = arguments[0],
            posIndex = arguments[1];
      return this.elt[geomIndex].get(posIndex);
    }
  }

  toString() {
    const buf = new StringBuffer();

    if (this.elt[0] !== null) {
      buf.append('A:');
      buf.append(this.elt[0].toString());
    }

    if (this.elt[1] !== null) {
      buf.append(' B:');
      buf.append(this.elt[1].toString());
    }

    return buf.toString();
  }

  isArea() {
    if (arguments.length === 0) {
      return this.elt[0].isArea() || this.elt[1].isArea();
    } else if (arguments.length === 1) {
      const geomIndex = arguments[0];
      return this.elt[geomIndex].isArea();
    }
  }

  isAnyNull(geomIndex) {
    return this.elt[geomIndex].isAnyNull();
  }

  setLocation() {
    if (arguments.length === 2) {
      const geomIndex = arguments[0],
            location = arguments[1];
      this.elt[geomIndex].setLocation(Position.ON, location);
    } else if (arguments.length === 3) {
      const geomIndex = arguments[0],
            posIndex = arguments[1],
            location = arguments[2];
      this.elt[geomIndex].setLocation(posIndex, location);
    }
  }

  isEqualOnSide(lbl, side) {
    return this.elt[0].isEqualOnSide(lbl.elt[0], side) && this.elt[1].isEqualOnSide(lbl.elt[1], side);
  }

  allPositionsEqual(geomIndex, loc) {
    return this.elt[geomIndex].allPositionsEqual(loc);
  }

  toLine(geomIndex) {
    if (this.elt[geomIndex].isArea()) this.elt[geomIndex] = new TopologyLocation(this.elt[geomIndex].location[0]);
  }

}

class EdgeIntersection {
  constructor() {
    EdgeIntersection.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.coord = null;
    this.segmentIndex = null;
    this.dist = null;
    const coord = arguments[0],
          segmentIndex = arguments[1],
          dist = arguments[2];
    this.coord = new Coordinate(coord);
    this.segmentIndex = segmentIndex;
    this.dist = dist;
  }

  getSegmentIndex() {
    return this.segmentIndex;
  }

  getCoordinate() {
    return this.coord;
  }

  print(out) {
    out.print(this.coord);
    out.print(' seg # = ' + this.segmentIndex);
    out.println(' dist = ' + this.dist);
  }

  compareTo(obj) {
    const other = obj;
    return this.compare(other.segmentIndex, other.dist);
  }

  isEndPoint(maxSegmentIndex) {
    if (this.segmentIndex === 0 && this.dist === 0.0) return true;
    if (this.segmentIndex === maxSegmentIndex) return true;
    return false;
  }

  toString() {
    return this.coord + ' seg # = ' + this.segmentIndex + ' dist = ' + this.dist;
  }

  getDistance() {
    return this.dist;
  }

  compare(segmentIndex, dist) {
    if (this.segmentIndex < segmentIndex) return -1;
    if (this.segmentIndex > segmentIndex) return 1;
    if (this.dist < dist) return -1;
    if (this.dist > dist) return 1;
    return 0;
  }

  get interfaces_() {
    return [Comparable];
  }

}

/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/SortedMap.html
 */

class SortedMap extends Map$1 {}

const BLACK = 0;
const RED = 1;

function colorOf(p) {
  return p == null ? BLACK : p.color;
}

function parentOf(p) {
  return p == null ? null : p.parent;
}

function setColor(p, c) {
  if (p !== null) p.color = c;
}

function leftOf(p) {
  return p == null ? null : p.left;
}

function rightOf(p) {
  return p == null ? null : p.right;
}
/**
 * @see http://download.oracle.com/javase/6/docs/api/java/util/TreeMap.html
 */


class TreeMap extends SortedMap {
  constructor() {
    super();
    this.root_ = null;
    this.size_ = 0;
  }

  get(key) {
    let p = this.root_;

    while (p !== null) {
      const cmp = key.compareTo(p.key);
      if (cmp < 0) p = p.left;else if (cmp > 0) p = p.right;else return p.value;
    }

    return null;
  }

  put(key, value) {
    if (this.root_ === null) {
      this.root_ = {
        key: key,
        value: value,
        left: null,
        right: null,
        parent: null,
        color: BLACK,

        getValue() {
          return this.value;
        },

        getKey() {
          return this.key;
        }

      };
      this.size_ = 1;
      return null;
    }

    let t = this.root_;
    let parent;
    let cmp;

    do {
      parent = t;
      cmp = key.compareTo(t.key);

      if (cmp < 0) {
        t = t.left;
      } else if (cmp > 0) {
        t = t.right;
      } else {
        const oldValue = t.value;
        t.value = value;
        return oldValue;
      }
    } while (t !== null);

    const e = {
      key: key,
      left: null,
      right: null,
      value: value,
      parent: parent,
      color: BLACK,

      getValue() {
        return this.value;
      },

      getKey() {
        return this.key;
      }

    };
    if (cmp < 0) parent.left = e;else parent.right = e;
    this.fixAfterInsertion(e);
    this.size_++;
    return null;
  }
  /**
   * @param {Object} x
   */


  fixAfterInsertion(x) {
    let y;
    x.color = RED;

    while (x != null && x !== this.root_ && x.parent.color === RED) if (parentOf(x) === leftOf(parentOf(parentOf(x)))) {
      y = rightOf(parentOf(parentOf(x)));

      if (colorOf(y) === RED) {
        setColor(parentOf(x), BLACK);
        setColor(y, BLACK);
        setColor(parentOf(parentOf(x)), RED);
        x = parentOf(parentOf(x));
      } else {
        if (x === rightOf(parentOf(x))) {
          x = parentOf(x);
          this.rotateLeft(x);
        }

        setColor(parentOf(x), BLACK);
        setColor(parentOf(parentOf(x)), RED);
        this.rotateRight(parentOf(parentOf(x)));
      }
    } else {
      y = leftOf(parentOf(parentOf(x)));

      if (colorOf(y) === RED) {
        setColor(parentOf(x), BLACK);
        setColor(y, BLACK);
        setColor(parentOf(parentOf(x)), RED);
        x = parentOf(parentOf(x));
      } else {
        if (x === leftOf(parentOf(x))) {
          x = parentOf(x);
          this.rotateRight(x);
        }

        setColor(parentOf(x), BLACK);
        setColor(parentOf(parentOf(x)), RED);
        this.rotateLeft(parentOf(parentOf(x)));
      }
    }

    this.root_.color = BLACK;
  }

  values() {
    const arrayList = new ArrayList();
    let p = this.getFirstEntry();

    if (p !== null) {
      arrayList.add(p.value);

      while ((p = TreeMap.successor(p)) !== null) arrayList.add(p.value);
    }

    return arrayList;
  }

  entrySet() {
    const hashSet = new HashSet();
    let p = this.getFirstEntry();

    if (p !== null) {
      hashSet.add(p);

      while ((p = TreeMap.successor(p)) !== null) hashSet.add(p);
    }

    return hashSet;
  }
  /**
   * @param {Object} p
   */


  rotateLeft(p) {
    if (p != null) {
      const r = p.right;
      p.right = r.left;
      if (r.left != null) r.left.parent = p;
      r.parent = p.parent;
      if (p.parent == null) this.root_ = r;else if (p.parent.left === p) p.parent.left = r;else p.parent.right = r;
      r.left = p;
      p.parent = r;
    }
  }
  /**
   * @param {Object} p
   */


  rotateRight(p) {
    if (p != null) {
      const l = p.left;
      p.left = l.right;
      if (l.right != null) l.right.parent = p;
      l.parent = p.parent;
      if (p.parent == null) this.root_ = l;else if (p.parent.right === p) p.parent.right = l;else p.parent.left = l;
      l.right = p;
      p.parent = l;
    }
  }
  /**
   * @return {Object}
   */


  getFirstEntry() {
    let p = this.root_;
    if (p != null) while (p.left != null) p = p.left;
    return p;
  }
  /**
   * @param {Object} t
   * @return {Object}
   * @private
   */


  static successor(t) {
    let p;

    if (t === null) {
      return null;
    } else if (t.right !== null) {
      p = t.right;

      while (p.left !== null) p = p.left;

      return p;
    } else {
      p = t.parent;
      let ch = t;

      while (p !== null && ch === p.right) {
        ch = p;
        p = p.parent;
      }

      return p;
    }
  }

  size() {
    return this.size_;
  }

  containsKey(key) {
    let p = this.root_;

    while (p !== null) {
      const cmp = key.compareTo(p.key);
      if (cmp < 0) p = p.left;else if (cmp > 0) p = p.right;else return true;
    }

    return false;
  }

}

class EdgeIntersectionList {
  constructor() {
    EdgeIntersectionList.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._nodeMap = new TreeMap();
    this.edge = null;
    const edge = arguments[0];
    this.edge = edge;
  }

  print(out) {
    out.println('Intersections:');

    for (let it = this.iterator(); it.hasNext();) {
      const ei = it.next();
      ei.print(out);
    }
  }

  iterator() {
    return this._nodeMap.values().iterator();
  }

  addSplitEdges(edgeList) {
    this.addEndpoints();
    const it = this.iterator();
    let eiPrev = it.next();

    while (it.hasNext()) {
      const ei = it.next();
      const newEdge = this.createSplitEdge(eiPrev, ei);
      edgeList.add(newEdge);
      eiPrev = ei;
    }
  }

  addEndpoints() {
    const maxSegIndex = this.edge.pts.length - 1;
    this.add(this.edge.pts[0], 0, 0.0);
    this.add(this.edge.pts[maxSegIndex], maxSegIndex, 0.0);
  }

  createSplitEdge(ei0, ei1) {
    let npts = ei1.segmentIndex - ei0.segmentIndex + 2;
    const lastSegStartPt = this.edge.pts[ei1.segmentIndex];
    const useIntPt1 = ei1.dist > 0.0 || !ei1.coord.equals2D(lastSegStartPt);
    if (!useIntPt1) npts--;
    const pts = new Array(npts).fill(null);
    let ipt = 0;
    pts[ipt++] = new Coordinate(ei0.coord);

    for (let i = ei0.segmentIndex + 1; i <= ei1.segmentIndex; i++) pts[ipt++] = this.edge.pts[i];

    if (useIntPt1) pts[ipt] = ei1.coord;
    return new Edge$1(pts, new Label(this.edge._label));
  }

  add(intPt, segmentIndex, dist) {
    const eiNew = new EdgeIntersection(intPt, segmentIndex, dist);

    const ei = this._nodeMap.get(eiNew);

    if (ei !== null) return ei;

    this._nodeMap.put(eiNew, eiNew);

    return eiNew;
  }

  isIntersection(pt) {
    for (let it = this.iterator(); it.hasNext();) {
      const ei = it.next();
      if (ei.coord.equals(pt)) return true;
    }

    return false;
  }

}

class IntArrayList {
  constructor() {
    IntArrayList.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._data = null;
    this._size = 0;

    if (arguments.length === 0) {
      IntArrayList.constructor_.call(this, 10);
    } else if (arguments.length === 1) {
      const initialCapacity = arguments[0];
      this._data = new Array(initialCapacity).fill(null);
    }
  }

  size() {
    return this._size;
  }

  addAll(values) {
    if (values === null) return null;
    if (values.length === 0) return null;
    this.ensureCapacity(this._size + values.length);
    System.arraycopy(values, 0, this._data, this._size, values.length);
    this._size += values.length;
  }

  ensureCapacity(capacity) {
    if (capacity <= this._data.length) return null;
    const newLength = Math.max(capacity, this._data.length * 2);
    this._data = Arrays.copyOf(this._data, newLength);
  }

  toArray() {
    const array = new Array(this._size).fill(null);
    System.arraycopy(this._data, 0, array, 0, this._size);
    return array;
  }

  add(value) {
    this.ensureCapacity(this._size + 1);
    this._data[this._size] = value;
    ++this._size;
  }

}

class MonotoneChainIndexer {
  static toIntArray(list) {
    const array = new Array(list.size()).fill(null);

    for (let i = 0; i < array.length; i++) array[i] = list.get(i).intValue();

    return array;
  }

  getChainStartIndices(pts) {
    let start = 0;
    const startIndexList = new IntArrayList(Math.trunc(pts.length / 2));
    startIndexList.add(start);

    do {
      const last = this.findChainEnd(pts, start);
      startIndexList.add(last);
      start = last;
    } while (start < pts.length - 1);

    return startIndexList.toArray();
  }

  findChainEnd(pts, start) {
    const chainQuad = Quadrant.quadrant(pts[start], pts[start + 1]);
    let last = start + 1;

    while (last < pts.length) {
      const quad = Quadrant.quadrant(pts[last - 1], pts[last]);
      if (quad !== chainQuad) break;
      last++;
    }

    return last - 1;
  }

  OLDgetChainStartIndices(pts) {
    let start = 0;
    const startIndexList = new ArrayList();
    startIndexList.add(start);

    do {
      const last = this.findChainEnd(pts, start);
      startIndexList.add(last);
      start = last;
    } while (start < pts.length - 1);

    const startIndex = MonotoneChainIndexer.toIntArray(startIndexList);
    return startIndex;
  }

}

class MonotoneChainEdge {
  constructor() {
    MonotoneChainEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.e = null;
    this.pts = null;
    this.startIndex = null;
    const e = arguments[0];
    this.e = e;
    this.pts = e.getCoordinates();
    const mcb = new MonotoneChainIndexer();
    this.startIndex = mcb.getChainStartIndices(this.pts);
  }

  getCoordinates() {
    return this.pts;
  }

  getMaxX(chainIndex) {
    const x1 = this.pts[this.startIndex[chainIndex]].x;
    const x2 = this.pts[this.startIndex[chainIndex + 1]].x;
    return x1 > x2 ? x1 : x2;
  }

  getMinX(chainIndex) {
    const x1 = this.pts[this.startIndex[chainIndex]].x;
    const x2 = this.pts[this.startIndex[chainIndex + 1]].x;
    return x1 < x2 ? x1 : x2;
  }

  computeIntersectsForChain() {
    if (arguments.length === 4) {
      const chainIndex0 = arguments[0],
            mce = arguments[1],
            chainIndex1 = arguments[2],
            si = arguments[3];
      this.computeIntersectsForChain(this.startIndex[chainIndex0], this.startIndex[chainIndex0 + 1], mce, mce.startIndex[chainIndex1], mce.startIndex[chainIndex1 + 1], si);
    } else if (arguments.length === 6) {
      const start0 = arguments[0],
            end0 = arguments[1],
            mce = arguments[2],
            start1 = arguments[3],
            end1 = arguments[4],
            ei = arguments[5];

      if (end0 - start0 === 1 && end1 - start1 === 1) {
        ei.addIntersections(this.e, start0, mce.e, start1);
        return null;
      }

      if (!this.overlaps(start0, end0, mce, start1, end1)) return null;
      const mid0 = Math.trunc((start0 + end0) / 2);
      const mid1 = Math.trunc((start1 + end1) / 2);

      if (start0 < mid0) {
        if (start1 < mid1) this.computeIntersectsForChain(start0, mid0, mce, start1, mid1, ei);
        if (mid1 < end1) this.computeIntersectsForChain(start0, mid0, mce, mid1, end1, ei);
      }

      if (mid0 < end0) {
        if (start1 < mid1) this.computeIntersectsForChain(mid0, end0, mce, start1, mid1, ei);
        if (mid1 < end1) this.computeIntersectsForChain(mid0, end0, mce, mid1, end1, ei);
      }
    }
  }

  overlaps(start0, end0, mce, start1, end1) {
    return Envelope.intersects(this.pts[start0], this.pts[end0], mce.pts[start1], mce.pts[end1]);
  }

  getStartIndexes() {
    return this.startIndex;
  }

  computeIntersects(mce, si) {
    for (let i = 0; i < this.startIndex.length - 1; i++) for (let j = 0; j < mce.startIndex.length - 1; j++) this.computeIntersectsForChain(i, mce, j, si);
  }

}

class Depth {
  constructor() {
    Depth.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._depth = Array(2).fill().map(() => Array(3));

    for (let i = 0; i < 2; i++) for (let j = 0; j < 3; j++) this._depth[i][j] = Depth.NULL_VALUE;
  }

  static depthAtLocation(location) {
    if (location === Location.EXTERIOR) return 0;
    if (location === Location.INTERIOR) return 1;
    return Depth.NULL_VALUE;
  }

  getDepth(geomIndex, posIndex) {
    return this._depth[geomIndex][posIndex];
  }

  setDepth(geomIndex, posIndex, depthValue) {
    this._depth[geomIndex][posIndex] = depthValue;
  }

  isNull() {
    if (arguments.length === 0) {
      for (let i = 0; i < 2; i++) for (let j = 0; j < 3; j++) if (this._depth[i][j] !== Depth.NULL_VALUE) return false;

      return true;
    } else if (arguments.length === 1) {
      const geomIndex = arguments[0];
      return this._depth[geomIndex][1] === Depth.NULL_VALUE;
    } else if (arguments.length === 2) {
      const geomIndex = arguments[0],
            posIndex = arguments[1];
      return this._depth[geomIndex][posIndex] === Depth.NULL_VALUE;
    }
  }

  normalize() {
    for (let i = 0; i < 2; i++) if (!this.isNull(i)) {
      let minDepth = this._depth[i][1];
      if (this._depth[i][2] < minDepth) minDepth = this._depth[i][2];
      if (minDepth < 0) minDepth = 0;

      for (let j = 1; j < 3; j++) {
        let newValue = 0;
        if (this._depth[i][j] > minDepth) newValue = 1;
        this._depth[i][j] = newValue;
      }
    }
  }

  getDelta(geomIndex) {
    return this._depth[geomIndex][Position.RIGHT] - this._depth[geomIndex][Position.LEFT];
  }

  getLocation(geomIndex, posIndex) {
    if (this._depth[geomIndex][posIndex] <= 0) return Location.EXTERIOR;
    return Location.INTERIOR;
  }

  toString() {
    return 'A: ' + this._depth[0][1] + ',' + this._depth[0][2] + ' B: ' + this._depth[1][1] + ',' + this._depth[1][2];
  }

  add() {
    if (arguments.length === 1) {
      const lbl = arguments[0];

      for (let i = 0; i < 2; i++) for (let j = 1; j < 3; j++) {
        const loc = lbl.getLocation(i, j);
        if (loc === Location.EXTERIOR || loc === Location.INTERIOR) if (this.isNull(i, j)) this._depth[i][j] = Depth.depthAtLocation(loc);else this._depth[i][j] += Depth.depthAtLocation(loc);
      }
    } else if (arguments.length === 3) {
      const geomIndex = arguments[0],
            posIndex = arguments[1],
            location = arguments[2];
      if (location === Location.INTERIOR) this._depth[geomIndex][posIndex]++;
    }
  }

}
Depth.NULL_VALUE = -1;

class GraphComponent$1 {
  constructor() {
    GraphComponent$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._label = null;
    this._isInResult = false;
    this._isCovered = false;
    this._isCoveredSet = false;
    this._isVisited = false;

    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const label = arguments[0];
      this._label = label;
    }
  }

  setVisited(isVisited) {
    this._isVisited = isVisited;
  }

  setInResult(isInResult) {
    this._isInResult = isInResult;
  }

  isCovered() {
    return this._isCovered;
  }

  isCoveredSet() {
    return this._isCoveredSet;
  }

  setLabel(label) {
    this._label = label;
  }

  getLabel() {
    return this._label;
  }

  setCovered(isCovered) {
    this._isCovered = isCovered;
    this._isCoveredSet = true;
  }

  updateIM(im) {
    Assert.isTrue(this._label.getGeometryCount() >= 2, 'found partial label');
    this.computeIM(im);
  }

  isInResult() {
    return this._isInResult;
  }

  isVisited() {
    return this._isVisited;
  }

}

class Edge$1 extends GraphComponent$1 {
  constructor() {
    super();
    Edge$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.pts = null;
    this._env = null;
    this.eiList = new EdgeIntersectionList(this);
    this._name = null;
    this._mce = null;
    this._isIsolated = true;
    this._depth = new Depth();
    this._depthDelta = 0;

    if (arguments.length === 1) {
      const pts = arguments[0];
      Edge$1.constructor_.call(this, pts, null);
    } else if (arguments.length === 2) {
      const pts = arguments[0],
            label = arguments[1];
      this.pts = pts;
      this._label = label;
    }
  }

  static updateIM() {
    if (arguments.length === 2 && arguments[1] instanceof IntersectionMatrix && arguments[0] instanceof Label) {
      const label = arguments[0],
            im = arguments[1];
      im.setAtLeastIfValid(label.getLocation(0, Position.ON), label.getLocation(1, Position.ON), 1);

      if (label.isArea()) {
        im.setAtLeastIfValid(label.getLocation(0, Position.LEFT), label.getLocation(1, Position.LEFT), 2);
        im.setAtLeastIfValid(label.getLocation(0, Position.RIGHT), label.getLocation(1, Position.RIGHT), 2);
      }
    } else {
      return super.updateIM.apply(this, arguments);
    }
  }

  getDepth() {
    return this._depth;
  }

  getCollapsedEdge() {
    const newPts = new Array(2).fill(null);
    newPts[0] = this.pts[0];
    newPts[1] = this.pts[1];
    const newe = new Edge$1(newPts, Label.toLineLabel(this._label));
    return newe;
  }

  isIsolated() {
    return this._isIsolated;
  }

  getCoordinates() {
    return this.pts;
  }

  setIsolated(isIsolated) {
    this._isIsolated = isIsolated;
  }

  setName(name) {
    this._name = name;
  }

  equals(o) {
    if (!(o instanceof Edge$1)) return false;
    const e = o;
    if (this.pts.length !== e.pts.length) return false;
    let isEqualForward = true;
    let isEqualReverse = true;
    let iRev = this.pts.length;

    for (let i = 0; i < this.pts.length; i++) {
      if (!this.pts[i].equals2D(e.pts[i])) isEqualForward = false;
      if (!this.pts[i].equals2D(e.pts[--iRev])) isEqualReverse = false;
      if (!isEqualForward && !isEqualReverse) return false;
    }

    return true;
  }

  getCoordinate() {
    if (arguments.length === 0) {
      if (this.pts.length > 0) return this.pts[0];
      return null;
    } else if (arguments.length === 1) {
      const i = arguments[0];
      return this.pts[i];
    }
  }

  print(out) {
    out.print('edge ' + this._name + ': ');
    out.print('LINESTRING (');

    for (let i = 0; i < this.pts.length; i++) {
      if (i > 0) out.print(',');
      out.print(this.pts[i].x + ' ' + this.pts[i].y);
    }

    out.print(')  ' + this._label + ' ' + this._depthDelta);
  }

  computeIM(im) {
    Edge$1.updateIM(this._label, im);
  }

  isCollapsed() {
    if (!this._label.isArea()) return false;
    if (this.pts.length !== 3) return false;
    if (this.pts[0].equals(this.pts[2])) return true;
    return false;
  }

  isClosed() {
    return this.pts[0].equals(this.pts[this.pts.length - 1]);
  }

  getMaximumSegmentIndex() {
    return this.pts.length - 1;
  }

  getDepthDelta() {
    return this._depthDelta;
  }

  getNumPoints() {
    return this.pts.length;
  }

  printReverse(out) {
    out.print('edge ' + this._name + ': ');

    for (let i = this.pts.length - 1; i >= 0; i--) out.print(this.pts[i] + ' ');

    out.println('');
  }

  getMonotoneChainEdge() {
    if (this._mce === null) this._mce = new MonotoneChainEdge(this);
    return this._mce;
  }

  getEnvelope() {
    if (this._env === null) {
      this._env = new Envelope();

      for (let i = 0; i < this.pts.length; i++) this._env.expandToInclude(this.pts[i]);
    }

    return this._env;
  }

  addIntersection(li, segmentIndex, geomIndex, intIndex) {
    const intPt = new Coordinate(li.getIntersection(intIndex));
    let normalizedSegmentIndex = segmentIndex;
    let dist = li.getEdgeDistance(geomIndex, intIndex);
    const nextSegIndex = normalizedSegmentIndex + 1;

    if (nextSegIndex < this.pts.length) {
      const nextPt = this.pts[nextSegIndex];

      if (intPt.equals2D(nextPt)) {
        normalizedSegmentIndex = nextSegIndex;
        dist = 0.0;
      }
    }

    this.eiList.add(intPt, normalizedSegmentIndex, dist);
  }

  toString() {
    const builder = new StringBuilder();
    builder.append('edge ' + this._name + ': ');
    builder.append('LINESTRING (');

    for (let i = 0; i < this.pts.length; i++) {
      if (i > 0) builder.append(',');
      builder.append(this.pts[i].x + ' ' + this.pts[i].y);
    }

    builder.append(')  ' + this._label + ' ' + this._depthDelta);
    return builder.toString();
  }

  isPointwiseEqual(e) {
    if (this.pts.length !== e.pts.length) return false;

    for (let i = 0; i < this.pts.length; i++) if (!this.pts[i].equals2D(e.pts[i])) return false;

    return true;
  }

  setDepthDelta(depthDelta) {
    this._depthDelta = depthDelta;
  }

  getEdgeIntersectionList() {
    return this.eiList;
  }

  addIntersections(li, segmentIndex, geomIndex) {
    for (let i = 0; i < li.getIntersectionNum(); i++) this.addIntersection(li, segmentIndex, geomIndex, i);
  }

}

class Node$2 extends GraphComponent$1 {
  constructor() {
    super();
    Node$2.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._coord = null;
    this._edges = null;
    const coord = arguments[0],
          edges = arguments[1];
    this._coord = coord;
    this._edges = edges;
    this._label = new Label(0, Location.NONE);
  }

  isIncidentEdgeInResult() {
    for (let it = this.getEdges().getEdges().iterator(); it.hasNext();) {
      const de = it.next();
      if (de.getEdge().isInResult()) return true;
    }

    return false;
  }

  isIsolated() {
    return this._label.getGeometryCount() === 1;
  }

  getCoordinate() {
    return this._coord;
  }

  print(out) {
    out.println('node ' + this._coord + ' lbl: ' + this._label);
  }

  computeIM(im) {}

  computeMergedLocation(label2, eltIndex) {
    let loc = Location.NONE;
    loc = this._label.getLocation(eltIndex);

    if (!label2.isNull(eltIndex)) {
      const nLoc = label2.getLocation(eltIndex);
      if (loc !== Location.BOUNDARY) loc = nLoc;
    }

    return loc;
  }

  setLabel() {
    if (arguments.length === 2 && Number.isInteger(arguments[1]) && Number.isInteger(arguments[0])) {
      const argIndex = arguments[0],
            onLocation = arguments[1];
      if (this._label === null) this._label = new Label(argIndex, onLocation);else this._label.setLocation(argIndex, onLocation);
    } else {
      return super.setLabel.apply(this, arguments);
    }
  }

  getEdges() {
    return this._edges;
  }

  mergeLabel() {
    if (arguments[0] instanceof Node$2) {
      const n = arguments[0];
      this.mergeLabel(n._label);
    } else if (arguments[0] instanceof Label) {
      const label2 = arguments[0];

      for (let i = 0; i < 2; i++) {
        const loc = this.computeMergedLocation(label2, i);

        const thisLoc = this._label.getLocation(i);

        if (thisLoc === Location.NONE) this._label.setLocation(i, loc);
      }
    }
  }

  add(e) {
    this._edges.insert(e);

    e.setNode(this);
  }

  setLabelBoundary(argIndex) {
    if (this._label === null) return null;
    let loc = Location.NONE;
    if (this._label !== null) loc = this._label.getLocation(argIndex);
    let newLoc = null;

    switch (loc) {
      case Location.BOUNDARY:
        newLoc = Location.INTERIOR;
        break;

      case Location.INTERIOR:
        newLoc = Location.BOUNDARY;
        break;

      default:
        newLoc = Location.BOUNDARY;
        break;
    }

    this._label.setLocation(argIndex, newLoc);
  }

}

class NodeMap$1 {
  constructor() {
    NodeMap$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.nodeMap = new TreeMap();
    this.nodeFact = null;
    const nodeFact = arguments[0];
    this.nodeFact = nodeFact;
  }

  find(coord) {
    return this.nodeMap.get(coord);
  }

  addNode() {
    if (arguments[0] instanceof Coordinate) {
      const coord = arguments[0];
      let node = this.nodeMap.get(coord);

      if (node === null) {
        node = this.nodeFact.createNode(coord);
        this.nodeMap.put(coord, node);
      }

      return node;
    } else if (arguments[0] instanceof Node$2) {
      const n = arguments[0];
      const node = this.nodeMap.get(n.getCoordinate());

      if (node === null) {
        this.nodeMap.put(n.getCoordinate(), n);
        return n;
      }

      node.mergeLabel(n);
      return node;
    }
  }

  print(out) {
    for (let it = this.iterator(); it.hasNext();) {
      const n = it.next();
      n.print(out);
    }
  }

  iterator() {
    return this.nodeMap.values().iterator();
  }

  values() {
    return this.nodeMap.values();
  }

  getBoundaryNodes(geomIndex) {
    const bdyNodes = new ArrayList();

    for (let i = this.iterator(); i.hasNext();) {
      const node = i.next();
      if (node.getLabel().getLocation(geomIndex) === Location.BOUNDARY) bdyNodes.add(node);
    }

    return bdyNodes;
  }

  add(e) {
    const p = e.getCoordinate();
    const n = this.addNode(p);
    n.add(e);
  }

}

class EdgeEnd {
  constructor() {
    EdgeEnd.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._edge = null;
    this._label = null;
    this._node = null;
    this._p0 = null;
    this._p1 = null;
    this._dx = null;
    this._dy = null;
    this._quadrant = null;

    if (arguments.length === 1) {
      const edge = arguments[0];
      this._edge = edge;
    } else if (arguments.length === 3) {
      const edge = arguments[0],
            p0 = arguments[1],
            p1 = arguments[2];
      EdgeEnd.constructor_.call(this, edge, p0, p1, null);
    } else if (arguments.length === 4) {
      const edge = arguments[0],
            p0 = arguments[1],
            p1 = arguments[2],
            label = arguments[3];
      EdgeEnd.constructor_.call(this, edge);
      this.init(p0, p1);
      this._label = label;
    }
  }

  compareDirection(e) {
    if (this._dx === e._dx && this._dy === e._dy) return 0;
    if (this._quadrant > e._quadrant) return 1;
    if (this._quadrant < e._quadrant) return -1;
    return Orientation.index(e._p0, e._p1, this._p1);
  }

  getDy() {
    return this._dy;
  }

  getCoordinate() {
    return this._p0;
  }

  setNode(node) {
    this._node = node;
  }

  print(out) {
    const angle = Math.atan2(this._dy, this._dx);
    const className = this.getClass().getName();
    const lastDotPos = className.lastIndexOf('.');
    const name = className.substring(lastDotPos + 1);
    out.print('  ' + name + ': ' + this._p0 + ' - ' + this._p1 + ' ' + this._quadrant + ':' + angle + '   ' + this._label);
  }

  compareTo(obj) {
    const e = obj;
    return this.compareDirection(e);
  }

  getDirectedCoordinate() {
    return this._p1;
  }

  getDx() {
    return this._dx;
  }

  getLabel() {
    return this._label;
  }

  getEdge() {
    return this._edge;
  }

  getQuadrant() {
    return this._quadrant;
  }

  getNode() {
    return this._node;
  }

  toString() {
    const angle = Math.atan2(this._dy, this._dx);
    const className = this.getClass().getName();
    const lastDotPos = className.lastIndexOf('.');
    const name = className.substring(lastDotPos + 1);
    return '  ' + name + ': ' + this._p0 + ' - ' + this._p1 + ' ' + this._quadrant + ':' + angle + '   ' + this._label;
  }

  computeLabel(boundaryNodeRule) {}

  init(p0, p1) {
    this._p0 = p0;
    this._p1 = p1;
    this._dx = p1.x - p0.x;
    this._dy = p1.y - p0.y;
    this._quadrant = Quadrant.quadrant(this._dx, this._dy);
    Assert.isTrue(!(this._dx === 0 && this._dy === 0), 'EdgeEnd with identical endpoints found');
  }

  get interfaces_() {
    return [Comparable];
  }

}

class TopologyException extends RuntimeException {
  constructor(msg, pt) {
    super(pt ? msg + ' [ ' + pt + ' ]' : msg);
    this.pt = pt ? new Coordinate(pt) : undefined;
    this.name = Object.keys({
      TopologyException
    })[0];
  }

  getCoordinate() {
    return this.pt;
  }

}

class DirectedEdge$1 extends EdgeEnd {
  constructor() {
    super();
    DirectedEdge$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._isForward = null;
    this._isInResult = false;
    this._isVisited = false;
    this._sym = null;
    this._next = null;
    this._nextMin = null;
    this._edgeRing = null;
    this._minEdgeRing = null;
    this._depth = [0, -999, -999];
    const edge = arguments[0],
          isForward = arguments[1];
    EdgeEnd.constructor_.call(this, edge);
    this._isForward = isForward;

    if (isForward) {
      this.init(edge.getCoordinate(0), edge.getCoordinate(1));
    } else {
      const n = edge.getNumPoints() - 1;
      this.init(edge.getCoordinate(n), edge.getCoordinate(n - 1));
    }

    this.computeDirectedLabel();
  }

  static depthFactor(currLocation, nextLocation) {
    if (currLocation === Location.EXTERIOR && nextLocation === Location.INTERIOR) return 1;else if (currLocation === Location.INTERIOR && nextLocation === Location.EXTERIOR) return -1;
    return 0;
  }

  getNextMin() {
    return this._nextMin;
  }

  getDepth(position) {
    return this._depth[position];
  }

  setVisited(isVisited) {
    this._isVisited = isVisited;
  }

  computeDirectedLabel() {
    this._label = new Label(this._edge.getLabel());
    if (!this._isForward) this._label.flip();
  }

  getNext() {
    return this._next;
  }

  setDepth(position, depthVal) {
    if (this._depth[position] !== -999) if (this._depth[position] !== depthVal) throw new TopologyException('assigned depths do not match', this.getCoordinate());
    this._depth[position] = depthVal;
  }

  isInteriorAreaEdge() {
    let isInteriorAreaEdge = true;

    for (let i = 0; i < 2; i++) if (!(this._label.isArea(i) && this._label.getLocation(i, Position.LEFT) === Location.INTERIOR && this._label.getLocation(i, Position.RIGHT) === Location.INTERIOR)) isInteriorAreaEdge = false;

    return isInteriorAreaEdge;
  }

  setNextMin(nextMin) {
    this._nextMin = nextMin;
  }

  print(out) {
    super.print.call(this, out);
    out.print(' ' + this._depth[Position.LEFT] + '/' + this._depth[Position.RIGHT]);
    out.print(' (' + this.getDepthDelta() + ')');
    if (this._isInResult) out.print(' inResult');
  }

  setMinEdgeRing(minEdgeRing) {
    this._minEdgeRing = minEdgeRing;
  }

  isLineEdge() {
    const isLine = this._label.isLine(0) || this._label.isLine(1);

    const isExteriorIfArea0 = !this._label.isArea(0) || this._label.allPositionsEqual(0, Location.EXTERIOR);

    const isExteriorIfArea1 = !this._label.isArea(1) || this._label.allPositionsEqual(1, Location.EXTERIOR);

    return isLine && isExteriorIfArea0 && isExteriorIfArea1;
  }

  setEdgeRing(edgeRing) {
    this._edgeRing = edgeRing;
  }

  getMinEdgeRing() {
    return this._minEdgeRing;
  }

  getDepthDelta() {
    let depthDelta = this._edge.getDepthDelta();

    if (!this._isForward) depthDelta = -depthDelta;
    return depthDelta;
  }

  setInResult(isInResult) {
    this._isInResult = isInResult;
  }

  getSym() {
    return this._sym;
  }

  isForward() {
    return this._isForward;
  }

  getEdge() {
    return this._edge;
  }

  printEdge(out) {
    this.print(out);
    out.print(' ');
    if (this._isForward) this._edge.print(out);else this._edge.printReverse(out);
  }

  setSym(de) {
    this._sym = de;
  }

  setVisitedEdge(isVisited) {
    this.setVisited(isVisited);

    this._sym.setVisited(isVisited);
  }

  setEdgeDepths(position, depth) {
    let depthDelta = this.getEdge().getDepthDelta();
    if (!this._isForward) depthDelta = -depthDelta;
    let directionFactor = 1;
    if (position === Position.LEFT) directionFactor = -1;
    const oppositePos = Position.opposite(position);
    const delta = depthDelta * directionFactor;
    const oppositeDepth = depth + delta;
    this.setDepth(position, depth);
    this.setDepth(oppositePos, oppositeDepth);
  }

  getEdgeRing() {
    return this._edgeRing;
  }

  isInResult() {
    return this._isInResult;
  }

  setNext(next) {
    this._next = next;
  }

  isVisited() {
    return this._isVisited;
  }

}

class NodeFactory {
  createNode(coord) {
    return new Node$2(coord, null);
  }

}

class PlanarGraph$1 {
  constructor() {
    PlanarGraph$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._edges = new ArrayList();
    this._nodes = null;
    this._edgeEndList = new ArrayList();

    if (arguments.length === 0) {
      this._nodes = new NodeMap$1(new NodeFactory());
    } else if (arguments.length === 1) {
      const nodeFact = arguments[0];
      this._nodes = new NodeMap$1(nodeFact);
    }
  }

  static linkResultDirectedEdges(nodes) {
    for (let nodeit = nodes.iterator(); nodeit.hasNext();) {
      const node = nodeit.next();
      node.getEdges().linkResultDirectedEdges();
    }
  }

  printEdges(out) {
    out.println('Edges:');

    for (let i = 0; i < this._edges.size(); i++) {
      out.println('edge ' + i + ':');

      const e = this._edges.get(i);

      e.print(out);
      e.eiList.print(out);
    }
  }

  find(coord) {
    return this._nodes.find(coord);
  }

  addNode() {
    if (arguments[0] instanceof Node$2) {
      const node = arguments[0];
      return this._nodes.addNode(node);
    } else if (arguments[0] instanceof Coordinate) {
      const coord = arguments[0];
      return this._nodes.addNode(coord);
    }
  }

  getNodeIterator() {
    return this._nodes.iterator();
  }

  linkResultDirectedEdges() {
    for (let nodeit = this._nodes.iterator(); nodeit.hasNext();) {
      const node = nodeit.next();
      node.getEdges().linkResultDirectedEdges();
    }
  }

  debugPrintln(o) {
    System.out.println(o);
  }

  isBoundaryNode(geomIndex, coord) {
    const node = this._nodes.find(coord);

    if (node === null) return false;
    const label = node.getLabel();
    if (label !== null && label.getLocation(geomIndex) === Location.BOUNDARY) return true;
    return false;
  }

  linkAllDirectedEdges() {
    for (let nodeit = this._nodes.iterator(); nodeit.hasNext();) {
      const node = nodeit.next();
      node.getEdges().linkAllDirectedEdges();
    }
  }

  matchInSameDirection(p0, p1, ep0, ep1) {
    if (!p0.equals(ep0)) return false;
    if (Orientation.index(p0, p1, ep1) === Orientation.COLLINEAR && Quadrant.quadrant(p0, p1) === Quadrant.quadrant(ep0, ep1)) return true;
    return false;
  }

  getEdgeEnds() {
    return this._edgeEndList;
  }

  debugPrint(o) {
    System.out.print(o);
  }

  getEdgeIterator() {
    return this._edges.iterator();
  }

  findEdgeInSameDirection(p0, p1) {
    for (let i = 0; i < this._edges.size(); i++) {
      const e = this._edges.get(i);

      const eCoord = e.getCoordinates();
      if (this.matchInSameDirection(p0, p1, eCoord[0], eCoord[1])) return e;
      if (this.matchInSameDirection(p0, p1, eCoord[eCoord.length - 1], eCoord[eCoord.length - 2])) return e;
    }

    return null;
  }

  insertEdge(e) {
    this._edges.add(e);
  }

  findEdgeEnd(e) {
    for (let i = this.getEdgeEnds().iterator(); i.hasNext();) {
      const ee = i.next();
      if (ee.getEdge() === e) return ee;
    }

    return null;
  }

  addEdges(edgesToAdd) {
    for (let it = edgesToAdd.iterator(); it.hasNext();) {
      const e = it.next();

      this._edges.add(e);

      const de1 = new DirectedEdge$1(e, true);
      const de2 = new DirectedEdge$1(e, false);
      de1.setSym(de2);
      de2.setSym(de1);
      this.add(de1);
      this.add(de2);
    }
  }

  add(e) {
    this._nodes.add(e);

    this._edgeEndList.add(e);
  }

  getNodes() {
    return this._nodes.values();
  }

  findEdge(p0, p1) {
    for (let i = 0; i < this._edges.size(); i++) {
      const e = this._edges.get(i);

      const eCoord = e.getCoordinates();
      if (p0.equals(eCoord[0]) && p1.equals(eCoord[1])) return e;
    }

    return null;
  }

}

class GeometryGraph extends PlanarGraph$1 {
  constructor() {
    super();
    GeometryGraph.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._parentGeom = null;
    this._lineEdgeMap = new HashMap();
    this._boundaryNodeRule = null;
    this._useBoundaryDeterminationRule = true;
    this._argIndex = null;
    this._boundaryNodes = null;
    this._hasTooFewPoints = false;
    this._invalidPoint = null;
    this._areaPtLocator = null;
    this._ptLocator = new PointLocator();

    if (arguments.length === 2) {
      const argIndex = arguments[0],
            parentGeom = arguments[1];
      GeometryGraph.constructor_.call(this, argIndex, parentGeom, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE);
    } else if (arguments.length === 3) {
      const argIndex = arguments[0],
            parentGeom = arguments[1],
            boundaryNodeRule = arguments[2];
      this._argIndex = argIndex;
      this._parentGeom = parentGeom;
      this._boundaryNodeRule = boundaryNodeRule;
      if (parentGeom !== null) this.add(parentGeom);
    }
  }

  static determineBoundary(boundaryNodeRule, boundaryCount) {
    return boundaryNodeRule.isInBoundary(boundaryCount) ? Location.BOUNDARY : Location.INTERIOR;
  }

  insertBoundaryPoint(argIndex, coord) {
    const n = this._nodes.addNode(coord);

    const lbl = n.getLabel();
    let boundaryCount = 1;
    let loc = Location.NONE;
    loc = lbl.getLocation(argIndex, Position.ON);
    if (loc === Location.BOUNDARY) boundaryCount++;
    const newLoc = GeometryGraph.determineBoundary(this._boundaryNodeRule, boundaryCount);
    lbl.setLocation(argIndex, newLoc);
  }

  computeSelfNodes() {
    if (arguments.length === 2) {
      const li = arguments[0],
            computeRingSelfNodes = arguments[1];
      return this.computeSelfNodes(li, computeRingSelfNodes, false);
    } else if (arguments.length === 3) {
      const li = arguments[0],
            computeRingSelfNodes = arguments[1],
            isDoneIfProperInt = arguments[2];
      const si = new SegmentIntersector$1(li, true, false);
      si.setIsDoneIfProperInt(isDoneIfProperInt);
      const esi = this.createEdgeSetIntersector();
      const isRings = this._parentGeom instanceof LinearRing || this._parentGeom instanceof Polygon || this._parentGeom instanceof MultiPolygon;
      const computeAllSegments = computeRingSelfNodes || !isRings;
      esi.computeIntersections(this._edges, si, computeAllSegments);
      this.addSelfIntersectionNodes(this._argIndex);
      return si;
    }
  }

  computeSplitEdges(edgelist) {
    for (let i = this._edges.iterator(); i.hasNext();) {
      const e = i.next();
      e.eiList.addSplitEdges(edgelist);
    }
  }

  computeEdgeIntersections(g, li, includeProper) {
    const si = new SegmentIntersector$1(li, includeProper, true);
    si.setBoundaryNodes(this.getBoundaryNodes(), g.getBoundaryNodes());
    const esi = this.createEdgeSetIntersector();
    esi.computeIntersections(this._edges, g._edges, si);
    return si;
  }

  getGeometry() {
    return this._parentGeom;
  }

  getBoundaryNodeRule() {
    return this._boundaryNodeRule;
  }

  hasTooFewPoints() {
    return this._hasTooFewPoints;
  }

  addPoint() {
    if (arguments[0] instanceof Point) {
      const p = arguments[0];
      const coord = p.getCoordinate();
      this.insertPoint(this._argIndex, coord, Location.INTERIOR);
    } else if (arguments[0] instanceof Coordinate) {
      const pt = arguments[0];
      this.insertPoint(this._argIndex, pt, Location.INTERIOR);
    }
  }

  addPolygon(p) {
    this.addPolygonRing(p.getExteriorRing(), Location.EXTERIOR, Location.INTERIOR);

    for (let i = 0; i < p.getNumInteriorRing(); i++) {
      const hole = p.getInteriorRingN(i);
      this.addPolygonRing(hole, Location.INTERIOR, Location.EXTERIOR);
    }
  }

  addEdge(e) {
    this.insertEdge(e);
    const coord = e.getCoordinates();
    this.insertPoint(this._argIndex, coord[0], Location.BOUNDARY);
    this.insertPoint(this._argIndex, coord[coord.length - 1], Location.BOUNDARY);
  }

  addLineString(line) {
    const coord = CoordinateArrays.removeRepeatedPoints(line.getCoordinates());

    if (coord.length < 2) {
      this._hasTooFewPoints = true;
      this._invalidPoint = coord[0];
      return null;
    }

    const e = new Edge$1(coord, new Label(this._argIndex, Location.INTERIOR));

    this._lineEdgeMap.put(line, e);

    this.insertEdge(e);
    Assert.isTrue(coord.length >= 2, 'found LineString with single point');
    this.insertBoundaryPoint(this._argIndex, coord[0]);
    this.insertBoundaryPoint(this._argIndex, coord[coord.length - 1]);
  }

  getInvalidPoint() {
    return this._invalidPoint;
  }

  getBoundaryPoints() {
    const coll = this.getBoundaryNodes();
    const pts = new Array(coll.size()).fill(null);
    let i = 0;

    for (let it = coll.iterator(); it.hasNext();) {
      const node = it.next();
      pts[i++] = node.getCoordinate().copy();
    }

    return pts;
  }

  getBoundaryNodes() {
    if (this._boundaryNodes === null) this._boundaryNodes = this._nodes.getBoundaryNodes(this._argIndex);
    return this._boundaryNodes;
  }

  addSelfIntersectionNode(argIndex, coord, loc) {
    if (this.isBoundaryNode(argIndex, coord)) return null;
    if (loc === Location.BOUNDARY && this._useBoundaryDeterminationRule) this.insertBoundaryPoint(argIndex, coord);else this.insertPoint(argIndex, coord, loc);
  }

  addPolygonRing(lr, cwLeft, cwRight) {
    if (lr.isEmpty()) return null;
    const coord = CoordinateArrays.removeRepeatedPoints(lr.getCoordinates());

    if (coord.length < 4) {
      this._hasTooFewPoints = true;
      this._invalidPoint = coord[0];
      return null;
    }

    let left = cwLeft;
    let right = cwRight;

    if (Orientation.isCCW(coord)) {
      left = cwRight;
      right = cwLeft;
    }

    const e = new Edge$1(coord, new Label(this._argIndex, Location.BOUNDARY, left, right));

    this._lineEdgeMap.put(lr, e);

    this.insertEdge(e);
    this.insertPoint(this._argIndex, coord[0], Location.BOUNDARY);
  }

  insertPoint(argIndex, coord, onLocation) {
    const n = this._nodes.addNode(coord);

    const lbl = n.getLabel();
    if (lbl === null) n._label = new Label(argIndex, onLocation);else lbl.setLocation(argIndex, onLocation);
  }

  createEdgeSetIntersector() {
    return new SimpleMCSweepLineIntersector();
  }

  addSelfIntersectionNodes(argIndex) {
    for (let i = this._edges.iterator(); i.hasNext();) {
      const e = i.next();
      const eLoc = e.getLabel().getLocation(argIndex);

      for (let eiIt = e.eiList.iterator(); eiIt.hasNext();) {
        const ei = eiIt.next();
        this.addSelfIntersectionNode(argIndex, ei.coord, eLoc);
      }
    }
  }

  add() {
    if (arguments.length === 1 && arguments[0] instanceof Geometry) {
      const g = arguments[0];
      if (g.isEmpty()) return null;
      if (g instanceof MultiPolygon) this._useBoundaryDeterminationRule = false;
      if (g instanceof Polygon) this.addPolygon(g);else if (g instanceof LineString) this.addLineString(g);else if (g instanceof Point) this.addPoint(g);else if (g instanceof MultiPoint) this.addCollection(g);else if (g instanceof MultiLineString) this.addCollection(g);else if (g instanceof MultiPolygon) this.addCollection(g);else if (g instanceof GeometryCollection) this.addCollection(g);else throw new UnsupportedOperationException(g.getGeometryType());
    } else {
      return super.add.apply(this, arguments);
    }
  }

  addCollection(gc) {
    for (let i = 0; i < gc.getNumGeometries(); i++) {
      const g = gc.getGeometryN(i);
      this.add(g);
    }
  }

  locate(pt) {
    if (hasInterface(this._parentGeom, Polygonal) && this._parentGeom.getNumGeometries() > 50) {
      if (this._areaPtLocator === null) this._areaPtLocator = new IndexedPointInAreaLocator(this._parentGeom);
      return this._areaPtLocator.locate(pt);
    }

    return this._ptLocator.locate(pt, this._parentGeom);
  }

  findEdge() {
    if (arguments.length === 1 && arguments[0] instanceof LineString) {
      const line = arguments[0];
      return this._lineEdgeMap.get(line);
    } else {
      return super.findEdge.apply(this, arguments);
    }
  }

}

var geomgraph = /*#__PURE__*/Object.freeze({
  __proto__: null,
  GeometryGraph: GeometryGraph
});

class KdNodeVisitor {
  visit(node) {}

}

class KdNode {
  constructor() {
    KdNode.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._p = null;
    this._data = null;
    this._left = null;
    this._right = null;
    this._count = null;

    if (arguments.length === 2) {
      const p = arguments[0],
            data = arguments[1];
      this._p = new Coordinate(p);
      this._left = null;
      this._right = null;
      this._count = 1;
      this._data = data;
    } else if (arguments.length === 3) {
      const _x = arguments[0],
            _y = arguments[1],
            data = arguments[2];
      this._p = new Coordinate(_x, _y);
      this._left = null;
      this._right = null;
      this._count = 1;
      this._data = data;
    }
  }

  isRepeated() {
    return this._count > 1;
  }

  getRight() {
    return this._right;
  }

  getCoordinate() {
    return this._p;
  }

  setLeft(_left) {
    this._left = _left;
  }

  getX() {
    return this._p.x;
  }

  getData() {
    return this._data;
  }

  getCount() {
    return this._count;
  }

  getLeft() {
    return this._left;
  }

  getY() {
    return this._p.y;
  }

  increment() {
    this._count = this._count + 1;
  }

  setRight(_right) {
    this._right = _right;
  }

}

class KdTree {
  constructor() {
    KdTree.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._root = null;
    this._numberOfNodes = null;
    this._tolerance = null;

    if (arguments.length === 0) {
      KdTree.constructor_.call(this, 0.0);
    } else if (arguments.length === 1) {
      const tolerance = arguments[0];
      this._tolerance = tolerance;
    }
  }

  static toCoordinates() {
    if (arguments.length === 1) {
      const kdnodes = arguments[0];
      return KdTree.toCoordinates(kdnodes, false);
    } else if (arguments.length === 2) {
      const kdnodes = arguments[0],
            includeRepeated = arguments[1];
      const coord = new CoordinateList();

      for (let it = kdnodes.iterator(); it.hasNext();) {
        const node = it.next();
        const count = includeRepeated ? node.getCount() : 1;

        for (let i = 0; i < count; i++) coord.add(node.getCoordinate(), true);
      }

      return coord.toCoordinateArray();
    }
  }

  insert() {
    if (arguments.length === 1) {
      const p = arguments[0];
      return this.insert(p, null);
    } else if (arguments.length === 2) {
      const p = arguments[0],
            data = arguments[1];

      if (this._root === null) {
        this._root = new KdNode(p, data);
        return this._root;
      }

      if (this._tolerance > 0) {
        const matchNode = this.findBestMatchNode(p);

        if (matchNode !== null) {
          matchNode.increment();
          return matchNode;
        }
      }

      return this.insertExact(p, data);
    }
  }

  query() {
    if (arguments.length === 1) {
      const queryEnv = arguments[0];
      const result = new ArrayList();
      this.query(queryEnv, result);
      return result;
    } else if (arguments.length === 2) {
      if (arguments[0] instanceof Envelope && hasInterface(arguments[1], List)) {
        const queryEnv = arguments[0],
              result = arguments[1];
        this.queryNode(this._root, queryEnv, true, new class {
          get interfaces_() {
            return [KdNodeVisitor];
          }

          visit(node) {
            result.add(node);
          }

        }());
      } else if (arguments[0] instanceof Envelope && hasInterface(arguments[1], KdNodeVisitor)) {
        const queryEnv = arguments[0],
              visitor = arguments[1];
        this.queryNode(this._root, queryEnv, true, visitor);
      }
    }
  }

  queryNode(currentNode, queryEnv, odd, visitor) {
    if (currentNode === null) return null;
    let min = null;
    let max = null;
    let discriminant = null;

    if (odd) {
      min = queryEnv.getMinX();
      max = queryEnv.getMaxX();
      discriminant = currentNode.getX();
    } else {
      min = queryEnv.getMinY();
      max = queryEnv.getMaxY();
      discriminant = currentNode.getY();
    }

    const searchLeft = min < discriminant;
    const searchRight = discriminant <= max;
    if (searchLeft) this.queryNode(currentNode.getLeft(), queryEnv, !odd, visitor);
    if (queryEnv.contains(currentNode.getCoordinate())) visitor.visit(currentNode);
    if (searchRight) this.queryNode(currentNode.getRight(), queryEnv, !odd, visitor);
  }

  findBestMatchNode(p) {
    const visitor = new BestMatchVisitor(p, this._tolerance);
    this.query(visitor.queryEnvelope(), visitor);
    return visitor.getNode();
  }

  isEmpty() {
    if (this._root === null) return true;
    return false;
  }

  insertExact(p, data) {
    let currentNode = this._root;
    let leafNode = this._root;
    let isOddLevel = true;
    let isLessThan = true;

    while (currentNode !== null) {
      if (currentNode !== null) {
        const isInTolerance = p.distance(currentNode.getCoordinate()) <= this._tolerance;

        if (isInTolerance) {
          currentNode.increment();
          return currentNode;
        }
      }

      if (isOddLevel) isLessThan = p.x < currentNode.getX();else isLessThan = p.y < currentNode.getY();
      leafNode = currentNode;
      if (isLessThan) currentNode = currentNode.getLeft();else currentNode = currentNode.getRight();
      isOddLevel = !isOddLevel;
    }

    this._numberOfNodes = this._numberOfNodes + 1;
    const node = new KdNode(p, data);
    if (isLessThan) leafNode.setLeft(node);else leafNode.setRight(node);
    return node;
  }

}

class BestMatchVisitor {
  constructor() {
    BestMatchVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._tolerance = null;
    this._matchNode = null;
    this._matchDist = 0.0;
    this._p = null;
    const p = arguments[0],
          tolerance = arguments[1];
    this._p = p;
    this._tolerance = tolerance;
  }

  visit(node) {
    const dist = this._p.distance(node.getCoordinate());

    const isInTolerance = dist <= this._tolerance;
    if (!isInTolerance) return null;
    let update = false;
    if (this._matchNode === null || dist < this._matchDist || this._matchNode !== null && dist === this._matchDist && node.getCoordinate().compareTo(this._matchNode.getCoordinate()) < 1) update = true;

    if (update) {
      this._matchNode = node;
      this._matchDist = dist;
    }
  }

  queryEnvelope() {
    const queryEnv = new Envelope(this._p);
    queryEnv.expandBy(this._tolerance);
    return queryEnv;
  }

  getNode() {
    return this._matchNode;
  }

  get interfaces_() {
    return [KdNodeVisitor];
  }

}

KdTree.BestMatchVisitor = BestMatchVisitor;

var kdtree = /*#__PURE__*/Object.freeze({
  __proto__: null,
  KdTree: KdTree
});

class NodeBase {
  constructor() {
    NodeBase.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._items = new ArrayList();
    this._subnode = new Array(4).fill(null);
  }

  static getSubnodeIndex(env, centrex, centrey) {
    let subnodeIndex = -1;

    if (env.getMinX() >= centrex) {
      if (env.getMinY() >= centrey) subnodeIndex = 3;
      if (env.getMaxY() <= centrey) subnodeIndex = 1;
    }

    if (env.getMaxX() <= centrex) {
      if (env.getMinY() >= centrey) subnodeIndex = 2;
      if (env.getMaxY() <= centrey) subnodeIndex = 0;
    }

    return subnodeIndex;
  }

  hasChildren() {
    for (let i = 0; i < 4; i++) if (this._subnode[i] !== null) return true;

    return false;
  }

  isPrunable() {
    return !(this.hasChildren() || this.hasItems());
  }

  addAllItems(resultItems) {
    resultItems.addAll(this._items);

    for (let i = 0; i < 4; i++) if (this._subnode[i] !== null) this._subnode[i].addAllItems(resultItems);

    return resultItems;
  }

  getNodeCount() {
    let subSize = 0;

    for (let i = 0; i < 4; i++) if (this._subnode[i] !== null) subSize += this._subnode[i].size();

    return subSize + 1;
  }

  size() {
    let subSize = 0;

    for (let i = 0; i < 4; i++) if (this._subnode[i] !== null) subSize += this._subnode[i].size();

    return subSize + this._items.size();
  }

  addAllItemsFromOverlapping(searchEnv, resultItems) {
    if (!this.isSearchMatch(searchEnv)) return null;
    resultItems.addAll(this._items);

    for (let i = 0; i < 4; i++) if (this._subnode[i] !== null) this._subnode[i].addAllItemsFromOverlapping(searchEnv, resultItems);
  }

  visitItems(searchEnv, visitor) {
    for (let i = this._items.iterator(); i.hasNext();) visitor.visitItem(i.next());
  }

  hasItems() {
    return !this._items.isEmpty();
  }

  remove(itemEnv, item) {
    if (!this.isSearchMatch(itemEnv)) return false;
    let found = false;

    for (let i = 0; i < 4; i++) if (this._subnode[i] !== null) {
      found = this._subnode[i].remove(itemEnv, item);

      if (found) {
        if (this._subnode[i].isPrunable()) this._subnode[i] = null;
        break;
      }
    }

    if (found) return found;
    found = this._items.remove(item);
    return found;
  }

  visit(searchEnv, visitor) {
    if (!this.isSearchMatch(searchEnv)) return null;
    this.visitItems(searchEnv, visitor);

    for (let i = 0; i < 4; i++) if (this._subnode[i] !== null) this._subnode[i].visit(searchEnv, visitor);
  }

  getItems() {
    return this._items;
  }

  depth() {
    let maxSubDepth = 0;

    for (let i = 0; i < 4; i++) if (this._subnode[i] !== null) {
      const sqd = this._subnode[i].depth();

      if (sqd > maxSubDepth) maxSubDepth = sqd;
    }

    return maxSubDepth + 1;
  }

  isEmpty() {
    let isEmpty = true;
    if (!this._items.isEmpty()) isEmpty = false;else for (let i = 0; i < 4; i++) if (this._subnode[i] !== null) if (!this._subnode[i].isEmpty()) {
      isEmpty = false;
      break;
    }
    return isEmpty;
  }

  add(item) {
    this._items.add(item);
  }

  get interfaces_() {
    return [Serializable];
  }

}

function DoubleBits() {}

DoubleBits.exponent = function (d) {
  return CVTFWD(64, d) - 1023;
};

DoubleBits.powerOf2 = function (exp) {
  return Math.pow(2, exp);
};
/**
 * Calculates the exponent of the bit-pattern for a number. Uses code from:
 * http://www.merlyn.demon.co.uk/js-exact.htm
 *
 * @param {Number}
 *          NumW 32 or 64 to denote the number of bits.
 * @param {Number}
 *          Qty the number to calculate the bit pattern for.
 * @return {Number} The integer value of the exponent.
 * @private
 */


function CVTFWD(NumW, Qty) {
  let Sign;
  let Expo;
  let Mant;
  let Bin;
  const Inf = {
    32: {
      d: 0x7F,
      c: 0x80,
      b: 0,
      a: 0
    },
    64: {
      d: 0x7FF0,
      c: 0,
      b: 0,
      a: 0
    }
  };
  const ExW = {
    32: 8,
    64: 11
  }[NumW];

  if (!Bin) {
    Sign = Qty < 0 || 1 / Qty < 0; // OK for +-0

    if (!isFinite(Qty)) {
      Bin = Inf[NumW];
      if (Sign) Bin.d += 1 << NumW / 4 - 1;
      Expo = Math.pow(2, ExW) - 1;
      Mant = 0;
    }
  }

  if (!Bin) {
    Expo = {
      32: 127,
      64: 1023
    }[NumW];
    Mant = Math.abs(Qty);

    while (Mant >= 2) {
      Expo++;
      Mant /= 2;
    }

    while (Mant < 1 && Expo > 0) {
      Expo--;
      Mant *= 2;
    }

    if (Expo <= 0) Mant /= 2;

    if (NumW === 32 && Expo > 254) {
      Bin = {
        d: Sign ? 0xFF : 0x7F,
        c: 0x80,
        b: 0,
        a: 0
      };
      Expo = Math.pow(2, ExW) - 1;
      Mant = 0;
    }
  }

  return Expo;
}

class Key {
  constructor() {
    Key.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pt = new Coordinate();
    this._level = 0;
    this._env = null;
    const itemEnv = arguments[0];
    this.computeKey(itemEnv);
  }

  static computeQuadLevel(env) {
    const dx = env.getWidth();
    const dy = env.getHeight();
    const dMax = dx > dy ? dx : dy;
    const level = DoubleBits.exponent(dMax) + 1;
    return level;
  }

  getLevel() {
    return this._level;
  }

  computeKey() {
    if (arguments.length === 1) {
      const itemEnv = arguments[0];
      this._level = Key.computeQuadLevel(itemEnv);
      this._env = new Envelope();
      this.computeKey(this._level, itemEnv);

      while (!this._env.contains(itemEnv)) {
        this._level += 1;
        this.computeKey(this._level, itemEnv);
      }
    } else if (arguments.length === 2) {
      const level = arguments[0],
            itemEnv = arguments[1];
      const quadSize = DoubleBits.powerOf2(level);
      this._pt.x = Math.floor(itemEnv.getMinX() / quadSize) * quadSize;
      this._pt.y = Math.floor(itemEnv.getMinY() / quadSize) * quadSize;

      this._env.init(this._pt.x, this._pt.x + quadSize, this._pt.y, this._pt.y + quadSize);
    }
  }

  getEnvelope() {
    return this._env;
  }

  getCentre() {
    return new Coordinate((this._env.getMinX() + this._env.getMaxX()) / 2, (this._env.getMinY() + this._env.getMaxY()) / 2);
  }

  getPoint() {
    return this._pt;
  }

}

class Node$1 extends NodeBase {
  constructor() {
    super();
    Node$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._env = null;
    this._centrex = null;
    this._centrey = null;
    this._level = null;
    const env = arguments[0],
          level = arguments[1];
    this._env = env;
    this._level = level;
    this._centrex = (env.getMinX() + env.getMaxX()) / 2;
    this._centrey = (env.getMinY() + env.getMaxY()) / 2;
  }

  static createNode(env) {
    const key = new Key(env);
    const node = new Node$1(key.getEnvelope(), key.getLevel());
    return node;
  }

  static createExpanded(node, addEnv) {
    const expandEnv = new Envelope(addEnv);
    if (node !== null) expandEnv.expandToInclude(node._env);
    const largerNode = Node$1.createNode(expandEnv);
    if (node !== null) largerNode.insertNode(node);
    return largerNode;
  }

  find(searchEnv) {
    const subnodeIndex = NodeBase.getSubnodeIndex(searchEnv, this._centrex, this._centrey);
    if (subnodeIndex === -1) return this;

    if (this._subnode[subnodeIndex] !== null) {
      const node = this._subnode[subnodeIndex];
      return node.find(searchEnv);
    }

    return this;
  }

  isSearchMatch(searchEnv) {
    if (searchEnv === null) return false;
    return this._env.intersects(searchEnv);
  }

  getSubnode(index) {
    if (this._subnode[index] === null) this._subnode[index] = this.createSubnode(index);
    return this._subnode[index];
  }

  getEnvelope() {
    return this._env;
  }

  getNode(searchEnv) {
    const subnodeIndex = NodeBase.getSubnodeIndex(searchEnv, this._centrex, this._centrey);

    if (subnodeIndex !== -1) {
      const node = this.getSubnode(subnodeIndex);
      return node.getNode(searchEnv);
    } else {
      return this;
    }
  }

  createSubnode(index) {
    let minx = 0.0;
    let maxx = 0.0;
    let miny = 0.0;
    let maxy = 0.0;

    switch (index) {
      case 0:
        minx = this._env.getMinX();
        maxx = this._centrex;
        miny = this._env.getMinY();
        maxy = this._centrey;
        break;

      case 1:
        minx = this._centrex;
        maxx = this._env.getMaxX();
        miny = this._env.getMinY();
        maxy = this._centrey;
        break;

      case 2:
        minx = this._env.getMinX();
        maxx = this._centrex;
        miny = this._centrey;
        maxy = this._env.getMaxY();
        break;

      case 3:
        minx = this._centrex;
        maxx = this._env.getMaxX();
        miny = this._centrey;
        maxy = this._env.getMaxY();
        break;
    }

    const sqEnv = new Envelope(minx, maxx, miny, maxy);
    const node = new Node$1(sqEnv, this._level - 1);
    return node;
  }

  insertNode(node) {
    Assert.isTrue(this._env === null || this._env.contains(node._env));
    const index = NodeBase.getSubnodeIndex(node._env, this._centrex, this._centrey);

    if (node._level === this._level - 1) {
      this._subnode[index] = node;
    } else {
      const childNode = this.createSubnode(index);
      childNode.insertNode(node);
      this._subnode[index] = childNode;
    }
  }

}

class IntervalSize {
  static isZeroWidth(min, max) {
    const width = max - min;
    if (width === 0.0) return true;
    const maxAbs = Math.max(Math.abs(min), Math.abs(max));
    const scaledInterval = width / maxAbs;
    const level = DoubleBits.exponent(scaledInterval);
    return level <= IntervalSize.MIN_BINARY_EXPONENT;
  }

}
IntervalSize.MIN_BINARY_EXPONENT = -50;

class Root extends NodeBase {
  constructor() {
    super();
  }

  insert(itemEnv, item) {
    const index = NodeBase.getSubnodeIndex(itemEnv, Root.origin.x, Root.origin.y);

    if (index === -1) {
      this.add(item);
      return null;
    }

    const node = this._subnode[index];

    if (node === null || !node.getEnvelope().contains(itemEnv)) {
      const largerNode = Node$1.createExpanded(node, itemEnv);
      this._subnode[index] = largerNode;
    }

    this.insertContained(this._subnode[index], itemEnv, item);
  }

  isSearchMatch(searchEnv) {
    return true;
  }

  insertContained(tree, itemEnv, item) {
    Assert.isTrue(tree.getEnvelope().contains(itemEnv));
    const isZeroX = IntervalSize.isZeroWidth(itemEnv.getMinX(), itemEnv.getMaxX());
    const isZeroY = IntervalSize.isZeroWidth(itemEnv.getMinY(), itemEnv.getMaxY());
    let node = null;
    if (isZeroX || isZeroY) node = tree.find(itemEnv);else node = tree.getNode(itemEnv);
    node.add(item);
  }

}
Root.origin = new Coordinate(0.0, 0.0);

class SpatialIndex {
  insert(itemEnv, item) {}

  remove(itemEnv, item) {}

  query() {
  }

}

class Quadtree {
  constructor() {
    Quadtree.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._root = null;
    this._minExtent = 1.0;
    this._root = new Root();
  }

  static ensureExtent(itemEnv, minExtent) {
    let minx = itemEnv.getMinX();
    let maxx = itemEnv.getMaxX();
    let miny = itemEnv.getMinY();
    let maxy = itemEnv.getMaxY();
    if (minx !== maxx && miny !== maxy) return itemEnv;

    if (minx === maxx) {
      minx = minx - minExtent / 2.0;
      maxx = maxx + minExtent / 2.0;
    }

    if (miny === maxy) {
      miny = miny - minExtent / 2.0;
      maxy = maxy + minExtent / 2.0;
    }

    return new Envelope(minx, maxx, miny, maxy);
  }

  size() {
    if (this._root !== null) return this._root.size();
    return 0;
  }

  insert(itemEnv, item) {
    this.collectStats(itemEnv);
    const insertEnv = Quadtree.ensureExtent(itemEnv, this._minExtent);

    this._root.insert(insertEnv, item);
  }

  query() {
    if (arguments.length === 1) {
      const searchEnv = arguments[0];
      const visitor = new ArrayListVisitor();
      this.query(searchEnv, visitor);
      return visitor.getItems();
    } else if (arguments.length === 2) {
      const searchEnv = arguments[0],
            visitor = arguments[1];

      this._root.visit(searchEnv, visitor);
    }
  }

  queryAll() {
    const foundItems = new ArrayList();

    this._root.addAllItems(foundItems);

    return foundItems;
  }

  remove(itemEnv, item) {
    const posEnv = Quadtree.ensureExtent(itemEnv, this._minExtent);
    return this._root.remove(posEnv, item);
  }

  collectStats(itemEnv) {
    const delX = itemEnv.getWidth();
    if (delX < this._minExtent && delX > 0.0) this._minExtent = delX;
    const delY = itemEnv.getHeight();
    if (delY < this._minExtent && delY > 0.0) this._minExtent = delY;
  }

  depth() {
    if (this._root !== null) return this._root.depth();
    return 0;
  }

  isEmpty() {
    if (this._root === null) return true;
    return this._root.isEmpty();
  }

  get interfaces_() {
    return [SpatialIndex, Serializable];
  }

}

var quadtree = /*#__PURE__*/Object.freeze({
  __proto__: null,
  Quadtree: Quadtree
});

class Boundable {
  getBounds() {}

}

class ItemBoundable {
  constructor() {
    ItemBoundable.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._bounds = null;
    this._item = null;
    const bounds = arguments[0],
          item = arguments[1];
    this._bounds = bounds;
    this._item = item;
  }

  getItem() {
    return this._item;
  }

  getBounds() {
    return this._bounds;
  }

  get interfaces_() {
    return [Boundable, Serializable];
  }

}

class PriorityQueue {
  constructor() {
    PriorityQueue.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._size = null;
    this._items = null;
    this._size = 0;
    this._items = new ArrayList();

    this._items.add(null);
  }

  poll() {
    if (this.isEmpty()) return null;

    const minItem = this._items.get(1);

    this._items.set(1, this._items.get(this._size));

    this._size -= 1;
    this.reorder(1);
    return minItem;
  }

  size() {
    return this._size;
  }

  reorder(hole) {
    let child = null;

    const tmp = this._items.get(hole);

    for (; hole * 2 <= this._size; hole = child) {
      child = hole * 2;
      if (child !== this._size && this._items.get(child + 1).compareTo(this._items.get(child)) < 0) child++;
      if (this._items.get(child).compareTo(tmp) < 0) this._items.set(hole, this._items.get(child));else break;
    }

    this._items.set(hole, tmp);
  }

  clear() {
    this._size = 0;

    this._items.clear();
  }

  peek() {
    if (this.isEmpty()) return null;

    const minItem = this._items.get(1);

    return minItem;
  }

  isEmpty() {
    return this._size === 0;
  }

  add(x) {
    this._items.add(null);

    this._size += 1;
    let hole = this._size;

    this._items.set(0, x);

    for (; x.compareTo(this._items.get(Math.trunc(hole / 2))) < 0; hole /= 2) this._items.set(hole, this._items.get(Math.trunc(hole / 2)));

    this._items.set(hole, x);
  }

}

class AbstractNode {
  constructor() {
    AbstractNode.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._childBoundables = new ArrayList();
    this._bounds = null;
    this._level = null;

    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const level = arguments[0];
      this._level = level;
    }
  }

  getLevel() {
    return this._level;
  }

  size() {
    return this._childBoundables.size();
  }

  getChildBoundables() {
    return this._childBoundables;
  }

  addChildBoundable(childBoundable) {
    Assert.isTrue(this._bounds === null);

    this._childBoundables.add(childBoundable);
  }

  isEmpty() {
    return this._childBoundables.isEmpty();
  }

  getBounds() {
    if (this._bounds === null) this._bounds = this.computeBounds();
    return this._bounds;
  }

  get interfaces_() {
    return [Boundable, Serializable];
  }

}

class EnvelopeDistance {
  static maxDistance(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) {
    let dist = EnvelopeDistance.distance(ax1, ay1, bx1, by1);
    dist = Math.max(dist, EnvelopeDistance.distance(ax1, ay1, bx2, by2));
    dist = Math.max(dist, EnvelopeDistance.distance(ax2, ay2, bx1, by1));
    dist = Math.max(dist, EnvelopeDistance.distance(ax2, ay2, bx2, by2));
    return dist;
  }

  static distance(x1, y1, x2, y2) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    return Math.sqrt(dx * dx + dy * dy);
  }

  static maximumDistance(env1, env2) {
    const minx = Math.min(env1.getMinX(), env2.getMinX());
    const miny = Math.min(env1.getMinY(), env2.getMinY());
    const maxx = Math.max(env1.getMaxX(), env2.getMaxX());
    const maxy = Math.max(env1.getMaxY(), env2.getMaxY());
    return EnvelopeDistance.distance(minx, miny, maxx, maxy);
  }

  static minMaxDistance(a, b) {
    const aminx = a.getMinX();
    const aminy = a.getMinY();
    const amaxx = a.getMaxX();
    const amaxy = a.getMaxY();
    const bminx = b.getMinX();
    const bminy = b.getMinY();
    const bmaxx = b.getMaxX();
    const bmaxy = b.getMaxY();
    let dist = EnvelopeDistance.maxDistance(aminx, aminy, aminx, amaxy, bminx, bminy, bminx, bmaxy);
    dist = Math.min(dist, EnvelopeDistance.maxDistance(aminx, aminy, aminx, amaxy, bminx, bminy, bmaxx, bminy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(aminx, aminy, aminx, amaxy, bmaxx, bmaxy, bminx, bmaxy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(aminx, aminy, aminx, amaxy, bmaxx, bmaxy, bmaxx, bminy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(aminx, aminy, amaxx, aminy, bminx, bminy, bminx, bmaxy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(aminx, aminy, amaxx, aminy, bminx, bminy, bmaxx, bminy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(aminx, aminy, amaxx, aminy, bmaxx, bmaxy, bminx, bmaxy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(aminx, aminy, amaxx, aminy, bmaxx, bmaxy, bmaxx, bminy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(amaxx, amaxy, aminx, amaxy, bminx, bminy, bminx, bmaxy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(amaxx, amaxy, aminx, amaxy, bminx, bminy, bmaxx, bminy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(amaxx, amaxy, aminx, amaxy, bmaxx, bmaxy, bminx, bmaxy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(amaxx, amaxy, aminx, amaxy, bmaxx, bmaxy, bmaxx, bminy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(amaxx, amaxy, amaxx, aminy, bminx, bminy, bminx, bmaxy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(amaxx, amaxy, amaxx, aminy, bminx, bminy, bmaxx, bminy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(amaxx, amaxy, amaxx, aminy, bmaxx, bmaxy, bminx, bmaxy));
    dist = Math.min(dist, EnvelopeDistance.maxDistance(amaxx, amaxy, amaxx, aminy, bmaxx, bmaxy, bmaxx, bminy));
    return dist;
  }

}

class BoundablePair {
  constructor() {
    BoundablePair.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._boundable1 = null;
    this._boundable2 = null;
    this._distance = null;
    this._itemDistance = null;
    const boundable1 = arguments[0],
          boundable2 = arguments[1],
          itemDistance = arguments[2];
    this._boundable1 = boundable1;
    this._boundable2 = boundable2;
    this._itemDistance = itemDistance;
    this._distance = this.distance();
  }

  static area(b) {
    return b.getBounds().getArea();
  }

  static isComposite(item) {
    return item instanceof AbstractNode;
  }

  maximumDistance() {
    return EnvelopeDistance.maximumDistance(this._boundable1.getBounds(), this._boundable2.getBounds());
  }

  expandToQueue(priQ, minDistance) {
    const isComp1 = BoundablePair.isComposite(this._boundable1);
    const isComp2 = BoundablePair.isComposite(this._boundable2);

    if (isComp1 && isComp2) {
      if (BoundablePair.area(this._boundable1) > BoundablePair.area(this._boundable2)) {
        this.expand(this._boundable1, this._boundable2, false, priQ, minDistance);
        return null;
      } else {
        this.expand(this._boundable2, this._boundable1, true, priQ, minDistance);
        return null;
      }
    } else if (isComp1) {
      this.expand(this._boundable1, this._boundable2, false, priQ, minDistance);
      return null;
    } else if (isComp2) {
      this.expand(this._boundable2, this._boundable1, true, priQ, minDistance);
      return null;
    }

    throw new IllegalArgumentException('neither boundable is composite');
  }

  isLeaves() {
    return !(BoundablePair.isComposite(this._boundable1) || BoundablePair.isComposite(this._boundable2));
  }

  compareTo(o) {
    const nd = o;
    if (this._distance < nd._distance) return -1;
    if (this._distance > nd._distance) return 1;
    return 0;
  }

  expand(bndComposite, bndOther, isFlipped, priQ, minDistance) {
    const children = bndComposite.getChildBoundables();

    for (let i = children.iterator(); i.hasNext();) {
      const child = i.next();
      let bp = null;
      if (isFlipped) bp = new BoundablePair(bndOther, child, this._itemDistance);else bp = new BoundablePair(child, bndOther, this._itemDistance);
      if (bp.getDistance() < minDistance) priQ.add(bp);
    }
  }

  getBoundable(i) {
    if (i === 0) return this._boundable1;
    return this._boundable2;
  }

  getDistance() {
    return this._distance;
  }

  distance() {
    if (this.isLeaves()) return this._itemDistance.distance(this._boundable1, this._boundable2);
    return this._boundable1.getBounds().distance(this._boundable2.getBounds());
  }

  get interfaces_() {
    return [Comparable];
  }

}

class AbstractSTRtree {
  constructor() {
    AbstractSTRtree.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._root = null;
    this._built = false;
    this._itemBoundables = new ArrayList();
    this._nodeCapacity = null;

    if (arguments.length === 0) {
      AbstractSTRtree.constructor_.call(this, AbstractSTRtree.DEFAULT_NODE_CAPACITY);
    } else if (arguments.length === 1) {
      const nodeCapacity = arguments[0];
      Assert.isTrue(nodeCapacity > 1, 'Node capacity must be greater than 1');
      this._nodeCapacity = nodeCapacity;
    }
  }

  static compareDoubles(a, b) {
    return a > b ? 1 : a < b ? -1 : 0;
  }

  queryInternal() {
    if (hasInterface(arguments[2], ItemVisitor) && arguments[0] instanceof Object && arguments[1] instanceof AbstractNode) {
      const searchBounds = arguments[0],
            node = arguments[1],
            visitor = arguments[2];
      const childBoundables = node.getChildBoundables();

      for (let i = 0; i < childBoundables.size(); i++) {
        const childBoundable = childBoundables.get(i);
        if (!this.getIntersectsOp().intersects(childBoundable.getBounds(), searchBounds)) continue;
        if (childBoundable instanceof AbstractNode) this.queryInternal(searchBounds, childBoundable, visitor);else if (childBoundable instanceof ItemBoundable) visitor.visitItem(childBoundable.getItem());else Assert.shouldNeverReachHere();
      }
    } else if (hasInterface(arguments[2], List) && arguments[0] instanceof Object && arguments[1] instanceof AbstractNode) {
      const searchBounds = arguments[0],
            node = arguments[1],
            matches = arguments[2];
      const childBoundables = node.getChildBoundables();

      for (let i = 0; i < childBoundables.size(); i++) {
        const childBoundable = childBoundables.get(i);
        if (!this.getIntersectsOp().intersects(childBoundable.getBounds(), searchBounds)) continue;
        if (childBoundable instanceof AbstractNode) this.queryInternal(searchBounds, childBoundable, matches);else if (childBoundable instanceof ItemBoundable) matches.add(childBoundable.getItem());else Assert.shouldNeverReachHere();
      }
    }
  }

  getNodeCapacity() {
    return this._nodeCapacity;
  }

  lastNode(nodes) {
    return nodes.get(nodes.size() - 1);
  }

  size() {
    if (arguments.length === 0) {
      if (this.isEmpty()) return 0;
      this.build();
      return this.size(this._root);
    } else if (arguments.length === 1) {
      const node = arguments[0];
      let size = 0;

      for (let i = node.getChildBoundables().iterator(); i.hasNext();) {
        const childBoundable = i.next();
        if (childBoundable instanceof AbstractNode) size += this.size(childBoundable);else if (childBoundable instanceof ItemBoundable) size += 1;
      }

      return size;
    }
  }

  removeItem(node, item) {
    let childToRemove = null;

    for (let i = node.getChildBoundables().iterator(); i.hasNext();) {
      const childBoundable = i.next();
      if (childBoundable instanceof ItemBoundable) if (childBoundable.getItem() === item) childToRemove = childBoundable;
    }

    if (childToRemove !== null) {
      node.getChildBoundables().remove(childToRemove);
      return true;
    }

    return false;
  }

  itemsTree() {
    if (arguments.length === 0) {
      this.build();
      const valuesTree = this.itemsTree(this._root);
      if (valuesTree === null) return new ArrayList();
      return valuesTree;
    } else if (arguments.length === 1) {
      const node = arguments[0];
      const valuesTreeForNode = new ArrayList();

      for (let i = node.getChildBoundables().iterator(); i.hasNext();) {
        const childBoundable = i.next();

        if (childBoundable instanceof AbstractNode) {
          const valuesTreeForChild = this.itemsTree(childBoundable);
          if (valuesTreeForChild !== null) valuesTreeForNode.add(valuesTreeForChild);
        } else if (childBoundable instanceof ItemBoundable) {
          valuesTreeForNode.add(childBoundable.getItem());
        } else {
          Assert.shouldNeverReachHere();
        }
      }

      if (valuesTreeForNode.size() <= 0) return null;
      return valuesTreeForNode;
    }
  }

  insert(bounds, item) {
    Assert.isTrue(!this._built, 'Cannot insert items into an STR packed R-tree after it has been built.');

    this._itemBoundables.add(new ItemBoundable(bounds, item));
  }

  boundablesAtLevel() {
    if (arguments.length === 1) {
      const level = arguments[0];
      const boundables = new ArrayList();
      this.boundablesAtLevel(level, this._root, boundables);
      return boundables;
    } else if (arguments.length === 3) {
      const level = arguments[0],
            top = arguments[1],
            boundables = arguments[2];
      Assert.isTrue(level > -2);

      if (top.getLevel() === level) {
        boundables.add(top);
        return null;
      }

      for (let i = top.getChildBoundables().iterator(); i.hasNext();) {
        const boundable = i.next();

        if (boundable instanceof AbstractNode) {
          this.boundablesAtLevel(level, boundable, boundables);
        } else {
          Assert.isTrue(boundable instanceof ItemBoundable);
          if (level === -1) boundables.add(boundable);
        }
      }

      return null;
    }
  }

  query() {
    if (arguments.length === 1) {
      const searchBounds = arguments[0];
      this.build();
      const matches = new ArrayList();
      if (this.isEmpty()) return matches;
      if (this.getIntersectsOp().intersects(this._root.getBounds(), searchBounds)) this.queryInternal(searchBounds, this._root, matches);
      return matches;
    } else if (arguments.length === 2) {
      const searchBounds = arguments[0],
            visitor = arguments[1];
      this.build();
      if (this.isEmpty()) return null;
      if (this.getIntersectsOp().intersects(this._root.getBounds(), searchBounds)) this.queryInternal(searchBounds, this._root, visitor);
    }
  }

  build() {
    if (this._built) return null;
    this._root = this._itemBoundables.isEmpty() ? this.createNode(0) : this.createHigherLevels(this._itemBoundables, -1);
    this._itemBoundables = null;
    this._built = true;
  }

  getRoot() {
    this.build();
    return this._root;
  }

  remove() {
    if (arguments.length === 2) {
      const searchBounds = arguments[0],
            item = arguments[1];
      this.build();
      if (this.getIntersectsOp().intersects(this._root.getBounds(), searchBounds)) return this.remove(searchBounds, this._root, item);
      return false;
    } else if (arguments.length === 3) {
      const searchBounds = arguments[0],
            node = arguments[1],
            item = arguments[2];
      let found = this.removeItem(node, item);
      if (found) return true;
      let childToPrune = null;

      for (let i = node.getChildBoundables().iterator(); i.hasNext();) {
        const childBoundable = i.next();
        if (!this.getIntersectsOp().intersects(childBoundable.getBounds(), searchBounds)) continue;

        if (childBoundable instanceof AbstractNode) {
          found = this.remove(searchBounds, childBoundable, item);

          if (found) {
            childToPrune = childBoundable;
            break;
          }
        }
      }

      if (childToPrune !== null) if (childToPrune.getChildBoundables().isEmpty()) node.getChildBoundables().remove(childToPrune);
      return found;
    }
  }

  createHigherLevels(boundablesOfALevel, level) {
    Assert.isTrue(!boundablesOfALevel.isEmpty());
    const parentBoundables = this.createParentBoundables(boundablesOfALevel, level + 1);
    if (parentBoundables.size() === 1) return parentBoundables.get(0);
    return this.createHigherLevels(parentBoundables, level + 1);
  }

  depth() {
    if (arguments.length === 0) {
      if (this.isEmpty()) return 0;
      this.build();
      return this.depth(this._root);
    } else if (arguments.length === 1) {
      const node = arguments[0];
      let maxChildDepth = 0;

      for (let i = node.getChildBoundables().iterator(); i.hasNext();) {
        const childBoundable = i.next();

        if (childBoundable instanceof AbstractNode) {
          const childDepth = this.depth(childBoundable);
          if (childDepth > maxChildDepth) maxChildDepth = childDepth;
        }
      }

      return maxChildDepth + 1;
    }
  }

  createParentBoundables(childBoundables, newLevel) {
    Assert.isTrue(!childBoundables.isEmpty());
    const parentBoundables = new ArrayList();
    parentBoundables.add(this.createNode(newLevel));
    const sortedChildBoundables = new ArrayList(childBoundables);
    Collections.sort(sortedChildBoundables, this.getComparator());

    for (let i = sortedChildBoundables.iterator(); i.hasNext();) {
      const childBoundable = i.next();
      if (this.lastNode(parentBoundables).getChildBoundables().size() === this.getNodeCapacity()) parentBoundables.add(this.createNode(newLevel));
      this.lastNode(parentBoundables).addChildBoundable(childBoundable);
    }

    return parentBoundables;
  }

  isEmpty() {
    if (!this._built) return this._itemBoundables.isEmpty();
    return this._root.isEmpty();
  }

  get interfaces_() {
    return [Serializable];
  }

}

function IntersectsOp$1() {}

AbstractSTRtree.IntersectsOp = IntersectsOp$1;
AbstractSTRtree.DEFAULT_NODE_CAPACITY = 10;

class ItemDistance {
  distance(item1, item2) {}

}

class STRtree extends AbstractSTRtree {
  constructor() {
    super();
    STRtree.constructor_.apply(this, arguments);
  }

  static constructor_() {
    if (arguments.length === 0) {
      STRtree.constructor_.call(this, STRtree.DEFAULT_NODE_CAPACITY);
    } else if (arguments.length === 1) {
      const nodeCapacity = arguments[0];
      AbstractSTRtree.constructor_.call(this, nodeCapacity);
    }
  }

  static centreX(e) {
    return STRtree.avg(e.getMinX(), e.getMaxX());
  }

  static avg(a, b) {
    return (a + b) / 2;
  }

  static getItems(kNearestNeighbors) {
    const items = new Array(kNearestNeighbors.size()).fill(null);
    let count = 0;

    while (!kNearestNeighbors.isEmpty()) {
      const bp = kNearestNeighbors.poll();
      items[count] = bp.getBoundable(0).getItem();
      count++;
    }

    return items;
  }

  static centreY(e) {
    return STRtree.avg(e.getMinY(), e.getMaxY());
  }

  createParentBoundablesFromVerticalSlices(verticalSlices, newLevel) {
    Assert.isTrue(verticalSlices.length > 0);
    const parentBoundables = new ArrayList();

    for (let i = 0; i < verticalSlices.length; i++) parentBoundables.addAll(this.createParentBoundablesFromVerticalSlice(verticalSlices[i], newLevel));

    return parentBoundables;
  }

  nearestNeighbourK() {
    if (arguments.length === 2) {
      const initBndPair = arguments[0],
            k = arguments[1];
      return this.nearestNeighbourK(initBndPair, Double.POSITIVE_INFINITY, k);
    } else if (arguments.length === 3) {
      const initBndPair = arguments[0],
            maxDistance = arguments[1],
            k = arguments[2];
      let distanceLowerBound = maxDistance;
      const priQ = new PriorityQueue();
      priQ.add(initBndPair);
      const kNearestNeighbors = new PriorityQueue();

      while (!priQ.isEmpty() && distanceLowerBound >= 0.0) {
        const bndPair = priQ.poll();
        const pairDistance = bndPair.getDistance();
        if (pairDistance >= distanceLowerBound) break;
        if (bndPair.isLeaves()) {
          if (kNearestNeighbors.size() < k) {
            kNearestNeighbors.add(bndPair);
          } else {
            const bp1 = kNearestNeighbors.peek();

            if (bp1.getDistance() > pairDistance) {
              kNearestNeighbors.poll();
              kNearestNeighbors.add(bndPair);
            }

            const bp2 = kNearestNeighbors.peek();
            distanceLowerBound = bp2.getDistance();
          }
        } else bndPair.expandToQueue(priQ, distanceLowerBound);
      }

      return STRtree.getItems(kNearestNeighbors);
    }
  }

  createNode(level) {
    return new STRtreeNode(level);
  }

  size() {
    if (arguments.length === 0) return super.size.call(this);else return super.size.apply(this, arguments);
  }

  insert() {
    if (arguments.length === 2 && arguments[1] instanceof Object && arguments[0] instanceof Envelope) {
      const itemEnv = arguments[0],
            item = arguments[1];
      if (itemEnv.isNull()) return null;
      super.insert.call(this, itemEnv, item);
    } else {
      return super.insert.apply(this, arguments);
    }
  }

  getIntersectsOp() {
    return STRtree.intersectsOp;
  }

  verticalSlices(childBoundables, sliceCount) {
    const sliceCapacity = Math.trunc(Math.ceil(childBoundables.size() / sliceCount));
    const slices = new Array(sliceCount).fill(null);
    const i = childBoundables.iterator();

    for (let j = 0; j < sliceCount; j++) {
      slices[j] = new ArrayList();
      let boundablesAddedToSlice = 0;

      while (i.hasNext() && boundablesAddedToSlice < sliceCapacity) {
        const childBoundable = i.next();
        slices[j].add(childBoundable);
        boundablesAddedToSlice++;
      }
    }

    return slices;
  }

  query() {
    if (arguments.length === 1) {
      const searchEnv = arguments[0];
      return super.query.call(this, searchEnv);
    } else if (arguments.length === 2) {
      const searchEnv = arguments[0],
            visitor = arguments[1];
      super.query.call(this, searchEnv, visitor);
    }
  }

  getComparator() {
    return STRtree.yComparator;
  }

  createParentBoundablesFromVerticalSlice(childBoundables, newLevel) {
    return super.createParentBoundables.call(this, childBoundables, newLevel);
  }

  remove() {
    if (arguments.length === 2 && arguments[1] instanceof Object && arguments[0] instanceof Envelope) {
      const itemEnv = arguments[0],
            item = arguments[1];
      return super.remove.call(this, itemEnv, item);
    } else {
      return super.remove.apply(this, arguments);
    }
  }

  depth() {
    if (arguments.length === 0) return super.depth.call(this);else return super.depth.apply(this, arguments);
  }

  createParentBoundables(childBoundables, newLevel) {
    Assert.isTrue(!childBoundables.isEmpty());
    const minLeafCount = Math.trunc(Math.ceil(childBoundables.size() / this.getNodeCapacity()));
    const sortedChildBoundables = new ArrayList(childBoundables);
    Collections.sort(sortedChildBoundables, STRtree.xComparator);
    const verticalSlices = this.verticalSlices(sortedChildBoundables, Math.trunc(Math.ceil(Math.sqrt(minLeafCount))));
    return this.createParentBoundablesFromVerticalSlices(verticalSlices, newLevel);
  }

  nearestNeighbour() {
    if (arguments.length === 1) {
      if (hasInterface(arguments[0], ItemDistance)) {
        const itemDist = arguments[0];
        if (this.isEmpty()) return null;
        const bp = new BoundablePair(this.getRoot(), this.getRoot(), itemDist);
        return this.nearestNeighbour(bp);
      } else if (arguments[0] instanceof BoundablePair) {
        const initBndPair = arguments[0];
        let distanceLowerBound = Double.POSITIVE_INFINITY;
        let minPair = null;
        const priQ = new PriorityQueue();
        priQ.add(initBndPair);

        while (!priQ.isEmpty() && distanceLowerBound > 0.0) {
          const bndPair = priQ.poll();
          const pairDistance = bndPair.getDistance();
          if (pairDistance >= distanceLowerBound) break;

          if (bndPair.isLeaves()) {
            distanceLowerBound = pairDistance;
            minPair = bndPair;
          } else {
            bndPair.expandToQueue(priQ, distanceLowerBound);
          }
        }

        if (minPair === null) return null;
        return [minPair.getBoundable(0).getItem(), minPair.getBoundable(1).getItem()];
      }
    } else if (arguments.length === 2) {
      const tree = arguments[0],
            itemDist = arguments[1];
      if (this.isEmpty() || tree.isEmpty()) return null;
      const bp = new BoundablePair(this.getRoot(), tree.getRoot(), itemDist);
      return this.nearestNeighbour(bp);
    } else if (arguments.length === 3) {
      const env = arguments[0],
            item = arguments[1],
            itemDist = arguments[2];
      const bnd = new ItemBoundable(env, item);
      const bp = new BoundablePair(this.getRoot(), bnd, itemDist);
      return this.nearestNeighbour(bp)[0];
    } else if (arguments.length === 4) {
      const env = arguments[0],
            item = arguments[1],
            itemDist = arguments[2],
            k = arguments[3];
      const bnd = new ItemBoundable(env, item);
      const bp = new BoundablePair(this.getRoot(), bnd, itemDist);
      return this.nearestNeighbourK(bp, k);
    }
  }

  isWithinDistance() {
    if (arguments.length === 2) {
      const initBndPair = arguments[0],
            maxDistance = arguments[1];
      let distanceUpperBound = Double.POSITIVE_INFINITY;
      const priQ = new PriorityQueue();
      priQ.add(initBndPair);

      while (!priQ.isEmpty()) {
        const bndPair = priQ.poll();
        const pairDistance = bndPair.getDistance();
        if (pairDistance > maxDistance) return false;
        if (bndPair.maximumDistance() <= maxDistance) return true;

        if (bndPair.isLeaves()) {
          distanceUpperBound = pairDistance;
          if (distanceUpperBound <= maxDistance) return true;
        } else {
          bndPair.expandToQueue(priQ, distanceUpperBound);
        }
      }

      return false;
    } else if (arguments.length === 3) {
      const tree = arguments[0],
            itemDist = arguments[1],
            maxDistance = arguments[2];
      const bp = new BoundablePair(this.getRoot(), tree.getRoot(), itemDist);
      return this.isWithinDistance(bp, maxDistance);
    }
  }

  get interfaces_() {
    return [SpatialIndex, Serializable];
  }

}

class STRtreeNode extends AbstractNode {
  constructor() {
    super();
    STRtreeNode.constructor_.apply(this, arguments);
  }

  static constructor_() {
    const level = arguments[0];
    AbstractNode.constructor_.call(this, level);
  }

  computeBounds() {
    let bounds = null;

    for (let i = this.getChildBoundables().iterator(); i.hasNext();) {
      const childBoundable = i.next();
      if (bounds === null) bounds = new Envelope(childBoundable.getBounds());else bounds.expandToInclude(childBoundable.getBounds());
    }

    return bounds;
  }

}

STRtree.STRtreeNode = STRtreeNode;
STRtree.xComparator = new class {
  get interfaces_() {
    return [Comparator];
  }

  compare(o1, o2) {
    return AbstractSTRtree.compareDoubles(STRtree.centreX(o1.getBounds()), STRtree.centreX(o2.getBounds()));
  }

}();
STRtree.yComparator = new class {
  get interfaces_() {
    return [Comparator];
  }

  compare(o1, o2) {
    return AbstractSTRtree.compareDoubles(STRtree.centreY(o1.getBounds()), STRtree.centreY(o2.getBounds()));
  }

}();
STRtree.intersectsOp = new class {
  get interfaces_() {
    return [IntersectsOp];
  }

  intersects(aBounds, bBounds) {
    return aBounds.intersects(bBounds);
  }

}();
STRtree.DEFAULT_NODE_CAPACITY = 10;

var strtree = /*#__PURE__*/Object.freeze({
  __proto__: null,
  STRtree: STRtree
});

var index = /*#__PURE__*/Object.freeze({
  __proto__: null,
  kdtree: kdtree,
  quadtree: quadtree,
  strtree: strtree
});

const geometryTypes = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon'];
/**
 * Class for reading and writing Well-Known Text.Create a new parser for GeoJSON
 * NOTE: Adapted from OpenLayers 2.11 implementation.
 */

/**
 * Create a new parser for GeoJSON
 *
 * @param {GeometryFactory} geometryFactory
 * @return An instance of GeoJsonParser.
 * @constructor
 * @private
 */

class GeoJSONParser {
  constructor(geometryFactory) {
    this.geometryFactory = geometryFactory || new GeometryFactory();
  }
  /**
   * Deserialize a GeoJSON object and return the Geometry or Feature(Collection) with JSTS Geometries
   *
   * @param {}
   *          A GeoJSON object.
   * @return {} A Geometry instance or object representing a Feature(Collection) with Geometry instances.
   * @private
   */


  read(json) {
    let obj;
    if (typeof json === 'string') obj = JSON.parse(json);else obj = json;
    const type = obj.type;
    if (!parse[type]) throw new Error('Unknown GeoJSON type: ' + obj.type);
    if (geometryTypes.indexOf(type) !== -1) return parse[type].call(this, obj.coordinates);else if (type === 'GeometryCollection') return parse[type].call(this, obj.geometries); // feature or feature collection

    return parse[type].call(this, obj);
  }
  /**
   * Serialize a Geometry object into GeoJSON
   *
   * @param {Geometry}
   *          geometry A Geometry or array of Geometries.
   * @return {Object} A GeoJSON object represting the input Geometry/Geometries.
   * @private
   */


  write(geometry) {
    const type = geometry.getGeometryType();
    if (!extract[type]) throw new Error('Geometry is not supported');
    return extract[type].call(this, geometry);
  }

}
const parse = {
  /**
   * Parse a GeoJSON Feature object
   *
   * @param {Object}
   *          obj Object to parse.
   *
   * @return {Object} Feature with geometry/bbox converted to JSTS Geometries.
   */
  Feature: function (obj) {
    const feature = {};

    for (const key in obj) feature[key] = obj[key];

    if (obj.geometry) {
      const type = obj.geometry.type;
      if (!parse[type]) throw new Error('Unknown GeoJSON type: ' + obj.type);
      feature.geometry = this.read(obj.geometry);
    }

    if (obj.bbox) feature.bbox = parse.bbox.call(this, obj.bbox);
    return feature;
  },

  /**
   * Parse a GeoJSON FeatureCollection object
   *
   * @param {Object}
   *          obj Object to parse.
   *
   * @return {Object} FeatureCollection with geometry/bbox converted to JSTS Geometries.
   */
  FeatureCollection: function (obj) {
    const featureCollection = {};

    if (obj.features) {
      featureCollection.features = [];

      for (let i = 0; i < obj.features.length; ++i) featureCollection.features.push(this.read(obj.features[i]));
    }

    if (obj.bbox) featureCollection.bbox = this.parse.bbox.call(this, obj.bbox);
    return featureCollection;
  },

  /**
   * Convert the ordinates in an array to an array of Coordinates
   *
   * @param {Array}
   *          array Array with {Number}s.
   *
   * @return {Array} Array with Coordinates.
   */
  coordinates: function (array) {
    const coordinates = [];

    for (let i = 0; i < array.length; ++i) {
      const sub = array[i];
      coordinates.push(new Coordinate(...sub));
    }

    return coordinates;
  },

  /**
   * Convert the bbox to a LinearRing
   *
   * @param {Array}
   *          array Array with [xMin, yMin, xMax, yMax].
   *
   * @return {Array} Array with Coordinates.
   */
  bbox: function (array) {
    return this.geometryFactory.createLinearRing([new Coordinate(array[0], array[1]), new Coordinate(array[2], array[1]), new Coordinate(array[2], array[3]), new Coordinate(array[0], array[3]), new Coordinate(array[0], array[1])]);
  },

  /**
   * Convert an Array with ordinates to a Point
   *
   * @param {Array}
   *          array Array with ordinates.
   *
   * @return {Point} Point.
   */
  Point: function (array) {
    const coordinate = new Coordinate(...array);
    return this.geometryFactory.createPoint(coordinate);
  },

  /**
   * Convert an Array with coordinates to a MultiPoint
   *
   * @param {Array}
   *          array Array with coordinates.
   *
   * @return {MultiPoint} MultiPoint.
   */
  MultiPoint: function (array) {
    const points = [];

    for (let i = 0; i < array.length; ++i) points.push(parse.Point.call(this, array[i]));

    return this.geometryFactory.createMultiPoint(points);
  },

  /**
   * Convert an Array with coordinates to a LineString
   *
   * @param {Array}
   *          array Array with coordinates.
   *
   * @return {LineString} LineString.
   */
  LineString: function (array) {
    const coordinates = parse.coordinates.call(this, array);
    return this.geometryFactory.createLineString(coordinates);
  },

  /**
   * Convert an Array with coordinates to a MultiLineString
   *
   * @param {Array}
   *          array Array with coordinates.
   *
   * @return {MultiLineString} MultiLineString.
   */
  MultiLineString: function (array) {
    const lineStrings = [];

    for (let i = 0; i < array.length; ++i) lineStrings.push(parse.LineString.call(this, array[i]));

    return this.geometryFactory.createMultiLineString(lineStrings);
  },

  /**
   * Convert an Array to a Polygon
   *
   * @param {Array}
   *          array Array with shell and holes.
   *
   * @return {Polygon} Polygon.
   */
  Polygon: function (array) {
    const shellCoordinates = parse.coordinates.call(this, array[0]);
    const shell = this.geometryFactory.createLinearRing(shellCoordinates);
    const holes = [];

    for (let i = 1; i < array.length; ++i) {
      const hole = array[i];
      const coordinates = parse.coordinates.call(this, hole);
      const linearRing = this.geometryFactory.createLinearRing(coordinates);
      holes.push(linearRing);
    }

    return this.geometryFactory.createPolygon(shell, holes);
  },

  /**
   * Convert an Array to a MultiPolygon
   *
   * @param {Array}
   *          array Array of arrays with shell and rings.
   *
   * @return {MultiPolygon} MultiPolygon.
   */
  MultiPolygon: function (array) {
    const polygons = [];

    for (let i = 0; i < array.length; ++i) {
      const polygon = array[i];
      polygons.push(parse.Polygon.call(this, polygon));
    }

    return this.geometryFactory.createMultiPolygon(polygons);
  },

  /**
   * Convert an Array to a GeometryCollection
   *
   * @param {Array}
   *          array Array of GeoJSON geometries.
   *
   * @return {GeometryCollection} GeometryCollection.
   */
  GeometryCollection: function (array) {
    const geometries = [];

    for (let i = 0; i < array.length; ++i) {
      const geometry = array[i];
      geometries.push(this.read(geometry));
    }

    return this.geometryFactory.createGeometryCollection(geometries);
  }
};
const extract = {
  /**
   * Convert a Coordinate to an Array
   *
   * @param {Coordinate}
   *          coordinate Coordinate to convert.
   *
   * @return {Array} Array of ordinates.
   */
  coordinate: function (coordinate) {
    const a = [coordinate.x, coordinate.y];
    if (coordinate.z) a.push(coordinate.z);
    if (coordinate.m) a.push(coordinate.m);
    return a;
  },

  /**
   * Convert a Point to a GeoJSON object
   *
   * @param {Point}
   *          point Point to convert.
   *
   * @return {Array} Array of 2 ordinates (paired to a coordinate).
   */
  Point: function (point) {
    const array = extract.coordinate.call(this, point.getCoordinate());
    return {
      type: 'Point',
      coordinates: array
    };
  },

  /**
   * Convert a MultiPoint to a GeoJSON object
   *
   * @param {MultiPoint}
   *          multipoint MultiPoint to convert.
   *
   * @return {Array} Array of coordinates.
   */
  MultiPoint: function (multipoint) {
    const array = [];

    for (let i = 0; i < multipoint._geometries.length; ++i) {
      const point = multipoint._geometries[i];
      const geoJson = extract.Point.call(this, point);
      array.push(geoJson.coordinates);
    }

    return {
      type: 'MultiPoint',
      coordinates: array
    };
  },

  /**
   * Convert a LineString to a GeoJSON object
   *
   * @param {LineString}
   *          linestring LineString to convert.
   *
   * @return {Array} Array of coordinates.
   */
  LineString: function (linestring) {
    const array = [];
    const coordinates = linestring.getCoordinates();

    for (let i = 0; i < coordinates.length; ++i) {
      const coordinate = coordinates[i];
      array.push(extract.coordinate.call(this, coordinate));
    }

    return {
      type: 'LineString',
      coordinates: array
    };
  },

  /**
   * Convert a MultiLineString to a GeoJSON object
   *
   * @param {MultiLineString}
   *          multilinestring MultiLineString to convert.
   *
   * @return {Array} Array of Array of coordinates.
   */
  MultiLineString: function (multilinestring) {
    const array = [];

    for (let i = 0; i < multilinestring._geometries.length; ++i) {
      const linestring = multilinestring._geometries[i];
      const geoJson = extract.LineString.call(this, linestring);
      array.push(geoJson.coordinates);
    }

    return {
      type: 'MultiLineString',
      coordinates: array
    };
  },

  /**
   * Convert a Polygon to a GeoJSON object
   *
   * @param {Polygon}
   *          polygon Polygon to convert.
   *
   * @return {Array} Array with shell, holes.
   */
  Polygon: function (polygon) {
    const array = [];
    const shellGeoJson = extract.LineString.call(this, polygon._shell);
    array.push(shellGeoJson.coordinates);

    for (let i = 0; i < polygon._holes.length; ++i) {
      const hole = polygon._holes[i];
      const holeGeoJson = extract.LineString.call(this, hole);
      array.push(holeGeoJson.coordinates);
    }

    return {
      type: 'Polygon',
      coordinates: array
    };
  },

  /**
   * Convert a MultiPolygon to a GeoJSON object
   *
   * @param {MultiPolygon}
   *          multipolygon MultiPolygon to convert.
   *
   * @return {Array} Array of polygons.
   */
  MultiPolygon: function (multipolygon) {
    const array = [];

    for (let i = 0; i < multipolygon._geometries.length; ++i) {
      const polygon = multipolygon._geometries[i];
      const geoJson = extract.Polygon.call(this, polygon);
      array.push(geoJson.coordinates);
    }

    return {
      type: 'MultiPolygon',
      coordinates: array
    };
  },

  /**
   * Convert a GeometryCollection to a GeoJSON object
   *
   * @param {GeometryCollection}
   *          collection GeometryCollection to convert.
   *
   * @return {Array} Array of geometries.
   */
  GeometryCollection: function (collection) {
    const array = [];

    for (let i = 0; i < collection._geometries.length; ++i) {
      const geometry = collection._geometries[i];
      const type = geometry.getGeometryType();
      array.push(extract[type].call(this, geometry));
    }

    return {
      type: 'GeometryCollection',
      geometries: array
    };
  }
};

/**
 * @module org/locationtech/jts/io/GeoJSONReader
 */
/**
 * Converts a geometry in GeoJSON to a {@link Geometry}.
 */

class GeoJSONReader {
  /**
   * A <code>GeoJSONReader</code> is parameterized by a <code>GeometryFactory</code>,
   * to allow it to create <code>Geometry</code> objects of the appropriate
   * implementation. In particular, the <code>GeometryFactory</code> determines
   * the <code>PrecisionModel</code> and <code>SRID</code> that is used.
   *
   * @param {GeometryFactory} geometryFactory
   */
  constructor(geometryFactory) {
    this.parser = new GeoJSONParser(geometryFactory || new GeometryFactory());
  }
  /**
   * Reads a GeoJSON representation of a {@link Geometry}
   *
   * Will also parse GeoJSON Features/FeatureCollections as custom objects.
   *
   * @param {Object|String} geoJson a GeoJSON Object or String.
   * @return {Geometry|Object} a <code>Geometry or Feature/FeatureCollection representation.</code>
   * @memberof module:org/locationtech/jts/io/GeoJSONReader#
   */


  read(geoJson) {
    const geometry = this.parser.read(geoJson);
    return geometry;
  }

}

/**
 * @module org/locationtech/jts/io/GeoJSONWriter
 */
/**
 * Writes the GeoJSON representation of a {@link Geometry}. The
 * The GeoJSON format is defined <A
 * HREF="http://geojson.org/geojson-spec.html">here</A>.
 */

class GeoJSONWriter {
  /**
   * The <code>GeoJSONWriter</code> outputs coordinates rounded to the precision
   * model. Only the maximum number of decimal places necessary to represent the
   * ordinates to the required precision will be output.
   *
   * @param {GeometryFactory} geometryFactory
   * @constructor
   */
  constructor() {
    this.parser = new GeoJSONParser(this.geometryFactory);
  }
  /**
   * Converts a <code>Geometry</code> to its GeoJSON representation.
   *
   * @param {Geometry}
   *          geometry a <code>Geometry</code> to process.
   * @return {Object} The GeoJSON representation of the Geometry.
   * @memberof module:org/locationtech/jts/io/GeoJSONWriter#
   */


  write(geometry) {
    return this.parser.write(geometry);
  }

}

/**
 * @module org/locationtech/jts/io/WKTReader
 */
/**
 * Converts a geometry in Well-Known Text format to a {@link Geometry}.
 * <p>
 * <code>WKTReader</code> supports extracting <code>Geometry</code> objects
 * from either {@link Reader}s or {@link String}s. This allows it to function
 * as a parser to read <code>Geometry</code> objects from text blocks embedded
 * in other data formats (e.g. XML).
 */

class WKTReader {
  /**
   * A <code>WKTReader</code> is parameterized by a <code>GeometryFactory</code>,
   * to allow it to create <code>Geometry</code> objects of the appropriate
   * implementation. In particular, the <code>GeometryFactory</code> determines
   * the <code>PrecisionModel</code> and <code>SRID</code> that is used.
   * @param {GeometryFactory} geometryFactory
   */
  constructor(geometryFactory) {
    this.parser = new WKTParser(geometryFactory || new GeometryFactory());
  }
  /**
   * Reads a Well-Known Text representation of a {@link Geometry}
   *
   * @param {string}
   *          wkt a <Geometry Tagged Text> string (see the OpenGIS Simple Features
   *          Specification).
   * @return {Geometry} a <code>Geometry</code> read from
   *         <code>string.</code>
   * @memberof module:org/locationtech/jts/io/WKTReader#
   */


  read(wkt) {
    return this.parser.read(wkt);
  }

}

/* eslint-disable no-undef */

function p2c(p) {
  return [p.x, p.y];
}

class OL3Parser {
  /**
   * OpenLayers Geometry parser and writer
   * @param {GeometryFactory} geometryFactory
   * @param {ol} olReference
   */
  constructor(geometryFactory, olReference) {
    this.geometryFactory = geometryFactory || new GeometryFactory();
    this.ol = olReference || typeof ol !== 'undefined' && ol;
  }
  /**
   * Inject OpenLayers geom classes
   */


  inject(Point, LineString, LinearRing, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection) {
    this.ol = {
      geom: {
        Point,
        LineString,
        LinearRing,
        Polygon,
        MultiPoint,
        MultiLineString,
        MultiPolygon,
        GeometryCollection
      }
    };
  }
  /**
   * @param geometry {ol.geom.Geometry}
   * @return {Geometry}
   * @memberof module:org/locationtech/jts/io/OL3Parser#
   */


  read(geometry) {
    const ol = this.ol;
    if (geometry instanceof ol.geom.Point) return this.convertFromPoint(geometry);else if (geometry instanceof ol.geom.LineString) return this.convertFromLineString(geometry);else if (geometry instanceof ol.geom.LinearRing) return this.convertFromLinearRing(geometry);else if (geometry instanceof ol.geom.Polygon) return this.convertFromPolygon(geometry);else if (geometry instanceof ol.geom.MultiPoint) return this.convertFromMultiPoint(geometry);else if (geometry instanceof ol.geom.MultiLineString) return this.convertFromMultiLineString(geometry);else if (geometry instanceof ol.geom.MultiPolygon) return this.convertFromMultiPolygon(geometry);else if (geometry instanceof ol.geom.GeometryCollection) return this.convertFromCollection(geometry);
  }

  convertFromPoint(point) {
    const coordinates = point.getCoordinates();
    return this.geometryFactory.createPoint(new Coordinate(coordinates[0], coordinates[1]));
  }

  convertFromLineString(lineString) {
    return this.geometryFactory.createLineString(lineString.getCoordinates().map(function (coordinates) {
      return new Coordinate(coordinates[0], coordinates[1]);
    }));
  }

  convertFromLinearRing(linearRing) {
    return this.geometryFactory.createLinearRing(linearRing.getCoordinates().map(function (coordinates) {
      return new Coordinate(coordinates[0], coordinates[1]);
    }));
  }

  convertFromPolygon(polygon) {
    const linearRings = polygon.getLinearRings();
    let shell = null;
    const holes = [];

    for (let i = 0; i < linearRings.length; i++) {
      const linearRing = this.convertFromLinearRing(linearRings[i]);
      if (i === 0) shell = linearRing;else holes.push(linearRing);
    }

    return this.geometryFactory.createPolygon(shell, holes);
  }

  convertFromMultiPoint(multiPoint) {
    const points = multiPoint.getPoints().map(function (point) {
      return this.convertFromPoint(point);
    }, this);
    return this.geometryFactory.createMultiPoint(points);
  }

  convertFromMultiLineString(multiLineString) {
    const lineStrings = multiLineString.getLineStrings().map(function (lineString) {
      return this.convertFromLineString(lineString);
    }, this);
    return this.geometryFactory.createMultiLineString(lineStrings);
  }

  convertFromMultiPolygon(multiPolygon) {
    const polygons = multiPolygon.getPolygons().map(function (polygon) {
      return this.convertFromPolygon(polygon);
    }, this);
    return this.geometryFactory.createMultiPolygon(polygons);
  }

  convertFromCollection(collection) {
    const geometries = collection.getGeometries().map(function (geometry) {
      return this.read(geometry);
    }, this);
    return this.geometryFactory.createGeometryCollection(geometries);
  }
  /**
   * @param geometry
   *          {Geometry}
   * @return {ol.geom.Geometry}
   * @memberof module:org/locationtech/jts/io/OL3Parser#
   */


  write(geometry) {
    if (geometry.getGeometryType() === 'Point') return this.convertToPoint(geometry.getCoordinate());else if (geometry.getGeometryType() === 'LineString') return this.convertToLineString(geometry);else if (geometry.getGeometryType() === 'LinearRing') return this.convertToLinearRing(geometry);else if (geometry.getGeometryType() === 'Polygon') return this.convertToPolygon(geometry);else if (geometry.getGeometryType() === 'MultiPoint') return this.convertToMultiPoint(geometry);else if (geometry.getGeometryType() === 'MultiLineString') return this.convertToMultiLineString(geometry);else if (geometry.getGeometryType() === 'MultiPolygon') return this.convertToMultiPolygon(geometry);else if (geometry.getGeometryType() === 'GeometryCollection') return this.convertToCollection(geometry);
  }

  convertToPoint(coordinate) {
    return new this.ol.geom.Point([coordinate.x, coordinate.y]);
  }

  convertToLineString(lineString) {
    const points = lineString._points._coordinates.map(p2c);

    return new this.ol.geom.LineString(points);
  }

  convertToLinearRing(linearRing) {
    const points = linearRing._points._coordinates.map(p2c);

    return new this.ol.geom.LinearRing(points);
  }

  convertToPolygon(polygon) {
    const rings = [polygon._shell._points._coordinates.map(p2c)];

    for (let i = 0; i < polygon._holes.length; i++) rings.push(polygon._holes[i]._points._coordinates.map(p2c));

    return new this.ol.geom.Polygon(rings);
  }

  convertToMultiPoint(multiPoint) {
    return new this.ol.geom.MultiPoint(multiPoint.getCoordinates().map(p2c));
  }

  convertToMultiLineString(multiLineString) {
    const lineStrings = [];

    for (let i = 0; i < multiLineString._geometries.length; i++) lineStrings.push(this.convertToLineString(multiLineString._geometries[i]).getCoordinates());

    return new this.ol.geom.MultiLineString(lineStrings);
  }

  convertToMultiPolygon(multiPolygon) {
    const polygons = [];

    for (let i = 0; i < multiPolygon._geometries.length; i++) polygons.push(this.convertToPolygon(multiPolygon._geometries[i]).getCoordinates());

    return new this.ol.geom.MultiPolygon(polygons);
  }

  convertToCollection(geometryCollection) {
    const geometries = [];

    for (let i = 0; i < geometryCollection._geometries.length; i++) {
      const geometry = geometryCollection._geometries[i];
      geometries.push(this.write(geometry));
    }

    return new this.ol.geom.GeometryCollection(geometries);
  }

}

var io = /*#__PURE__*/Object.freeze({
  __proto__: null,
  GeoJSONReader: GeoJSONReader,
  GeoJSONWriter: GeoJSONWriter,
  OL3Parser: OL3Parser,
  WKTReader: WKTReader,
  WKTWriter: WKTWriter
});

class SegmentPointComparator {
  static relativeSign(x0, x1) {
    if (x0 < x1) return -1;
    if (x0 > x1) return 1;
    return 0;
  }

  static compare(octant, p0, p1) {
    if (p0.equals2D(p1)) return 0;
    const xSign = SegmentPointComparator.relativeSign(p0.x, p1.x);
    const ySign = SegmentPointComparator.relativeSign(p0.y, p1.y);

    switch (octant) {
      case 0:
        return SegmentPointComparator.compareValue(xSign, ySign);

      case 1:
        return SegmentPointComparator.compareValue(ySign, xSign);

      case 2:
        return SegmentPointComparator.compareValue(ySign, -xSign);

      case 3:
        return SegmentPointComparator.compareValue(-xSign, ySign);

      case 4:
        return SegmentPointComparator.compareValue(-xSign, -ySign);

      case 5:
        return SegmentPointComparator.compareValue(-ySign, -xSign);

      case 6:
        return SegmentPointComparator.compareValue(-ySign, xSign);

      case 7:
        return SegmentPointComparator.compareValue(xSign, -ySign);
    }

    Assert.shouldNeverReachHere('invalid octant value');
    return 0;
  }

  static compareValue(compareSign0, compareSign1) {
    if (compareSign0 < 0) return -1;
    if (compareSign0 > 0) return 1;
    if (compareSign1 < 0) return -1;
    if (compareSign1 > 0) return 1;
    return 0;
  }

}

class SegmentNode {
  constructor() {
    SegmentNode.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._segString = null;
    this.coord = null;
    this.segmentIndex = null;
    this._segmentOctant = null;
    this._isInterior = null;
    const segString = arguments[0],
          coord = arguments[1],
          segmentIndex = arguments[2],
          segmentOctant = arguments[3];
    this._segString = segString;
    this.coord = new Coordinate(coord);
    this.segmentIndex = segmentIndex;
    this._segmentOctant = segmentOctant;
    this._isInterior = !coord.equals2D(segString.getCoordinate(segmentIndex));
  }

  getCoordinate() {
    return this.coord;
  }

  print(out) {
    out.print(this.coord);
    out.print(' seg # = ' + this.segmentIndex);
  }

  compareTo(obj) {
    const other = obj;
    if (this.segmentIndex < other.segmentIndex) return -1;
    if (this.segmentIndex > other.segmentIndex) return 1;
    if (this.coord.equals2D(other.coord)) return 0;
    if (!this._isInterior) return -1;
    if (!other._isInterior) return 1;
    return SegmentPointComparator.compare(this._segmentOctant, this.coord, other.coord);
  }

  isEndPoint(maxSegmentIndex) {
    if (this.segmentIndex === 0 && !this._isInterior) return true;
    if (this.segmentIndex === maxSegmentIndex) return true;
    return false;
  }

  toString() {
    return this.segmentIndex + ':' + this.coord.toString();
  }

  isInterior() {
    return this._isInterior;
  }

  get interfaces_() {
    return [Comparable];
  }

}

class SegmentNodeList {
  constructor() {
    SegmentNodeList.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._nodeMap = new TreeMap();
    this._edge = null;
    const edge = arguments[0];
    this._edge = edge;
  }

  getSplitCoordinates() {
    const coordList = new CoordinateList();
    this.addEndpoints();
    const it = this.iterator();
    let eiPrev = it.next();

    while (it.hasNext()) {
      const ei = it.next();
      this.addEdgeCoordinates(eiPrev, ei, coordList);
      eiPrev = ei;
    }

    return coordList.toCoordinateArray();
  }

  addCollapsedNodes() {
    const collapsedVertexIndexes = new ArrayList();
    this.findCollapsesFromInsertedNodes(collapsedVertexIndexes);
    this.findCollapsesFromExistingVertices(collapsedVertexIndexes);

    for (let it = collapsedVertexIndexes.iterator(); it.hasNext();) {
      const vertexIndex = it.next().intValue();
      this.add(this._edge.getCoordinate(vertexIndex), vertexIndex);
    }
  }

  createSplitEdgePts(ei0, ei1) {
    let npts = ei1.segmentIndex - ei0.segmentIndex + 2;
    if (npts === 2) return [new Coordinate(ei0.coord), new Coordinate(ei1.coord)];

    const lastSegStartPt = this._edge.getCoordinate(ei1.segmentIndex);

    const useIntPt1 = ei1.isInterior() || !ei1.coord.equals2D(lastSegStartPt);
    if (!useIntPt1) npts--;
    const pts = new Array(npts).fill(null);
    let ipt = 0;
    pts[ipt++] = new Coordinate(ei0.coord);

    for (let i = ei0.segmentIndex + 1; i <= ei1.segmentIndex; i++) pts[ipt++] = this._edge.getCoordinate(i);

    if (useIntPt1) pts[ipt] = new Coordinate(ei1.coord);
    return pts;
  }

  print(out) {
    out.println('Intersections:');

    for (let it = this.iterator(); it.hasNext();) {
      const ei = it.next();
      ei.print(out);
    }
  }

  findCollapsesFromExistingVertices(collapsedVertexIndexes) {
    for (let i = 0; i < this._edge.size() - 2; i++) {
      const p0 = this._edge.getCoordinate(i);

      this._edge.getCoordinate(i + 1);

      const p2 = this._edge.getCoordinate(i + 2);

      if (p0.equals2D(p2)) collapsedVertexIndexes.add(Integer.valueOf(i + 1));
    }
  }

  addEdgeCoordinates(ei0, ei1, coordList) {
    const pts = this.createSplitEdgePts(ei0, ei1);
    coordList.add(pts, false);
  }

  iterator() {
    return this._nodeMap.values().iterator();
  }

  addSplitEdges(edgeList) {
    this.addEndpoints();
    this.addCollapsedNodes();
    const it = this.iterator();
    let eiPrev = it.next();

    while (it.hasNext()) {
      const ei = it.next();
      const newEdge = this.createSplitEdge(eiPrev, ei);
      edgeList.add(newEdge);
      eiPrev = ei;
    }
  }

  findCollapseIndex(ei0, ei1, collapsedVertexIndex) {
    if (!ei0.coord.equals2D(ei1.coord)) return false;
    let numVerticesBetween = ei1.segmentIndex - ei0.segmentIndex;
    if (!ei1.isInterior()) numVerticesBetween--;

    if (numVerticesBetween === 1) {
      collapsedVertexIndex[0] = ei0.segmentIndex + 1;
      return true;
    }

    return false;
  }

  findCollapsesFromInsertedNodes(collapsedVertexIndexes) {
    const collapsedVertexIndex = new Array(1).fill(null);
    const it = this.iterator();
    let eiPrev = it.next();

    while (it.hasNext()) {
      const ei = it.next();
      const isCollapsed = this.findCollapseIndex(eiPrev, ei, collapsedVertexIndex);
      if (isCollapsed) collapsedVertexIndexes.add(Integer.valueOf(collapsedVertexIndex[0]));
      eiPrev = ei;
    }
  }

  getEdge() {
    return this._edge;
  }

  addEndpoints() {
    const maxSegIndex = this._edge.size() - 1;
    this.add(this._edge.getCoordinate(0), 0);
    this.add(this._edge.getCoordinate(maxSegIndex), maxSegIndex);
  }

  createSplitEdge(ei0, ei1) {
    const pts = this.createSplitEdgePts(ei0, ei1);
    return new NodedSegmentString(pts, this._edge.getData());
  }

  add(intPt, segmentIndex) {
    const eiNew = new SegmentNode(this._edge, intPt, segmentIndex, this._edge.getSegmentOctant(segmentIndex));

    const ei = this._nodeMap.get(eiNew);

    if (ei !== null) {
      Assert.isTrue(ei.coord.equals2D(intPt), 'Found equal nodes with different coordinates');
      return ei;
    }

    this._nodeMap.put(eiNew, eiNew);

    return eiNew;
  }

  checkSplitEdgesCorrectness(splitEdges) {
    const edgePts = this._edge.getCoordinates();

    const split0 = splitEdges.get(0);
    const pt0 = split0.getCoordinate(0);
    if (!pt0.equals2D(edgePts[0])) throw new RuntimeException('bad split edge start point at ' + pt0);
    const splitn = splitEdges.get(splitEdges.size() - 1);
    const splitnPts = splitn.getCoordinates();
    const ptn = splitnPts[splitnPts.length - 1];
    if (!ptn.equals2D(edgePts[edgePts.length - 1])) throw new RuntimeException('bad split edge end point at ' + ptn);
  }

}

class Octant {
  static octant() {
    if (typeof arguments[0] === 'number' && typeof arguments[1] === 'number') {
      const dx = arguments[0],
            dy = arguments[1];
      if (dx === 0.0 && dy === 0.0) throw new IllegalArgumentException('Cannot compute the octant for point ( ' + dx + ', ' + dy + ' )');
      const adx = Math.abs(dx);
      const ady = Math.abs(dy);
      if (dx >= 0) {
        if (dy >= 0) {
          if (adx >= ady) return 0;else return 1;
        } else if (adx >= ady) return 7;else return 6;
      } else if (dy >= 0) {
        if (adx >= ady) return 3;else return 2;
      } else if (adx >= ady) return 4;else return 5;
    } else if (arguments[0] instanceof Coordinate && arguments[1] instanceof Coordinate) {
      const p0 = arguments[0],
            p1 = arguments[1];
      const dx = p1.x - p0.x;
      const dy = p1.y - p0.y;
      if (dx === 0.0 && dy === 0.0) throw new IllegalArgumentException('Cannot compute the octant for two identical points ' + p0);
      return Octant.octant(dx, dy);
    }
  }

}

class SegmentString {
  getCoordinates() {}

  size() {}

  getCoordinate(i) {}

  isClosed() {}

  setData(data) {}

  getData() {}

}

class NodableSegmentString {
  addIntersection(intPt, segmentIndex) {}

  get interfaces_() {
    return [SegmentString];
  }

}

class NodedSegmentString {
  constructor() {
    NodedSegmentString.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._nodeList = new SegmentNodeList(this);
    this._pts = null;
    this._data = null;
    const pts = arguments[0],
          data = arguments[1];
    this._pts = pts;
    this._data = data;
  }

  static getNodedSubstrings() {
    if (arguments.length === 1) {
      const segStrings = arguments[0];
      const resultEdgelist = new ArrayList();
      NodedSegmentString.getNodedSubstrings(segStrings, resultEdgelist);
      return resultEdgelist;
    } else if (arguments.length === 2) {
      const segStrings = arguments[0],
            resultEdgelist = arguments[1];

      for (let i = segStrings.iterator(); i.hasNext();) {
        const ss = i.next();
        ss.getNodeList().addSplitEdges(resultEdgelist);
      }
    }
  }

  getCoordinates() {
    return this._pts;
  }

  size() {
    return this._pts.length;
  }

  getCoordinate(i) {
    return this._pts[i];
  }

  isClosed() {
    return this._pts[0].equals(this._pts[this._pts.length - 1]);
  }

  getSegmentOctant(index) {
    if (index === this._pts.length - 1) return -1;
    return this.safeOctant(this.getCoordinate(index), this.getCoordinate(index + 1));
  }

  setData(data) {
    this._data = data;
  }

  safeOctant(p0, p1) {
    if (p0.equals2D(p1)) return 0;
    return Octant.octant(p0, p1);
  }

  getData() {
    return this._data;
  }

  addIntersection() {
    if (arguments.length === 2) {
      const intPt = arguments[0],
            segmentIndex = arguments[1];
      this.addIntersectionNode(intPt, segmentIndex);
    } else if (arguments.length === 4) {
      const li = arguments[0],
            segmentIndex = arguments[1],
            intIndex = arguments[3];
      const intPt = new Coordinate(li.getIntersection(intIndex));
      this.addIntersection(intPt, segmentIndex);
    }
  }

  toString() {
    return WKTWriter.toLineString(new CoordinateArraySequence(this._pts));
  }

  getNodeList() {
    return this._nodeList;
  }

  addIntersectionNode(intPt, segmentIndex) {
    let normalizedSegmentIndex = segmentIndex;
    const nextSegIndex = normalizedSegmentIndex + 1;

    if (nextSegIndex < this._pts.length) {
      const nextPt = this._pts[nextSegIndex];
      if (intPt.equals2D(nextPt)) normalizedSegmentIndex = nextSegIndex;
    }

    const ei = this._nodeList.add(intPt, normalizedSegmentIndex);

    return ei;
  }

  addIntersections(li, segmentIndex, geomIndex) {
    for (let i = 0; i < li.getIntersectionNum(); i++) this.addIntersection(li, segmentIndex, geomIndex, i);
  }

  get interfaces_() {
    return [NodableSegmentString];
  }

}

class MonotoneChainOverlapAction {
  constructor() {
    MonotoneChainOverlapAction.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._overlapSeg1 = new LineSegment();
    this._overlapSeg2 = new LineSegment();
  }

  overlap() {
    if (arguments.length === 2) ; else if (arguments.length === 4) {
      const mc1 = arguments[0],
            start1 = arguments[1],
            mc2 = arguments[2],
            start2 = arguments[3];
      mc1.getLineSegment(start1, this._overlapSeg1);
      mc2.getLineSegment(start2, this._overlapSeg2);
      this.overlap(this._overlapSeg1, this._overlapSeg2);
    }
  }

}

class MonotoneChain {
  constructor() {
    MonotoneChain.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pts = null;
    this._start = null;
    this._end = null;
    this._env = null;
    this._context = null;
    this._id = null;
    const pts = arguments[0],
          start = arguments[1],
          end = arguments[2],
          context = arguments[3];
    this._pts = pts;
    this._start = start;
    this._end = end;
    this._context = context;
  }

  getLineSegment(index, ls) {
    ls.p0 = this._pts[index];
    ls.p1 = this._pts[index + 1];
  }

  computeSelect(searchEnv, start0, end0, mcs) {
    const p0 = this._pts[start0];
    const p1 = this._pts[end0];

    if (end0 - start0 === 1) {
      mcs.select(this, start0);
      return null;
    }

    if (!searchEnv.intersects(p0, p1)) return null;
    const mid = Math.trunc((start0 + end0) / 2);
    if (start0 < mid) this.computeSelect(searchEnv, start0, mid, mcs);
    if (mid < end0) this.computeSelect(searchEnv, mid, end0, mcs);
  }

  getCoordinates() {
    const coord = new Array(this._end - this._start + 1).fill(null);
    let index = 0;

    for (let i = this._start; i <= this._end; i++) coord[index++] = this._pts[i];

    return coord;
  }

  computeOverlaps() {
    if (arguments.length === 2) {
      const mc = arguments[0],
            mco = arguments[1];
      this.computeOverlaps(this._start, this._end, mc, mc._start, mc._end, mco);
    } else if (arguments.length === 6) {
      const start0 = arguments[0],
            end0 = arguments[1],
            mc = arguments[2],
            start1 = arguments[3],
            end1 = arguments[4],
            mco = arguments[5];

      if (end0 - start0 === 1 && end1 - start1 === 1) {
        mco.overlap(this, start0, mc, start1);
        return null;
      }

      if (!this.overlaps(start0, end0, mc, start1, end1)) return null;
      const mid0 = Math.trunc((start0 + end0) / 2);
      const mid1 = Math.trunc((start1 + end1) / 2);

      if (start0 < mid0) {
        if (start1 < mid1) this.computeOverlaps(start0, mid0, mc, start1, mid1, mco);
        if (mid1 < end1) this.computeOverlaps(start0, mid0, mc, mid1, end1, mco);
      }

      if (mid0 < end0) {
        if (start1 < mid1) this.computeOverlaps(mid0, end0, mc, start1, mid1, mco);
        if (mid1 < end1) this.computeOverlaps(mid0, end0, mc, mid1, end1, mco);
      }
    }
  }

  setId(id) {
    this._id = id;
  }

  select(searchEnv, mcs) {
    this.computeSelect(searchEnv, this._start, this._end, mcs);
  }

  getEnvelope() {
    if (this._env === null) {
      const p0 = this._pts[this._start];
      const p1 = this._pts[this._end];
      this._env = new Envelope(p0, p1);
    }

    return this._env;
  }

  overlaps(start0, end0, mc, start1, end1) {
    return Envelope.intersects(this._pts[start0], this._pts[end0], mc._pts[start1], mc._pts[end1]);
  }

  getEndIndex() {
    return this._end;
  }

  getStartIndex() {
    return this._start;
  }

  getContext() {
    return this._context;
  }

  getId() {
    return this._id;
  }

}

class MonotoneChainBuilder {
  static findChainEnd(pts, start) {
    let safeStart = start;

    while (safeStart < pts.length - 1 && pts[safeStart].equals2D(pts[safeStart + 1])) safeStart++;

    if (safeStart >= pts.length - 1) return pts.length - 1;
    const chainQuad = Quadrant.quadrant(pts[safeStart], pts[safeStart + 1]);
    let last = start + 1;

    while (last < pts.length) {
      if (!pts[last - 1].equals2D(pts[last])) {
        const quad = Quadrant.quadrant(pts[last - 1], pts[last]);
        if (quad !== chainQuad) break;
      }

      last++;
    }

    return last - 1;
  }

  static getChains() {
    if (arguments.length === 1) {
      const pts = arguments[0];
      return MonotoneChainBuilder.getChains(pts, null);
    } else if (arguments.length === 2) {
      const pts = arguments[0],
            context = arguments[1];
      const mcList = new ArrayList();
      let chainStart = 0;

      do {
        const chainEnd = MonotoneChainBuilder.findChainEnd(pts, chainStart);
        const mc = new MonotoneChain(pts, chainStart, chainEnd, context);
        mcList.add(mc);
        chainStart = chainEnd;
      } while (chainStart < pts.length - 1);

      return mcList;
    }
  }

}

class Noder {
  computeNodes(segStrings) {}

  getNodedSubstrings() {}

}

class SinglePassNoder {
  constructor() {
    SinglePassNoder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._segInt = null;

    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const segInt = arguments[0];
      this.setSegmentIntersector(segInt);
    }
  }

  setSegmentIntersector(segInt) {
    this._segInt = segInt;
  }

  get interfaces_() {
    return [Noder];
  }

}

class MCIndexNoder extends SinglePassNoder {
  constructor() {
    super();
    MCIndexNoder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._monoChains = new ArrayList();
    this._index = new STRtree();
    this._idCounter = 0;
    this._nodedSegStrings = null;
    this._nOverlaps = 0;

    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const si = arguments[0];
      SinglePassNoder.constructor_.call(this, si);
    }
  }

  getMonotoneChains() {
    return this._monoChains;
  }

  getNodedSubstrings() {
    return NodedSegmentString.getNodedSubstrings(this._nodedSegStrings);
  }

  getIndex() {
    return this._index;
  }

  add(segStr) {
    const segChains = MonotoneChainBuilder.getChains(segStr.getCoordinates(), segStr);

    for (let i = segChains.iterator(); i.hasNext();) {
      const mc = i.next();
      mc.setId(this._idCounter++);

      this._index.insert(mc.getEnvelope(), mc);

      this._monoChains.add(mc);
    }
  }

  computeNodes(inputSegStrings) {
    this._nodedSegStrings = inputSegStrings;

    for (let i = inputSegStrings.iterator(); i.hasNext();) this.add(i.next());

    this.intersectChains();
  }

  intersectChains() {
    const overlapAction = new SegmentOverlapAction(this._segInt);

    for (let i = this._monoChains.iterator(); i.hasNext();) {
      const queryChain = i.next();

      const overlapChains = this._index.query(queryChain.getEnvelope());

      for (let j = overlapChains.iterator(); j.hasNext();) {
        const testChain = j.next();

        if (testChain.getId() > queryChain.getId()) {
          queryChain.computeOverlaps(testChain, overlapAction);
          this._nOverlaps++;
        }

        if (this._segInt.isDone()) return null;
      }
    }
  }

}

class SegmentOverlapAction extends MonotoneChainOverlapAction {
  constructor() {
    super();
    SegmentOverlapAction.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._si = null;
    const si = arguments[0];
    this._si = si;
  }

  overlap() {
    if (arguments.length === 4) {
      const mc1 = arguments[0],
            start1 = arguments[1],
            mc2 = arguments[2],
            start2 = arguments[3];
      const ss1 = mc1.getContext();
      const ss2 = mc2.getContext();

      this._si.processIntersections(ss1, start1, ss2, start2);
    } else {
      return super.overlap.apply(this, arguments);
    }
  }

}

MCIndexNoder.SegmentOverlapAction = SegmentOverlapAction;

class ScaledNoder {
  constructor() {
    ScaledNoder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._noder = null;
    this._scaleFactor = null;
    this._offsetX = null;
    this._offsetY = null;
    this._isScaled = false;

    if (arguments.length === 2) {
      const noder = arguments[0],
            scaleFactor = arguments[1];
      ScaledNoder.constructor_.call(this, noder, scaleFactor, 0, 0);
    } else if (arguments.length === 4) {
      const noder = arguments[0],
            scaleFactor = arguments[1];
      this._noder = noder;
      this._scaleFactor = scaleFactor;
      this._isScaled = !this.isIntegerPrecision();
    }
  }

  rescale() {
    if (hasInterface(arguments[0], Collection)) {
      const segStrings = arguments[0];

      for (let i = segStrings.iterator(); i.hasNext();) {
        const ss = i.next();
        this.rescale(ss.getCoordinates());
      }
    } else if (arguments[0] instanceof Array) {
      const pts = arguments[0];

      for (let i = 0; i < pts.length; i++) {
        pts[i].x = pts[i].x / this._scaleFactor + this._offsetX;
        pts[i].y = pts[i].y / this._scaleFactor + this._offsetY;
      }

      if (pts.length === 2 && pts[0].equals2D(pts[1])) System.out.println(pts);
    }
  }

  scale() {
    if (hasInterface(arguments[0], Collection)) {
      const segStrings = arguments[0];
      const nodedSegmentStrings = new ArrayList(segStrings.size());

      for (let i = segStrings.iterator(); i.hasNext();) {
        const ss = i.next();
        nodedSegmentStrings.add(new NodedSegmentString(this.scale(ss.getCoordinates()), ss.getData()));
      }

      return nodedSegmentStrings;
    } else if (arguments[0] instanceof Array) {
      const pts = arguments[0];
      const roundPts = new Array(pts.length).fill(null);

      for (let i = 0; i < pts.length; i++) roundPts[i] = new Coordinate(Math.round((pts[i].x - this._offsetX) * this._scaleFactor), Math.round((pts[i].y - this._offsetY) * this._scaleFactor), pts[i].getZ());

      const roundPtsNoDup = CoordinateArrays.removeRepeatedPoints(roundPts);
      return roundPtsNoDup;
    }
  }

  isIntegerPrecision() {
    return this._scaleFactor === 1.0;
  }

  getNodedSubstrings() {
    const splitSS = this._noder.getNodedSubstrings();

    if (this._isScaled) this.rescale(splitSS);
    return splitSS;
  }

  computeNodes(inputSegStrings) {
    let intSegStrings = inputSegStrings;
    if (this._isScaled) intSegStrings = this.scale(inputSegStrings);

    this._noder.computeNodes(intSegStrings);
  }

  get interfaces_() {
    return [Noder];
  }

}

var noding = /*#__PURE__*/Object.freeze({
  __proto__: null,
  MCIndexNoder: MCIndexNoder,
  ScaledNoder: ScaledNoder,
  SegmentString: SegmentString
});

class BoundaryOp {
  constructor() {
    BoundaryOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geom = null;
    this._geomFact = null;
    this._bnRule = null;
    this._endpointMap = null;

    if (arguments.length === 1) {
      const geom = arguments[0];
      BoundaryOp.constructor_.call(this, geom, BoundaryNodeRule.MOD2_BOUNDARY_RULE);
    } else if (arguments.length === 2) {
      const geom = arguments[0],
            bnRule = arguments[1];
      this._geom = geom;
      this._geomFact = geom.getFactory();
      this._bnRule = bnRule;
    }
  }

  static getBoundary() {
    if (arguments.length === 1) {
      const g = arguments[0];
      const bop = new BoundaryOp(g);
      return bop.getBoundary();
    } else if (arguments.length === 2) {
      const g = arguments[0],
            bnRule = arguments[1];
      const bop = new BoundaryOp(g, bnRule);
      return bop.getBoundary();
    }
  }

  boundaryMultiLineString(mLine) {
    if (this._geom.isEmpty()) return this.getEmptyMultiPoint();
    const bdyPts = this.computeBoundaryCoordinates(mLine);
    if (bdyPts.length === 1) return this._geomFact.createPoint(bdyPts[0]);
    return this._geomFact.createMultiPointFromCoords(bdyPts);
  }

  getBoundary() {
    if (this._geom instanceof LineString) return this.boundaryLineString(this._geom);
    if (this._geom instanceof MultiLineString) return this.boundaryMultiLineString(this._geom);
    return this._geom.getBoundary();
  }

  boundaryLineString(line) {
    if (this._geom.isEmpty()) return this.getEmptyMultiPoint();

    if (line.isClosed()) {
      const closedEndpointOnBoundary = this._bnRule.isInBoundary(2);

      if (closedEndpointOnBoundary) return line.getStartPoint();else return this._geomFact.createMultiPoint();
    }

    return this._geomFact.createMultiPoint([line.getStartPoint(), line.getEndPoint()]);
  }

  getEmptyMultiPoint() {
    return this._geomFact.createMultiPoint();
  }

  computeBoundaryCoordinates(mLine) {
    const bdyPts = new ArrayList();
    this._endpointMap = new TreeMap();

    for (let i = 0; i < mLine.getNumGeometries(); i++) {
      const line = mLine.getGeometryN(i);
      if (line.getNumPoints() === 0) continue;
      this.addEndpoint(line.getCoordinateN(0));
      this.addEndpoint(line.getCoordinateN(line.getNumPoints() - 1));
    }

    for (let it = this._endpointMap.entrySet().iterator(); it.hasNext();) {
      const entry = it.next();
      const counter = entry.getValue();
      const valence = counter.count;
      if (this._bnRule.isInBoundary(valence)) bdyPts.add(entry.getKey());
    }

    return CoordinateArrays.toCoordinateArray(bdyPts);
  }

  addEndpoint(pt) {
    let counter = this._endpointMap.get(pt);

    if (counter === null) {
      counter = new Counter$1();

      this._endpointMap.put(pt, counter);
    }

    counter.count++;
  }

}

class Counter$1 {
  constructor() {
    Counter$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.count = null;
  }

}

class IsSimpleOp {
  constructor() {
    IsSimpleOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._isClosedEndpointsInInterior = true;
    this._nonSimpleLocation = null;

    if (arguments.length === 1) {
      const geom = arguments[0];
      this._inputGeom = geom;
    } else if (arguments.length === 2) {
      const geom = arguments[0],
            boundaryNodeRule = arguments[1];
      this._inputGeom = geom;
      this._isClosedEndpointsInInterior = !boundaryNodeRule.isInBoundary(2);
    }
  }

  static isSimple() {
    if (arguments.length === 1) {
      const geom = arguments[0];
      const op = new IsSimpleOp(geom);
      return op.isSimple();
    } else if (arguments.length === 2) {
      const geom = arguments[0],
            boundaryNodeRule = arguments[1];
      const op = new IsSimpleOp(geom, boundaryNodeRule);
      return op.isSimple();
    }
  }

  isSimpleMultiPoint(mp) {
    if (mp.isEmpty()) return true;
    const points = new TreeSet();

    for (let i = 0; i < mp.getNumGeometries(); i++) {
      const pt = mp.getGeometryN(i);
      const p = pt.getCoordinate();

      if (points.contains(p)) {
        this._nonSimpleLocation = p;
        return false;
      }

      points.add(p);
    }

    return true;
  }

  isSimplePolygonal(geom) {
    const rings = LinearComponentExtracter.getLines(geom);

    for (let i = rings.iterator(); i.hasNext();) {
      const ring = i.next();
      if (!this.isSimpleLinearGeometry(ring)) return false;
    }

    return true;
  }

  hasClosedEndpointIntersection(graph) {
    const endPoints = new TreeMap();

    for (let i = graph.getEdgeIterator(); i.hasNext();) {
      const e = i.next();
      const isClosed = e.isClosed();
      const p0 = e.getCoordinate(0);
      this.addEndpoint(endPoints, p0, isClosed);
      const p1 = e.getCoordinate(e.getNumPoints() - 1);
      this.addEndpoint(endPoints, p1, isClosed);
    }

    for (let i = endPoints.values().iterator(); i.hasNext();) {
      const eiInfo = i.next();

      if (eiInfo.isClosed && eiInfo.degree !== 2) {
        this._nonSimpleLocation = eiInfo.getCoordinate();
        return true;
      }
    }

    return false;
  }

  getNonSimpleLocation() {
    return this._nonSimpleLocation;
  }

  isSimpleLinearGeometry(geom) {
    if (geom.isEmpty()) return true;
    const graph = new GeometryGraph(0, geom);
    const li = new RobustLineIntersector();
    const si = graph.computeSelfNodes(li, true);
    if (!si.hasIntersection()) return true;

    if (si.hasProperIntersection()) {
      this._nonSimpleLocation = si.getProperIntersectionPoint();
      return false;
    }

    if (this.hasNonEndpointIntersection(graph)) return false;
    if (this._isClosedEndpointsInInterior) if (this.hasClosedEndpointIntersection(graph)) return false;
    return true;
  }

  hasNonEndpointIntersection(graph) {
    for (let i = graph.getEdgeIterator(); i.hasNext();) {
      const e = i.next();
      const maxSegmentIndex = e.getMaximumSegmentIndex();

      for (let eiIt = e.getEdgeIntersectionList().iterator(); eiIt.hasNext();) {
        const ei = eiIt.next();

        if (!ei.isEndPoint(maxSegmentIndex)) {
          this._nonSimpleLocation = ei.getCoordinate();
          return true;
        }
      }
    }

    return false;
  }

  addEndpoint(endPoints, p, isClosed) {
    let eiInfo = endPoints.get(p);

    if (eiInfo === null) {
      eiInfo = new EndpointInfo(p);
      endPoints.put(p, eiInfo);
    }

    eiInfo.addEndpoint(isClosed);
  }

  computeSimple(geom) {
    this._nonSimpleLocation = null;
    if (geom.isEmpty()) return true;
    if (geom instanceof LineString) return this.isSimpleLinearGeometry(geom);
    if (geom instanceof MultiLineString) return this.isSimpleLinearGeometry(geom);
    if (geom instanceof MultiPoint) return this.isSimpleMultiPoint(geom);
    if (hasInterface(geom, Polygonal)) return this.isSimplePolygonal(geom);
    if (geom instanceof GeometryCollection) return this.isSimpleGeometryCollection(geom);
    return true;
  }

  isSimple() {
    this._nonSimpleLocation = null;
    return this.computeSimple(this._inputGeom);
  }

  isSimpleGeometryCollection(geom) {
    for (let i = 0; i < geom.getNumGeometries(); i++) {
      const comp = geom.getGeometryN(i);
      if (!this.computeSimple(comp)) return false;
    }

    return true;
  }

}

class EndpointInfo {
  constructor() {
    EndpointInfo.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.pt = null;
    this.isClosed = null;
    this.degree = null;
    const pt = arguments[0];
    this.pt = pt;
    this.isClosed = false;
    this.degree = 0;
  }

  addEndpoint(isClosed) {
    this.degree++;
    this.isClosed |= isClosed;
  }

  getCoordinate() {
    return this.pt;
  }

}

IsSimpleOp.EndpointInfo = EndpointInfo;

class BufferParameters {
  constructor() {
    BufferParameters.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._quadrantSegments = BufferParameters.DEFAULT_QUADRANT_SEGMENTS;
    this._endCapStyle = BufferParameters.CAP_ROUND;
    this._joinStyle = BufferParameters.JOIN_ROUND;
    this._mitreLimit = BufferParameters.DEFAULT_MITRE_LIMIT;
    this._isSingleSided = false;
    this._simplifyFactor = BufferParameters.DEFAULT_SIMPLIFY_FACTOR;

    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const quadrantSegments = arguments[0];
      this.setQuadrantSegments(quadrantSegments);
    } else if (arguments.length === 2) {
      const quadrantSegments = arguments[0],
            endCapStyle = arguments[1];
      this.setQuadrantSegments(quadrantSegments);
      this.setEndCapStyle(endCapStyle);
    } else if (arguments.length === 4) {
      const quadrantSegments = arguments[0],
            endCapStyle = arguments[1],
            joinStyle = arguments[2],
            mitreLimit = arguments[3];
      this.setQuadrantSegments(quadrantSegments);
      this.setEndCapStyle(endCapStyle);
      this.setJoinStyle(joinStyle);
      this.setMitreLimit(mitreLimit);
    }
  }

  static bufferDistanceError(quadSegs) {
    const alpha = Math.PI / 2.0 / quadSegs;
    return 1 - Math.cos(alpha / 2.0);
  }

  getEndCapStyle() {
    return this._endCapStyle;
  }

  isSingleSided() {
    return this._isSingleSided;
  }

  setQuadrantSegments(quadSegs) {
    this._quadrantSegments = quadSegs;
    if (this._quadrantSegments === 0) this._joinStyle = BufferParameters.JOIN_BEVEL;

    if (this._quadrantSegments < 0) {
      this._joinStyle = BufferParameters.JOIN_MITRE;
      this._mitreLimit = Math.abs(this._quadrantSegments);
    }

    if (quadSegs <= 0) this._quadrantSegments = 1;
    if (this._joinStyle !== BufferParameters.JOIN_ROUND) this._quadrantSegments = BufferParameters.DEFAULT_QUADRANT_SEGMENTS;
  }

  getJoinStyle() {
    return this._joinStyle;
  }

  setJoinStyle(joinStyle) {
    this._joinStyle = joinStyle;
  }

  setSimplifyFactor(simplifyFactor) {
    this._simplifyFactor = simplifyFactor < 0 ? 0 : simplifyFactor;
  }

  getSimplifyFactor() {
    return this._simplifyFactor;
  }

  getQuadrantSegments() {
    return this._quadrantSegments;
  }

  setEndCapStyle(endCapStyle) {
    this._endCapStyle = endCapStyle;
  }

  getMitreLimit() {
    return this._mitreLimit;
  }

  setMitreLimit(mitreLimit) {
    this._mitreLimit = mitreLimit;
  }

  setSingleSided(isSingleSided) {
    this._isSingleSided = isSingleSided;
  }

}
BufferParameters.CAP_ROUND = 1;
BufferParameters.CAP_FLAT = 2;
BufferParameters.CAP_SQUARE = 3;
BufferParameters.JOIN_ROUND = 1;
BufferParameters.JOIN_MITRE = 2;
BufferParameters.JOIN_BEVEL = 3;
BufferParameters.DEFAULT_QUADRANT_SEGMENTS = 8;
BufferParameters.DEFAULT_MITRE_LIMIT = 5.0;
BufferParameters.DEFAULT_SIMPLIFY_FACTOR = 0.01;

class RightmostEdgeFinder {
  constructor() {
    RightmostEdgeFinder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._minIndex = -1;
    this._minCoord = null;
    this._minDe = null;
    this._orientedDe = null;
  }

  getCoordinate() {
    return this._minCoord;
  }

  getRightmostSide(de, index) {
    let side = this.getRightmostSideOfSegment(de, index);
    if (side < 0) side = this.getRightmostSideOfSegment(de, index - 1);

    if (side < 0) {
      this._minCoord = null;
      this.checkForRightmostCoordinate(de);
    }

    return side;
  }

  findRightmostEdgeAtVertex() {
    const pts = this._minDe.getEdge().getCoordinates();

    Assert.isTrue(this._minIndex > 0 && this._minIndex < pts.length, 'rightmost point expected to be interior vertex of edge');
    const pPrev = pts[this._minIndex - 1];
    const pNext = pts[this._minIndex + 1];
    const orientation = Orientation.index(this._minCoord, pNext, pPrev);
    let usePrev = false;
    if (pPrev.y < this._minCoord.y && pNext.y < this._minCoord.y && orientation === Orientation.COUNTERCLOCKWISE) usePrev = true;else if (pPrev.y > this._minCoord.y && pNext.y > this._minCoord.y && orientation === Orientation.CLOCKWISE) usePrev = true;
    if (usePrev) this._minIndex = this._minIndex - 1;
  }

  getRightmostSideOfSegment(de, i) {
    const e = de.getEdge();
    const coord = e.getCoordinates();
    if (i < 0 || i + 1 >= coord.length) return -1;
    if (coord[i].y === coord[i + 1].y) return -1;
    let pos = Position.LEFT;
    if (coord[i].y < coord[i + 1].y) pos = Position.RIGHT;
    return pos;
  }

  getEdge() {
    return this._orientedDe;
  }

  checkForRightmostCoordinate(de) {
    const coord = de.getEdge().getCoordinates();

    for (let i = 0; i < coord.length - 1; i++) if (this._minCoord === null || coord[i].x > this._minCoord.x) {
      this._minDe = de;
      this._minIndex = i;
      this._minCoord = coord[i];
    }
  }

  findRightmostEdgeAtNode() {
    const node = this._minDe.getNode();

    const star = node.getEdges();
    this._minDe = star.getRightmostEdge();

    if (!this._minDe.isForward()) {
      this._minDe = this._minDe.getSym();
      this._minIndex = this._minDe.getEdge().getCoordinates().length - 1;
    }
  }

  findEdge(dirEdgeList) {
    for (let i = dirEdgeList.iterator(); i.hasNext();) {
      const de = i.next();
      if (!de.isForward()) continue;
      this.checkForRightmostCoordinate(de);
    }

    Assert.isTrue(this._minIndex !== 0 || this._minCoord.equals(this._minDe.getCoordinate()), 'inconsistency in rightmost processing');
    if (this._minIndex === 0) this.findRightmostEdgeAtNode();else this.findRightmostEdgeAtVertex();
    this._orientedDe = this._minDe;
    const rightmostSide = this.getRightmostSide(this._minDe, this._minIndex);
    if (rightmostSide === Position.LEFT) this._orientedDe = this._minDe.getSym();
  }

}

class LinkedList {
  constructor() {
    this.array = [];
  }

  addLast(e) {
    this.array.push(e);
  }

  removeFirst() {
    return this.array.shift();
  }

  isEmpty() {
    return this.array.length === 0;
  }

}

class BufferSubgraph {
  constructor() {
    BufferSubgraph.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._finder = null;
    this._dirEdgeList = new ArrayList();
    this._nodes = new ArrayList();
    this._rightMostCoord = null;
    this._env = null;
    this._finder = new RightmostEdgeFinder();
  }

  clearVisitedEdges() {
    for (let it = this._dirEdgeList.iterator(); it.hasNext();) {
      const de = it.next();
      de.setVisited(false);
    }
  }

  getRightmostCoordinate() {
    return this._rightMostCoord;
  }

  computeNodeDepth(n) {
    let startEdge = null;

    for (let i = n.getEdges().iterator(); i.hasNext();) {
      const de = i.next();

      if (de.isVisited() || de.getSym().isVisited()) {
        startEdge = de;
        break;
      }
    }

    if (startEdge === null) throw new TopologyException('unable to find edge to compute depths at ' + n.getCoordinate());
    n.getEdges().computeDepths(startEdge);

    for (let i = n.getEdges().iterator(); i.hasNext();) {
      const de = i.next();
      de.setVisited(true);
      this.copySymDepths(de);
    }
  }

  computeDepth(outsideDepth) {
    this.clearVisitedEdges();

    const de = this._finder.getEdge();

    de.getNode();
    de.getLabel();
    de.setEdgeDepths(Position.RIGHT, outsideDepth);
    this.copySymDepths(de);
    this.computeDepths(de);
  }

  create(node) {
    this.addReachable(node);

    this._finder.findEdge(this._dirEdgeList);

    this._rightMostCoord = this._finder.getCoordinate();
  }

  findResultEdges() {
    for (let it = this._dirEdgeList.iterator(); it.hasNext();) {
      const de = it.next();
      if (de.getDepth(Position.RIGHT) >= 1 && de.getDepth(Position.LEFT) <= 0 && !de.isInteriorAreaEdge()) de.setInResult(true);
    }
  }

  computeDepths(startEdge) {
    const nodesVisited = new HashSet();
    const nodeQueue = new LinkedList();
    const startNode = startEdge.getNode();
    nodeQueue.addLast(startNode);
    nodesVisited.add(startNode);
    startEdge.setVisited(true);

    while (!nodeQueue.isEmpty()) {
      const n = nodeQueue.removeFirst();
      nodesVisited.add(n);
      this.computeNodeDepth(n);

      for (let i = n.getEdges().iterator(); i.hasNext();) {
        const de = i.next();
        const sym = de.getSym();
        if (sym.isVisited()) continue;
        const adjNode = sym.getNode();

        if (!nodesVisited.contains(adjNode)) {
          nodeQueue.addLast(adjNode);
          nodesVisited.add(adjNode);
        }
      }
    }
  }

  compareTo(o) {
    const graph = o;
    if (this._rightMostCoord.x < graph._rightMostCoord.x) return -1;
    if (this._rightMostCoord.x > graph._rightMostCoord.x) return 1;
    return 0;
  }

  getEnvelope() {
    if (this._env === null) {
      const edgeEnv = new Envelope();

      for (let it = this._dirEdgeList.iterator(); it.hasNext();) {
        const dirEdge = it.next();
        const pts = dirEdge.getEdge().getCoordinates();

        for (let i = 0; i < pts.length - 1; i++) edgeEnv.expandToInclude(pts[i]);
      }

      this._env = edgeEnv;
    }

    return this._env;
  }

  addReachable(startNode) {
    const nodeStack = new Stack();
    nodeStack.add(startNode);

    while (!nodeStack.empty()) {
      const node = nodeStack.pop();
      this.add(node, nodeStack);
    }
  }

  copySymDepths(de) {
    const sym = de.getSym();
    sym.setDepth(Position.LEFT, de.getDepth(Position.RIGHT));
    sym.setDepth(Position.RIGHT, de.getDepth(Position.LEFT));
  }

  add(node, nodeStack) {
    node.setVisited(true);

    this._nodes.add(node);

    for (let i = node.getEdges().iterator(); i.hasNext();) {
      const de = i.next();

      this._dirEdgeList.add(de);

      const sym = de.getSym();
      const symNode = sym.getNode();
      if (!symNode.isVisited()) nodeStack.push(symNode);
    }
  }

  getNodes() {
    return this._nodes;
  }

  getDirectedEdges() {
    return this._dirEdgeList;
  }

  get interfaces_() {
    return [Comparable];
  }

}

class EdgeRing$1 {
  constructor() {
    EdgeRing$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._startDe = null;
    this._maxNodeDegree = -1;
    this._edges = new ArrayList();
    this._pts = new ArrayList();
    this._label = new Label(Location.NONE);
    this._ring = null;
    this._isHole = null;
    this._shell = null;
    this._holes = new ArrayList();
    this._geometryFactory = null;

    if (arguments.length === 0) ; else if (arguments.length === 2) {
      const start = arguments[0],
            geometryFactory = arguments[1];
      this._geometryFactory = geometryFactory;
      this.computePoints(start);
      this.computeRing();
    }
  }

  computeRing() {
    if (this._ring !== null) return null;
    const coord = new Array(this._pts.size()).fill(null);

    for (let i = 0; i < this._pts.size(); i++) coord[i] = this._pts.get(i);

    this._ring = this._geometryFactory.createLinearRing(coord);
    this._isHole = Orientation.isCCW(this._ring.getCoordinates());
  }

  isIsolated() {
    return this._label.getGeometryCount() === 1;
  }

  computePoints(start) {
    this._startDe = start;
    let de = start;
    let isFirstEdge = true;

    do {
      if (de === null) throw new TopologyException('Found null DirectedEdge');
      if (de.getEdgeRing() === this) throw new TopologyException('Directed Edge visited twice during ring-building at ' + de.getCoordinate());

      this._edges.add(de);

      const label = de.getLabel();
      Assert.isTrue(label.isArea());
      this.mergeLabel(label);
      this.addPoints(de.getEdge(), de.isForward(), isFirstEdge);
      isFirstEdge = false;
      this.setEdgeRing(de, this);
      de = this.getNext(de);
    } while (de !== this._startDe);
  }

  getLinearRing() {
    return this._ring;
  }

  getCoordinate(i) {
    return this._pts.get(i);
  }

  computeMaxNodeDegree() {
    this._maxNodeDegree = 0;
    let de = this._startDe;

    do {
      const node = de.getNode();
      const degree = node.getEdges().getOutgoingDegree(this);
      if (degree > this._maxNodeDegree) this._maxNodeDegree = degree;
      de = this.getNext(de);
    } while (de !== this._startDe);

    this._maxNodeDegree *= 2;
  }

  addPoints(edge, isForward, isFirstEdge) {
    const edgePts = edge.getCoordinates();

    if (isForward) {
      let startIndex = 1;
      if (isFirstEdge) startIndex = 0;

      for (let i = startIndex; i < edgePts.length; i++) this._pts.add(edgePts[i]);
    } else {
      let startIndex = edgePts.length - 2;
      if (isFirstEdge) startIndex = edgePts.length - 1;

      for (let i = startIndex; i >= 0; i--) this._pts.add(edgePts[i]);
    }
  }

  isHole() {
    return this._isHole;
  }

  setInResult() {
    let de = this._startDe;

    do {
      de.getEdge().setInResult(true);
      de = de.getNext();
    } while (de !== this._startDe);
  }

  containsPoint(p) {
    const shell = this.getLinearRing();
    const env = shell.getEnvelopeInternal();
    if (!env.contains(p)) return false;
    if (!PointLocation.isInRing(p, shell.getCoordinates())) return false;

    for (let i = this._holes.iterator(); i.hasNext();) {
      const hole = i.next();
      if (hole.containsPoint(p)) return false;
    }

    return true;
  }

  addHole(ring) {
    this._holes.add(ring);
  }

  isShell() {
    return this._shell === null;
  }

  getLabel() {
    return this._label;
  }

  getEdges() {
    return this._edges;
  }

  getMaxNodeDegree() {
    if (this._maxNodeDegree < 0) this.computeMaxNodeDegree();
    return this._maxNodeDegree;
  }

  getShell() {
    return this._shell;
  }

  mergeLabel() {
    if (arguments.length === 1) {
      const deLabel = arguments[0];
      this.mergeLabel(deLabel, 0);
      this.mergeLabel(deLabel, 1);
    } else if (arguments.length === 2) {
      const deLabel = arguments[0],
            geomIndex = arguments[1];
      const loc = deLabel.getLocation(geomIndex, Position.RIGHT);
      if (loc === Location.NONE) return null;

      if (this._label.getLocation(geomIndex) === Location.NONE) {
        this._label.setLocation(geomIndex, loc);

        return null;
      }
    }
  }

  setShell(shell) {
    this._shell = shell;
    if (shell !== null) shell.addHole(this);
  }

  toPolygon(geometryFactory) {
    const holeLR = new Array(this._holes.size()).fill(null);

    for (let i = 0; i < this._holes.size(); i++) holeLR[i] = this._holes.get(i).getLinearRing();

    const poly = geometryFactory.createPolygon(this.getLinearRing(), holeLR);
    return poly;
  }

}

class MinimalEdgeRing extends EdgeRing$1 {
  constructor() {
    super();
    MinimalEdgeRing.constructor_.apply(this, arguments);
  }

  static constructor_() {
    const start = arguments[0],
          geometryFactory = arguments[1];
    EdgeRing$1.constructor_.call(this, start, geometryFactory);
  }

  setEdgeRing(de, er) {
    de.setMinEdgeRing(er);
  }

  getNext(de) {
    return de.getNextMin();
  }

}

class MaximalEdgeRing extends EdgeRing$1 {
  constructor() {
    super();
    MaximalEdgeRing.constructor_.apply(this, arguments);
  }

  static constructor_() {
    const start = arguments[0],
          geometryFactory = arguments[1];
    EdgeRing$1.constructor_.call(this, start, geometryFactory);
  }

  buildMinimalRings() {
    const minEdgeRings = new ArrayList();
    let de = this._startDe;

    do {
      if (de.getMinEdgeRing() === null) {
        const minEr = new MinimalEdgeRing(de, this._geometryFactory);
        minEdgeRings.add(minEr);
      }

      de = de.getNext();
    } while (de !== this._startDe);

    return minEdgeRings;
  }

  setEdgeRing(de, er) {
    de.setEdgeRing(er);
  }

  linkDirectedEdgesForMinimalEdgeRings() {
    let de = this._startDe;

    do {
      const node = de.getNode();
      node.getEdges().linkMinimalDirectedEdges(this);
      de = de.getNext();
    } while (de !== this._startDe);
  }

  getNext(de) {
    return de.getNext();
  }

}

class PolygonBuilder {
  constructor() {
    PolygonBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geometryFactory = null;
    this._shellList = new ArrayList();
    const geometryFactory = arguments[0];
    this._geometryFactory = geometryFactory;
  }

  static findEdgeRingContaining(testEr, shellList) {
    const testRing = testEr.getLinearRing();
    const testEnv = testRing.getEnvelopeInternal();
    let testPt = testRing.getCoordinateN(0);
    let minShell = null;
    let minShellEnv = null;

    for (let it = shellList.iterator(); it.hasNext();) {
      const tryShell = it.next();
      const tryShellRing = tryShell.getLinearRing();
      const tryShellEnv = tryShellRing.getEnvelopeInternal();
      if (tryShellEnv.equals(testEnv)) continue;
      if (!tryShellEnv.contains(testEnv)) continue;
      testPt = CoordinateArrays.ptNotInList(testRing.getCoordinates(), tryShellRing.getCoordinates());
      let isContained = false;
      if (PointLocation.isInRing(testPt, tryShellRing.getCoordinates())) isContained = true;
      if (isContained) if (minShell === null || minShellEnv.contains(tryShellEnv)) {
        minShell = tryShell;
        minShellEnv = minShell.getLinearRing().getEnvelopeInternal();
      }
    }

    return minShell;
  }

  sortShellsAndHoles(edgeRings, shellList, freeHoleList) {
    for (let it = edgeRings.iterator(); it.hasNext();) {
      const er = it.next();
      if (er.isHole()) freeHoleList.add(er);else shellList.add(er);
    }
  }

  computePolygons(shellList) {
    const resultPolyList = new ArrayList();

    for (let it = shellList.iterator(); it.hasNext();) {
      const er = it.next();
      const poly = er.toPolygon(this._geometryFactory);
      resultPolyList.add(poly);
    }

    return resultPolyList;
  }

  placeFreeHoles(shellList, freeHoleList) {
    for (let it = freeHoleList.iterator(); it.hasNext();) {
      const hole = it.next();

      if (hole.getShell() === null) {
        const shell = PolygonBuilder.findEdgeRingContaining(hole, shellList);
        if (shell === null) throw new TopologyException('unable to assign hole to a shell', hole.getCoordinate(0));
        hole.setShell(shell);
      }
    }
  }

  buildMinimalEdgeRings(maxEdgeRings, shellList, freeHoleList) {
    const edgeRings = new ArrayList();

    for (let it = maxEdgeRings.iterator(); it.hasNext();) {
      const er = it.next();

      if (er.getMaxNodeDegree() > 2) {
        er.linkDirectedEdgesForMinimalEdgeRings();
        const minEdgeRings = er.buildMinimalRings();
        const shell = this.findShell(minEdgeRings);

        if (shell !== null) {
          this.placePolygonHoles(shell, minEdgeRings);
          shellList.add(shell);
        } else {
          freeHoleList.addAll(minEdgeRings);
        }
      } else {
        edgeRings.add(er);
      }
    }

    return edgeRings;
  }

  buildMaximalEdgeRings(dirEdges) {
    const maxEdgeRings = new ArrayList();

    for (let it = dirEdges.iterator(); it.hasNext();) {
      const de = it.next();
      if (de.isInResult() && de.getLabel().isArea()) if (de.getEdgeRing() === null) {
        const er = new MaximalEdgeRing(de, this._geometryFactory);
        maxEdgeRings.add(er);
        er.setInResult();
      }
    }

    return maxEdgeRings;
  }

  placePolygonHoles(shell, minEdgeRings) {
    for (let it = minEdgeRings.iterator(); it.hasNext();) {
      const er = it.next();
      if (er.isHole()) er.setShell(shell);
    }
  }

  getPolygons() {
    const resultPolyList = this.computePolygons(this._shellList);
    return resultPolyList;
  }

  findShell(minEdgeRings) {
    let shellCount = 0;
    let shell = null;

    for (let it = minEdgeRings.iterator(); it.hasNext();) {
      const er = it.next();

      if (!er.isHole()) {
        shell = er;
        shellCount++;
      }
    }

    Assert.isTrue(shellCount <= 1, 'found two shells in MinimalEdgeRing list');
    return shell;
  }

  add() {
    if (arguments.length === 1) {
      const graph = arguments[0];
      this.add(graph.getEdgeEnds(), graph.getNodes());
    } else if (arguments.length === 2) {
      const dirEdges = arguments[0],
            nodes = arguments[1];
      PlanarGraph$1.linkResultDirectedEdges(nodes);
      const maxEdgeRings = this.buildMaximalEdgeRings(dirEdges);
      const freeHoleList = new ArrayList();
      const edgeRings = this.buildMinimalEdgeRings(maxEdgeRings, this._shellList, freeHoleList);
      this.sortShellsAndHoles(edgeRings, this._shellList, freeHoleList);
      this.placeFreeHoles(this._shellList, freeHoleList);
    }
  }

}

class BufferInputLineSimplifier {
  constructor() {
    BufferInputLineSimplifier.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputLine = null;
    this._distanceTol = null;
    this._isDeleted = null;
    this._angleOrientation = Orientation.COUNTERCLOCKWISE;
    const inputLine = arguments[0];
    this._inputLine = inputLine;
  }

  static simplify(inputLine, distanceTol) {
    const simp = new BufferInputLineSimplifier(inputLine);
    return simp.simplify(distanceTol);
  }

  isDeletable(i0, i1, i2, distanceTol) {
    const p0 = this._inputLine[i0];
    const p1 = this._inputLine[i1];
    const p2 = this._inputLine[i2];
    if (!this.isConcave(p0, p1, p2)) return false;
    if (!this.isShallow(p0, p1, p2, distanceTol)) return false;
    return this.isShallowSampled(p0, p1, i0, i2, distanceTol);
  }

  deleteShallowConcavities() {
    let index = 1;
    let midIndex = this.findNextNonDeletedIndex(index);
    let lastIndex = this.findNextNonDeletedIndex(midIndex);
    let isChanged = false;

    while (lastIndex < this._inputLine.length) {
      let isMiddleVertexDeleted = false;

      if (this.isDeletable(index, midIndex, lastIndex, this._distanceTol)) {
        this._isDeleted[midIndex] = BufferInputLineSimplifier.DELETE;
        isMiddleVertexDeleted = true;
        isChanged = true;
      }

      if (isMiddleVertexDeleted) index = lastIndex;else index = midIndex;
      midIndex = this.findNextNonDeletedIndex(index);
      lastIndex = this.findNextNonDeletedIndex(midIndex);
    }

    return isChanged;
  }

  isShallowConcavity(p0, p1, p2, distanceTol) {
    const orientation = Orientation.index(p0, p1, p2);
    const isAngleToSimplify = orientation === this._angleOrientation;
    if (!isAngleToSimplify) return false;
    const dist = Distance.pointToSegment(p1, p0, p2);
    return dist < distanceTol;
  }

  isShallowSampled(p0, p2, i0, i2, distanceTol) {
    let inc = Math.trunc((i2 - i0) / BufferInputLineSimplifier.NUM_PTS_TO_CHECK);
    if (inc <= 0) inc = 1;

    for (let i = i0; i < i2; i += inc) if (!this.isShallow(p0, p2, this._inputLine[i], distanceTol)) return false;

    return true;
  }

  isConcave(p0, p1, p2) {
    const orientation = Orientation.index(p0, p1, p2);
    const isConcave = orientation === this._angleOrientation;
    return isConcave;
  }

  simplify(distanceTol) {
    this._distanceTol = Math.abs(distanceTol);
    if (distanceTol < 0) this._angleOrientation = Orientation.CLOCKWISE;
    this._isDeleted = new Array(this._inputLine.length).fill(null);
    let isChanged = false;

    do isChanged = this.deleteShallowConcavities(); while (isChanged);

    return this.collapseLine();
  }

  findNextNonDeletedIndex(index) {
    let next = index + 1;

    while (next < this._inputLine.length && this._isDeleted[next] === BufferInputLineSimplifier.DELETE) next++;

    return next;
  }

  isShallow(p0, p1, p2, distanceTol) {
    const dist = Distance.pointToSegment(p1, p0, p2);
    return dist < distanceTol;
  }

  collapseLine() {
    const coordList = new CoordinateList();

    for (let i = 0; i < this._inputLine.length; i++) if (this._isDeleted[i] !== BufferInputLineSimplifier.DELETE) coordList.add(this._inputLine[i]);

    return coordList.toCoordinateArray();
  }

}
BufferInputLineSimplifier.INIT = 0;
BufferInputLineSimplifier.DELETE = 1;
BufferInputLineSimplifier.KEEP = 1;
BufferInputLineSimplifier.NUM_PTS_TO_CHECK = 10;

class OffsetSegmentString {
  constructor() {
    OffsetSegmentString.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._ptList = null;
    this._precisionModel = null;
    this._minimimVertexDistance = 0.0;
    this._ptList = new ArrayList();
  }

  getCoordinates() {
    const coord = this._ptList.toArray(OffsetSegmentString.COORDINATE_ARRAY_TYPE);

    return coord;
  }

  setPrecisionModel(precisionModel) {
    this._precisionModel = precisionModel;
  }

  addPt(pt) {
    const bufPt = new Coordinate(pt);

    this._precisionModel.makePrecise(bufPt);

    if (this.isRedundant(bufPt)) return null;

    this._ptList.add(bufPt);
  }

  reverse() {}

  addPts(pt, isForward) {
    if (isForward) for (let i = 0; i < pt.length; i++) this.addPt(pt[i]);else for (let i = pt.length - 1; i >= 0; i--) this.addPt(pt[i]);
  }

  isRedundant(pt) {
    if (this._ptList.size() < 1) return false;

    const lastPt = this._ptList.get(this._ptList.size() - 1);

    const ptDist = pt.distance(lastPt);
    if (ptDist < this._minimimVertexDistance) return true;
    return false;
  }

  toString() {
    const fact = new GeometryFactory();
    const line = fact.createLineString(this.getCoordinates());
    return line.toString();
  }

  closeRing() {
    if (this._ptList.size() < 1) return null;
    const startPt = new Coordinate(this._ptList.get(0));

    const lastPt = this._ptList.get(this._ptList.size() - 1);

    if (startPt.equals(lastPt)) return null;

    this._ptList.add(startPt);
  }

  setMinimumVertexDistance(minimimVertexDistance) {
    this._minimimVertexDistance = minimimVertexDistance;
  }

}
OffsetSegmentString.COORDINATE_ARRAY_TYPE = new Array(0).fill(null);

class OffsetSegmentGenerator {
  constructor() {
    OffsetSegmentGenerator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._maxCurveSegmentError = 0.0;
    this._filletAngleQuantum = null;
    this._closingSegLengthFactor = 1;
    this._segList = null;
    this._distance = 0.0;
    this._precisionModel = null;
    this._bufParams = null;
    this._li = null;
    this._s0 = null;
    this._s1 = null;
    this._s2 = null;
    this._seg0 = new LineSegment();
    this._seg1 = new LineSegment();
    this._offset0 = new LineSegment();
    this._offset1 = new LineSegment();
    this._side = 0;
    this._hasNarrowConcaveAngle = false;
    const precisionModel = arguments[0],
          bufParams = arguments[1],
          distance = arguments[2];
    this._precisionModel = precisionModel;
    this._bufParams = bufParams;
    this._li = new RobustLineIntersector();
    this._filletAngleQuantum = Math.PI / 2.0 / bufParams.getQuadrantSegments();
    if (bufParams.getQuadrantSegments() >= 8 && bufParams.getJoinStyle() === BufferParameters.JOIN_ROUND) this._closingSegLengthFactor = OffsetSegmentGenerator.MAX_CLOSING_SEG_LEN_FACTOR;
    this.init(distance);
  }

  addNextSegment(p, addStartPoint) {
    this._s0 = this._s1;
    this._s1 = this._s2;
    this._s2 = p;

    this._seg0.setCoordinates(this._s0, this._s1);

    this.computeOffsetSegment(this._seg0, this._side, this._distance, this._offset0);

    this._seg1.setCoordinates(this._s1, this._s2);

    this.computeOffsetSegment(this._seg1, this._side, this._distance, this._offset1);
    if (this._s1.equals(this._s2)) return null;
    const orientation = Orientation.index(this._s0, this._s1, this._s2);
    const outsideTurn = orientation === Orientation.CLOCKWISE && this._side === Position.LEFT || orientation === Orientation.COUNTERCLOCKWISE && this._side === Position.RIGHT;
    if (orientation === 0) this.addCollinear(addStartPoint);else if (outsideTurn) this.addOutsideTurn(orientation, addStartPoint);else this.addInsideTurn(orientation, addStartPoint);
  }

  addLineEndCap(p0, p1) {
    const seg = new LineSegment(p0, p1);
    const offsetL = new LineSegment();
    this.computeOffsetSegment(seg, Position.LEFT, this._distance, offsetL);
    const offsetR = new LineSegment();
    this.computeOffsetSegment(seg, Position.RIGHT, this._distance, offsetR);
    const dx = p1.x - p0.x;
    const dy = p1.y - p0.y;
    const angle = Math.atan2(dy, dx);

    switch (this._bufParams.getEndCapStyle()) {
      case BufferParameters.CAP_ROUND:
        this._segList.addPt(offsetL.p1);

        this.addDirectedFillet(p1, angle + Math.PI / 2, angle - Math.PI / 2, Orientation.CLOCKWISE, this._distance);

        this._segList.addPt(offsetR.p1);

        break;

      case BufferParameters.CAP_FLAT:
        this._segList.addPt(offsetL.p1);

        this._segList.addPt(offsetR.p1);

        break;

      case BufferParameters.CAP_SQUARE:
        const squareCapSideOffset = new Coordinate();
        squareCapSideOffset.x = Math.abs(this._distance) * Math.cos(angle);
        squareCapSideOffset.y = Math.abs(this._distance) * Math.sin(angle);
        const squareCapLOffset = new Coordinate(offsetL.p1.x + squareCapSideOffset.x, offsetL.p1.y + squareCapSideOffset.y);
        const squareCapROffset = new Coordinate(offsetR.p1.x + squareCapSideOffset.x, offsetR.p1.y + squareCapSideOffset.y);

        this._segList.addPt(squareCapLOffset);

        this._segList.addPt(squareCapROffset);

        break;
    }
  }

  getCoordinates() {
    const pts = this._segList.getCoordinates();

    return pts;
  }

  addMitreJoin(p, offset0, offset1, distance) {
    const intPt = Intersection.intersection(offset0.p0, offset0.p1, offset1.p0, offset1.p1);

    if (intPt !== null) {
      const mitreRatio = distance <= 0.0 ? 1.0 : intPt.distance(p) / Math.abs(distance);

      if (mitreRatio <= this._bufParams.getMitreLimit()) {
        this._segList.addPt(intPt);

        return null;
      }
    }

    this.addLimitedMitreJoin(offset0, offset1, distance, this._bufParams.getMitreLimit());
  }

  addOutsideTurn(orientation, addStartPoint) {
    if (this._offset0.p1.distance(this._offset1.p0) < this._distance * OffsetSegmentGenerator.OFFSET_SEGMENT_SEPARATION_FACTOR) {
      this._segList.addPt(this._offset0.p1);

      return null;
    }

    if (this._bufParams.getJoinStyle() === BufferParameters.JOIN_MITRE) {
      this.addMitreJoin(this._s1, this._offset0, this._offset1, this._distance);
    } else if (this._bufParams.getJoinStyle() === BufferParameters.JOIN_BEVEL) {
      this.addBevelJoin(this._offset0, this._offset1);
    } else {
      if (addStartPoint) this._segList.addPt(this._offset0.p1);
      this.addCornerFillet(this._s1, this._offset0.p1, this._offset1.p0, orientation, this._distance);

      this._segList.addPt(this._offset1.p0);
    }
  }

  createSquare(p) {
    this._segList.addPt(new Coordinate(p.x + this._distance, p.y + this._distance));

    this._segList.addPt(new Coordinate(p.x + this._distance, p.y - this._distance));

    this._segList.addPt(new Coordinate(p.x - this._distance, p.y - this._distance));

    this._segList.addPt(new Coordinate(p.x - this._distance, p.y + this._distance));

    this._segList.closeRing();
  }

  addSegments(pt, isForward) {
    this._segList.addPts(pt, isForward);
  }

  addFirstSegment() {
    this._segList.addPt(this._offset1.p0);
  }

  addCornerFillet(p, p0, p1, direction, radius) {
    const dx0 = p0.x - p.x;
    const dy0 = p0.y - p.y;
    let startAngle = Math.atan2(dy0, dx0);
    const dx1 = p1.x - p.x;
    const dy1 = p1.y - p.y;
    const endAngle = Math.atan2(dy1, dx1);

    if (direction === Orientation.CLOCKWISE) {
      if (startAngle <= endAngle) startAngle += 2.0 * Math.PI;
    } else {
      if (startAngle >= endAngle) startAngle -= 2.0 * Math.PI;
    }

    this._segList.addPt(p0);

    this.addDirectedFillet(p, startAngle, endAngle, direction, radius);

    this._segList.addPt(p1);
  }

  addLastSegment() {
    this._segList.addPt(this._offset1.p1);
  }

  initSideSegments(s1, s2, side) {
    this._s1 = s1;
    this._s2 = s2;
    this._side = side;

    this._seg1.setCoordinates(s1, s2);

    this.computeOffsetSegment(this._seg1, side, this._distance, this._offset1);
  }

  addLimitedMitreJoin(offset0, offset1, distance, mitreLimit) {
    const basePt = this._seg0.p1;
    const ang0 = Angle.angle(basePt, this._seg0.p0);
    const angDiff = Angle.angleBetweenOriented(this._seg0.p0, basePt, this._seg1.p1);
    const angDiffHalf = angDiff / 2;
    const midAng = Angle.normalize(ang0 + angDiffHalf);
    const mitreMidAng = Angle.normalize(midAng + Math.PI);
    const mitreDist = mitreLimit * distance;
    const bevelDelta = mitreDist * Math.abs(Math.sin(angDiffHalf));
    const bevelHalfLen = distance - bevelDelta;
    const bevelMidX = basePt.x + mitreDist * Math.cos(mitreMidAng);
    const bevelMidY = basePt.y + mitreDist * Math.sin(mitreMidAng);
    const bevelMidPt = new Coordinate(bevelMidX, bevelMidY);
    const mitreMidLine = new LineSegment(basePt, bevelMidPt);
    const bevelEndLeft = mitreMidLine.pointAlongOffset(1.0, bevelHalfLen);
    const bevelEndRight = mitreMidLine.pointAlongOffset(1.0, -bevelHalfLen);

    if (this._side === Position.LEFT) {
      this._segList.addPt(bevelEndLeft);

      this._segList.addPt(bevelEndRight);
    } else {
      this._segList.addPt(bevelEndRight);

      this._segList.addPt(bevelEndLeft);
    }
  }

  addDirectedFillet(p, startAngle, endAngle, direction, radius) {
    const directionFactor = direction === Orientation.CLOCKWISE ? -1 : 1;
    const totalAngle = Math.abs(startAngle - endAngle);
    const nSegs = Math.trunc(totalAngle / this._filletAngleQuantum + 0.5);
    if (nSegs < 1) return null;
    const angleInc = totalAngle / nSegs;
    const pt = new Coordinate();

    for (let i = 0; i < nSegs; i++) {
      const angle = startAngle + directionFactor * i * angleInc;
      pt.x = p.x + radius * Math.cos(angle);
      pt.y = p.y + radius * Math.sin(angle);

      this._segList.addPt(pt);
    }
  }

  computeOffsetSegment(seg, side, distance, offset) {
    const sideSign = side === Position.LEFT ? 1 : -1;
    const dx = seg.p1.x - seg.p0.x;
    const dy = seg.p1.y - seg.p0.y;
    const len = Math.sqrt(dx * dx + dy * dy);
    const ux = sideSign * distance * dx / len;
    const uy = sideSign * distance * dy / len;
    offset.p0.x = seg.p0.x - uy;
    offset.p0.y = seg.p0.y + ux;
    offset.p1.x = seg.p1.x - uy;
    offset.p1.y = seg.p1.y + ux;
  }

  addInsideTurn(orientation, addStartPoint) {
    this._li.computeIntersection(this._offset0.p0, this._offset0.p1, this._offset1.p0, this._offset1.p1);

    if (this._li.hasIntersection()) {
      this._segList.addPt(this._li.getIntersection(0));
    } else {
      this._hasNarrowConcaveAngle = true;

      if (this._offset0.p1.distance(this._offset1.p0) < this._distance * OffsetSegmentGenerator.INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR) {
        this._segList.addPt(this._offset0.p1);
      } else {
        this._segList.addPt(this._offset0.p1);

        if (this._closingSegLengthFactor > 0) {
          const mid0 = new Coordinate((this._closingSegLengthFactor * this._offset0.p1.x + this._s1.x) / (this._closingSegLengthFactor + 1), (this._closingSegLengthFactor * this._offset0.p1.y + this._s1.y) / (this._closingSegLengthFactor + 1));

          this._segList.addPt(mid0);

          const mid1 = new Coordinate((this._closingSegLengthFactor * this._offset1.p0.x + this._s1.x) / (this._closingSegLengthFactor + 1), (this._closingSegLengthFactor * this._offset1.p0.y + this._s1.y) / (this._closingSegLengthFactor + 1));

          this._segList.addPt(mid1);
        } else {
          this._segList.addPt(this._s1);
        }

        this._segList.addPt(this._offset1.p0);
      }
    }
  }

  createCircle(p) {
    const pt = new Coordinate(p.x + this._distance, p.y);

    this._segList.addPt(pt);

    this.addDirectedFillet(p, 0.0, 2.0 * Math.PI, -1, this._distance);

    this._segList.closeRing();
  }

  addBevelJoin(offset0, offset1) {
    this._segList.addPt(offset0.p1);

    this._segList.addPt(offset1.p0);
  }

  init(distance) {
    this._distance = distance;
    this._maxCurveSegmentError = distance * (1 - Math.cos(this._filletAngleQuantum / 2.0));
    this._segList = new OffsetSegmentString();

    this._segList.setPrecisionModel(this._precisionModel);

    this._segList.setMinimumVertexDistance(distance * OffsetSegmentGenerator.CURVE_VERTEX_SNAP_DISTANCE_FACTOR);
  }

  addCollinear(addStartPoint) {
    this._li.computeIntersection(this._s0, this._s1, this._s1, this._s2);

    const numInt = this._li.getIntersectionNum();

    if (numInt >= 2) if (this._bufParams.getJoinStyle() === BufferParameters.JOIN_BEVEL || this._bufParams.getJoinStyle() === BufferParameters.JOIN_MITRE) {
      if (addStartPoint) this._segList.addPt(this._offset0.p1);

      this._segList.addPt(this._offset1.p0);
    } else {
      this.addCornerFillet(this._s1, this._offset0.p1, this._offset1.p0, Orientation.CLOCKWISE, this._distance);
    }
  }

  closeRing() {
    this._segList.closeRing();
  }

  hasNarrowConcaveAngle() {
    return this._hasNarrowConcaveAngle;
  }

}
OffsetSegmentGenerator.OFFSET_SEGMENT_SEPARATION_FACTOR = 1.0E-3;
OffsetSegmentGenerator.INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR = 1.0E-3;
OffsetSegmentGenerator.CURVE_VERTEX_SNAP_DISTANCE_FACTOR = 1.0E-6;
OffsetSegmentGenerator.MAX_CLOSING_SEG_LEN_FACTOR = 80;

class OffsetCurveBuilder {
  constructor() {
    OffsetCurveBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._distance = 0.0;
    this._precisionModel = null;
    this._bufParams = null;
    const precisionModel = arguments[0],
          bufParams = arguments[1];
    this._precisionModel = precisionModel;
    this._bufParams = bufParams;
  }

  static copyCoordinates(pts) {
    const copy = new Array(pts.length).fill(null);

    for (let i = 0; i < copy.length; i++) copy[i] = new Coordinate(pts[i]);

    return copy;
  }

  getOffsetCurve(inputPts, distance) {
    this._distance = distance;
    if (distance === 0.0) return null;
    const isRightSide = distance < 0.0;
    const posDistance = Math.abs(distance);
    const segGen = this.getSegGen(posDistance);
    if (inputPts.length <= 1) this.computePointCurve(inputPts[0], segGen);else this.computeOffsetCurve(inputPts, isRightSide, segGen);
    const curvePts = segGen.getCoordinates();
    if (isRightSide) CoordinateArrays.reverse(curvePts);
    return curvePts;
  }

  computeSingleSidedBufferCurve(inputPts, isRightSide, segGen) {
    const distTol = this.simplifyTolerance(this._distance);

    if (isRightSide) {
      segGen.addSegments(inputPts, true);
      const simp2 = BufferInputLineSimplifier.simplify(inputPts, -distTol);
      const n2 = simp2.length - 1;
      segGen.initSideSegments(simp2[n2], simp2[n2 - 1], Position.LEFT);
      segGen.addFirstSegment();

      for (let i = n2 - 2; i >= 0; i--) segGen.addNextSegment(simp2[i], true);
    } else {
      segGen.addSegments(inputPts, false);
      const simp1 = BufferInputLineSimplifier.simplify(inputPts, distTol);
      const n1 = simp1.length - 1;
      segGen.initSideSegments(simp1[0], simp1[1], Position.LEFT);
      segGen.addFirstSegment();

      for (let i = 2; i <= n1; i++) segGen.addNextSegment(simp1[i], true);
    }

    segGen.addLastSegment();
    segGen.closeRing();
  }

  computeRingBufferCurve(inputPts, side, segGen) {
    let distTol = this.simplifyTolerance(this._distance);
    if (side === Position.RIGHT) distTol = -distTol;
    const simp = BufferInputLineSimplifier.simplify(inputPts, distTol);
    const n = simp.length - 1;
    segGen.initSideSegments(simp[n - 1], simp[0], side);

    for (let i = 1; i <= n; i++) {
      const addStartPoint = i !== 1;
      segGen.addNextSegment(simp[i], addStartPoint);
    }

    segGen.closeRing();
  }

  computeLineBufferCurve(inputPts, segGen) {
    const distTol = this.simplifyTolerance(this._distance);
    const simp1 = BufferInputLineSimplifier.simplify(inputPts, distTol);
    const n1 = simp1.length - 1;
    segGen.initSideSegments(simp1[0], simp1[1], Position.LEFT);

    for (let i = 2; i <= n1; i++) segGen.addNextSegment(simp1[i], true);

    segGen.addLastSegment();
    segGen.addLineEndCap(simp1[n1 - 1], simp1[n1]);
    const simp2 = BufferInputLineSimplifier.simplify(inputPts, -distTol);
    const n2 = simp2.length - 1;
    segGen.initSideSegments(simp2[n2], simp2[n2 - 1], Position.LEFT);

    for (let i = n2 - 2; i >= 0; i--) segGen.addNextSegment(simp2[i], true);

    segGen.addLastSegment();
    segGen.addLineEndCap(simp2[1], simp2[0]);
    segGen.closeRing();
  }

  computePointCurve(pt, segGen) {
    switch (this._bufParams.getEndCapStyle()) {
      case BufferParameters.CAP_ROUND:
        segGen.createCircle(pt);
        break;

      case BufferParameters.CAP_SQUARE:
        segGen.createSquare(pt);
        break;
    }
  }

  getLineCurve(inputPts, distance) {
    this._distance = distance;
    if (this.isLineOffsetEmpty(distance)) return null;
    const posDistance = Math.abs(distance);
    const segGen = this.getSegGen(posDistance);

    if (inputPts.length <= 1) {
      this.computePointCurve(inputPts[0], segGen);
    } else if (this._bufParams.isSingleSided()) {
      const isRightSide = distance < 0.0;
      this.computeSingleSidedBufferCurve(inputPts, isRightSide, segGen);
    } else {
      this.computeLineBufferCurve(inputPts, segGen);
    }

    const lineCoord = segGen.getCoordinates();
    return lineCoord;
  }

  getBufferParameters() {
    return this._bufParams;
  }

  simplifyTolerance(bufDistance) {
    return bufDistance * this._bufParams.getSimplifyFactor();
  }

  getRingCurve(inputPts, side, distance) {
    this._distance = distance;
    if (inputPts.length <= 2) return this.getLineCurve(inputPts, distance);
    if (distance === 0.0) return OffsetCurveBuilder.copyCoordinates(inputPts);
    const segGen = this.getSegGen(distance);
    this.computeRingBufferCurve(inputPts, side, segGen);
    return segGen.getCoordinates();
  }

  computeOffsetCurve(inputPts, isRightSide, segGen) {
    const distTol = this.simplifyTolerance(this._distance);

    if (isRightSide) {
      const simp2 = BufferInputLineSimplifier.simplify(inputPts, -distTol);
      const n2 = simp2.length - 1;
      segGen.initSideSegments(simp2[n2], simp2[n2 - 1], Position.LEFT);
      segGen.addFirstSegment();

      for (let i = n2 - 2; i >= 0; i--) segGen.addNextSegment(simp2[i], true);
    } else {
      const simp1 = BufferInputLineSimplifier.simplify(inputPts, distTol);
      const n1 = simp1.length - 1;
      segGen.initSideSegments(simp1[0], simp1[1], Position.LEFT);
      segGen.addFirstSegment();

      for (let i = 2; i <= n1; i++) segGen.addNextSegment(simp1[i], true);
    }

    segGen.addLastSegment();
  }

  isLineOffsetEmpty(distance) {
    if (distance === 0.0) return true;
    if (distance < 0.0 && !this._bufParams.isSingleSided()) return true;
    return false;
  }

  getSegGen(distance) {
    return new OffsetSegmentGenerator(this._precisionModel, this._bufParams, distance);
  }

}

class SubgraphDepthLocater {
  constructor() {
    SubgraphDepthLocater.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._subgraphs = null;
    this._seg = new LineSegment();
    const subgraphs = arguments[0];
    this._subgraphs = subgraphs;
  }

  findStabbedSegments() {
    if (arguments.length === 1) {
      const stabbingRayLeftPt = arguments[0];
      const stabbedSegments = new ArrayList();

      for (let i = this._subgraphs.iterator(); i.hasNext();) {
        const bsg = i.next();
        const env = bsg.getEnvelope();
        if (stabbingRayLeftPt.y < env.getMinY() || stabbingRayLeftPt.y > env.getMaxY()) continue;
        this.findStabbedSegments(stabbingRayLeftPt, bsg.getDirectedEdges(), stabbedSegments);
      }

      return stabbedSegments;
    } else if (arguments.length === 3) {
      if (hasInterface(arguments[2], List) && arguments[0] instanceof Coordinate && arguments[1] instanceof DirectedEdge$1) {
        const stabbingRayLeftPt = arguments[0],
              dirEdge = arguments[1],
              stabbedSegments = arguments[2];
        const pts = dirEdge.getEdge().getCoordinates();

        for (let i = 0; i < pts.length - 1; i++) {
          this._seg.p0 = pts[i];
          this._seg.p1 = pts[i + 1];
          if (this._seg.p0.y > this._seg.p1.y) this._seg.reverse();
          const maxx = Math.max(this._seg.p0.x, this._seg.p1.x);
          if (maxx < stabbingRayLeftPt.x) continue;
          if (this._seg.isHorizontal()) continue;
          if (stabbingRayLeftPt.y < this._seg.p0.y || stabbingRayLeftPt.y > this._seg.p1.y) continue;
          if (Orientation.index(this._seg.p0, this._seg.p1, stabbingRayLeftPt) === Orientation.RIGHT) continue;
          let depth = dirEdge.getDepth(Position.LEFT);
          if (!this._seg.p0.equals(pts[i])) depth = dirEdge.getDepth(Position.RIGHT);
          const ds = new DepthSegment(this._seg, depth);
          stabbedSegments.add(ds);
        }
      } else if (hasInterface(arguments[2], List) && arguments[0] instanceof Coordinate && hasInterface(arguments[1], List)) {
        const stabbingRayLeftPt = arguments[0],
              dirEdges = arguments[1],
              stabbedSegments = arguments[2];

        for (let i = dirEdges.iterator(); i.hasNext();) {
          const de = i.next();
          if (!de.isForward()) continue;
          this.findStabbedSegments(stabbingRayLeftPt, de, stabbedSegments);
        }
      }
    }
  }

  getDepth(p) {
    const stabbedSegments = this.findStabbedSegments(p);
    if (stabbedSegments.size() === 0) return 0;
    const ds = Collections.min(stabbedSegments);
    return ds._leftDepth;
  }

}

class DepthSegment {
  constructor() {
    DepthSegment.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._upwardSeg = null;
    this._leftDepth = null;
    const seg = arguments[0],
          depth = arguments[1];
    this._upwardSeg = new LineSegment(seg);
    this._leftDepth = depth;
  }

  compareTo(obj) {
    const other = obj;
    if (this._upwardSeg.minX() >= other._upwardSeg.maxX()) return 1;
    if (this._upwardSeg.maxX() <= other._upwardSeg.minX()) return -1;

    let orientIndex = this._upwardSeg.orientationIndex(other._upwardSeg);

    if (orientIndex !== 0) return orientIndex;
    orientIndex = -1 * other._upwardSeg.orientationIndex(this._upwardSeg);
    if (orientIndex !== 0) return orientIndex;
    return this._upwardSeg.compareTo(other._upwardSeg);
  }

  compareX(seg0, seg1) {
    const compare0 = seg0.p0.compareTo(seg1.p0);
    if (compare0 !== 0) return compare0;
    return seg0.p1.compareTo(seg1.p1);
  }

  toString() {
    return this._upwardSeg.toString();
  }

  get interfaces_() {
    return [Comparable];
  }

}

SubgraphDepthLocater.DepthSegment = DepthSegment;

class OffsetCurveSetBuilder {
  constructor() {
    OffsetCurveSetBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._distance = null;
    this._curveBuilder = null;
    this._curveList = new ArrayList();
    const inputGeom = arguments[0],
          distance = arguments[1],
          curveBuilder = arguments[2];
    this._inputGeom = inputGeom;
    this._distance = distance;
    this._curveBuilder = curveBuilder;
  }

  addRingSide(coord, offsetDistance, side, cwLeftLoc, cwRightLoc) {
    if (offsetDistance === 0.0 && coord.length < LinearRing.MINIMUM_VALID_SIZE) return null;
    let leftLoc = cwLeftLoc;
    let rightLoc = cwRightLoc;

    if (coord.length >= LinearRing.MINIMUM_VALID_SIZE && Orientation.isCCW(coord)) {
      leftLoc = cwRightLoc;
      rightLoc = cwLeftLoc;
      side = Position.opposite(side);
    }

    const curve = this._curveBuilder.getRingCurve(coord, side, offsetDistance);

    this.addCurve(curve, leftLoc, rightLoc);
  }

  addRingBothSides(coord, distance) {
    this.addRingSide(coord, distance, Position.LEFT, Location.EXTERIOR, Location.INTERIOR);
    this.addRingSide(coord, distance, Position.RIGHT, Location.INTERIOR, Location.EXTERIOR);
  }

  addPoint(p) {
    if (this._distance <= 0.0) return null;
    const coord = p.getCoordinates();

    const curve = this._curveBuilder.getLineCurve(coord, this._distance);

    this.addCurve(curve, Location.EXTERIOR, Location.INTERIOR);
  }

  addPolygon(p) {
    let offsetDistance = this._distance;
    let offsetSide = Position.LEFT;

    if (this._distance < 0.0) {
      offsetDistance = -this._distance;
      offsetSide = Position.RIGHT;
    }

    const shell = p.getExteriorRing();
    const shellCoord = CoordinateArrays.removeRepeatedPoints(shell.getCoordinates());
    if (this._distance < 0.0 && this.isErodedCompletely(shell, this._distance)) return null;
    if (this._distance <= 0.0 && shellCoord.length < 3) return null;
    this.addRingSide(shellCoord, offsetDistance, offsetSide, Location.EXTERIOR, Location.INTERIOR);

    for (let i = 0; i < p.getNumInteriorRing(); i++) {
      const hole = p.getInteriorRingN(i);
      const holeCoord = CoordinateArrays.removeRepeatedPoints(hole.getCoordinates());
      if (this._distance > 0.0 && this.isErodedCompletely(hole, -this._distance)) continue;
      this.addRingSide(holeCoord, offsetDistance, Position.opposite(offsetSide), Location.INTERIOR, Location.EXTERIOR);
    }
  }

  isTriangleErodedCompletely(triangleCoord, bufferDistance) {
    const tri = new Triangle(triangleCoord[0], triangleCoord[1], triangleCoord[2]);
    const inCentre = tri.inCentre();
    const distToCentre = Distance.pointToSegment(inCentre, tri.p0, tri.p1);
    return distToCentre < Math.abs(bufferDistance);
  }

  addLineString(line) {
    if (this._curveBuilder.isLineOffsetEmpty(this._distance)) return null;
    const coord = CoordinateArrays.removeRepeatedPoints(line.getCoordinates());

    if (CoordinateArrays.isRing(coord) && !this._curveBuilder.getBufferParameters().isSingleSided()) {
      this.addRingBothSides(coord, this._distance);
    } else {
      const curve = this._curveBuilder.getLineCurve(coord, this._distance);

      this.addCurve(curve, Location.EXTERIOR, Location.INTERIOR);
    }
  }

  addCurve(coord, leftLoc, rightLoc) {
    if (coord === null || coord.length < 2) return null;
    const e = new NodedSegmentString(coord, new Label(0, Location.BOUNDARY, leftLoc, rightLoc));

    this._curveList.add(e);
  }

  getCurves() {
    this.add(this._inputGeom);
    return this._curveList;
  }

  add(g) {
    if (g.isEmpty()) return null;
    if (g instanceof Polygon) this.addPolygon(g);else if (g instanceof LineString) this.addLineString(g);else if (g instanceof Point) this.addPoint(g);else if (g instanceof MultiPoint) this.addCollection(g);else if (g instanceof MultiLineString) this.addCollection(g);else if (g instanceof MultiPolygon) this.addCollection(g);else if (g instanceof GeometryCollection) this.addCollection(g);else throw new UnsupportedOperationException(g.getGeometryType());
  }

  isErodedCompletely(ring, bufferDistance) {
    const ringCoord = ring.getCoordinates();
    if (ringCoord.length < 4) return bufferDistance < 0;
    if (ringCoord.length === 4) return this.isTriangleErodedCompletely(ringCoord, bufferDistance);
    const env = ring.getEnvelopeInternal();
    const envMinDimension = Math.min(env.getHeight(), env.getWidth());
    if (bufferDistance < 0.0 && 2 * Math.abs(bufferDistance) > envMinDimension) return true;
    return false;
  }

  addCollection(gc) {
    for (let i = 0; i < gc.getNumGeometries(); i++) {
      const g = gc.getGeometryN(i);
      this.add(g);
    }
  }

}

class EdgeEndStar {
  constructor() {
    EdgeEndStar.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._edgeMap = new TreeMap();
    this._edgeList = null;
    this._ptInAreaLocation = [Location.NONE, Location.NONE];
  }

  getNextCW(ee) {
    this.getEdges();

    const i = this._edgeList.indexOf(ee);

    let iNextCW = i - 1;
    if (i === 0) iNextCW = this._edgeList.size() - 1;
    return this._edgeList.get(iNextCW);
  }

  propagateSideLabels(geomIndex) {
    let startLoc = Location.NONE;

    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();
      const label = e.getLabel();
      if (label.isArea(geomIndex) && label.getLocation(geomIndex, Position.LEFT) !== Location.NONE) startLoc = label.getLocation(geomIndex, Position.LEFT);
    }

    if (startLoc === Location.NONE) return null;
    let currLoc = startLoc;

    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();
      const label = e.getLabel();
      if (label.getLocation(geomIndex, Position.ON) === Location.NONE) label.setLocation(geomIndex, Position.ON, currLoc);

      if (label.isArea(geomIndex)) {
        const leftLoc = label.getLocation(geomIndex, Position.LEFT);
        const rightLoc = label.getLocation(geomIndex, Position.RIGHT);

        if (rightLoc !== Location.NONE) {
          if (rightLoc !== currLoc) throw new TopologyException('side location conflict', e.getCoordinate());
          if (leftLoc === Location.NONE) Assert.shouldNeverReachHere('found single null side (at ' + e.getCoordinate() + ')');
          currLoc = leftLoc;
        } else {
          Assert.isTrue(label.getLocation(geomIndex, Position.LEFT) === Location.NONE, 'found single null side');
          label.setLocation(geomIndex, Position.RIGHT, currLoc);
          label.setLocation(geomIndex, Position.LEFT, currLoc);
        }
      }
    }
  }

  getCoordinate() {
    const it = this.iterator();
    if (!it.hasNext()) return null;
    const e = it.next();
    return e.getCoordinate();
  }

  print(out) {
    System.out.println('EdgeEndStar:   ' + this.getCoordinate());

    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();
      e.print(out);
    }
  }

  isAreaLabelsConsistent(geomGraph) {
    this.computeEdgeEndLabels(geomGraph.getBoundaryNodeRule());
    return this.checkAreaLabelsConsistent(0);
  }

  checkAreaLabelsConsistent(geomIndex) {
    const edges = this.getEdges();
    if (edges.size() <= 0) return true;
    const lastEdgeIndex = edges.size() - 1;
    const startLabel = edges.get(lastEdgeIndex).getLabel();
    const startLoc = startLabel.getLocation(geomIndex, Position.LEFT);
    Assert.isTrue(startLoc !== Location.NONE, 'Found unlabelled area edge');
    let currLoc = startLoc;

    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();
      const label = e.getLabel();
      Assert.isTrue(label.isArea(geomIndex), 'Found non-area edge');
      const leftLoc = label.getLocation(geomIndex, Position.LEFT);
      const rightLoc = label.getLocation(geomIndex, Position.RIGHT);
      if (leftLoc === rightLoc) return false;
      if (rightLoc !== currLoc) return false;
      currLoc = leftLoc;
    }

    return true;
  }

  findIndex(eSearch) {
    this.iterator();

    for (let i = 0; i < this._edgeList.size(); i++) {
      const e = this._edgeList.get(i);

      if (e === eSearch) return i;
    }

    return -1;
  }

  iterator() {
    return this.getEdges().iterator();
  }

  getEdges() {
    if (this._edgeList === null) this._edgeList = new ArrayList(this._edgeMap.values());
    return this._edgeList;
  }

  getLocation(geomIndex, p, geom) {
    if (this._ptInAreaLocation[geomIndex] === Location.NONE) this._ptInAreaLocation[geomIndex] = SimplePointInAreaLocator.locate(p, geom[geomIndex].getGeometry());
    return this._ptInAreaLocation[geomIndex];
  }

  toString() {
    const buf = new StringBuffer();
    buf.append('EdgeEndStar:   ' + this.getCoordinate());
    buf.append('\n');

    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();
      buf.append(e);
      buf.append('\n');
    }

    return buf.toString();
  }

  computeEdgeEndLabels(boundaryNodeRule) {
    for (let it = this.iterator(); it.hasNext();) {
      const ee = it.next();
      ee.computeLabel(boundaryNodeRule);
    }
  }

  computeLabelling(geomGraph) {
    this.computeEdgeEndLabels(geomGraph[0].getBoundaryNodeRule());
    this.propagateSideLabels(0);
    this.propagateSideLabels(1);
    const hasDimensionalCollapseEdge = [false, false];

    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();
      const label = e.getLabel();

      for (let geomi = 0; geomi < 2; geomi++) if (label.isLine(geomi) && label.getLocation(geomi) === Location.BOUNDARY) hasDimensionalCollapseEdge[geomi] = true;
    }

    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();
      const label = e.getLabel();

      for (let geomi = 0; geomi < 2; geomi++) if (label.isAnyNull(geomi)) {
        let loc = Location.NONE;

        if (hasDimensionalCollapseEdge[geomi]) {
          loc = Location.EXTERIOR;
        } else {
          const p = e.getCoordinate();
          loc = this.getLocation(geomi, p, geomGraph);
        }

        label.setAllLocationsIfNull(geomi, loc);
      }
    }
  }

  getDegree() {
    return this._edgeMap.size();
  }

  insertEdgeEnd(e, obj) {
    this._edgeMap.put(e, obj);

    this._edgeList = null;
  }

}

class DirectedEdgeStar$1 extends EdgeEndStar {
  constructor() {
    super();
    DirectedEdgeStar$1.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._resultAreaEdgeList = null;
    this._label = null;
    this._SCANNING_FOR_INCOMING = 1;
    this._LINKING_TO_OUTGOING = 2;
  }

  linkResultDirectedEdges() {
    this.getResultAreaEdges();
    let firstOut = null;
    let incoming = null;
    let state = this._SCANNING_FOR_INCOMING;

    for (let i = 0; i < this._resultAreaEdgeList.size(); i++) {
      const nextOut = this._resultAreaEdgeList.get(i);

      const nextIn = nextOut.getSym();
      if (!nextOut.getLabel().isArea()) continue;
      if (firstOut === null && nextOut.isInResult()) firstOut = nextOut;

      switch (state) {
        case this._SCANNING_FOR_INCOMING:
          if (!nextIn.isInResult()) continue;
          incoming = nextIn;
          state = this._LINKING_TO_OUTGOING;
          break;

        case this._LINKING_TO_OUTGOING:
          if (!nextOut.isInResult()) continue;
          incoming.setNext(nextOut);
          state = this._SCANNING_FOR_INCOMING;
          break;
      }
    }

    if (state === this._LINKING_TO_OUTGOING) {
      if (firstOut === null) throw new TopologyException('no outgoing dirEdge found', this.getCoordinate());
      Assert.isTrue(firstOut.isInResult(), 'unable to link last incoming dirEdge');
      incoming.setNext(firstOut);
    }
  }

  insert(ee) {
    const de = ee;
    this.insertEdgeEnd(de, de);
  }

  getRightmostEdge() {
    const edges = this.getEdges();
    const size = edges.size();
    if (size < 1) return null;
    const de0 = edges.get(0);
    if (size === 1) return de0;
    const deLast = edges.get(size - 1);
    const quad0 = de0.getQuadrant();
    const quad1 = deLast.getQuadrant();

    if (Quadrant.isNorthern(quad0) && Quadrant.isNorthern(quad1)) {
      return de0;
    } else if (!Quadrant.isNorthern(quad0) && !Quadrant.isNorthern(quad1)) {
      return deLast;
    } else {
      if (de0.getDy() !== 0) return de0;else if (deLast.getDy() !== 0) return deLast;
    }

    Assert.shouldNeverReachHere('found two horizontal edges incident on node');
    return null;
  }

  print(out) {
    System.out.println('DirectedEdgeStar: ' + this.getCoordinate());

    for (let it = this.iterator(); it.hasNext();) {
      const de = it.next();
      out.print('out ');
      de.print(out);
      out.println();
      out.print('in ');
      de.getSym().print(out);
      out.println();
    }
  }

  getResultAreaEdges() {
    if (this._resultAreaEdgeList !== null) return this._resultAreaEdgeList;
    this._resultAreaEdgeList = new ArrayList();

    for (let it = this.iterator(); it.hasNext();) {
      const de = it.next();
      if (de.isInResult() || de.getSym().isInResult()) this._resultAreaEdgeList.add(de);
    }

    return this._resultAreaEdgeList;
  }

  updateLabelling(nodeLabel) {
    for (let it = this.iterator(); it.hasNext();) {
      const de = it.next();
      const label = de.getLabel();
      label.setAllLocationsIfNull(0, nodeLabel.getLocation(0));
      label.setAllLocationsIfNull(1, nodeLabel.getLocation(1));
    }
  }

  linkAllDirectedEdges() {
    this.getEdges();
    let prevOut = null;
    let firstIn = null;

    for (let i = this._edgeList.size() - 1; i >= 0; i--) {
      const nextOut = this._edgeList.get(i);

      const nextIn = nextOut.getSym();
      if (firstIn === null) firstIn = nextIn;
      if (prevOut !== null) nextIn.setNext(prevOut);
      prevOut = nextOut;
    }

    firstIn.setNext(prevOut);
  }

  computeDepths() {
    if (arguments.length === 1) {
      const de = arguments[0];
      const edgeIndex = this.findIndex(de);
      const startDepth = de.getDepth(Position.LEFT);
      const targetLastDepth = de.getDepth(Position.RIGHT);
      const nextDepth = this.computeDepths(edgeIndex + 1, this._edgeList.size(), startDepth);
      const lastDepth = this.computeDepths(0, edgeIndex, nextDepth);
      if (lastDepth !== targetLastDepth) throw new TopologyException('depth mismatch at ' + de.getCoordinate());
    } else if (arguments.length === 3) {
      const startIndex = arguments[0],
            endIndex = arguments[1],
            startDepth = arguments[2];
      let currDepth = startDepth;

      for (let i = startIndex; i < endIndex; i++) {
        const nextDe = this._edgeList.get(i);

        nextDe.setEdgeDepths(Position.RIGHT, currDepth);
        currDepth = nextDe.getDepth(Position.LEFT);
      }

      return currDepth;
    }
  }

  mergeSymLabels() {
    for (let it = this.iterator(); it.hasNext();) {
      const de = it.next();
      const label = de.getLabel();
      label.merge(de.getSym().getLabel());
    }
  }

  linkMinimalDirectedEdges(er) {
    let firstOut = null;
    let incoming = null;
    let state = this._SCANNING_FOR_INCOMING;

    for (let i = this._resultAreaEdgeList.size() - 1; i >= 0; i--) {
      const nextOut = this._resultAreaEdgeList.get(i);

      const nextIn = nextOut.getSym();
      if (firstOut === null && nextOut.getEdgeRing() === er) firstOut = nextOut;

      switch (state) {
        case this._SCANNING_FOR_INCOMING:
          if (nextIn.getEdgeRing() !== er) continue;
          incoming = nextIn;
          state = this._LINKING_TO_OUTGOING;
          break;

        case this._LINKING_TO_OUTGOING:
          if (nextOut.getEdgeRing() !== er) continue;
          incoming.setNextMin(nextOut);
          state = this._SCANNING_FOR_INCOMING;
          break;
      }
    }

    if (state === this._LINKING_TO_OUTGOING) {
      Assert.isTrue(firstOut !== null, 'found null for first outgoing dirEdge');
      Assert.isTrue(firstOut.getEdgeRing() === er, 'unable to link last incoming dirEdge');
      incoming.setNextMin(firstOut);
    }
  }

  getOutgoingDegree() {
    if (arguments.length === 0) {
      let degree = 0;

      for (let it = this.iterator(); it.hasNext();) {
        const de = it.next();
        if (de.isInResult()) degree++;
      }

      return degree;
    } else if (arguments.length === 1) {
      const er = arguments[0];
      let degree = 0;

      for (let it = this.iterator(); it.hasNext();) {
        const de = it.next();
        if (de.getEdgeRing() === er) degree++;
      }

      return degree;
    }
  }

  getLabel() {
    return this._label;
  }

  findCoveredLineEdges() {
    let startLoc = Location.NONE;

    for (let it = this.iterator(); it.hasNext();) {
      const nextOut = it.next();
      const nextIn = nextOut.getSym();

      if (!nextOut.isLineEdge()) {
        if (nextOut.isInResult()) {
          startLoc = Location.INTERIOR;
          break;
        }

        if (nextIn.isInResult()) {
          startLoc = Location.EXTERIOR;
          break;
        }
      }
    }

    if (startLoc === Location.NONE) return null;
    let currLoc = startLoc;

    for (let it = this.iterator(); it.hasNext();) {
      const nextOut = it.next();
      const nextIn = nextOut.getSym();

      if (nextOut.isLineEdge()) {
        nextOut.getEdge().setCovered(currLoc === Location.INTERIOR);
      } else {
        if (nextOut.isInResult()) currLoc = Location.EXTERIOR;
        if (nextIn.isInResult()) currLoc = Location.INTERIOR;
      }
    }
  }

  computeLabelling(geom) {
    super.computeLabelling.call(this, geom);
    this._label = new Label(Location.NONE);

    for (let it = this.iterator(); it.hasNext();) {
      const ee = it.next();
      const e = ee.getEdge();
      const eLabel = e.getLabel();

      for (let i = 0; i < 2; i++) {
        const eLoc = eLabel.getLocation(i);
        if (eLoc === Location.INTERIOR || eLoc === Location.BOUNDARY) this._label.setLocation(i, Location.INTERIOR);
      }
    }
  }

}

class OverlayNodeFactory extends NodeFactory {
  constructor() {
    super();
  }

  createNode(coord) {
    return new Node$2(coord, new DirectedEdgeStar$1());
  }

}

class OrientedCoordinateArray {
  constructor() {
    OrientedCoordinateArray.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pts = null;
    this._orientation = null;
    const pts = arguments[0];
    this._pts = pts;
    this._orientation = OrientedCoordinateArray.orientation(pts);
  }

  static orientation(pts) {
    return CoordinateArrays.increasingDirection(pts) === 1;
  }

  static compareOriented(pts1, orientation1, pts2, orientation2) {
    const dir1 = orientation1 ? 1 : -1;
    const dir2 = orientation2 ? 1 : -1;
    const limit1 = orientation1 ? pts1.length : -1;
    const limit2 = orientation2 ? pts2.length : -1;
    let i1 = orientation1 ? 0 : pts1.length - 1;
    let i2 = orientation2 ? 0 : pts2.length - 1;

    while (true) {
      const compPt = pts1[i1].compareTo(pts2[i2]);
      if (compPt !== 0) return compPt;
      i1 += dir1;
      i2 += dir2;
      const done1 = i1 === limit1;
      const done2 = i2 === limit2;
      if (done1 && !done2) return -1;
      if (!done1 && done2) return 1;
      if (done1 && done2) return 0;
    }
  }

  compareTo(o1) {
    const oca = o1;
    const comp = OrientedCoordinateArray.compareOriented(this._pts, this._orientation, oca._pts, oca._orientation);
    return comp;
  }

  get interfaces_() {
    return [Comparable];
  }

}

class EdgeList {
  constructor() {
    EdgeList.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._edges = new ArrayList();
    this._ocaMap = new TreeMap();
  }

  print(out) {
    out.print('MULTILINESTRING ( ');

    for (let j = 0; j < this._edges.size(); j++) {
      const e = this._edges.get(j);

      if (j > 0) out.print(',');
      out.print('(');
      const pts = e.getCoordinates();

      for (let i = 0; i < pts.length; i++) {
        if (i > 0) out.print(',');
        out.print(pts[i].x + ' ' + pts[i].y);
      }

      out.println(')');
    }

    out.print(')  ');
  }

  addAll(edgeColl) {
    for (let i = edgeColl.iterator(); i.hasNext();) this.add(i.next());
  }

  findEdgeIndex(e) {
    for (let i = 0; i < this._edges.size(); i++) if (this._edges.get(i).equals(e)) return i;

    return -1;
  }

  iterator() {
    return this._edges.iterator();
  }

  getEdges() {
    return this._edges;
  }

  get(i) {
    return this._edges.get(i);
  }

  findEqualEdge(e) {
    const oca = new OrientedCoordinateArray(e.getCoordinates());

    const matchEdge = this._ocaMap.get(oca);

    return matchEdge;
  }

  add(e) {
    this._edges.add(e);

    const oca = new OrientedCoordinateArray(e.getCoordinates());

    this._ocaMap.put(oca, e);
  }

}

class SegmentIntersector {
  processIntersections(e0, segIndex0, e1, segIndex1) {}

  isDone() {}

}

class IntersectionAdder {
  constructor() {
    IntersectionAdder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._hasIntersection = false;
    this._hasProper = false;
    this._hasProperInterior = false;
    this._hasInterior = false;
    this._properIntersectionPoint = null;
    this._li = null;
    this._isSelfIntersection = null;
    this.numIntersections = 0;
    this.numInteriorIntersections = 0;
    this.numProperIntersections = 0;
    this.numTests = 0;
    const li = arguments[0];
    this._li = li;
  }

  static isAdjacentSegments(i1, i2) {
    return Math.abs(i1 - i2) === 1;
  }

  isTrivialIntersection(e0, segIndex0, e1, segIndex1) {
    if (e0 === e1) if (this._li.getIntersectionNum() === 1) {
      if (IntersectionAdder.isAdjacentSegments(segIndex0, segIndex1)) return true;

      if (e0.isClosed()) {
        const maxSegIndex = e0.size() - 1;
        if (segIndex0 === 0 && segIndex1 === maxSegIndex || segIndex1 === 0 && segIndex0 === maxSegIndex) return true;
      }
    }
    return false;
  }

  getProperIntersectionPoint() {
    return this._properIntersectionPoint;
  }

  hasProperInteriorIntersection() {
    return this._hasProperInterior;
  }

  getLineIntersector() {
    return this._li;
  }

  hasProperIntersection() {
    return this._hasProper;
  }

  processIntersections(e0, segIndex0, e1, segIndex1) {
    if (e0 === e1 && segIndex0 === segIndex1) return null;
    this.numTests++;
    const p00 = e0.getCoordinates()[segIndex0];
    const p01 = e0.getCoordinates()[segIndex0 + 1];
    const p10 = e1.getCoordinates()[segIndex1];
    const p11 = e1.getCoordinates()[segIndex1 + 1];

    this._li.computeIntersection(p00, p01, p10, p11);

    if (this._li.hasIntersection()) {
      this.numIntersections++;

      if (this._li.isInteriorIntersection()) {
        this.numInteriorIntersections++;
        this._hasInterior = true;
      }

      if (!this.isTrivialIntersection(e0, segIndex0, e1, segIndex1)) {
        this._hasIntersection = true;
        e0.addIntersections(this._li, segIndex0, 0);
        e1.addIntersections(this._li, segIndex1, 1);

        if (this._li.isProper()) {
          this.numProperIntersections++;
          this._hasProper = true;
          this._hasProperInterior = true;
        }
      }
    }
  }

  hasIntersection() {
    return this._hasIntersection;
  }

  isDone() {
    return false;
  }

  hasInteriorIntersection() {
    return this._hasInterior;
  }

  get interfaces_() {
    return [SegmentIntersector];
  }

}

class BufferBuilder {
  constructor() {
    BufferBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._bufParams = null;
    this._workingPrecisionModel = null;
    this._workingNoder = null;
    this._geomFact = null;
    this._graph = null;
    this._edgeList = new EdgeList();
    const bufParams = arguments[0];
    this._bufParams = bufParams;
  }

  static depthDelta(label) {
    const lLoc = label.getLocation(0, Position.LEFT);
    const rLoc = label.getLocation(0, Position.RIGHT);
    if (lLoc === Location.INTERIOR && rLoc === Location.EXTERIOR) return 1;else if (lLoc === Location.EXTERIOR && rLoc === Location.INTERIOR) return -1;
    return 0;
  }

  static convertSegStrings(it) {
    const fact = new GeometryFactory();
    const lines = new ArrayList();

    while (it.hasNext()) {
      const ss = it.next();
      const line = fact.createLineString(ss.getCoordinates());
      lines.add(line);
    }

    return fact.buildGeometry(lines);
  }

  setWorkingPrecisionModel(pm) {
    this._workingPrecisionModel = pm;
  }

  insertUniqueEdge(e) {
    const existingEdge = this._edgeList.findEqualEdge(e);

    if (existingEdge !== null) {
      const existingLabel = existingEdge.getLabel();
      let labelToMerge = e.getLabel();

      if (!existingEdge.isPointwiseEqual(e)) {
        labelToMerge = new Label(e.getLabel());
        labelToMerge.flip();
      }

      existingLabel.merge(labelToMerge);
      const mergeDelta = BufferBuilder.depthDelta(labelToMerge);
      const existingDelta = existingEdge.getDepthDelta();
      const newDelta = existingDelta + mergeDelta;
      existingEdge.setDepthDelta(newDelta);
    } else {
      this._edgeList.add(e);

      e.setDepthDelta(BufferBuilder.depthDelta(e.getLabel()));
    }
  }

  buildSubgraphs(subgraphList, polyBuilder) {
    const processedGraphs = new ArrayList();

    for (let i = subgraphList.iterator(); i.hasNext();) {
      const subgraph = i.next();
      const p = subgraph.getRightmostCoordinate();
      const locater = new SubgraphDepthLocater(processedGraphs);
      const outsideDepth = locater.getDepth(p);
      subgraph.computeDepth(outsideDepth);
      subgraph.findResultEdges();
      processedGraphs.add(subgraph);
      polyBuilder.add(subgraph.getDirectedEdges(), subgraph.getNodes());
    }
  }

  createSubgraphs(graph) {
    const subgraphList = new ArrayList();

    for (let i = graph.getNodes().iterator(); i.hasNext();) {
      const node = i.next();

      if (!node.isVisited()) {
        const subgraph = new BufferSubgraph();
        subgraph.create(node);
        subgraphList.add(subgraph);
      }
    }

    Collections.sort(subgraphList, Collections.reverseOrder());
    return subgraphList;
  }

  createEmptyResultGeometry() {
    const emptyGeom = this._geomFact.createPolygon();

    return emptyGeom;
  }

  getNoder(precisionModel) {
    if (this._workingNoder !== null) return this._workingNoder;
    const noder = new MCIndexNoder();
    const li = new RobustLineIntersector();
    li.setPrecisionModel(precisionModel);
    noder.setSegmentIntersector(new IntersectionAdder(li));
    return noder;
  }

  buffer(g, distance) {
    let precisionModel = this._workingPrecisionModel;
    if (precisionModel === null) precisionModel = g.getPrecisionModel();
    this._geomFact = g.getFactory();
    const curveBuilder = new OffsetCurveBuilder(precisionModel, this._bufParams);
    const curveSetBuilder = new OffsetCurveSetBuilder(g, distance, curveBuilder);
    const bufferSegStrList = curveSetBuilder.getCurves();
    if (bufferSegStrList.size() <= 0) return this.createEmptyResultGeometry();
    this.computeNodedEdges(bufferSegStrList, precisionModel);
    this._graph = new PlanarGraph$1(new OverlayNodeFactory());

    this._graph.addEdges(this._edgeList.getEdges());

    const subgraphList = this.createSubgraphs(this._graph);
    const polyBuilder = new PolygonBuilder(this._geomFact);
    this.buildSubgraphs(subgraphList, polyBuilder);
    const resultPolyList = polyBuilder.getPolygons();
    if (resultPolyList.size() <= 0) return this.createEmptyResultGeometry();

    const resultGeom = this._geomFact.buildGeometry(resultPolyList);

    return resultGeom;
  }

  computeNodedEdges(bufferSegStrList, precisionModel) {
    const noder = this.getNoder(precisionModel);
    noder.computeNodes(bufferSegStrList);
    const nodedSegStrings = noder.getNodedSubstrings();

    for (let i = nodedSegStrings.iterator(); i.hasNext();) {
      const segStr = i.next();
      const pts = segStr.getCoordinates();
      if (pts.length === 2 && pts[0].equals2D(pts[1])) continue;
      const oldLabel = segStr.getData();
      const edge = new Edge$1(segStr.getCoordinates(), new Label(oldLabel));
      this.insertUniqueEdge(edge);
    }
  }

  setNoder(noder) {
    this._workingNoder = noder;
  }

}

class NodingValidator {
  constructor() {
    NodingValidator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._li = new RobustLineIntersector();
    this._segStrings = null;
    const segStrings = arguments[0];
    this._segStrings = segStrings;
  }

  checkEndPtVertexIntersections() {
    if (arguments.length === 0) {
      for (let i = this._segStrings.iterator(); i.hasNext();) {
        const ss = i.next();
        const pts = ss.getCoordinates();
        this.checkEndPtVertexIntersections(pts[0], this._segStrings);
        this.checkEndPtVertexIntersections(pts[pts.length - 1], this._segStrings);
      }
    } else if (arguments.length === 2) {
      const testPt = arguments[0],
            segStrings = arguments[1];

      for (let i = segStrings.iterator(); i.hasNext();) {
        const ss = i.next();
        const pts = ss.getCoordinates();

        for (let j = 1; j < pts.length - 1; j++) if (pts[j].equals(testPt)) throw new RuntimeException('found endpt/interior pt intersection at index ' + j + ' :pt ' + testPt);
      }
    }
  }

  checkInteriorIntersections() {
    if (arguments.length === 0) {
      for (let i = this._segStrings.iterator(); i.hasNext();) {
        const ss0 = i.next();

        for (let j = this._segStrings.iterator(); j.hasNext();) {
          const ss1 = j.next();
          this.checkInteriorIntersections(ss0, ss1);
        }
      }
    } else if (arguments.length === 2) {
      const ss0 = arguments[0],
            ss1 = arguments[1];
      const pts0 = ss0.getCoordinates();
      const pts1 = ss1.getCoordinates();

      for (let i0 = 0; i0 < pts0.length - 1; i0++) for (let i1 = 0; i1 < pts1.length - 1; i1++) this.checkInteriorIntersections(ss0, i0, ss1, i1);
    } else if (arguments.length === 4) {
      const e0 = arguments[0],
            segIndex0 = arguments[1],
            e1 = arguments[2],
            segIndex1 = arguments[3];
      if (e0 === e1 && segIndex0 === segIndex1) return null;
      const p00 = e0.getCoordinates()[segIndex0];
      const p01 = e0.getCoordinates()[segIndex0 + 1];
      const p10 = e1.getCoordinates()[segIndex1];
      const p11 = e1.getCoordinates()[segIndex1 + 1];

      this._li.computeIntersection(p00, p01, p10, p11);

      if (this._li.hasIntersection()) if (this._li.isProper() || this.hasInteriorIntersection(this._li, p00, p01) || this.hasInteriorIntersection(this._li, p10, p11)) throw new RuntimeException('found non-noded intersection at ' + p00 + '-' + p01 + ' and ' + p10 + '-' + p11);
    }
  }

  checkValid() {
    this.checkEndPtVertexIntersections();
    this.checkInteriorIntersections();
    this.checkCollapses();
  }

  checkCollapses() {
    if (arguments.length === 0) {
      for (let i = this._segStrings.iterator(); i.hasNext();) {
        const ss = i.next();
        this.checkCollapses(ss);
      }
    } else if (arguments.length === 1) {
      const ss = arguments[0];
      const pts = ss.getCoordinates();

      for (let i = 0; i < pts.length - 2; i++) this.checkCollapse(pts[i], pts[i + 1], pts[i + 2]);
    }
  }

  hasInteriorIntersection(li, p0, p1) {
    for (let i = 0; i < li.getIntersectionNum(); i++) {
      const intPt = li.getIntersection(i);
      if (!(intPt.equals(p0) || intPt.equals(p1))) return true;
    }

    return false;
  }

  checkCollapse(p0, p1, p2) {
    if (p0.equals(p2)) throw new RuntimeException('found non-noded collapse at ' + NodingValidator.fact.createLineString([p0, p1, p2]));
  }

}
NodingValidator.fact = new GeometryFactory();

class HotPixel {
  constructor() {
    HotPixel.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._li = null;
    this._pt = null;
    this._originalPt = null;
    this._ptScaled = null;
    this._p0Scaled = null;
    this._p1Scaled = null;
    this._scaleFactor = null;
    this._minx = null;
    this._maxx = null;
    this._miny = null;
    this._maxy = null;
    this._corner = new Array(4).fill(null);
    this._safeEnv = null;
    const pt = arguments[0],
          scaleFactor = arguments[1],
          li = arguments[2];
    this._originalPt = pt;
    this._pt = pt;
    this._scaleFactor = scaleFactor;
    this._li = li;
    if (scaleFactor <= 0) throw new IllegalArgumentException('Scale factor must be non-zero');

    if (scaleFactor !== 1.0) {
      this._pt = new Coordinate(this.scale(pt.x), this.scale(pt.y));
      this._p0Scaled = new Coordinate();
      this._p1Scaled = new Coordinate();
    }

    this.initCorners(this._pt);
  }

  intersectsScaled(p0, p1) {
    const segMinx = Math.min(p0.x, p1.x);
    const segMaxx = Math.max(p0.x, p1.x);
    const segMiny = Math.min(p0.y, p1.y);
    const segMaxy = Math.max(p0.y, p1.y);
    const isOutsidePixelEnv = this._maxx < segMinx || this._minx > segMaxx || this._maxy < segMiny || this._miny > segMaxy;
    if (isOutsidePixelEnv) return false;
    const intersects = this.intersectsToleranceSquare(p0, p1);
    Assert.isTrue(!(isOutsidePixelEnv && intersects), 'Found bad envelope test');
    return intersects;
  }

  initCorners(pt) {
    const tolerance = 0.5;
    this._minx = pt.x - tolerance;
    this._maxx = pt.x + tolerance;
    this._miny = pt.y - tolerance;
    this._maxy = pt.y + tolerance;
    this._corner[0] = new Coordinate(this._maxx, this._maxy);
    this._corner[1] = new Coordinate(this._minx, this._maxy);
    this._corner[2] = new Coordinate(this._minx, this._miny);
    this._corner[3] = new Coordinate(this._maxx, this._miny);
  }

  intersects(p0, p1) {
    if (this._scaleFactor === 1.0) return this.intersectsScaled(p0, p1);
    this.copyScaled(p0, this._p0Scaled);
    this.copyScaled(p1, this._p1Scaled);
    return this.intersectsScaled(this._p0Scaled, this._p1Scaled);
  }

  scale(val) {
    return Math.round(val * this._scaleFactor);
  }

  getCoordinate() {
    return this._originalPt;
  }

  copyScaled(p, pScaled) {
    pScaled.x = this.scale(p.x);
    pScaled.y = this.scale(p.y);
  }

  getSafeEnvelope() {
    if (this._safeEnv === null) {
      const safeTolerance = HotPixel.SAFE_ENV_EXPANSION_FACTOR / this._scaleFactor;
      this._safeEnv = new Envelope(this._originalPt.x - safeTolerance, this._originalPt.x + safeTolerance, this._originalPt.y - safeTolerance, this._originalPt.y + safeTolerance);
    }

    return this._safeEnv;
  }

  intersectsPixelClosure(p0, p1) {
    this._li.computeIntersection(p0, p1, this._corner[0], this._corner[1]);

    if (this._li.hasIntersection()) return true;

    this._li.computeIntersection(p0, p1, this._corner[1], this._corner[2]);

    if (this._li.hasIntersection()) return true;

    this._li.computeIntersection(p0, p1, this._corner[2], this._corner[3]);

    if (this._li.hasIntersection()) return true;

    this._li.computeIntersection(p0, p1, this._corner[3], this._corner[0]);

    if (this._li.hasIntersection()) return true;
    return false;
  }

  intersectsToleranceSquare(p0, p1) {
    let intersectsLeft = false;
    let intersectsBottom = false;

    this._li.computeIntersection(p0, p1, this._corner[0], this._corner[1]);

    if (this._li.isProper()) return true;

    this._li.computeIntersection(p0, p1, this._corner[1], this._corner[2]);

    if (this._li.isProper()) return true;
    if (this._li.hasIntersection()) intersectsLeft = true;

    this._li.computeIntersection(p0, p1, this._corner[2], this._corner[3]);

    if (this._li.isProper()) return true;
    if (this._li.hasIntersection()) intersectsBottom = true;

    this._li.computeIntersection(p0, p1, this._corner[3], this._corner[0]);

    if (this._li.isProper()) return true;
    if (intersectsLeft && intersectsBottom) return true;
    if (p0.equals(this._pt)) return true;
    if (p1.equals(this._pt)) return true;
    return false;
  }

  addSnappedNode(segStr, segIndex) {
    const p0 = segStr.getCoordinate(segIndex);
    const p1 = segStr.getCoordinate(segIndex + 1);

    if (this.intersects(p0, p1)) {
      segStr.addIntersection(this.getCoordinate(), segIndex);
      return true;
    }

    return false;
  }

}
HotPixel.SAFE_ENV_EXPANSION_FACTOR = 0.75;

class MonotoneChainSelectAction {
  constructor() {
    MonotoneChainSelectAction.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.selectedSegment = new LineSegment();
  }

  select() {
    if (arguments.length === 1) ; else if (arguments.length === 2) {
      const mc = arguments[0],
            startIndex = arguments[1];
      mc.getLineSegment(startIndex, this.selectedSegment);
      this.select(this.selectedSegment);
    }
  }

}

class MCIndexPointSnapper {
  constructor() {
    MCIndexPointSnapper.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._index = null;
    const index = arguments[0];
    this._index = index;
  }

  snap() {
    if (arguments.length === 1) {
      const hotPixel = arguments[0];
      return this.snap(hotPixel, null, -1);
    } else if (arguments.length === 3) {
      const hotPixel = arguments[0],
            parentEdge = arguments[1],
            hotPixelVertexIndex = arguments[2];
      const pixelEnv = hotPixel.getSafeEnvelope();
      const hotPixelSnapAction = new HotPixelSnapAction(hotPixel, parentEdge, hotPixelVertexIndex);

      this._index.query(pixelEnv, new class {
        get interfaces_() {
          return [ItemVisitor];
        }

        visitItem(item) {
          const testChain = item;
          testChain.select(pixelEnv, hotPixelSnapAction);
        }

      }());

      return hotPixelSnapAction.isNodeAdded();
    }
  }

}

class HotPixelSnapAction extends MonotoneChainSelectAction {
  constructor() {
    super();
    HotPixelSnapAction.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._hotPixel = null;
    this._parentEdge = null;
    this._hotPixelVertexIndex = null;
    this._isNodeAdded = false;
    const hotPixel = arguments[0],
          parentEdge = arguments[1],
          hotPixelVertexIndex = arguments[2];
    this._hotPixel = hotPixel;
    this._parentEdge = parentEdge;
    this._hotPixelVertexIndex = hotPixelVertexIndex;
  }

  isNodeAdded() {
    return this._isNodeAdded;
  }

  select() {
    if (arguments.length === 2 && Number.isInteger(arguments[1]) && arguments[0] instanceof MonotoneChain) {
      const mc = arguments[0],
            startIndex = arguments[1];
      const ss = mc.getContext();
      if (this._parentEdge === ss) if (startIndex === this._hotPixelVertexIndex || startIndex + 1 === this._hotPixelVertexIndex) return null;
      this._isNodeAdded |= this._hotPixel.addSnappedNode(ss, startIndex);
    } else {
      return super.select.apply(this, arguments);
    }
  }

}

MCIndexPointSnapper.HotPixelSnapAction = HotPixelSnapAction;

class InteriorIntersectionFinderAdder {
  constructor() {
    InteriorIntersectionFinderAdder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._li = null;
    this._interiorIntersections = null;
    const li = arguments[0];
    this._li = li;
    this._interiorIntersections = new ArrayList();
  }

  processIntersections(e0, segIndex0, e1, segIndex1) {
    if (e0 === e1 && segIndex0 === segIndex1) return null;
    const p00 = e0.getCoordinates()[segIndex0];
    const p01 = e0.getCoordinates()[segIndex0 + 1];
    const p10 = e1.getCoordinates()[segIndex1];
    const p11 = e1.getCoordinates()[segIndex1 + 1];

    this._li.computeIntersection(p00, p01, p10, p11);

    if (this._li.hasIntersection()) if (this._li.isInteriorIntersection()) {
      for (let intIndex = 0; intIndex < this._li.getIntersectionNum(); intIndex++) this._interiorIntersections.add(this._li.getIntersection(intIndex));

      e0.addIntersections(this._li, segIndex0, 0);
      e1.addIntersections(this._li, segIndex1, 1);
    }
  }

  isDone() {
    return false;
  }

  getInteriorIntersections() {
    return this._interiorIntersections;
  }

  get interfaces_() {
    return [SegmentIntersector];
  }

}

class MCIndexSnapRounder {
  constructor() {
    MCIndexSnapRounder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pm = null;
    this._li = null;
    this._scaleFactor = null;
    this._noder = null;
    this._pointSnapper = null;
    this._nodedSegStrings = null;
    const pm = arguments[0];
    this._pm = pm;
    this._li = new RobustLineIntersector();

    this._li.setPrecisionModel(pm);

    this._scaleFactor = pm.getScale();
  }

  checkCorrectness(inputSegmentStrings) {
    const resultSegStrings = NodedSegmentString.getNodedSubstrings(inputSegmentStrings);
    const nv = new NodingValidator(resultSegStrings);

    try {
      nv.checkValid();
    } catch (ex) {
      if (ex instanceof Exception) ex.printStackTrace();else throw ex;
    } finally {}
  }

  getNodedSubstrings() {
    return NodedSegmentString.getNodedSubstrings(this._nodedSegStrings);
  }

  snapRound(segStrings, li) {
    const intersections = this.findInteriorIntersections(segStrings, li);
    this.computeIntersectionSnaps(intersections);
    this.computeVertexSnaps(segStrings);
  }

  findInteriorIntersections(segStrings, li) {
    const intFinderAdder = new InteriorIntersectionFinderAdder(li);

    this._noder.setSegmentIntersector(intFinderAdder);

    this._noder.computeNodes(segStrings);

    return intFinderAdder.getInteriorIntersections();
  }

  computeVertexSnaps() {
    if (hasInterface(arguments[0], Collection)) {
      const edges = arguments[0];

      for (let i0 = edges.iterator(); i0.hasNext();) {
        const edge0 = i0.next();
        this.computeVertexSnaps(edge0);
      }
    } else if (arguments[0] instanceof NodedSegmentString) {
      const e = arguments[0];
      const pts0 = e.getCoordinates();

      for (let i = 0; i < pts0.length; i++) {
        const hotPixel = new HotPixel(pts0[i], this._scaleFactor, this._li);

        const isNodeAdded = this._pointSnapper.snap(hotPixel, e, i);

        if (isNodeAdded) e.addIntersection(pts0[i], i);
      }
    }
  }

  computeNodes(inputSegmentStrings) {
    this._nodedSegStrings = inputSegmentStrings;
    this._noder = new MCIndexNoder();
    this._pointSnapper = new MCIndexPointSnapper(this._noder.getIndex());
    this.snapRound(inputSegmentStrings, this._li);
  }

  computeIntersectionSnaps(snapPts) {
    for (let it = snapPts.iterator(); it.hasNext();) {
      const snapPt = it.next();
      const hotPixel = new HotPixel(snapPt, this._scaleFactor, this._li);

      this._pointSnapper.snap(hotPixel);
    }
  }

  get interfaces_() {
    return [Noder];
  }

}

class BufferOp {
  constructor() {
    BufferOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._argGeom = null;
    this._distance = null;
    this._bufParams = new BufferParameters();
    this._resultGeometry = null;
    this._saveException = null;

    if (arguments.length === 1) {
      const g = arguments[0];
      this._argGeom = g;
    } else if (arguments.length === 2) {
      const g = arguments[0],
            bufParams = arguments[1];
      this._argGeom = g;
      this._bufParams = bufParams;
    }
  }

  static bufferOp() {
    if (arguments.length === 2) {
      const g = arguments[0],
            distance = arguments[1];
      const gBuf = new BufferOp(g);
      const geomBuf = gBuf.getResultGeometry(distance);
      return geomBuf;
    } else if (arguments.length === 3) {
      if (Number.isInteger(arguments[2]) && arguments[0] instanceof Geometry && typeof arguments[1] === 'number') {
        const g = arguments[0],
              distance = arguments[1],
              quadrantSegments = arguments[2];
        const bufOp = new BufferOp(g);
        bufOp.setQuadrantSegments(quadrantSegments);
        const geomBuf = bufOp.getResultGeometry(distance);
        return geomBuf;
      } else if (arguments[2] instanceof BufferParameters && arguments[0] instanceof Geometry && typeof arguments[1] === 'number') {
        const g = arguments[0],
              distance = arguments[1],
              params = arguments[2];
        const bufOp = new BufferOp(g, params);
        const geomBuf = bufOp.getResultGeometry(distance);
        return geomBuf;
      }
    } else if (arguments.length === 4) {
      const g = arguments[0],
            distance = arguments[1],
            quadrantSegments = arguments[2],
            endCapStyle = arguments[3];
      const bufOp = new BufferOp(g);
      bufOp.setQuadrantSegments(quadrantSegments);
      bufOp.setEndCapStyle(endCapStyle);
      const geomBuf = bufOp.getResultGeometry(distance);
      return geomBuf;
    }
  }

  static precisionScaleFactor(g, distance, maxPrecisionDigits) {
    const env = g.getEnvelopeInternal();
    const envMax = MathUtil.max(Math.abs(env.getMaxX()), Math.abs(env.getMaxY()), Math.abs(env.getMinX()), Math.abs(env.getMinY()));
    const expandByDistance = distance > 0.0 ? distance : 0.0;
    const bufEnvMax = envMax + 2 * expandByDistance;
    const bufEnvPrecisionDigits = Math.trunc(Math.log(bufEnvMax) / Math.log(10) + 1.0);
    const minUnitLog10 = maxPrecisionDigits - bufEnvPrecisionDigits;
    const scaleFactor = Math.pow(10.0, minUnitLog10);
    return scaleFactor;
  }

  bufferFixedPrecision(fixedPM) {
    const noder = new ScaledNoder(new MCIndexSnapRounder(new PrecisionModel(1.0)), fixedPM.getScale());
    const bufBuilder = new BufferBuilder(this._bufParams);
    bufBuilder.setWorkingPrecisionModel(fixedPM);
    bufBuilder.setNoder(noder);
    this._resultGeometry = bufBuilder.buffer(this._argGeom, this._distance);
  }

  bufferReducedPrecision() {
    if (arguments.length === 0) {
      for (let precDigits = BufferOp.MAX_PRECISION_DIGITS; precDigits >= 0; precDigits--) {
        try {
          this.bufferReducedPrecision(precDigits);
        } catch (ex) {
          if (ex instanceof TopologyException) this._saveException = ex;else throw ex;
        } finally {}

        if (this._resultGeometry !== null) return null;
      }

      throw this._saveException;
    } else if (arguments.length === 1) {
      const precisionDigits = arguments[0];
      const sizeBasedScaleFactor = BufferOp.precisionScaleFactor(this._argGeom, this._distance, precisionDigits);
      const fixedPM = new PrecisionModel(sizeBasedScaleFactor);
      this.bufferFixedPrecision(fixedPM);
    }
  }

  computeGeometry() {
    this.bufferOriginalPrecision();
    if (this._resultGeometry !== null) return null;

    const argPM = this._argGeom.getFactory().getPrecisionModel();

    if (argPM.getType() === PrecisionModel.FIXED) this.bufferFixedPrecision(argPM);else this.bufferReducedPrecision();
  }

  setQuadrantSegments(quadrantSegments) {
    this._bufParams.setQuadrantSegments(quadrantSegments);
  }

  bufferOriginalPrecision() {
    try {
      const bufBuilder = new BufferBuilder(this._bufParams);
      this._resultGeometry = bufBuilder.buffer(this._argGeom, this._distance);
    } catch (ex) {
      if (ex instanceof RuntimeException) this._saveException = ex;else throw ex;
    } finally {}
  }

  getResultGeometry(distance) {
    this._distance = distance;
    this.computeGeometry();
    return this._resultGeometry;
  }

  setEndCapStyle(endCapStyle) {
    this._bufParams.setEndCapStyle(endCapStyle);
  }

}
BufferOp.CAP_ROUND = BufferParameters.CAP_ROUND;
BufferOp.CAP_BUTT = BufferParameters.CAP_FLAT;
BufferOp.CAP_FLAT = BufferParameters.CAP_FLAT;
BufferOp.CAP_SQUARE = BufferParameters.CAP_SQUARE;
BufferOp.MAX_PRECISION_DIGITS = 12;

var buffer = /*#__PURE__*/Object.freeze({
  __proto__: null,
  BufferOp: BufferOp,
  BufferParameters: BufferParameters
});

class GeometryLocation {
  constructor() {
    GeometryLocation.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._component = null;
    this._segIndex = null;
    this._pt = null;

    if (arguments.length === 2) {
      const component = arguments[0],
            pt = arguments[1];
      GeometryLocation.constructor_.call(this, component, GeometryLocation.INSIDE_AREA, pt);
    } else if (arguments.length === 3) {
      const component = arguments[0],
            segIndex = arguments[1],
            pt = arguments[2];
      this._component = component;
      this._segIndex = segIndex;
      this._pt = pt;
    }
  }

  getSegmentIndex() {
    return this._segIndex;
  }

  getCoordinate() {
    return this._pt;
  }

  isInsideArea() {
    return this._segIndex === GeometryLocation.INSIDE_AREA;
  }

  toString() {
    return this._component.getGeometryType() + '[' + this._segIndex + ']' + '-' + WKTWriter.toPoint(this._pt);
  }

  getGeometryComponent() {
    return this._component;
  }

}
GeometryLocation.INSIDE_AREA = -1;

class ConnectedElementLocationFilter {
  constructor() {
    ConnectedElementLocationFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._locations = null;
    const locations = arguments[0];
    this._locations = locations;
  }

  static getLocations(geom) {
    const locations = new ArrayList();
    geom.apply(new ConnectedElementLocationFilter(locations));
    return locations;
  }

  filter(geom) {
    if (geom.isEmpty()) return null;
    if (geom instanceof Point || geom instanceof LineString || geom instanceof Polygon) this._locations.add(new GeometryLocation(geom, 0, geom.getCoordinate()));
  }

  get interfaces_() {
    return [GeometryFilter];
  }

}

class DistanceOp {
  constructor() {
    DistanceOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geom = null;
    this._terminateDistance = 0.0;
    this._ptLocator = new PointLocator();
    this._minDistanceLocation = null;
    this._minDistance = Double.MAX_VALUE;

    if (arguments.length === 2) {
      const g0 = arguments[0],
            g1 = arguments[1];
      DistanceOp.constructor_.call(this, g0, g1, 0.0);
    } else if (arguments.length === 3) {
      const g0 = arguments[0],
            g1 = arguments[1],
            terminateDistance = arguments[2];
      this._geom = new Array(2).fill(null);
      this._geom[0] = g0;
      this._geom[1] = g1;
      this._terminateDistance = terminateDistance;
    }
  }

  static distance(g0, g1) {
    const distOp = new DistanceOp(g0, g1);
    return distOp.distance();
  }

  static isWithinDistance(g0, g1, distance) {
    const envDist = g0.getEnvelopeInternal().distance(g1.getEnvelopeInternal());
    if (envDist > distance) return false;
    const distOp = new DistanceOp(g0, g1, distance);
    return distOp.distance() <= distance;
  }

  static nearestPoints(g0, g1) {
    const distOp = new DistanceOp(g0, g1);
    return distOp.nearestPoints();
  }

  computeContainmentDistance() {
    if (arguments.length === 0) {
      const locPtPoly = new Array(2).fill(null);
      this.computeContainmentDistance(0, locPtPoly);
      if (this._minDistance <= this._terminateDistance) return null;
      this.computeContainmentDistance(1, locPtPoly);
    } else if (arguments.length === 2) {
      const polyGeomIndex = arguments[0],
            locPtPoly = arguments[1];
      const polyGeom = this._geom[polyGeomIndex];
      if (polyGeom.getDimension() < 2) return null;
      const locationsIndex = 1 - polyGeomIndex;
      const polys = PolygonExtracter.getPolygons(polyGeom);

      if (polys.size() > 0) {
        const insideLocs = ConnectedElementLocationFilter.getLocations(this._geom[locationsIndex]);
        this.computeContainmentDistance(insideLocs, polys, locPtPoly);

        if (this._minDistance <= this._terminateDistance) {
          this._minDistanceLocation[locationsIndex] = locPtPoly[0];
          this._minDistanceLocation[polyGeomIndex] = locPtPoly[1];
          return null;
        }
      }
    } else if (arguments.length === 3) {
      if (arguments[2] instanceof Array && hasInterface(arguments[0], List) && hasInterface(arguments[1], List)) {
        const locs = arguments[0],
              polys = arguments[1],
              locPtPoly = arguments[2];

        for (let i = 0; i < locs.size(); i++) {
          const loc = locs.get(i);

          for (let j = 0; j < polys.size(); j++) {
            this.computeContainmentDistance(loc, polys.get(j), locPtPoly);
            if (this._minDistance <= this._terminateDistance) return null;
          }
        }
      } else if (arguments[2] instanceof Array && arguments[0] instanceof GeometryLocation && arguments[1] instanceof Polygon) {
        const ptLoc = arguments[0],
              poly = arguments[1],
              locPtPoly = arguments[2];
        const pt = ptLoc.getCoordinate();

        if (Location.EXTERIOR !== this._ptLocator.locate(pt, poly)) {
          this._minDistance = 0.0;
          locPtPoly[0] = ptLoc;
          locPtPoly[1] = new GeometryLocation(poly, pt);
          return null;
        }
      }
    }
  }

  computeMinDistanceLinesPoints(lines, points, locGeom) {
    for (let i = 0; i < lines.size(); i++) {
      const line = lines.get(i);

      for (let j = 0; j < points.size(); j++) {
        const pt = points.get(j);
        this.computeMinDistance(line, pt, locGeom);
        if (this._minDistance <= this._terminateDistance) return null;
      }
    }
  }

  computeFacetDistance() {
    const locGeom = new Array(2).fill(null);
    const lines0 = LinearComponentExtracter.getLines(this._geom[0]);
    const lines1 = LinearComponentExtracter.getLines(this._geom[1]);
    const pts0 = PointExtracter.getPoints(this._geom[0]);
    const pts1 = PointExtracter.getPoints(this._geom[1]);
    this.computeMinDistanceLines(lines0, lines1, locGeom);
    this.updateMinDistance(locGeom, false);
    if (this._minDistance <= this._terminateDistance) return null;
    locGeom[0] = null;
    locGeom[1] = null;
    this.computeMinDistanceLinesPoints(lines0, pts1, locGeom);
    this.updateMinDistance(locGeom, false);
    if (this._minDistance <= this._terminateDistance) return null;
    locGeom[0] = null;
    locGeom[1] = null;
    this.computeMinDistanceLinesPoints(lines1, pts0, locGeom);
    this.updateMinDistance(locGeom, true);
    if (this._minDistance <= this._terminateDistance) return null;
    locGeom[0] = null;
    locGeom[1] = null;
    this.computeMinDistancePoints(pts0, pts1, locGeom);
    this.updateMinDistance(locGeom, false);
  }

  nearestLocations() {
    this.computeMinDistance();
    return this._minDistanceLocation;
  }

  updateMinDistance(locGeom, flip) {
    if (locGeom[0] === null) return null;

    if (flip) {
      this._minDistanceLocation[0] = locGeom[1];
      this._minDistanceLocation[1] = locGeom[0];
    } else {
      this._minDistanceLocation[0] = locGeom[0];
      this._minDistanceLocation[1] = locGeom[1];
    }
  }

  nearestPoints() {
    this.computeMinDistance();
    const nearestPts = [this._minDistanceLocation[0].getCoordinate(), this._minDistanceLocation[1].getCoordinate()];
    return nearestPts;
  }

  computeMinDistance() {
    if (arguments.length === 0) {
      if (this._minDistanceLocation !== null) return null;
      this._minDistanceLocation = new Array(2).fill(null);
      this.computeContainmentDistance();
      if (this._minDistance <= this._terminateDistance) return null;
      this.computeFacetDistance();
    } else if (arguments.length === 3) {
      if (arguments[2] instanceof Array && arguments[0] instanceof LineString && arguments[1] instanceof Point) {
        const line = arguments[0],
              pt = arguments[1],
              locGeom = arguments[2];
        if (line.getEnvelopeInternal().distance(pt.getEnvelopeInternal()) > this._minDistance) return null;
        const coord0 = line.getCoordinates();
        const coord = pt.getCoordinate();

        for (let i = 0; i < coord0.length - 1; i++) {
          const dist = Distance.pointToSegment(coord, coord0[i], coord0[i + 1]);

          if (dist < this._minDistance) {
            this._minDistance = dist;
            const seg = new LineSegment(coord0[i], coord0[i + 1]);
            const segClosestPoint = seg.closestPoint(coord);
            locGeom[0] = new GeometryLocation(line, i, segClosestPoint);
            locGeom[1] = new GeometryLocation(pt, 0, coord);
          }

          if (this._minDistance <= this._terminateDistance) return null;
        }
      } else if (arguments[2] instanceof Array && arguments[0] instanceof LineString && arguments[1] instanceof LineString) {
        const line0 = arguments[0],
              line1 = arguments[1],
              locGeom = arguments[2];
        if (line0.getEnvelopeInternal().distance(line1.getEnvelopeInternal()) > this._minDistance) return null;
        const coord0 = line0.getCoordinates();
        const coord1 = line1.getCoordinates();

        for (let i = 0; i < coord0.length - 1; i++) {
          const segEnv0 = new Envelope(coord0[i], coord0[i + 1]);
          if (segEnv0.distance(line1.getEnvelopeInternal()) > this._minDistance) continue;

          for (let j = 0; j < coord1.length - 1; j++) {
            const segEnv1 = new Envelope(coord1[j], coord1[j + 1]);
            if (segEnv0.distance(segEnv1) > this._minDistance) continue;
            const dist = Distance.segmentToSegment(coord0[i], coord0[i + 1], coord1[j], coord1[j + 1]);

            if (dist < this._minDistance) {
              this._minDistance = dist;
              const seg0 = new LineSegment(coord0[i], coord0[i + 1]);
              const seg1 = new LineSegment(coord1[j], coord1[j + 1]);
              const closestPt = seg0.closestPoints(seg1);
              locGeom[0] = new GeometryLocation(line0, i, closestPt[0]);
              locGeom[1] = new GeometryLocation(line1, j, closestPt[1]);
            }

            if (this._minDistance <= this._terminateDistance) return null;
          }
        }
      }
    }
  }

  computeMinDistancePoints(points0, points1, locGeom) {
    for (let i = 0; i < points0.size(); i++) {
      const pt0 = points0.get(i);

      for (let j = 0; j < points1.size(); j++) {
        const pt1 = points1.get(j);
        const dist = pt0.getCoordinate().distance(pt1.getCoordinate());

        if (dist < this._minDistance) {
          this._minDistance = dist;
          locGeom[0] = new GeometryLocation(pt0, 0, pt0.getCoordinate());
          locGeom[1] = new GeometryLocation(pt1, 0, pt1.getCoordinate());
        }

        if (this._minDistance <= this._terminateDistance) return null;
      }
    }
  }

  distance() {
    if (this._geom[0] === null || this._geom[1] === null) throw new IllegalArgumentException('null geometries are not supported');
    if (this._geom[0].isEmpty() || this._geom[1].isEmpty()) return 0.0;
    this.computeMinDistance();
    return this._minDistance;
  }

  computeMinDistanceLines(lines0, lines1, locGeom) {
    for (let i = 0; i < lines0.size(); i++) {
      const line0 = lines0.get(i);

      for (let j = 0; j < lines1.size(); j++) {
        const line1 = lines1.get(j);
        this.computeMinDistance(line0, line1, locGeom);
        if (this._minDistance <= this._terminateDistance) return null;
      }
    }
  }

}

var distance = /*#__PURE__*/Object.freeze({
  __proto__: null,
  DistanceOp: DistanceOp
});

class EdgeString {
  constructor() {
    EdgeString.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._factory = null;
    this._directedEdges = new ArrayList();
    this._coordinates = null;
    const factory = arguments[0];
    this._factory = factory;
  }

  getCoordinates() {
    if (this._coordinates === null) {
      let forwardDirectedEdges = 0;
      let reverseDirectedEdges = 0;
      const coordinateList = new CoordinateList();

      for (let i = this._directedEdges.iterator(); i.hasNext();) {
        const directedEdge = i.next();
        if (directedEdge.getEdgeDirection()) forwardDirectedEdges++;else reverseDirectedEdges++;
        coordinateList.add(directedEdge.getEdge().getLine().getCoordinates(), false, directedEdge.getEdgeDirection());
      }

      this._coordinates = coordinateList.toCoordinateArray();
      if (reverseDirectedEdges > forwardDirectedEdges) CoordinateArrays.reverse(this._coordinates);
    }

    return this._coordinates;
  }

  toLineString() {
    return this._factory.createLineString(this.getCoordinates());
  }

  add(directedEdge) {
    this._directedEdges.add(directedEdge);
  }

}

class GraphComponent {
  constructor() {
    GraphComponent.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._isMarked = false;
    this._isVisited = false;
    this._data = null;
  }

  static getComponentWithVisitedState(i, visitedState) {
    while (i.hasNext()) {
      const comp = i.next();
      if (comp.isVisited() === visitedState) return comp;
    }

    return null;
  }

  static setVisited(i, visited) {
    while (i.hasNext()) {
      const comp = i.next();
      comp.setVisited(visited);
    }
  }

  static setMarked(i, marked) {
    while (i.hasNext()) {
      const comp = i.next();
      comp.setMarked(marked);
    }
  }

  setVisited(isVisited) {
    this._isVisited = isVisited;
  }

  isMarked() {
    return this._isMarked;
  }

  setData(data) {
    this._data = data;
  }

  getData() {
    return this._data;
  }

  setMarked(isMarked) {
    this._isMarked = isMarked;
  }

  getContext() {
    return this._data;
  }

  isVisited() {
    return this._isVisited;
  }

  setContext(data) {
    this._data = data;
  }

}

class DirectedEdge extends GraphComponent {
  constructor() {
    super();
    DirectedEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._parentEdge = null;
    this._from = null;
    this._to = null;
    this._p0 = null;
    this._p1 = null;
    this._sym = null;
    this._edgeDirection = null;
    this._quadrant = null;
    this._angle = null;

    if (arguments.length === 0) ; else if (arguments.length === 4) {
      const from = arguments[0],
            to = arguments[1],
            directionPt = arguments[2],
            edgeDirection = arguments[3];
      this._from = from;
      this._to = to;
      this._edgeDirection = edgeDirection;
      this._p0 = from.getCoordinate();
      this._p1 = directionPt;
      const dx = this._p1.x - this._p0.x;
      const dy = this._p1.y - this._p0.y;
      this._quadrant = Quadrant.quadrant(dx, dy);
      this._angle = Math.atan2(dy, dx);
    }
  }

  static toEdges(dirEdges) {
    const edges = new ArrayList();

    for (let i = dirEdges.iterator(); i.hasNext();) edges.add(i.next()._parentEdge);

    return edges;
  }

  isRemoved() {
    return this._parentEdge === null;
  }

  compareDirection(e) {
    if (this._quadrant > e._quadrant) return 1;
    if (this._quadrant < e._quadrant) return -1;
    return Orientation.index(e._p0, e._p1, this._p1);
  }

  getCoordinate() {
    return this._from.getCoordinate();
  }

  print(out) {
    const className = this.getClass().getName();
    const lastDotPos = className.lastIndexOf('.');
    const name = className.substring(lastDotPos + 1);
    out.print('  ' + name + ': ' + this._p0 + ' - ' + this._p1 + ' ' + this._quadrant + ':' + this._angle);
  }

  getDirectionPt() {
    return this._p1;
  }

  getAngle() {
    return this._angle;
  }

  compareTo(obj) {
    const de = obj;
    return this.compareDirection(de);
  }

  getFromNode() {
    return this._from;
  }

  getSym() {
    return this._sym;
  }

  setEdge(parentEdge) {
    this._parentEdge = parentEdge;
  }

  remove() {
    this._sym = null;
    this._parentEdge = null;
  }

  getEdge() {
    return this._parentEdge;
  }

  getQuadrant() {
    return this._quadrant;
  }

  setSym(sym) {
    this._sym = sym;
  }

  getToNode() {
    return this._to;
  }

  getEdgeDirection() {
    return this._edgeDirection;
  }

  get interfaces_() {
    return [Comparable];
  }

}

class LineMergeDirectedEdge extends DirectedEdge {
  constructor() {
    super();
    LineMergeDirectedEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    const from = arguments[0],
          to = arguments[1],
          directionPt = arguments[2],
          edgeDirection = arguments[3];
    DirectedEdge.constructor_.call(this, from, to, directionPt, edgeDirection);
  }

  getNext() {
    if (this.getToNode().getDegree() !== 2) return null;
    if (this.getToNode().getOutEdges().getEdges().get(0) === this.getSym()) return this.getToNode().getOutEdges().getEdges().get(1);
    Assert.isTrue(this.getToNode().getOutEdges().getEdges().get(1) === this.getSym());
    return this.getToNode().getOutEdges().getEdges().get(0);
  }

}

class Edge extends GraphComponent {
  constructor() {
    super();
    Edge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._dirEdge = null;

    if (arguments.length === 0) ; else if (arguments.length === 2) {
      const de0 = arguments[0],
            de1 = arguments[1];
      this.setDirectedEdges(de0, de1);
    }
  }

  isRemoved() {
    return this._dirEdge === null;
  }

  setDirectedEdges(de0, de1) {
    this._dirEdge = [de0, de1];
    de0.setEdge(this);
    de1.setEdge(this);
    de0.setSym(de1);
    de1.setSym(de0);
    de0.getFromNode().addOutEdge(de0);
    de1.getFromNode().addOutEdge(de1);
  }

  getDirEdge() {
    if (Number.isInteger(arguments[0])) {
      const i = arguments[0];
      return this._dirEdge[i];
    } else if (arguments[0] instanceof Node) {
      const fromNode = arguments[0];
      if (this._dirEdge[0].getFromNode() === fromNode) return this._dirEdge[0];
      if (this._dirEdge[1].getFromNode() === fromNode) return this._dirEdge[1];
      return null;
    }
  }

  remove() {
    this._dirEdge = null;
  }

  getOppositeNode(node) {
    if (this._dirEdge[0].getFromNode() === node) return this._dirEdge[0].getToNode();
    if (this._dirEdge[1].getFromNode() === node) return this._dirEdge[1].getToNode();
    return null;
  }

}

class DirectedEdgeStar {
  constructor() {
    DirectedEdgeStar.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._outEdges = new ArrayList();
    this._sorted = false;
  }

  getNextEdge(dirEdge) {
    const i = this.getIndex(dirEdge);
    return this._outEdges.get(this.getIndex(i + 1));
  }

  getCoordinate() {
    const it = this.iterator();
    if (!it.hasNext()) return null;
    const e = it.next();
    return e.getCoordinate();
  }

  iterator() {
    this.sortEdges();
    return this._outEdges.iterator();
  }

  sortEdges() {
    if (!this._sorted) {
      Collections.sort(this._outEdges);
      this._sorted = true;
    }
  }

  remove(de) {
    this._outEdges.remove(de);
  }

  getEdges() {
    this.sortEdges();
    return this._outEdges;
  }

  getNextCWEdge(dirEdge) {
    const i = this.getIndex(dirEdge);
    return this._outEdges.get(this.getIndex(i - 1));
  }

  getIndex() {
    if (arguments[0] instanceof Edge) {
      const edge = arguments[0];
      this.sortEdges();

      for (let i = 0; i < this._outEdges.size(); i++) {
        const de = this._outEdges.get(i);

        if (de.getEdge() === edge) return i;
      }

      return -1;
    } else if (arguments[0] instanceof DirectedEdge) {
      const dirEdge = arguments[0];
      this.sortEdges();

      for (let i = 0; i < this._outEdges.size(); i++) {
        const de = this._outEdges.get(i);

        if (de === dirEdge) return i;
      }

      return -1;
    } else if (Number.isInteger(arguments[0])) {
      const i = arguments[0];

      let modi = i % this._outEdges.size();

      if (modi < 0) modi += this._outEdges.size();
      return modi;
    }
  }

  add(de) {
    this._outEdges.add(de);

    this._sorted = false;
  }

  getDegree() {
    return this._outEdges.size();
  }

}

class Node extends GraphComponent {
  constructor() {
    super();
    Node.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pt = null;
    this._deStar = null;

    if (arguments.length === 1) {
      const pt = arguments[0];
      Node.constructor_.call(this, pt, new DirectedEdgeStar());
    } else if (arguments.length === 2) {
      const pt = arguments[0],
            deStar = arguments[1];
      this._pt = pt;
      this._deStar = deStar;
    }
  }

  static getEdgesBetween(node0, node1) {
    const edges0 = DirectedEdge.toEdges(node0.getOutEdges().getEdges());
    const commonEdges = new HashSet(edges0);
    const edges1 = DirectedEdge.toEdges(node1.getOutEdges().getEdges());
    commonEdges.retainAll(edges1);
    return commonEdges;
  }

  isRemoved() {
    return this._pt === null;
  }

  addOutEdge(de) {
    this._deStar.add(de);
  }

  getCoordinate() {
    return this._pt;
  }

  getOutEdges() {
    return this._deStar;
  }

  remove() {
    if (arguments.length === 0) {
      this._pt = null;
    } else if (arguments.length === 1) {
      const de = arguments[0];

      this._deStar.remove(de);
    }
  }

  getIndex(edge) {
    return this._deStar.getIndex(edge);
  }

  getDegree() {
    return this._deStar.getDegree();
  }

}

class LineMergeEdge extends Edge {
  constructor() {
    super();
    LineMergeEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._line = null;
    const line = arguments[0];
    this._line = line;
  }

  getLine() {
    return this._line;
  }

}

class NodeMap {
  constructor() {
    NodeMap.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._nodeMap = new TreeMap();
  }

  find(coord) {
    return this._nodeMap.get(coord);
  }

  iterator() {
    return this._nodeMap.values().iterator();
  }

  remove(pt) {
    return this._nodeMap.remove(pt);
  }

  values() {
    return this._nodeMap.values();
  }

  add(n) {
    this._nodeMap.put(n.getCoordinate(), n);

    return n;
  }

}

class PlanarGraph {
  constructor() {
    PlanarGraph.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._edges = new HashSet();
    this._dirEdges = new HashSet();
    this._nodeMap = new NodeMap();
  }

  findNodesOfDegree(degree) {
    const nodesFound = new ArrayList();

    for (let i = this.nodeIterator(); i.hasNext();) {
      const node = i.next();
      if (node.getDegree() === degree) nodesFound.add(node);
    }

    return nodesFound;
  }

  dirEdgeIterator() {
    return this._dirEdges.iterator();
  }

  edgeIterator() {
    return this._edges.iterator();
  }

  remove() {
    if (arguments[0] instanceof Edge) {
      const edge = arguments[0];
      this.remove(edge.getDirEdge(0));
      this.remove(edge.getDirEdge(1));

      this._edges.remove(edge);

      edge.remove();
    } else if (arguments[0] instanceof DirectedEdge) {
      const de = arguments[0];
      const sym = de.getSym();
      if (sym !== null) sym.setSym(null);
      de.getFromNode().remove(de);
      de.remove();

      this._dirEdges.remove(de);
    } else if (arguments[0] instanceof Node) {
      const node = arguments[0];
      const outEdges = node.getOutEdges().getEdges();

      for (let i = outEdges.iterator(); i.hasNext();) {
        const de = i.next();
        const sym = de.getSym();
        if (sym !== null) this.remove(sym);

        this._dirEdges.remove(de);

        const edge = de.getEdge();
        if (edge !== null) this._edges.remove(edge);
      }

      this._nodeMap.remove(node.getCoordinate());

      node.remove();
    }
  }

  findNode(pt) {
    return this._nodeMap.find(pt);
  }

  getEdges() {
    return this._edges;
  }

  nodeIterator() {
    return this._nodeMap.iterator();
  }

  contains() {
    if (arguments[0] instanceof Edge) {
      const e = arguments[0];
      return this._edges.contains(e);
    } else if (arguments[0] instanceof DirectedEdge) {
      const de = arguments[0];
      return this._dirEdges.contains(de);
    }
  }

  add() {
    if (arguments[0] instanceof Node) {
      const node = arguments[0];

      this._nodeMap.add(node);
    } else if (arguments[0] instanceof Edge) {
      const edge = arguments[0];

      this._edges.add(edge);

      this.add(edge.getDirEdge(0));
      this.add(edge.getDirEdge(1));
    } else if (arguments[0] instanceof DirectedEdge) {
      const dirEdge = arguments[0];

      this._dirEdges.add(dirEdge);
    }
  }

  getNodes() {
    return this._nodeMap.values();
  }

}

class LineMergeGraph extends PlanarGraph {
  constructor() {
    super();
  }

  addEdge(lineString) {
    if (lineString.isEmpty()) return null;
    const coordinates = CoordinateArrays.removeRepeatedPoints(lineString.getCoordinates());
    if (coordinates.length <= 1) return null;
    const startCoordinate = coordinates[0];
    const endCoordinate = coordinates[coordinates.length - 1];
    const startNode = this.getNode(startCoordinate);
    const endNode = this.getNode(endCoordinate);
    const directedEdge0 = new LineMergeDirectedEdge(startNode, endNode, coordinates[1], true);
    const directedEdge1 = new LineMergeDirectedEdge(endNode, startNode, coordinates[coordinates.length - 2], false);
    const edge = new LineMergeEdge(lineString);
    edge.setDirectedEdges(directedEdge0, directedEdge1);
    this.add(edge);
  }

  getNode(coordinate) {
    let node = this.findNode(coordinate);

    if (node === null) {
      node = new Node(coordinate);
      this.add(node);
    }

    return node;
  }

}

class LineMerger {
  constructor() {
    LineMerger.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._graph = new LineMergeGraph();
    this._mergedLineStrings = null;
    this._factory = null;
    this._edgeStrings = null;
  }

  buildEdgeStringsForUnprocessedNodes() {
    for (let i = this._graph.getNodes().iterator(); i.hasNext();) {
      const node = i.next();

      if (!node.isMarked()) {
        Assert.isTrue(node.getDegree() === 2);
        this.buildEdgeStringsStartingAt(node);
        node.setMarked(true);
      }
    }
  }

  buildEdgeStringsForNonDegree2Nodes() {
    for (let i = this._graph.getNodes().iterator(); i.hasNext();) {
      const node = i.next();

      if (node.getDegree() !== 2) {
        this.buildEdgeStringsStartingAt(node);
        node.setMarked(true);
      }
    }
  }

  buildEdgeStringsForObviousStartNodes() {
    this.buildEdgeStringsForNonDegree2Nodes();
  }

  getMergedLineStrings() {
    this.merge();
    return this._mergedLineStrings;
  }

  buildEdgeStringsStartingAt(node) {
    for (let i = node.getOutEdges().iterator(); i.hasNext();) {
      const directedEdge = i.next();
      if (directedEdge.getEdge().isMarked()) continue;

      this._edgeStrings.add(this.buildEdgeStringStartingWith(directedEdge));
    }
  }

  merge() {
    if (this._mergedLineStrings !== null) return null;
    GraphComponent.setMarked(this._graph.nodeIterator(), false);
    GraphComponent.setMarked(this._graph.edgeIterator(), false);
    this._edgeStrings = new ArrayList();
    this.buildEdgeStringsForObviousStartNodes();
    this.buildEdgeStringsForIsolatedLoops();
    this._mergedLineStrings = new ArrayList();

    for (let i = this._edgeStrings.iterator(); i.hasNext();) {
      const edgeString = i.next();

      this._mergedLineStrings.add(edgeString.toLineString());
    }
  }

  addLineString(lineString) {
    if (this._factory === null) this._factory = lineString.getFactory();

    this._graph.addEdge(lineString);
  }

  buildEdgeStringStartingWith(start) {
    const edgeString = new EdgeString(this._factory);
    let current = start;

    do {
      edgeString.add(current);
      current.getEdge().setMarked(true);
      current = current.getNext();
    } while (current !== null && current !== start);

    return edgeString;
  }

  add() {
    if (arguments[0] instanceof Geometry) {
      const geometry = arguments[0];

      for (let i = 0; i < geometry.getNumGeometries(); i++) {
        const component = geometry.getGeometryN(i);
        if (component instanceof LineString) this.addLineString(component);
      }
    } else if (hasInterface(arguments[0], Collection)) {
      const geometries = arguments[0];
      this._mergedLineStrings = null;

      for (let i = geometries.iterator(); i.hasNext();) {
        const geometry = i.next();
        this.add(geometry);
      }
    }
  }

  buildEdgeStringsForIsolatedLoops() {
    this.buildEdgeStringsForUnprocessedNodes();
  }

}

class Subgraph {
  constructor() {
    Subgraph.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._parentGraph = null;
    this._edges = new HashSet();
    this._dirEdges = new ArrayList();
    this._nodeMap = new NodeMap();
    const parentGraph = arguments[0];
    this._parentGraph = parentGraph;
  }

  dirEdgeIterator() {
    return this._dirEdges.iterator();
  }

  edgeIterator() {
    return this._edges.iterator();
  }

  getParent() {
    return this._parentGraph;
  }

  nodeIterator() {
    return this._nodeMap.iterator();
  }

  contains(e) {
    return this._edges.contains(e);
  }

  add(e) {
    if (this._edges.contains(e)) return null;

    this._edges.add(e);

    this._dirEdges.add(e.getDirEdge(0));

    this._dirEdges.add(e.getDirEdge(1));

    this._nodeMap.add(e.getDirEdge(0).getFromNode());

    this._nodeMap.add(e.getDirEdge(1).getFromNode());
  }

}

class ConnectedSubgraphFinder {
  constructor() {
    ConnectedSubgraphFinder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._graph = null;
    const graph = arguments[0];
    this._graph = graph;
  }

  addReachable(startNode, subgraph) {
    const nodeStack = new Stack();
    nodeStack.add(startNode);

    while (!nodeStack.empty()) {
      const node = nodeStack.pop();
      this.addEdges(node, nodeStack, subgraph);
    }
  }

  findSubgraph(node) {
    const subgraph = new Subgraph(this._graph);
    this.addReachable(node, subgraph);
    return subgraph;
  }

  getConnectedSubgraphs() {
    const subgraphs = new ArrayList();
    GraphComponent.setVisited(this._graph.nodeIterator(), false);

    for (let i = this._graph.edgeIterator(); i.hasNext();) {
      const e = i.next();
      const node = e.getDirEdge(0).getFromNode();
      if (!node.isVisited()) subgraphs.add(this.findSubgraph(node));
    }

    return subgraphs;
  }

  addEdges(node, nodeStack, subgraph) {
    node.setVisited(true);

    for (let i = node.getOutEdges().iterator(); i.hasNext();) {
      const de = i.next();
      subgraph.add(de.getEdge());
      const toNode = de.getToNode();
      if (!toNode.isVisited()) nodeStack.push(toNode);
    }
  }

}

class LineSequencer {
  constructor() {
    LineSequencer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._graph = new LineMergeGraph();
    this._factory = new GeometryFactory();
    this._lineCount = 0;
    this._isRun = false;
    this._sequencedGeometry = null;
    this._isSequenceable = false;
  }

  static findUnvisitedBestOrientedDE(node) {
    let wellOrientedDE = null;
    let unvisitedDE = null;

    for (let i = node.getOutEdges().iterator(); i.hasNext();) {
      const de = i.next();

      if (!de.getEdge().isVisited()) {
        unvisitedDE = de;
        if (de.getEdgeDirection()) wellOrientedDE = de;
      }
    }

    if (wellOrientedDE !== null) return wellOrientedDE;
    return unvisitedDE;
  }

  static findLowestDegreeNode(graph) {
    let minDegree = Integer.MAX_VALUE;
    let minDegreeNode = null;

    for (let i = graph.nodeIterator(); i.hasNext();) {
      const node = i.next();

      if (minDegreeNode === null || node.getDegree() < minDegree) {
        minDegree = node.getDegree();
        minDegreeNode = node;
      }
    }

    return minDegreeNode;
  }

  static isSequenced(geom) {
    if (!(geom instanceof MultiLineString)) return true;
    const mls = geom;
    const prevSubgraphNodes = new TreeSet();
    let lastNode = null;
    const currNodes = new ArrayList();

    for (let i = 0; i < mls.getNumGeometries(); i++) {
      const line = mls.getGeometryN(i);
      const startNode = line.getCoordinateN(0);
      const endNode = line.getCoordinateN(line.getNumPoints() - 1);
      if (prevSubgraphNodes.contains(startNode)) return false;
      if (prevSubgraphNodes.contains(endNode)) return false;
      if (lastNode !== null) if (!startNode.equals(lastNode)) {
        prevSubgraphNodes.addAll(currNodes);
        currNodes.clear();
      }
      currNodes.add(startNode);
      currNodes.add(endNode);
      lastNode = endNode;
    }

    return true;
  }

  static reverse(line) {
    const pts = line.getCoordinates();
    const revPts = new Array(pts.length).fill(null);
    const len = pts.length;

    for (let i = 0; i < len; i++) revPts[len - 1 - i] = new Coordinate(pts[i]);

    return line.getFactory().createLineString(revPts);
  }

  static sequence(geom) {
    const sequencer = new LineSequencer();
    sequencer.add(geom);
    return sequencer.getSequencedLineStrings();
  }

  addLine(lineString) {
    if (this._factory === null) this._factory = lineString.getFactory();

    this._graph.addEdge(lineString);

    this._lineCount++;
  }

  hasSequence(graph) {
    let oddDegreeCount = 0;

    for (let i = graph.nodeIterator(); i.hasNext();) {
      const node = i.next();
      if (node.getDegree() % 2 === 1) oddDegreeCount++;
    }

    return oddDegreeCount <= 2;
  }

  computeSequence() {
    if (this._isRun) return null;
    this._isRun = true;
    const sequences = this.findSequences();
    if (sequences === null) return null;
    this._sequencedGeometry = this.buildSequencedGeometry(sequences);
    this._isSequenceable = true;

    const finalLineCount = this._sequencedGeometry.getNumGeometries();

    Assert.isTrue(this._lineCount === finalLineCount, 'Lines were missing from result');
    Assert.isTrue(this._sequencedGeometry instanceof LineString || this._sequencedGeometry instanceof MultiLineString, 'Result is not lineal');
  }

  findSequences() {
    const sequences = new ArrayList();
    const csFinder = new ConnectedSubgraphFinder(this._graph);
    const subgraphs = csFinder.getConnectedSubgraphs();

    for (let i = subgraphs.iterator(); i.hasNext();) {
      const subgraph = i.next();

      if (this.hasSequence(subgraph)) {
        const seq = this.findSequence(subgraph);
        sequences.add(seq);
      } else {
        return null;
      }
    }

    return sequences;
  }

  addReverseSubpath(de, lit, expectedClosed) {
    const endNode = de.getToNode();
    let fromNode = null;

    while (true) {
      lit.add(de.getSym());
      de.getEdge().setVisited(true);
      fromNode = de.getFromNode();
      const unvisitedOutDE = LineSequencer.findUnvisitedBestOrientedDE(fromNode);
      if (unvisitedOutDE === null) break;
      de = unvisitedOutDE.getSym();
    }

    if (expectedClosed) Assert.isTrue(fromNode === endNode, 'path not contiguous');
  }

  findSequence(graph) {
    GraphComponent.setVisited(graph.edgeIterator(), false);
    const startNode = LineSequencer.findLowestDegreeNode(graph);
    const startDE = startNode.getOutEdges().iterator().next();
    const startDESym = startDE.getSym();
    const seq = new LinkedList();
    const lit = seq.listIterator();
    this.addReverseSubpath(startDESym, lit, false);

    while (lit.hasPrevious()) {
      const prev = lit.previous();
      const unvisitedOutDE = LineSequencer.findUnvisitedBestOrientedDE(prev.getFromNode());
      if (unvisitedOutDE !== null) this.addReverseSubpath(unvisitedOutDE.getSym(), lit, true);
    }

    const orientedSeq = this.orient(seq);
    return orientedSeq;
  }

  reverse(seq) {
    const newSeq = new LinkedList();

    for (let i = seq.iterator(); i.hasNext();) {
      const de = i.next();
      newSeq.addFirst(de.getSym());
    }

    return newSeq;
  }

  orient(seq) {
    const startEdge = seq.get(0);
    const endEdge = seq.get(seq.size() - 1);
    const startNode = startEdge.getFromNode();
    const endNode = endEdge.getToNode();
    let flipSeq = false;
    const hasDegree1Node = startNode.getDegree() === 1 || endNode.getDegree() === 1;

    if (hasDegree1Node) {
      let hasObviousStartNode = false;

      if (endEdge.getToNode().getDegree() === 1 && endEdge.getEdgeDirection() === false) {
        hasObviousStartNode = true;
        flipSeq = true;
      }

      if (startEdge.getFromNode().getDegree() === 1 && startEdge.getEdgeDirection() === true) {
        hasObviousStartNode = true;
        flipSeq = false;
      }

      if (!hasObviousStartNode) if (startEdge.getFromNode().getDegree() === 1) flipSeq = true;
    }

    if (flipSeq) return this.reverse(seq);
    return seq;
  }

  buildSequencedGeometry(sequences) {
    const lines = new ArrayList();

    for (let i1 = sequences.iterator(); i1.hasNext();) {
      const seq = i1.next();

      for (let i2 = seq.iterator(); i2.hasNext();) {
        const de = i2.next();
        const e = de.getEdge();
        const line = e.getLine();
        let lineToAdd = line;
        if (!de.getEdgeDirection() && !line.isClosed()) lineToAdd = LineSequencer.reverse(line);
        lines.add(lineToAdd);
      }
    }

    if (lines.size() === 0) return this._factory.createMultiLineString(new Array(0).fill(null));
    return this._factory.buildGeometry(lines);
  }

  getSequencedLineStrings() {
    this.computeSequence();
    return this._sequencedGeometry;
  }

  isSequenceable() {
    this.computeSequence();
    return this._isSequenceable;
  }

  add() {
    if (hasInterface(arguments[0], Collection)) {
      const geometries = arguments[0];

      for (let i = geometries.iterator(); i.hasNext();) {
        const geometry = i.next();
        this.add(geometry);
      }
    } else if (arguments[0] instanceof Geometry) {
      const geometry = arguments[0];
      geometry.apply(new class {
        get interfaces_() {
          return [GeometryComponentFilter];
        }

        filter(component) {
          if (component instanceof LineString) this.addLine(component);
        }

      }());
    }
  }

}

var linemerge = /*#__PURE__*/Object.freeze({
  __proto__: null,
  LineMerger: LineMerger,
  LineSequencer: LineSequencer
});

class LineStringSnapper {
  constructor() {
    LineStringSnapper.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._snapTolerance = 0.0;
    this._srcPts = null;
    this._seg = new LineSegment();
    this._allowSnappingToSourceVertices = false;
    this._isClosed = false;

    if (arguments[0] instanceof LineString && typeof arguments[1] === 'number') {
      const srcLine = arguments[0],
            snapTolerance = arguments[1];
      LineStringSnapper.constructor_.call(this, srcLine.getCoordinates(), snapTolerance);
    } else if (arguments[0] instanceof Array && typeof arguments[1] === 'number') {
      const srcPts = arguments[0],
            snapTolerance = arguments[1];
      this._srcPts = srcPts;
      this._isClosed = LineStringSnapper.isClosed(srcPts);
      this._snapTolerance = snapTolerance;
    }
  }

  static isClosed(pts) {
    if (pts.length <= 1) return false;
    return pts[0].equals2D(pts[pts.length - 1]);
  }

  snapVertices(srcCoords, snapPts) {
    const end = this._isClosed ? srcCoords.size() - 1 : srcCoords.size();

    for (let i = 0; i < end; i++) {
      const srcPt = srcCoords.get(i);
      const snapVert = this.findSnapForVertex(srcPt, snapPts);

      if (snapVert !== null) {
        srcCoords.set(i, new Coordinate(snapVert));
        if (i === 0 && this._isClosed) srcCoords.set(srcCoords.size() - 1, new Coordinate(snapVert));
      }
    }
  }

  findSnapForVertex(pt, snapPts) {
    for (let i = 0; i < snapPts.length; i++) {
      if (pt.equals2D(snapPts[i])) return null;
      if (pt.distance(snapPts[i]) < this._snapTolerance) return snapPts[i];
    }

    return null;
  }

  snapTo(snapPts) {
    const coordList = new CoordinateList(this._srcPts);
    this.snapVertices(coordList, snapPts);
    this.snapSegments(coordList, snapPts);
    const newPts = coordList.toCoordinateArray();
    return newPts;
  }

  snapSegments(srcCoords, snapPts) {
    if (snapPts.length === 0) return null;
    let distinctPtCount = snapPts.length;
    if (snapPts[0].equals2D(snapPts[snapPts.length - 1])) distinctPtCount = snapPts.length - 1;

    for (let i = 0; i < distinctPtCount; i++) {
      const snapPt = snapPts[i];
      const index = this.findSegmentIndexToSnap(snapPt, srcCoords);
      if (index >= 0) srcCoords.add(index + 1, new Coordinate(snapPt), false);
    }
  }

  findSegmentIndexToSnap(snapPt, srcCoords) {
    let minDist = Double.MAX_VALUE;
    let snapIndex = -1;

    for (let i = 0; i < srcCoords.size() - 1; i++) {
      this._seg.p0 = srcCoords.get(i);
      this._seg.p1 = srcCoords.get(i + 1);
      if (this._seg.p0.equals2D(snapPt) || this._seg.p1.equals2D(snapPt)) if (this._allowSnappingToSourceVertices) continue;else return -1;

      const dist = this._seg.distance(snapPt);

      if (dist < this._snapTolerance && dist < minDist) {
        minDist = dist;
        snapIndex = i;
      }
    }

    return snapIndex;
  }

  setAllowSnappingToSourceVertices(allowSnappingToSourceVertices) {
    this._allowSnappingToSourceVertices = allowSnappingToSourceVertices;
  }

}

class GeometrySnapper {
  constructor() {
    GeometrySnapper.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._srcGeom = null;
    const srcGeom = arguments[0];
    this._srcGeom = srcGeom;
  }

  static snap(g0, g1, snapTolerance) {
    const snapGeom = new Array(2).fill(null);
    const snapper0 = new GeometrySnapper(g0);
    snapGeom[0] = snapper0.snapTo(g1, snapTolerance);
    const snapper1 = new GeometrySnapper(g1);
    snapGeom[1] = snapper1.snapTo(snapGeom[0], snapTolerance);
    return snapGeom;
  }

  static computeOverlaySnapTolerance() {
    if (arguments.length === 1) {
      const g = arguments[0];
      let snapTolerance = GeometrySnapper.computeSizeBasedSnapTolerance(g);
      const pm = g.getPrecisionModel();

      if (pm.getType() === PrecisionModel.FIXED) {
        const fixedSnapTol = 1 / pm.getScale() * 2 / 1.415;
        if (fixedSnapTol > snapTolerance) snapTolerance = fixedSnapTol;
      }

      return snapTolerance;
    } else if (arguments.length === 2) {
      const g0 = arguments[0],
            g1 = arguments[1];
      return Math.min(GeometrySnapper.computeOverlaySnapTolerance(g0), GeometrySnapper.computeOverlaySnapTolerance(g1));
    }
  }

  static computeSizeBasedSnapTolerance(g) {
    const env = g.getEnvelopeInternal();
    const minDimension = Math.min(env.getHeight(), env.getWidth());
    const snapTol = minDimension * GeometrySnapper.SNAP_PRECISION_FACTOR;
    return snapTol;
  }

  static snapToSelf(geom, snapTolerance, cleanResult) {
    const snapper0 = new GeometrySnapper(geom);
    return snapper0.snapToSelf(snapTolerance, cleanResult);
  }

  snapTo(snapGeom, snapTolerance) {
    const snapPts = this.extractTargetCoordinates(snapGeom);
    const snapTrans = new SnapTransformer(snapTolerance, snapPts);
    return snapTrans.transform(this._srcGeom);
  }

  snapToSelf(snapTolerance, cleanResult) {
    const snapPts = this.extractTargetCoordinates(this._srcGeom);
    const snapTrans = new SnapTransformer(snapTolerance, snapPts, true);
    const snappedGeom = snapTrans.transform(this._srcGeom);
    let result = snappedGeom;
    if (cleanResult && hasInterface(result, Polygonal)) result = snappedGeom.buffer(0);
    return result;
  }

  computeSnapTolerance(ringPts) {
    const minSegLen = this.computeMinimumSegmentLength(ringPts);
    const snapTol = minSegLen / 10;
    return snapTol;
  }

  extractTargetCoordinates(g) {
    const ptSet = new TreeSet();
    const pts = g.getCoordinates();

    for (let i = 0; i < pts.length; i++) ptSet.add(pts[i]);

    return ptSet.toArray(new Array(0).fill(null));
  }

  computeMinimumSegmentLength(pts) {
    let minSegLen = Double.MAX_VALUE;

    for (let i = 0; i < pts.length - 1; i++) {
      const segLen = pts[i].distance(pts[i + 1]);
      if (segLen < minSegLen) minSegLen = segLen;
    }

    return minSegLen;
  }

}
GeometrySnapper.SNAP_PRECISION_FACTOR = 1e-9;

class SnapTransformer extends GeometryTransformer {
  constructor() {
    super();
    SnapTransformer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._snapTolerance = null;
    this._snapPts = null;
    this._isSelfSnap = false;

    if (arguments.length === 2) {
      const snapTolerance = arguments[0],
            snapPts = arguments[1];
      this._snapTolerance = snapTolerance;
      this._snapPts = snapPts;
    } else if (arguments.length === 3) {
      const snapTolerance = arguments[0],
            snapPts = arguments[1],
            isSelfSnap = arguments[2];
      this._snapTolerance = snapTolerance;
      this._snapPts = snapPts;
      this._isSelfSnap = isSelfSnap;
    }
  }

  snapLine(srcPts, snapPts) {
    const snapper = new LineStringSnapper(srcPts, this._snapTolerance);
    snapper.setAllowSnappingToSourceVertices(this._isSelfSnap);
    return snapper.snapTo(snapPts);
  }

  transformCoordinates(coords, parent) {
    const srcPts = coords.toCoordinateArray();
    const newPts = this.snapLine(srcPts, this._snapPts);
    return this._factory.getCoordinateSequenceFactory().create(newPts);
  }

}

var snap = /*#__PURE__*/Object.freeze({
  __proto__: null,
  GeometrySnapper: GeometrySnapper,
  LineStringSnapper: LineStringSnapper
});

class BasicSegmentString {
  constructor() {
    BasicSegmentString.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pts = null;
    this._data = null;
    const pts = arguments[0],
          data = arguments[1];
    this._pts = pts;
    this._data = data;
  }

  getCoordinates() {
    return this._pts;
  }

  size() {
    return this._pts.length;
  }

  getCoordinate(i) {
    return this._pts[i];
  }

  isClosed() {
    return this._pts[0].equals(this._pts[this._pts.length - 1]);
  }

  getSegmentOctant(index) {
    if (index === this._pts.length - 1) return -1;
    return Octant.octant(this.getCoordinate(index), this.getCoordinate(index + 1));
  }

  setData(data) {
    this._data = data;
  }

  getData() {
    return this._data;
  }

  toString() {
    return WKTWriter.toLineString(new CoordinateArraySequence(this._pts));
  }

  get interfaces_() {
    return [SegmentString];
  }

}

class NodingIntersectionFinder {
  constructor() {
    NodingIntersectionFinder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._findAllIntersections = false;
    this._isCheckEndSegmentsOnly = false;
    this._keepIntersections = true;
    this._isInteriorIntersectionsOnly = false;
    this._li = null;
    this._interiorIntersection = null;
    this._intSegments = null;
    this._intersections = new ArrayList();
    this._intersectionCount = 0;
    const li = arguments[0];
    this._li = li;
    this._interiorIntersection = null;
  }

  static createAllIntersectionsFinder(li) {
    const finder = new NodingIntersectionFinder(li);
    finder.setFindAllIntersections(true);
    return finder;
  }

  static isInteriorVertexIntersection() {
    if (arguments.length === 4) {
      const p0 = arguments[0],
            p1 = arguments[1],
            isEnd0 = arguments[2],
            isEnd1 = arguments[3];
      if (isEnd0 && isEnd1) return false;
      if (p0.equals2D(p1)) return true;
      return false;
    } else if (arguments.length === 8) {
      const p00 = arguments[0],
            p01 = arguments[1],
            p10 = arguments[2],
            p11 = arguments[3],
            isEnd00 = arguments[4],
            isEnd01 = arguments[5],
            isEnd10 = arguments[6],
            isEnd11 = arguments[7];
      if (NodingIntersectionFinder.isInteriorVertexIntersection(p00, p10, isEnd00, isEnd10)) return true;
      if (NodingIntersectionFinder.isInteriorVertexIntersection(p00, p11, isEnd00, isEnd11)) return true;
      if (NodingIntersectionFinder.isInteriorVertexIntersection(p01, p10, isEnd01, isEnd10)) return true;
      if (NodingIntersectionFinder.isInteriorVertexIntersection(p01, p11, isEnd01, isEnd11)) return true;
      return false;
    }
  }

  static createInteriorIntersectionCounter(li) {
    const finder = new NodingIntersectionFinder(li);
    finder.setInteriorIntersectionsOnly(true);
    finder.setFindAllIntersections(true);
    finder.setKeepIntersections(false);
    return finder;
  }

  static createIntersectionCounter(li) {
    const finder = new NodingIntersectionFinder(li);
    finder.setFindAllIntersections(true);
    finder.setKeepIntersections(false);
    return finder;
  }

  static isEndSegment(segStr, index) {
    if (index === 0) return true;
    if (index >= segStr.size() - 2) return true;
    return false;
  }

  static createAnyIntersectionFinder(li) {
    return new NodingIntersectionFinder(li);
  }

  static createInteriorIntersectionsFinder(li) {
    const finder = new NodingIntersectionFinder(li);
    finder.setFindAllIntersections(true);
    finder.setInteriorIntersectionsOnly(true);
    return finder;
  }

  setCheckEndSegmentsOnly(isCheckEndSegmentsOnly) {
    this._isCheckEndSegmentsOnly = isCheckEndSegmentsOnly;
  }

  getIntersectionSegments() {
    return this._intSegments;
  }

  count() {
    return this._intersectionCount;
  }

  getIntersections() {
    return this._intersections;
  }

  setFindAllIntersections(findAllIntersections) {
    this._findAllIntersections = findAllIntersections;
  }

  setKeepIntersections(keepIntersections) {
    this._keepIntersections = keepIntersections;
  }

  getIntersection() {
    return this._interiorIntersection;
  }

  processIntersections(e0, segIndex0, e1, segIndex1) {
    if (!this._findAllIntersections && this.hasIntersection()) return null;
    const isSameSegString = e0 === e1;
    const isSameSegment = isSameSegString && segIndex0 === segIndex1;
    if (isSameSegment) return null;

    if (this._isCheckEndSegmentsOnly) {
      const isEndSegPresent = NodingIntersectionFinder.isEndSegment(e0, segIndex0) || NodingIntersectionFinder.isEndSegment(e1, segIndex1);
      if (!isEndSegPresent) return null;
    }

    const p00 = e0.getCoordinate(segIndex0);
    const p01 = e0.getCoordinate(segIndex0 + 1);
    const p10 = e1.getCoordinate(segIndex1);
    const p11 = e1.getCoordinate(segIndex1 + 1);
    const isEnd00 = segIndex0 === 0;
    const isEnd01 = segIndex0 + 2 === e0.size();
    const isEnd10 = segIndex1 === 0;
    const isEnd11 = segIndex1 + 2 === e1.size();

    this._li.computeIntersection(p00, p01, p10, p11);

    const isInteriorInt = this._li.hasIntersection() && this._li.isInteriorIntersection();

    let isInteriorVertexInt = false;

    if (!this._isInteriorIntersectionsOnly) {
      const isAdjacentSegment = isSameSegString && Math.abs(segIndex1 - segIndex0) <= 1;
      isInteriorVertexInt = !isAdjacentSegment && NodingIntersectionFinder.isInteriorVertexIntersection(p00, p01, p10, p11, isEnd00, isEnd01, isEnd10, isEnd11);
    }

    if (isInteriorInt || isInteriorVertexInt) {
      this._intSegments = new Array(4).fill(null);
      this._intSegments[0] = p00;
      this._intSegments[1] = p01;
      this._intSegments[2] = p10;
      this._intSegments[3] = p11;
      this._interiorIntersection = this._li.getIntersection(0);
      if (this._keepIntersections) this._intersections.add(this._interiorIntersection);
      this._intersectionCount++;
    }
  }

  hasIntersection() {
    return this._interiorIntersection !== null;
  }

  isDone() {
    if (this._findAllIntersections) return false;
    return this._interiorIntersection !== null;
  }

  setInteriorIntersectionsOnly(isInteriorIntersectionsOnly) {
    this._isInteriorIntersectionsOnly = isInteriorIntersectionsOnly;
  }

  get interfaces_() {
    return [SegmentIntersector];
  }

}

class FastNodingValidator {
  constructor() {
    FastNodingValidator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._li = new RobustLineIntersector();
    this._segStrings = null;
    this._findAllIntersections = false;
    this._segInt = null;
    this._isValid = true;
    const segStrings = arguments[0];
    this._segStrings = segStrings;
  }

  static computeIntersections(segStrings) {
    const nv = new FastNodingValidator(segStrings);
    nv.setFindAllIntersections(true);
    nv.isValid();
    return nv.getIntersections();
  }

  execute() {
    if (this._segInt !== null) return null;
    this.checkInteriorIntersections();
  }

  getIntersections() {
    return this._segInt.getIntersections();
  }

  isValid() {
    this.execute();
    return this._isValid;
  }

  setFindAllIntersections(findAllIntersections) {
    this._findAllIntersections = findAllIntersections;
  }

  checkInteriorIntersections() {
    this._isValid = true;
    this._segInt = new NodingIntersectionFinder(this._li);

    this._segInt.setFindAllIntersections(this._findAllIntersections);

    const noder = new MCIndexNoder();
    noder.setSegmentIntersector(this._segInt);
    noder.computeNodes(this._segStrings);

    if (this._segInt.hasIntersection()) {
      this._isValid = false;
      return null;
    }
  }

  checkValid() {
    this.execute();
    if (!this._isValid) throw new TopologyException(this.getErrorMessage(), this._segInt.getIntersection());
  }

  getErrorMessage() {
    if (this._isValid) return 'no intersections found';

    const intSegs = this._segInt.getIntersectionSegments();

    return 'found non-noded intersection between ' + WKTWriter.toLineString(intSegs[0], intSegs[1]) + ' and ' + WKTWriter.toLineString(intSegs[2], intSegs[3]);
  }

}

class EdgeNodingValidator {
  constructor() {
    EdgeNodingValidator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._nv = null;
    const edges = arguments[0];
    this._nv = new FastNodingValidator(EdgeNodingValidator.toSegmentStrings(edges));
  }

  static toSegmentStrings(edges) {
    const segStrings = new ArrayList();

    for (let i = edges.iterator(); i.hasNext();) {
      const e = i.next();
      segStrings.add(new BasicSegmentString(e.getCoordinates(), e));
    }

    return segStrings;
  }

  static checkValid(edges) {
    const validator = new EdgeNodingValidator(edges);
    validator.checkValid();
  }

  checkValid() {
    this._nv.checkValid();
  }

}

class LineBuilder {
  constructor() {
    LineBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._op = null;
    this._geometryFactory = null;
    this._ptLocator = null;
    this._lineEdgesList = new ArrayList();
    this._resultLineList = new ArrayList();
    const op = arguments[0],
          geometryFactory = arguments[1],
          ptLocator = arguments[2];
    this._op = op;
    this._geometryFactory = geometryFactory;
    this._ptLocator = ptLocator;
  }

  collectLines(opCode) {
    for (let it = this._op.getGraph().getEdgeEnds().iterator(); it.hasNext();) {
      const de = it.next();
      this.collectLineEdge(de, opCode, this._lineEdgesList);
      this.collectBoundaryTouchEdge(de, opCode, this._lineEdgesList);
    }
  }

  labelIsolatedLine(e, targetIndex) {
    const loc = this._ptLocator.locate(e.getCoordinate(), this._op.getArgGeometry(targetIndex));

    e.getLabel().setLocation(targetIndex, loc);
  }

  build(opCode) {
    this.findCoveredLineEdges();
    this.collectLines(opCode);
    this.buildLines(opCode);
    return this._resultLineList;
  }

  collectLineEdge(de, opCode, edges) {
    const label = de.getLabel();
    const e = de.getEdge();
    if (de.isLineEdge()) if (!de.isVisited() && OverlayOp.isResultOfOp(label, opCode) && !e.isCovered()) {
      edges.add(e);
      de.setVisitedEdge(true);
    }
  }

  findCoveredLineEdges() {
    for (let nodeit = this._op.getGraph().getNodes().iterator(); nodeit.hasNext();) {
      const node = nodeit.next();
      node.getEdges().findCoveredLineEdges();
    }

    for (let it = this._op.getGraph().getEdgeEnds().iterator(); it.hasNext();) {
      const de = it.next();
      const e = de.getEdge();

      if (de.isLineEdge() && !e.isCoveredSet()) {
        const isCovered = this._op.isCoveredByA(de.getCoordinate());

        e.setCovered(isCovered);
      }
    }
  }

  labelIsolatedLines(edgesList) {
    for (let it = edgesList.iterator(); it.hasNext();) {
      const e = it.next();
      const label = e.getLabel();
      if (e.isIsolated()) if (label.isNull(0)) this.labelIsolatedLine(e, 0);else this.labelIsolatedLine(e, 1);
    }
  }

  buildLines(opCode) {
    for (let it = this._lineEdgesList.iterator(); it.hasNext();) {
      const e = it.next();

      const line = this._geometryFactory.createLineString(e.getCoordinates());

      this._resultLineList.add(line);

      e.setInResult(true);
    }
  }

  collectBoundaryTouchEdge(de, opCode, edges) {
    const label = de.getLabel();
    if (de.isLineEdge()) return null;
    if (de.isVisited()) return null;
    if (de.isInteriorAreaEdge()) return null;
    if (de.getEdge().isInResult()) return null;
    Assert.isTrue(!(de.isInResult() || de.getSym().isInResult()) || !de.getEdge().isInResult());

    if (OverlayOp.isResultOfOp(label, opCode) && opCode === OverlayOp.INTERSECTION) {
      edges.add(de.getEdge());
      de.setVisitedEdge(true);
    }
  }

}

class PointBuilder {
  constructor() {
    PointBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._op = null;
    this._geometryFactory = null;
    this._resultPointList = new ArrayList();
    const op = arguments[0],
          geometryFactory = arguments[1];
    this._op = op;
    this._geometryFactory = geometryFactory;
  }

  filterCoveredNodeToPoint(n) {
    const coord = n.getCoordinate();

    if (!this._op.isCoveredByLA(coord)) {
      const pt = this._geometryFactory.createPoint(coord);

      this._resultPointList.add(pt);
    }
  }

  extractNonCoveredResultNodes(opCode) {
    for (let nodeit = this._op.getGraph().getNodes().iterator(); nodeit.hasNext();) {
      const n = nodeit.next();
      if (n.isInResult()) continue;
      if (n.isIncidentEdgeInResult()) continue;

      if (n.getEdges().getDegree() === 0 || opCode === OverlayOp.INTERSECTION) {
        const label = n.getLabel();
        if (OverlayOp.isResultOfOp(label, opCode)) this.filterCoveredNodeToPoint(n);
      }
    }
  }

  build(opCode) {
    this.extractNonCoveredResultNodes(opCode);
    return this._resultPointList;
  }

}

class CommonBits {
  constructor() {
    this._isFirst = true;
    this._commonMantissaBitsCount = 53;
    this._commonBits = new Long();
    this._commonSignExp = null;
  }

  getCommon() {
    return Double.longBitsToDouble(this._commonBits);
  }

  add(num) {
    const numBits = Double.doubleToLongBits(num);

    if (this._isFirst) {
      this._commonBits = numBits;
      this._commonSignExp = CommonBits.signExpBits(this._commonBits);
      this._isFirst = false;
      return null;
    }

    const numSignExp = CommonBits.signExpBits(numBits);

    if (numSignExp !== this._commonSignExp) {
      this._commonBits.high = 0 | 0;
      this._commonBits.low = 0 | 0;
      return null;
    }

    this._commonMantissaBitsCount = CommonBits.numCommonMostSigMantissaBits(this._commonBits, numBits);
    this._commonBits = CommonBits.zeroLowerBits(this._commonBits, 64 - (12 + this._commonMantissaBitsCount));
  }

  toString() {
    if (arguments.length === 1) {
      const bits = arguments[0];
      const x = Double.longBitsToDouble(bits);
      const numStr = Long.toBinaryString(bits);
      const padStr = '0000000000000000000000000000000000000000000000000000000000000000' + numStr;
      const bitStr = padStr.substring(padStr.length - 64);
      const str = bitStr.substring(0, 1) + '  ' + bitStr.substring(1, 12) + '(exp) ' + bitStr.substring(12) + ' [ ' + x + ' ]';
      return str;
    }
  }

  getClass() {
    return CommonBits;
  }

  get interfaces_() {
    return [];
  }

  static getBit(bits, i) {
    const mask = 1 << i % 32;
    if (i < 32) return (bits.low & mask) !== 0 ? 1 : 0;
    return (bits.high & mask) !== 0 ? 1 : 0;
  }

  static signExpBits(num) {
    return num.high >>> 20;
  }

  static zeroLowerBits(bits, nBits) {
    let prop = 'low';

    if (nBits > 32) {
      bits.low = 0 | 0;
      nBits %= 32;
      prop = 'high';
    }

    if (nBits > 0) {
      const mask = nBits < 32 ? ~((1 << nBits) - 1) : 0;
      bits[prop] &= mask;
    }

    return bits;
  }

  static numCommonMostSigMantissaBits(num1, num2) {
    let count = 0;

    for (let i = 52; i >= 0; i--) {
      if (CommonBits.getBit(num1, i) !== CommonBits.getBit(num2, i)) return count;
      count++;
    }

    return 52;
  }

}

class CommonBitsRemover {
  constructor() {
    CommonBitsRemover.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._commonCoord = null;
    this._ccFilter = new CommonCoordinateFilter();
  }

  addCommonBits(geom) {
    const trans = new Translater(this._commonCoord);
    geom.apply(trans);
    geom.geometryChanged();
  }

  removeCommonBits(geom) {
    if (this._commonCoord.x === 0.0 && this._commonCoord.y === 0.0) return geom;
    const invCoord = new Coordinate(this._commonCoord);
    invCoord.x = -invCoord.x;
    invCoord.y = -invCoord.y;
    const trans = new Translater(invCoord);
    geom.apply(trans);
    geom.geometryChanged();
    return geom;
  }

  getCommonCoordinate() {
    return this._commonCoord;
  }

  add(geom) {
    geom.apply(this._ccFilter);
    this._commonCoord = this._ccFilter.getCommonCoordinate();
  }

}

class CommonCoordinateFilter {
  constructor() {
    CommonCoordinateFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._commonBitsX = new CommonBits();
    this._commonBitsY = new CommonBits();
  }

  filter(coord) {
    this._commonBitsX.add(coord.x);

    this._commonBitsY.add(coord.y);
  }

  getCommonCoordinate() {
    return new Coordinate(this._commonBitsX.getCommon(), this._commonBitsY.getCommon());
  }

  get interfaces_() {
    return [CoordinateFilter];
  }

}

class Translater {
  constructor() {
    Translater.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.trans = null;
    const trans = arguments[0];
    this.trans = trans;
  }

  filter(seq, i) {
    const xp = seq.getOrdinate(i, 0) + this.trans.x;
    const yp = seq.getOrdinate(i, 1) + this.trans.y;
    seq.setOrdinate(i, 0, xp);
    seq.setOrdinate(i, 1, yp);
  }

  isDone() {
    return false;
  }

  isGeometryChanged() {
    return true;
  }

  get interfaces_() {
    return [CoordinateSequenceFilter];
  }

}

CommonBitsRemover.CommonCoordinateFilter = CommonCoordinateFilter;
CommonBitsRemover.Translater = Translater;

class SnapOverlayOp {
  constructor() {
    SnapOverlayOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geom = new Array(2).fill(null);
    this._snapTolerance = null;
    this._cbr = null;
    const g1 = arguments[0],
          g2 = arguments[1];
    this._geom[0] = g1;
    this._geom[1] = g2;
    this.computeSnapTolerance();
  }

  static overlayOp(g0, g1, opCode) {
    const op = new SnapOverlayOp(g0, g1);
    return op.getResultGeometry(opCode);
  }

  static union(g0, g1) {
    return SnapOverlayOp.overlayOp(g0, g1, OverlayOp.UNION);
  }

  static intersection(g0, g1) {
    return SnapOverlayOp.overlayOp(g0, g1, OverlayOp.INTERSECTION);
  }

  static symDifference(g0, g1) {
    return SnapOverlayOp.overlayOp(g0, g1, OverlayOp.SYMDIFFERENCE);
  }

  static difference(g0, g1) {
    return SnapOverlayOp.overlayOp(g0, g1, OverlayOp.DIFFERENCE);
  }

  selfSnap(geom) {
    const snapper0 = new GeometrySnapper(geom);
    const snapGeom = snapper0.snapTo(geom, this._snapTolerance);
    return snapGeom;
  }

  removeCommonBits(geom) {
    this._cbr = new CommonBitsRemover();

    this._cbr.add(geom[0]);

    this._cbr.add(geom[1]);

    const remGeom = new Array(2).fill(null);
    remGeom[0] = this._cbr.removeCommonBits(geom[0].copy());
    remGeom[1] = this._cbr.removeCommonBits(geom[1].copy());
    return remGeom;
  }

  prepareResult(geom) {
    this._cbr.addCommonBits(geom);

    return geom;
  }

  getResultGeometry(opCode) {
    const prepGeom = this.snap(this._geom);
    const result = OverlayOp.overlayOp(prepGeom[0], prepGeom[1], opCode);
    return this.prepareResult(result);
  }

  checkValid(g) {
    if (!g.isValid()) System.out.println('Snapped geometry is invalid');
  }

  computeSnapTolerance() {
    this._snapTolerance = GeometrySnapper.computeOverlaySnapTolerance(this._geom[0], this._geom[1]);
  }

  snap(geom) {
    const remGeom = this.removeCommonBits(geom);
    const snapGeom = GeometrySnapper.snap(remGeom[0], remGeom[1], this._snapTolerance);
    return snapGeom;
  }

}

class SnapIfNeededOverlayOp {
  constructor() {
    SnapIfNeededOverlayOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geom = new Array(2).fill(null);
    const g1 = arguments[0],
          g2 = arguments[1];
    this._geom[0] = g1;
    this._geom[1] = g2;
  }

  static overlayOp(g0, g1, opCode) {
    const op = new SnapIfNeededOverlayOp(g0, g1);
    return op.getResultGeometry(opCode);
  }

  static union(g0, g1) {
    return SnapIfNeededOverlayOp.overlayOp(g0, g1, OverlayOp.UNION);
  }

  static intersection(g0, g1) {
    return SnapIfNeededOverlayOp.overlayOp(g0, g1, OverlayOp.INTERSECTION);
  }

  static symDifference(g0, g1) {
    return SnapIfNeededOverlayOp.overlayOp(g0, g1, OverlayOp.SYMDIFFERENCE);
  }

  static difference(g0, g1) {
    return SnapIfNeededOverlayOp.overlayOp(g0, g1, OverlayOp.DIFFERENCE);
  }

  getResultGeometry(opCode) {
    let result = null;
    let isSuccess = false;
    let savedException = null;

    try {
      result = OverlayOp.overlayOp(this._geom[0], this._geom[1], opCode);
      const isValid = true;
      if (isValid) isSuccess = true;
    } catch (ex) {
      if (ex instanceof RuntimeException) savedException = ex;else throw ex;
    } finally {}

    if (!isSuccess) try {
      result = SnapOverlayOp.overlayOp(this._geom[0], this._geom[1], opCode);
    } catch (ex) {
      if (ex instanceof RuntimeException) throw savedException;else throw ex;
    } finally {}
    return result;
  }

}

class GeometryGraphOperation {
  constructor() {
    GeometryGraphOperation.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._li = new RobustLineIntersector();
    this._resultPrecisionModel = null;
    this._arg = null;

    if (arguments.length === 1) {
      const g0 = arguments[0];
      this.setComputationPrecision(g0.getPrecisionModel());
      this._arg = new Array(1).fill(null);
      this._arg[0] = new GeometryGraph(0, g0);
    } else if (arguments.length === 2) {
      const g0 = arguments[0],
            g1 = arguments[1];
      GeometryGraphOperation.constructor_.call(this, g0, g1, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE);
    } else if (arguments.length === 3) {
      const g0 = arguments[0],
            g1 = arguments[1],
            boundaryNodeRule = arguments[2];
      if (g0.getPrecisionModel().compareTo(g1.getPrecisionModel()) >= 0) this.setComputationPrecision(g0.getPrecisionModel());else this.setComputationPrecision(g1.getPrecisionModel());
      this._arg = new Array(2).fill(null);
      this._arg[0] = new GeometryGraph(0, g0, boundaryNodeRule);
      this._arg[1] = new GeometryGraph(1, g1, boundaryNodeRule);
    }
  }

  getArgGeometry(i) {
    return this._arg[i].getGeometry();
  }

  setComputationPrecision(pm) {
    this._resultPrecisionModel = pm;

    this._li.setPrecisionModel(this._resultPrecisionModel);
  }

}

class OverlayOp extends GeometryGraphOperation {
  constructor() {
    super();
    OverlayOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._ptLocator = new PointLocator();
    this._geomFact = null;
    this._resultGeom = null;
    this._graph = null;
    this._edgeList = new EdgeList();
    this._resultPolyList = new ArrayList();
    this._resultLineList = new ArrayList();
    this._resultPointList = new ArrayList();
    const g0 = arguments[0],
          g1 = arguments[1];
    GeometryGraphOperation.constructor_.call(this, g0, g1);
    this._graph = new PlanarGraph$1(new OverlayNodeFactory());
    this._geomFact = g0.getFactory();
  }

  static overlayOp(geom0, geom1, opCode) {
    const gov = new OverlayOp(geom0, geom1);
    const geomOv = gov.getResultGeometry(opCode);
    return geomOv;
  }

  static union(geom, other) {
    if (geom.isEmpty() || other.isEmpty()) {
      if (geom.isEmpty() && other.isEmpty()) return OverlayOp.createEmptyResult(OverlayOp.UNION, geom, other, geom.getFactory());
      if (geom.isEmpty()) return other.copy();
      if (other.isEmpty()) return geom.copy();
    }

    if (geom.isGeometryCollection() || other.isGeometryCollection()) throw new IllegalArgumentException('This method does not support GeometryCollection arguments');
    return SnapIfNeededOverlayOp.overlayOp(geom, other, OverlayOp.UNION);
  }

  static intersection(geom, other) {
    if (geom.isEmpty() || other.isEmpty()) return OverlayOp.createEmptyResult(OverlayOp.INTERSECTION, geom, other, geom.getFactory());

    if (geom.isGeometryCollection()) {
      const g2 = other;
      return GeometryCollectionMapper.map(geom, new class {
        get interfaces_() {
          return [MapOp];
        }

        map(g) {
          return OverlayOp.intersection(g, g2);
        }

      }());
    }

    return SnapIfNeededOverlayOp.overlayOp(geom, other, OverlayOp.INTERSECTION);
  }

  static symDifference(geom, other) {
    if (geom.isEmpty() || other.isEmpty()) {
      if (geom.isEmpty() && other.isEmpty()) return OverlayOp.createEmptyResult(OverlayOp.SYMDIFFERENCE, geom, other, geom.getFactory());
      if (geom.isEmpty()) return other.copy();
      if (other.isEmpty()) return geom.copy();
    }

    if (geom.isGeometryCollection() || other.isGeometryCollection()) throw new IllegalArgumentException('This method does not support GeometryCollection arguments');
    return SnapIfNeededOverlayOp.overlayOp(geom, other, OverlayOp.SYMDIFFERENCE);
  }

  static resultDimension(opCode, g0, g1) {
    const dim0 = g0.getDimension();
    const dim1 = g1.getDimension();
    let resultDimension = -1;

    switch (opCode) {
      case OverlayOp.INTERSECTION:
        resultDimension = Math.min(dim0, dim1);
        break;

      case OverlayOp.UNION:
        resultDimension = Math.max(dim0, dim1);
        break;

      case OverlayOp.DIFFERENCE:
        resultDimension = dim0;
        break;

      case OverlayOp.SYMDIFFERENCE:
        resultDimension = Math.max(dim0, dim1);
        break;
    }

    return resultDimension;
  }

  static createEmptyResult(overlayOpCode, a, b, geomFact) {
    const resultDim = OverlayOp.resultDimension(overlayOpCode, a, b);
    return geomFact.createEmpty(resultDim);
  }

  static difference(geom, other) {
    if (geom.isEmpty()) return OverlayOp.createEmptyResult(OverlayOp.DIFFERENCE, geom, other, geom.getFactory());
    if (other.isEmpty()) return geom.copy();
    if (geom.isGeometryCollection() || other.isGeometryCollection()) throw new IllegalArgumentException('This method does not support GeometryCollection arguments');
    return SnapIfNeededOverlayOp.overlayOp(geom, other, OverlayOp.DIFFERENCE);
  }

  static isResultOfOp() {
    if (arguments.length === 2) {
      const label = arguments[0],
            opCode = arguments[1];
      const loc0 = label.getLocation(0);
      const loc1 = label.getLocation(1);
      return OverlayOp.isResultOfOp(loc0, loc1, opCode);
    } else if (arguments.length === 3) {
      let loc0 = arguments[0],
          loc1 = arguments[1],
          overlayOpCode = arguments[2];
      if (loc0 === Location.BOUNDARY) loc0 = Location.INTERIOR;
      if (loc1 === Location.BOUNDARY) loc1 = Location.INTERIOR;

      switch (overlayOpCode) {
        case OverlayOp.INTERSECTION:
          return loc0 === Location.INTERIOR && loc1 === Location.INTERIOR;

        case OverlayOp.UNION:
          return loc0 === Location.INTERIOR || loc1 === Location.INTERIOR;

        case OverlayOp.DIFFERENCE:
          return loc0 === Location.INTERIOR && loc1 !== Location.INTERIOR;

        case OverlayOp.SYMDIFFERENCE:
          return loc0 === Location.INTERIOR && loc1 !== Location.INTERIOR || loc0 !== Location.INTERIOR && loc1 === Location.INTERIOR;
      }

      return false;
    }
  }

  insertUniqueEdge(e) {
    const existingEdge = this._edgeList.findEqualEdge(e);

    if (existingEdge !== null) {
      const existingLabel = existingEdge.getLabel();
      let labelToMerge = e.getLabel();

      if (!existingEdge.isPointwiseEqual(e)) {
        labelToMerge = new Label(e.getLabel());
        labelToMerge.flip();
      }

      const depth = existingEdge.getDepth();
      if (depth.isNull()) depth.add(existingLabel);
      depth.add(labelToMerge);
      existingLabel.merge(labelToMerge);
    } else {
      this._edgeList.add(e);
    }
  }

  getGraph() {
    return this._graph;
  }

  cancelDuplicateResultEdges() {
    for (let it = this._graph.getEdgeEnds().iterator(); it.hasNext();) {
      const de = it.next();
      const sym = de.getSym();

      if (de.isInResult() && sym.isInResult()) {
        de.setInResult(false);
        sym.setInResult(false);
      }
    }
  }

  isCoveredByLA(coord) {
    if (this.isCovered(coord, this._resultLineList)) return true;
    if (this.isCovered(coord, this._resultPolyList)) return true;
    return false;
  }

  computeGeometry(resultPointList, resultLineList, resultPolyList, opcode) {
    const geomList = new ArrayList();
    geomList.addAll(resultPointList);
    geomList.addAll(resultLineList);
    geomList.addAll(resultPolyList);
    if (geomList.isEmpty()) return OverlayOp.createEmptyResult(opcode, this._arg[0].getGeometry(), this._arg[1].getGeometry(), this._geomFact);
    return this._geomFact.buildGeometry(geomList);
  }

  mergeSymLabels() {
    for (let nodeit = this._graph.getNodes().iterator(); nodeit.hasNext();) {
      const node = nodeit.next();
      node.getEdges().mergeSymLabels();
    }
  }

  isCovered(coord, geomList) {
    for (let it = geomList.iterator(); it.hasNext();) {
      const geom = it.next();

      const loc = this._ptLocator.locate(coord, geom);

      if (loc !== Location.EXTERIOR) return true;
    }

    return false;
  }

  replaceCollapsedEdges() {
    const newEdges = new ArrayList();

    for (let it = this._edgeList.iterator(); it.hasNext();) {
      const e = it.next();

      if (e.isCollapsed()) {
        it.remove();
        newEdges.add(e.getCollapsedEdge());
      }
    }

    this._edgeList.addAll(newEdges);
  }

  updateNodeLabelling() {
    for (let nodeit = this._graph.getNodes().iterator(); nodeit.hasNext();) {
      const node = nodeit.next();
      const lbl = node.getEdges().getLabel();
      node.getLabel().merge(lbl);
    }
  }

  getResultGeometry(overlayOpCode) {
    this.computeOverlay(overlayOpCode);
    return this._resultGeom;
  }

  insertUniqueEdges(edges) {
    for (let i = edges.iterator(); i.hasNext();) {
      const e = i.next();
      this.insertUniqueEdge(e);
    }
  }

  computeOverlay(opCode) {
    this.copyPoints(0);
    this.copyPoints(1);

    this._arg[0].computeSelfNodes(this._li, false);

    this._arg[1].computeSelfNodes(this._li, false);

    this._arg[0].computeEdgeIntersections(this._arg[1], this._li, true);

    const baseSplitEdges = new ArrayList();

    this._arg[0].computeSplitEdges(baseSplitEdges);

    this._arg[1].computeSplitEdges(baseSplitEdges);
    this.insertUniqueEdges(baseSplitEdges);
    this.computeLabelsFromDepths();
    this.replaceCollapsedEdges();
    EdgeNodingValidator.checkValid(this._edgeList.getEdges());

    this._graph.addEdges(this._edgeList.getEdges());

    this.computeLabelling();
    this.labelIncompleteNodes();
    this.findResultAreaEdges(opCode);
    this.cancelDuplicateResultEdges();
    const polyBuilder = new PolygonBuilder(this._geomFact);
    polyBuilder.add(this._graph);
    this._resultPolyList = polyBuilder.getPolygons();
    const lineBuilder = new LineBuilder(this, this._geomFact, this._ptLocator);
    this._resultLineList = lineBuilder.build(opCode);
    const pointBuilder = new PointBuilder(this, this._geomFact, this._ptLocator);
    this._resultPointList = pointBuilder.build(opCode);
    this._resultGeom = this.computeGeometry(this._resultPointList, this._resultLineList, this._resultPolyList, opCode);
  }

  labelIncompleteNode(n, targetIndex) {
    const loc = this._ptLocator.locate(n.getCoordinate(), this._arg[targetIndex].getGeometry());

    n.getLabel().setLocation(targetIndex, loc);
  }

  copyPoints(argIndex) {
    for (let i = this._arg[argIndex].getNodeIterator(); i.hasNext();) {
      const graphNode = i.next();

      const newNode = this._graph.addNode(graphNode.getCoordinate());

      newNode.setLabel(argIndex, graphNode.getLabel().getLocation(argIndex));
    }
  }

  findResultAreaEdges(opCode) {
    for (let it = this._graph.getEdgeEnds().iterator(); it.hasNext();) {
      const de = it.next();
      const label = de.getLabel();
      if (label.isArea() && !de.isInteriorAreaEdge() && OverlayOp.isResultOfOp(label.getLocation(0, Position.RIGHT), label.getLocation(1, Position.RIGHT), opCode)) de.setInResult(true);
    }
  }

  computeLabelsFromDepths() {
    for (let it = this._edgeList.iterator(); it.hasNext();) {
      const e = it.next();
      const lbl = e.getLabel();
      const depth = e.getDepth();

      if (!depth.isNull()) {
        depth.normalize();

        for (let i = 0; i < 2; i++) if (!lbl.isNull(i) && lbl.isArea() && !depth.isNull(i)) if (depth.getDelta(i) === 0) {
          lbl.toLine(i);
        } else {
          Assert.isTrue(!depth.isNull(i, Position.LEFT), 'depth of LEFT side has not been initialized');
          lbl.setLocation(i, Position.LEFT, depth.getLocation(i, Position.LEFT));
          Assert.isTrue(!depth.isNull(i, Position.RIGHT), 'depth of RIGHT side has not been initialized');
          lbl.setLocation(i, Position.RIGHT, depth.getLocation(i, Position.RIGHT));
        }
      }
    }
  }

  computeLabelling() {
    for (let nodeit = this._graph.getNodes().iterator(); nodeit.hasNext();) {
      const node = nodeit.next();
      node.getEdges().computeLabelling(this._arg);
    }

    this.mergeSymLabels();
    this.updateNodeLabelling();
  }

  labelIncompleteNodes() {
    for (let ni = this._graph.getNodes().iterator(); ni.hasNext();) {
      const n = ni.next();
      const label = n.getLabel();
      if (n.isIsolated()) if (label.isNull(0)) this.labelIncompleteNode(n, 0);else this.labelIncompleteNode(n, 1);
      n.getEdges().updateLabelling(label);
    }
  }

  isCoveredByA(coord) {
    if (this.isCovered(coord, this._resultPolyList)) return true;
    return false;
  }

}
OverlayOp.INTERSECTION = 1;
OverlayOp.UNION = 2;
OverlayOp.DIFFERENCE = 3;
OverlayOp.SYMDIFFERENCE = 4;

var overlay = /*#__PURE__*/Object.freeze({
  __proto__: null,
  snap: snap,
  OverlayOp: OverlayOp
});

class PolygonizeDirectedEdge extends DirectedEdge {
  constructor() {
    super();
    PolygonizeDirectedEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._edgeRing = null;
    this._next = null;
    this._label = -1;
    const from = arguments[0],
          to = arguments[1],
          directionPt = arguments[2],
          edgeDirection = arguments[3];
    DirectedEdge.constructor_.call(this, from, to, directionPt, edgeDirection);
  }

  getNext() {
    return this._next;
  }

  isInRing() {
    return this._edgeRing !== null;
  }

  setRing(edgeRing) {
    this._edgeRing = edgeRing;
  }

  setLabel(label) {
    this._label = label;
  }

  getLabel() {
    return this._label;
  }

  setNext(next) {
    this._next = next;
  }

  getRing() {
    return this._edgeRing;
  }

}

class PolygonizeEdge extends Edge {
  constructor() {
    super();
    PolygonizeEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._line = null;
    const line = arguments[0];
    this._line = line;
  }

  getLine() {
    return this._line;
  }

}

class ConnectedInteriorTester {
  constructor() {
    ConnectedInteriorTester.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geometryFactory = new GeometryFactory();
    this._geomGraph = null;
    this._disconnectedRingcoord = null;
    const geomGraph = arguments[0];
    this._geomGraph = geomGraph;
  }

  static findDifferentPoint(coord, pt) {
    for (let i = 0; i < coord.length; i++) if (!coord[i].equals(pt)) return coord[i];

    return null;
  }

  visitInteriorRing(ring, graph) {
    if (ring.isEmpty()) return null;
    const pts = ring.getCoordinates();
    const pt0 = pts[0];
    const pt1 = ConnectedInteriorTester.findDifferentPoint(pts, pt0);
    const e = graph.findEdgeInSameDirection(pt0, pt1);
    const de = graph.findEdgeEnd(e);
    let intDe = null;
    if (de.getLabel().getLocation(0, Position.RIGHT) === Location.INTERIOR) intDe = de;else if (de.getSym().getLabel().getLocation(0, Position.RIGHT) === Location.INTERIOR) intDe = de.getSym();
    Assert.isTrue(intDe !== null, 'unable to find dirEdge with Interior on RHS');
    this.visitLinkedDirectedEdges(intDe);
  }

  visitShellInteriors(g, graph) {
    if (g instanceof Polygon) {
      const p = g;
      this.visitInteriorRing(p.getExteriorRing(), graph);
    }

    if (g instanceof MultiPolygon) {
      const mp = g;

      for (let i = 0; i < mp.getNumGeometries(); i++) {
        const p = mp.getGeometryN(i);
        this.visitInteriorRing(p.getExteriorRing(), graph);
      }
    }
  }

  getCoordinate() {
    return this._disconnectedRingcoord;
  }

  setInteriorEdgesInResult(graph) {
    for (let it = graph.getEdgeEnds().iterator(); it.hasNext();) {
      const de = it.next();
      if (de.getLabel().getLocation(0, Position.RIGHT) === Location.INTERIOR) de.setInResult(true);
    }
  }

  visitLinkedDirectedEdges(start) {
    const startDe = start;
    let de = start;

    do {
      Assert.isTrue(de !== null, 'found null Directed Edge');
      de.setVisited(true);
      de = de.getNext();
    } while (de !== startDe);
  }

  buildEdgeRings(dirEdges) {
    const edgeRings = new ArrayList();

    for (let it = dirEdges.iterator(); it.hasNext();) {
      const de = it.next();

      if (de.isInResult() && de.getEdgeRing() === null) {
        const er = new MaximalEdgeRing(de, this._geometryFactory);
        er.linkDirectedEdgesForMinimalEdgeRings();
        const minEdgeRings = er.buildMinimalRings();
        edgeRings.addAll(minEdgeRings);
      }
    }

    return edgeRings;
  }

  hasUnvisitedShellEdge(edgeRings) {
    for (let i = 0; i < edgeRings.size(); i++) {
      const er = edgeRings.get(i);
      if (er.isHole()) continue;
      const edges = er.getEdges();
      let de = edges.get(0);
      if (de.getLabel().getLocation(0, Position.RIGHT) !== Location.INTERIOR) continue;

      for (let j = 0; j < edges.size(); j++) {
        de = edges.get(j);

        if (!de.isVisited()) {
          this._disconnectedRingcoord = de.getCoordinate();
          return true;
        }
      }
    }

    return false;
  }

  isInteriorsConnected() {
    const splitEdges = new ArrayList();

    this._geomGraph.computeSplitEdges(splitEdges);

    const graph = new PlanarGraph$1(new OverlayNodeFactory());
    graph.addEdges(splitEdges);
    this.setInteriorEdgesInResult(graph);
    graph.linkResultDirectedEdges();
    const edgeRings = this.buildEdgeRings(graph.getEdgeEnds());
    this.visitShellInteriors(this._geomGraph.getGeometry(), graph);
    return !this.hasUnvisitedShellEdge(edgeRings);
  }

}

class EdgeEndBuilder {
  createEdgeEndForNext(edge, l, eiCurr, eiNext) {
    const iNext = eiCurr.segmentIndex + 1;
    if (iNext >= edge.getNumPoints() && eiNext === null) return null;
    let pNext = edge.getCoordinate(iNext);
    if (eiNext !== null && eiNext.segmentIndex === eiCurr.segmentIndex) pNext = eiNext.coord;
    const e = new EdgeEnd(edge, eiCurr.coord, pNext, new Label(edge.getLabel()));
    l.add(e);
  }

  createEdgeEndForPrev(edge, l, eiCurr, eiPrev) {
    let iPrev = eiCurr.segmentIndex;

    if (eiCurr.dist === 0.0) {
      if (iPrev === 0) return null;
      iPrev--;
    }

    let pPrev = edge.getCoordinate(iPrev);
    if (eiPrev !== null && eiPrev.segmentIndex >= iPrev) pPrev = eiPrev.coord;
    const label = new Label(edge.getLabel());
    label.flip();
    const e = new EdgeEnd(edge, eiCurr.coord, pPrev, label);
    l.add(e);
  }

  computeEdgeEnds() {
    if (arguments.length === 1) {
      const edges = arguments[0];
      const l = new ArrayList();

      for (let i = edges; i.hasNext();) {
        const e = i.next();
        this.computeEdgeEnds(e, l);
      }

      return l;
    } else if (arguments.length === 2) {
      const edge = arguments[0],
            l = arguments[1];
      const eiList = edge.getEdgeIntersectionList();
      eiList.addEndpoints();
      const it = eiList.iterator();
      let eiPrev = null;
      let eiCurr = null;
      if (!it.hasNext()) return null;
      let eiNext = it.next();

      do {
        eiPrev = eiCurr;
        eiCurr = eiNext;
        eiNext = null;
        if (it.hasNext()) eiNext = it.next();

        if (eiCurr !== null) {
          this.createEdgeEndForPrev(edge, l, eiCurr, eiPrev);
          this.createEdgeEndForNext(edge, l, eiCurr, eiNext);
        }
      } while (eiCurr !== null);
    }
  }

}

class EdgeEndBundle extends EdgeEnd {
  constructor() {
    super();
    EdgeEndBundle.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._edgeEnds = new ArrayList();

    if (arguments.length === 1) {
      const e = arguments[0];
      EdgeEndBundle.constructor_.call(this, null, e);
    } else if (arguments.length === 2) {
      const e = arguments[1];
      EdgeEnd.constructor_.call(this, e.getEdge(), e.getCoordinate(), e.getDirectedCoordinate(), new Label(e.getLabel()));
      this.insert(e);
    }
  }

  insert(e) {
    this._edgeEnds.add(e);
  }

  print(out) {
    out.println('EdgeEndBundle--> Label: ' + this._label);

    for (let it = this.iterator(); it.hasNext();) {
      const ee = it.next();
      ee.print(out);
      out.println();
    }
  }

  iterator() {
    return this._edgeEnds.iterator();
  }

  getEdgeEnds() {
    return this._edgeEnds;
  }

  computeLabelOn(geomIndex, boundaryNodeRule) {
    let boundaryCount = 0;
    let foundInterior = false;

    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();
      const loc = e.getLabel().getLocation(geomIndex);
      if (loc === Location.BOUNDARY) boundaryCount++;
      if (loc === Location.INTERIOR) foundInterior = true;
    }

    let loc = Location.NONE;
    if (foundInterior) loc = Location.INTERIOR;
    if (boundaryCount > 0) loc = GeometryGraph.determineBoundary(boundaryNodeRule, boundaryCount);

    this._label.setLocation(geomIndex, loc);
  }

  computeLabelSide(geomIndex, side) {
    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();

      if (e.getLabel().isArea()) {
        const loc = e.getLabel().getLocation(geomIndex, side);

        if (loc === Location.INTERIOR) {
          this._label.setLocation(geomIndex, side, Location.INTERIOR);

          return null;
        } else if (loc === Location.EXTERIOR) {
          this._label.setLocation(geomIndex, side, Location.EXTERIOR);
        }
      }
    }
  }

  getLabel() {
    return this._label;
  }

  computeLabelSides(geomIndex) {
    this.computeLabelSide(geomIndex, Position.LEFT);
    this.computeLabelSide(geomIndex, Position.RIGHT);
  }

  updateIM(im) {
    Edge$1.updateIM(this._label, im);
  }

  computeLabel(boundaryNodeRule) {
    let isArea = false;

    for (let it = this.iterator(); it.hasNext();) {
      const e = it.next();
      if (e.getLabel().isArea()) isArea = true;
    }

    if (isArea) this._label = new Label(Location.NONE, Location.NONE, Location.NONE);else this._label = new Label(Location.NONE);

    for (let i = 0; i < 2; i++) {
      this.computeLabelOn(i, boundaryNodeRule);
      if (isArea) this.computeLabelSides(i);
    }
  }

}

class EdgeEndBundleStar extends EdgeEndStar {
  constructor() {
    super();
  }

  updateIM(im) {
    for (let it = this.iterator(); it.hasNext();) {
      const esb = it.next();
      esb.updateIM(im);
    }
  }

  insert(e) {
    let eb = this._edgeMap.get(e);

    if (eb === null) {
      eb = new EdgeEndBundle(e);
      this.insertEdgeEnd(e, eb);
    } else {
      eb.insert(e);
    }
  }

}

class RelateNode extends Node$2 {
  constructor() {
    super();
    RelateNode.constructor_.apply(this, arguments);
  }

  static constructor_() {
    const coord = arguments[0],
          edges = arguments[1];
    Node$2.constructor_.call(this, coord, edges);
  }

  updateIMFromEdges(im) {
    this._edges.updateIM(im);
  }

  computeIM(im) {
    im.setAtLeastIfValid(this._label.getLocation(0), this._label.getLocation(1), 0);
  }

}

class RelateNodeFactory extends NodeFactory {
  constructor() {
    super();
  }

  createNode(coord) {
    return new RelateNode(coord, new EdgeEndBundleStar());
  }

}

class RelateNodeGraph {
  constructor() {
    RelateNodeGraph.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._nodes = new NodeMap$1(new RelateNodeFactory());
  }

  insertEdgeEnds(ee) {
    for (let i = ee.iterator(); i.hasNext();) {
      const e = i.next();

      this._nodes.add(e);
    }
  }

  getNodeIterator() {
    return this._nodes.iterator();
  }

  copyNodesAndLabels(geomGraph, argIndex) {
    for (let nodeIt = geomGraph.getNodeIterator(); nodeIt.hasNext();) {
      const graphNode = nodeIt.next();

      const newNode = this._nodes.addNode(graphNode.getCoordinate());

      newNode.setLabel(argIndex, graphNode.getLabel().getLocation(argIndex));
    }
  }

  build(geomGraph) {
    this.computeIntersectionNodes(geomGraph, 0);
    this.copyNodesAndLabels(geomGraph, 0);
    const eeBuilder = new EdgeEndBuilder();
    const eeList = eeBuilder.computeEdgeEnds(geomGraph.getEdgeIterator());
    this.insertEdgeEnds(eeList);
  }

  computeIntersectionNodes(geomGraph, argIndex) {
    for (let edgeIt = geomGraph.getEdgeIterator(); edgeIt.hasNext();) {
      const e = edgeIt.next();
      const eLoc = e.getLabel().getLocation(argIndex);

      for (let eiIt = e.getEdgeIntersectionList().iterator(); eiIt.hasNext();) {
        const ei = eiIt.next();

        const n = this._nodes.addNode(ei.coord);

        if (eLoc === Location.BOUNDARY) n.setLabelBoundary(argIndex);else if (n.getLabel().isNull(argIndex)) n.setLabel(argIndex, Location.INTERIOR);
      }
    }
  }

}

class ConsistentAreaTester {
  constructor() {
    ConsistentAreaTester.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._li = new RobustLineIntersector();
    this._geomGraph = null;
    this._nodeGraph = new RelateNodeGraph();
    this._invalidPoint = null;
    const geomGraph = arguments[0];
    this._geomGraph = geomGraph;
  }

  isNodeEdgeAreaLabelsConsistent() {
    for (let nodeIt = this._nodeGraph.getNodeIterator(); nodeIt.hasNext();) {
      const node = nodeIt.next();

      if (!node.getEdges().isAreaLabelsConsistent(this._geomGraph)) {
        this._invalidPoint = node.getCoordinate().copy();
        return false;
      }
    }

    return true;
  }

  getInvalidPoint() {
    return this._invalidPoint;
  }

  hasDuplicateRings() {
    for (let nodeIt = this._nodeGraph.getNodeIterator(); nodeIt.hasNext();) {
      const node = nodeIt.next();

      for (let i = node.getEdges().iterator(); i.hasNext();) {
        const eeb = i.next();

        if (eeb.getEdgeEnds().size() > 1) {
          this._invalidPoint = eeb.getEdge().getCoordinate(0);
          return true;
        }
      }
    }

    return false;
  }

  isNodeConsistentArea() {
    const intersector = this._geomGraph.computeSelfNodes(this._li, true, true);

    if (intersector.hasProperIntersection()) {
      this._invalidPoint = intersector.getProperIntersectionPoint();
      return false;
    }

    this._nodeGraph.build(this._geomGraph);

    return this.isNodeEdgeAreaLabelsConsistent();
  }

}

class IndexedNestedRingTester {
  constructor() {
    IndexedNestedRingTester.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._graph = null;
    this._rings = new ArrayList();
    this._totalEnv = new Envelope();
    this._index = null;
    this._nestedPt = null;
    const graph = arguments[0];
    this._graph = graph;
  }

  buildIndex() {
    this._index = new STRtree();

    for (let i = 0; i < this._rings.size(); i++) {
      const ring = this._rings.get(i);

      const env = ring.getEnvelopeInternal();

      this._index.insert(env, ring);
    }
  }

  getNestedPoint() {
    return this._nestedPt;
  }

  isNonNested() {
    this.buildIndex();

    for (let i = 0; i < this._rings.size(); i++) {
      const innerRing = this._rings.get(i);

      const innerRingPts = innerRing.getCoordinates();

      const results = this._index.query(innerRing.getEnvelopeInternal());

      for (let j = 0; j < results.size(); j++) {
        const searchRing = results.get(j);
        const searchRingPts = searchRing.getCoordinates();
        if (innerRing === searchRing) continue;
        if (!innerRing.getEnvelopeInternal().intersects(searchRing.getEnvelopeInternal())) continue;
        const innerRingPt = IsValidOp.findPtNotNode(innerRingPts, searchRing, this._graph);
        if (innerRingPt === null) continue;
        const isInside = PointLocation.isInRing(innerRingPt, searchRingPts);

        if (isInside) {
          this._nestedPt = innerRingPt;
          return false;
        }
      }
    }

    return true;
  }

  add(ring) {
    this._rings.add(ring);

    this._totalEnv.expandToInclude(ring.getEnvelopeInternal());
  }

}

class TopologyValidationError {
  constructor() {
    TopologyValidationError.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._errorType = null;
    this._pt = null;

    if (arguments.length === 1) {
      const errorType = arguments[0];
      TopologyValidationError.constructor_.call(this, errorType, null);
    } else if (arguments.length === 2) {
      const errorType = arguments[0],
            pt = arguments[1];
      this._errorType = errorType;
      if (pt !== null) this._pt = pt.copy();
    }
  }

  getErrorType() {
    return this._errorType;
  }

  getMessage() {
    return TopologyValidationError.errMsg[this._errorType];
  }

  getCoordinate() {
    return this._pt;
  }

  toString() {
    let locStr = '';
    if (this._pt !== null) locStr = ' at or near point ' + this._pt;
    return this.getMessage() + locStr;
  }

}
TopologyValidationError.ERROR = 0;
TopologyValidationError.REPEATED_POINT = 1;
TopologyValidationError.HOLE_OUTSIDE_SHELL = 2;
TopologyValidationError.NESTED_HOLES = 3;
TopologyValidationError.DISCONNECTED_INTERIOR = 4;
TopologyValidationError.SELF_INTERSECTION = 5;
TopologyValidationError.RING_SELF_INTERSECTION = 6;
TopologyValidationError.NESTED_SHELLS = 7;
TopologyValidationError.DUPLICATE_RINGS = 8;
TopologyValidationError.TOO_FEW_POINTS = 9;
TopologyValidationError.INVALID_COORDINATE = 10;
TopologyValidationError.RING_NOT_CLOSED = 11;
TopologyValidationError.errMsg = ['Topology Validation Error', 'Repeated Point', 'Hole lies outside shell', 'Holes are nested', 'Interior is disconnected', 'Self-intersection', 'Ring Self-intersection', 'Nested shells', 'Duplicate Rings', 'Too few distinct points in geometry component', 'Invalid Coordinate', 'Ring is not closed'];

class IsValidOp {
  constructor() {
    IsValidOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._parentGeometry = null;
    this._isSelfTouchingRingFormingHoleValid = false;
    this._validErr = null;
    const parentGeometry = arguments[0];
    this._parentGeometry = parentGeometry;
  }

  static findPtNotNode(testCoords, searchRing, graph) {
    const searchEdge = graph.findEdge(searchRing);
    const eiList = searchEdge.getEdgeIntersectionList();

    for (let i = 0; i < testCoords.length; i++) {
      const pt = testCoords[i];
      if (!eiList.isIntersection(pt)) return pt;
    }

    return null;
  }

  static isValid() {
    if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];
      const isValidOp = new IsValidOp(geom);
      return isValidOp.isValid();
    } else if (arguments[0] instanceof Coordinate) {
      const coord = arguments[0];
      if (Double.isNaN(coord.x)) return false;
      if (Double.isInfinite(coord.x)) return false;
      if (Double.isNaN(coord.y)) return false;
      if (Double.isInfinite(coord.y)) return false;
      return true;
    }
  }

  checkInvalidCoordinates() {
    if (arguments[0] instanceof Array) {
      const coords = arguments[0];

      for (let i = 0; i < coords.length; i++) if (!IsValidOp.isValid(coords[i])) {
        this._validErr = new TopologyValidationError(TopologyValidationError.INVALID_COORDINATE, coords[i]);
        return null;
      }
    } else if (arguments[0] instanceof Polygon) {
      const poly = arguments[0];
      this.checkInvalidCoordinates(poly.getExteriorRing().getCoordinates());
      if (this._validErr !== null) return null;

      for (let i = 0; i < poly.getNumInteriorRing(); i++) {
        this.checkInvalidCoordinates(poly.getInteriorRingN(i).getCoordinates());
        if (this._validErr !== null) return null;
      }
    }
  }

  checkHolesNotNested(p, graph) {
    if (p.getNumInteriorRing() <= 0) return null;
    const nestedTester = new IndexedNestedRingTester(graph);

    for (let i = 0; i < p.getNumInteriorRing(); i++) {
      const innerHole = p.getInteriorRingN(i);
      if (innerHole.isEmpty()) continue;
      nestedTester.add(innerHole);
    }

    const isNonNested = nestedTester.isNonNested();
    if (!isNonNested) this._validErr = new TopologyValidationError(TopologyValidationError.NESTED_HOLES, nestedTester.getNestedPoint());
  }

  checkConsistentArea(graph) {
    const cat = new ConsistentAreaTester(graph);
    const isValidArea = cat.isNodeConsistentArea();

    if (!isValidArea) {
      this._validErr = new TopologyValidationError(TopologyValidationError.SELF_INTERSECTION, cat.getInvalidPoint());
      return null;
    }

    if (cat.hasDuplicateRings()) this._validErr = new TopologyValidationError(TopologyValidationError.DUPLICATE_RINGS, cat.getInvalidPoint());
  }

  isValid() {
    this.checkValid(this._parentGeometry);
    return this._validErr === null;
  }

  checkShellInsideHole(shell, hole, graph) {
    const shellPts = shell.getCoordinates();
    const holePts = hole.getCoordinates();
    const shellPt = IsValidOp.findPtNotNode(shellPts, hole, graph);

    if (shellPt !== null) {
      const insideHole = PointLocation.isInRing(shellPt, holePts);
      if (!insideHole) return shellPt;
    }

    const holePt = IsValidOp.findPtNotNode(holePts, shell, graph);

    if (holePt !== null) {
      const insideShell = PointLocation.isInRing(holePt, shellPts);
      if (insideShell) return holePt;
      return null;
    }

    Assert.shouldNeverReachHere('points in shell and hole appear to be equal');
    return null;
  }

  checkNoSelfIntersectingRings(graph) {
    for (let i = graph.getEdgeIterator(); i.hasNext();) {
      const e = i.next();
      this.checkNoSelfIntersectingRing(e.getEdgeIntersectionList());
      if (this._validErr !== null) return null;
    }
  }

  checkConnectedInteriors(graph) {
    const cit = new ConnectedInteriorTester(graph);
    if (!cit.isInteriorsConnected()) this._validErr = new TopologyValidationError(TopologyValidationError.DISCONNECTED_INTERIOR, cit.getCoordinate());
  }

  checkNoSelfIntersectingRing(eiList) {
    const nodeSet = new TreeSet();
    let isFirst = true;

    for (let i = eiList.iterator(); i.hasNext();) {
      const ei = i.next();

      if (isFirst) {
        isFirst = false;
        continue;
      }

      if (nodeSet.contains(ei.coord)) {
        this._validErr = new TopologyValidationError(TopologyValidationError.RING_SELF_INTERSECTION, ei.coord);
        return null;
      } else {
        nodeSet.add(ei.coord);
      }
    }
  }

  checkHolesInShell(p, graph) {
    if (p.getNumInteriorRing() <= 0) return null;
    const shell = p.getExteriorRing();
    const isShellEmpty = shell.isEmpty();
    const pir = new IndexedPointInAreaLocator(shell);

    for (let i = 0; i < p.getNumInteriorRing(); i++) {
      const hole = p.getInteriorRingN(i);
      let holePt = null;
      if (hole.isEmpty()) continue;
      holePt = IsValidOp.findPtNotNode(hole.getCoordinates(), shell, graph);
      if (holePt === null) return null;
      const outside = isShellEmpty || Location.EXTERIOR === pir.locate(holePt);

      if (outside) {
        this._validErr = new TopologyValidationError(TopologyValidationError.HOLE_OUTSIDE_SHELL, holePt);
        return null;
      }
    }
  }

  checkTooFewPoints(graph) {
    if (graph.hasTooFewPoints()) {
      this._validErr = new TopologyValidationError(TopologyValidationError.TOO_FEW_POINTS, graph.getInvalidPoint());
      return null;
    }
  }

  getValidationError() {
    this.checkValid(this._parentGeometry);
    return this._validErr;
  }

  checkValid() {
    if (arguments[0] instanceof Point) {
      const g = arguments[0];
      this.checkInvalidCoordinates(g.getCoordinates());
    } else if (arguments[0] instanceof MultiPoint) {
      const g = arguments[0];
      this.checkInvalidCoordinates(g.getCoordinates());
    } else if (arguments[0] instanceof LinearRing) {
      const g = arguments[0];
      this.checkInvalidCoordinates(g.getCoordinates());
      if (this._validErr !== null) return null;
      this.checkClosedRing(g);
      if (this._validErr !== null) return null;
      const graph = new GeometryGraph(0, g);
      this.checkTooFewPoints(graph);
      if (this._validErr !== null) return null;
      const li = new RobustLineIntersector();
      graph.computeSelfNodes(li, true, true);
      this.checkNoSelfIntersectingRings(graph);
    } else if (arguments[0] instanceof LineString) {
      const g = arguments[0];
      this.checkInvalidCoordinates(g.getCoordinates());
      if (this._validErr !== null) return null;
      const graph = new GeometryGraph(0, g);
      this.checkTooFewPoints(graph);
    } else if (arguments[0] instanceof Polygon) {
      const g = arguments[0];
      this.checkInvalidCoordinates(g);
      if (this._validErr !== null) return null;
      this.checkClosedRings(g);
      if (this._validErr !== null) return null;
      const graph = new GeometryGraph(0, g);
      this.checkTooFewPoints(graph);
      if (this._validErr !== null) return null;
      this.checkConsistentArea(graph);
      if (this._validErr !== null) return null;

      if (!this._isSelfTouchingRingFormingHoleValid) {
        this.checkNoSelfIntersectingRings(graph);
        if (this._validErr !== null) return null;
      }

      this.checkHolesInShell(g, graph);
      if (this._validErr !== null) return null;
      this.checkHolesNotNested(g, graph);
      if (this._validErr !== null) return null;
      this.checkConnectedInteriors(graph);
    } else if (arguments[0] instanceof MultiPolygon) {
      const g = arguments[0];

      for (let i = 0; i < g.getNumGeometries(); i++) {
        const p = g.getGeometryN(i);
        this.checkInvalidCoordinates(p);
        if (this._validErr !== null) return null;
        this.checkClosedRings(p);
        if (this._validErr !== null) return null;
      }

      const graph = new GeometryGraph(0, g);
      this.checkTooFewPoints(graph);
      if (this._validErr !== null) return null;
      this.checkConsistentArea(graph);
      if (this._validErr !== null) return null;

      if (!this._isSelfTouchingRingFormingHoleValid) {
        this.checkNoSelfIntersectingRings(graph);
        if (this._validErr !== null) return null;
      }

      for (let i = 0; i < g.getNumGeometries(); i++) {
        const p = g.getGeometryN(i);
        this.checkHolesInShell(p, graph);
        if (this._validErr !== null) return null;
      }

      for (let i = 0; i < g.getNumGeometries(); i++) {
        const p = g.getGeometryN(i);
        this.checkHolesNotNested(p, graph);
        if (this._validErr !== null) return null;
      }

      this.checkShellsNotNested(g, graph);
      if (this._validErr !== null) return null;
      this.checkConnectedInteriors(graph);
    } else if (arguments[0] instanceof GeometryCollection) {
      const gc = arguments[0];

      for (let i = 0; i < gc.getNumGeometries(); i++) {
        const g = gc.getGeometryN(i);
        this.checkValid(g);
        if (this._validErr !== null) return null;
      }
    } else if (arguments[0] instanceof Geometry) {
      const g = arguments[0];
      this._validErr = null;
      if (g.isEmpty()) return null;
      if (g instanceof Point) this.checkValid(g);else if (g instanceof MultiPoint) this.checkValid(g);else if (g instanceof LinearRing) this.checkValid(g);else if (g instanceof LineString) this.checkValid(g);else if (g instanceof Polygon) this.checkValid(g);else if (g instanceof MultiPolygon) this.checkValid(g);else if (g instanceof GeometryCollection) this.checkValid(g);else throw new UnsupportedOperationException(g.getGeometryType());
    }
  }

  setSelfTouchingRingFormingHoleValid(isValid) {
    this._isSelfTouchingRingFormingHoleValid = isValid;
  }

  checkShellNotNested(shell, p, graph) {
    const shellPts = shell.getCoordinates();
    const polyShell = p.getExteriorRing();
    if (polyShell.isEmpty()) return null;
    const polyPts = polyShell.getCoordinates();
    const shellPt = IsValidOp.findPtNotNode(shellPts, polyShell, graph);
    if (shellPt === null) return null;
    const insidePolyShell = PointLocation.isInRing(shellPt, polyPts);
    if (!insidePolyShell) return null;

    if (p.getNumInteriorRing() <= 0) {
      this._validErr = new TopologyValidationError(TopologyValidationError.NESTED_SHELLS, shellPt);
      return null;
    }

    let badNestedPt = null;

    for (let i = 0; i < p.getNumInteriorRing(); i++) {
      const hole = p.getInteriorRingN(i);
      badNestedPt = this.checkShellInsideHole(shell, hole, graph);
      if (badNestedPt === null) return null;
    }

    this._validErr = new TopologyValidationError(TopologyValidationError.NESTED_SHELLS, badNestedPt);
  }

  checkClosedRings(poly) {
    this.checkClosedRing(poly.getExteriorRing());
    if (this._validErr !== null) return null;

    for (let i = 0; i < poly.getNumInteriorRing(); i++) {
      this.checkClosedRing(poly.getInteriorRingN(i));
      if (this._validErr !== null) return null;
    }
  }

  checkClosedRing(ring) {
    if (ring.isEmpty()) return null;

    if (!ring.isClosed()) {
      let pt = null;
      if (ring.getNumPoints() >= 1) pt = ring.getCoordinateN(0);
      this._validErr = new TopologyValidationError(TopologyValidationError.RING_NOT_CLOSED, pt);
    }
  }

  checkShellsNotNested(mp, graph) {
    for (let i = 0; i < mp.getNumGeometries(); i++) {
      const p = mp.getGeometryN(i);
      const shell = p.getExteriorRing();

      for (let j = 0; j < mp.getNumGeometries(); j++) {
        if (i === j) continue;
        const p2 = mp.getGeometryN(j);
        this.checkShellNotNested(shell, p2, graph);
        if (this._validErr !== null) return null;
      }
    }
  }

}

class EdgeRing {
  constructor() {
    EdgeRing.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._factory = null;
    this._deList = new ArrayList();
    this._lowestEdge = null;
    this._ring = null;
    this._locator = null;
    this._ringPts = null;
    this._holes = null;
    this._shell = null;
    this._isHole = null;
    this._isProcessed = false;
    this._isIncludedSet = false;
    this._isIncluded = false;
    const factory = arguments[0];
    this._factory = factory;
  }

  static findDirEdgesInRing(startDE) {
    let de = startDE;
    const edges = new ArrayList();

    do {
      edges.add(de);
      de = de.getNext();
      Assert.isTrue(de !== null, 'found null DE in ring');
      Assert.isTrue(de === startDE || !de.isInRing(), 'found DE already in ring');
    } while (de !== startDE);

    return edges;
  }

  static addEdge(coords, isForward, coordList) {
    if (isForward) for (let i = 0; i < coords.length; i++) coordList.add(coords[i], false);else for (let i = coords.length - 1; i >= 0; i--) coordList.add(coords[i], false);
  }

  static findEdgeRingContaining(testEr, erList) {
    const testRing = testEr.getRing();
    const testEnv = testRing.getEnvelopeInternal();
    let testPt = testRing.getCoordinateN(0);
    let minRing = null;
    let minRingEnv = null;

    for (let it = erList.iterator(); it.hasNext();) {
      const tryEdgeRing = it.next();
      const tryRing = tryEdgeRing.getRing();
      const tryShellEnv = tryRing.getEnvelopeInternal();
      if (tryShellEnv.equals(testEnv)) continue;
      if (!tryShellEnv.contains(testEnv)) continue;
      testPt = CoordinateArrays.ptNotInList(testRing.getCoordinates(), tryEdgeRing.getCoordinates());
      const isContained = tryEdgeRing.isInRing(testPt);
      if (isContained) if (minRing === null || minRingEnv.contains(tryShellEnv)) {
        minRing = tryEdgeRing;
        minRingEnv = minRing.getRing().getEnvelopeInternal();
      }
    }

    return minRing;
  }

  isIncluded() {
    return this._isIncluded;
  }

  getCoordinates() {
    if (this._ringPts === null) {
      const coordList = new CoordinateList();

      for (let i = this._deList.iterator(); i.hasNext();) {
        const de = i.next();
        const edge = de.getEdge();
        EdgeRing.addEdge(edge.getLine().getCoordinates(), de.getEdgeDirection(), coordList);
      }

      this._ringPts = coordList.toCoordinateArray();
    }

    return this._ringPts;
  }

  isIncludedSet() {
    return this._isIncludedSet;
  }

  isValid() {
    this.getCoordinates();
    if (this._ringPts.length <= 3) return false;
    this.getRing();
    return IsValidOp.isValid(this._ring);
  }

  build(startDE) {
    let de = startDE;

    do {
      this.add(de);
      de.setRing(this);
      de = de.getNext();
      Assert.isTrue(de !== null, 'found null DE in ring');
      Assert.isTrue(de === startDE || !de.isInRing(), 'found DE already in ring');
    } while (de !== startDE);
  }

  isInRing(pt) {
    return Location.EXTERIOR !== this.getLocator().locate(pt);
  }

  isOuterHole() {
    if (!this._isHole) return false;
    return !this.hasShell();
  }

  getPolygon() {
    let holeLR = null;

    if (this._holes !== null) {
      holeLR = new Array(this._holes.size()).fill(null);

      for (let i = 0; i < this._holes.size(); i++) holeLR[i] = this._holes.get(i);
    }

    const poly = this._factory.createPolygon(this._ring, holeLR);

    return poly;
  }

  isHole() {
    return this._isHole;
  }

  isProcessed() {
    return this._isProcessed;
  }

  addHole() {
    if (arguments[0] instanceof LinearRing) {
      const hole = arguments[0];
      if (this._holes === null) this._holes = new ArrayList();

      this._holes.add(hole);
    } else if (arguments[0] instanceof EdgeRing) {
      const holeER = arguments[0];
      holeER.setShell(this);
      const hole = holeER.getRing();
      if (this._holes === null) this._holes = new ArrayList();

      this._holes.add(hole);
    }
  }

  setIncluded(isIncluded) {
    this._isIncluded = isIncluded;
    this._isIncludedSet = true;
  }

  getOuterHole() {
    if (this.isHole()) return null;

    for (let i = 0; i < this._deList.size(); i++) {
      const de = this._deList.get(i);

      const adjRing = de.getSym().getRing();
      if (adjRing.isOuterHole()) return adjRing;
    }

    return null;
  }

  computeHole() {
    const ring = this.getRing();
    this._isHole = Orientation.isCCW(ring.getCoordinates());
  }

  hasShell() {
    return this._shell !== null;
  }

  isOuterShell() {
    return this.getOuterHole() !== null;
  }

  getLineString() {
    this.getCoordinates();
    return this._factory.createLineString(this._ringPts);
  }

  toString() {
    return WKTWriter.toLineString(new CoordinateArraySequence(this.getCoordinates()));
  }

  getLocator() {
    if (this._locator === null) this._locator = new IndexedPointInAreaLocator(this.getRing());
    return this._locator;
  }

  getShell() {
    if (this.isHole()) return this._shell;
    return this;
  }

  add(de) {
    this._deList.add(de);
  }

  getRing() {
    if (this._ring !== null) return this._ring;
    this.getCoordinates();
    if (this._ringPts.length < 3) System.out.println(this._ringPts);

    try {
      this._ring = this._factory.createLinearRing(this._ringPts);
    } catch (ex) {
      if (ex instanceof Exception) System.out.println(this._ringPts);else throw ex;
    } finally {}

    return this._ring;
  }

  updateIncluded() {
    if (this.isHole()) return null;

    for (let i = 0; i < this._deList.size(); i++) {
      const de = this._deList.get(i);

      const adjShell = de.getSym().getRing().getShell();

      if (adjShell !== null && adjShell.isIncludedSet()) {
        this.setIncluded(!adjShell.isIncluded());
        return null;
      }
    }
  }

  setShell(shell) {
    this._shell = shell;
  }

  setProcessed(isProcessed) {
    this._isProcessed = isProcessed;
  }

}

class EnvelopeComparator {
  compare(obj0, obj1) {
    const r0 = obj0;
    const r1 = obj1;
    return r0.getRing().getEnvelope().compareTo(r1.getRing().getEnvelope());
  }

  get interfaces_() {
    return [Comparator];
  }

}

EdgeRing.EnvelopeComparator = EnvelopeComparator;

class PolygonizeGraph extends PlanarGraph {
  constructor() {
    super();
    PolygonizeGraph.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._factory = null;
    const factory = arguments[0];
    this._factory = factory;
  }

  static findLabeledEdgeRings(dirEdges) {
    const edgeRingStarts = new ArrayList();
    let currLabel = 1;

    for (let i = dirEdges.iterator(); i.hasNext();) {
      const de = i.next();
      if (de.isMarked()) continue;
      if (de.getLabel() >= 0) continue;
      edgeRingStarts.add(de);
      const edges = EdgeRing.findDirEdgesInRing(de);
      PolygonizeGraph.label(edges, currLabel);
      currLabel++;
    }

    return edgeRingStarts;
  }

  static getDegreeNonDeleted(node) {
    const edges = node.getOutEdges().getEdges();
    let degree = 0;

    for (let i = edges.iterator(); i.hasNext();) {
      const de = i.next();
      if (!de.isMarked()) degree++;
    }

    return degree;
  }

  static deleteAllEdges(node) {
    const edges = node.getOutEdges().getEdges();

    for (let i = edges.iterator(); i.hasNext();) {
      const de = i.next();
      de.setMarked(true);
      const sym = de.getSym();
      if (sym !== null) sym.setMarked(true);
    }
  }

  static label(dirEdges, label) {
    for (let i = dirEdges.iterator(); i.hasNext();) {
      const de = i.next();
      de.setLabel(label);
    }
  }

  static computeNextCWEdges(node) {
    const deStar = node.getOutEdges();
    let startDE = null;
    let prevDE = null;

    for (let i = deStar.getEdges().iterator(); i.hasNext();) {
      const outDE = i.next();
      if (outDE.isMarked()) continue;
      if (startDE === null) startDE = outDE;

      if (prevDE !== null) {
        const sym = prevDE.getSym();
        sym.setNext(outDE);
      }

      prevDE = outDE;
    }

    if (prevDE !== null) {
      const sym = prevDE.getSym();
      sym.setNext(startDE);
    }
  }

  static computeNextCCWEdges(node, label) {
    const deStar = node.getOutEdges();
    let firstOutDE = null;
    let prevInDE = null;
    const edges = deStar.getEdges();

    for (let i = edges.size() - 1; i >= 0; i--) {
      const de = edges.get(i);
      const sym = de.getSym();
      let outDE = null;
      if (de.getLabel() === label) outDE = de;
      let inDE = null;
      if (sym.getLabel() === label) inDE = sym;
      if (outDE === null && inDE === null) continue;
      if (inDE !== null) prevInDE = inDE;

      if (outDE !== null) {
        if (prevInDE !== null) {
          prevInDE.setNext(outDE);
          prevInDE = null;
        }

        if (firstOutDE === null) firstOutDE = outDE;
      }
    }

    if (prevInDE !== null) {
      Assert.isTrue(firstOutDE !== null);
      prevInDE.setNext(firstOutDE);
    }
  }

  static getDegree(node, label) {
    const edges = node.getOutEdges().getEdges();
    let degree = 0;

    for (let i = edges.iterator(); i.hasNext();) {
      const de = i.next();
      if (de.getLabel() === label) degree++;
    }

    return degree;
  }

  static findIntersectionNodes(startDE, label) {
    let de = startDE;
    let intNodes = null;

    do {
      const node = de.getFromNode();

      if (PolygonizeGraph.getDegree(node, label) > 1) {
        if (intNodes === null) intNodes = new ArrayList();
        intNodes.add(node);
      }

      de = de.getNext();
      Assert.isTrue(de !== null, 'found null DE in ring');
      Assert.isTrue(de === startDE || !de.isInRing(), 'found DE already in ring');
    } while (de !== startDE);

    return intNodes;
  }

  findEdgeRing(startDE) {
    const er = new EdgeRing(this._factory);
    er.build(startDE);
    return er;
  }

  computeDepthParity() {
    if (arguments.length === 0) {
      while (true) {
        return null;
      }
    }
  }

  computeNextCWEdges() {
    for (let iNode = this.nodeIterator(); iNode.hasNext();) {
      const node = iNode.next();
      PolygonizeGraph.computeNextCWEdges(node);
    }
  }

  addEdge(line) {
    if (line.isEmpty()) return null;
    const linePts = CoordinateArrays.removeRepeatedPoints(line.getCoordinates());
    if (linePts.length < 2) return null;
    const startPt = linePts[0];
    const endPt = linePts[linePts.length - 1];
    const nStart = this.getNode(startPt);
    const nEnd = this.getNode(endPt);
    const de0 = new PolygonizeDirectedEdge(nStart, nEnd, linePts[1], true);
    const de1 = new PolygonizeDirectedEdge(nEnd, nStart, linePts[linePts.length - 2], false);
    const edge = new PolygonizeEdge(line);
    edge.setDirectedEdges(de0, de1);
    this.add(edge);
  }

  deleteCutEdges() {
    this.computeNextCWEdges();
    PolygonizeGraph.findLabeledEdgeRings(this._dirEdges);
    const cutLines = new ArrayList();

    for (let i = this._dirEdges.iterator(); i.hasNext();) {
      const de = i.next();
      if (de.isMarked()) continue;
      const sym = de.getSym();

      if (de.getLabel() === sym.getLabel()) {
        de.setMarked(true);
        sym.setMarked(true);
        const e = de.getEdge();
        cutLines.add(e.getLine());
      }
    }

    return cutLines;
  }

  getEdgeRings() {
    this.computeNextCWEdges();
    PolygonizeGraph.label(this._dirEdges, -1);
    const maximalRings = PolygonizeGraph.findLabeledEdgeRings(this._dirEdges);
    this.convertMaximalToMinimalEdgeRings(maximalRings);
    const edgeRingList = new ArrayList();

    for (let i = this._dirEdges.iterator(); i.hasNext();) {
      const de = i.next();
      if (de.isMarked()) continue;
      if (de.isInRing()) continue;
      const er = this.findEdgeRing(de);
      edgeRingList.add(er);
    }

    return edgeRingList;
  }

  getNode(pt) {
    let node = this.findNode(pt);

    if (node === null) {
      node = new Node(pt);
      this.add(node);
    }

    return node;
  }

  convertMaximalToMinimalEdgeRings(ringEdges) {
    for (let i = ringEdges.iterator(); i.hasNext();) {
      const de = i.next();
      const label = de.getLabel();
      const intNodes = PolygonizeGraph.findIntersectionNodes(de, label);
      if (intNodes === null) continue;

      for (let iNode = intNodes.iterator(); iNode.hasNext();) {
        const node = iNode.next();
        PolygonizeGraph.computeNextCCWEdges(node, label);
      }
    }
  }

  deleteDangles() {
    const nodesToRemove = this.findNodesOfDegree(1);
    const dangleLines = new HashSet();
    const nodeStack = new Stack();

    for (let i = nodesToRemove.iterator(); i.hasNext();) nodeStack.push(i.next());

    while (!nodeStack.isEmpty()) {
      const node = nodeStack.pop();
      PolygonizeGraph.deleteAllEdges(node);
      const nodeOutEdges = node.getOutEdges().getEdges();

      for (let i = nodeOutEdges.iterator(); i.hasNext();) {
        const de = i.next();
        de.setMarked(true);
        const sym = de.getSym();
        if (sym !== null) sym.setMarked(true);
        const e = de.getEdge();
        dangleLines.add(e.getLine());
        const toNode = de.getToNode();
        if (PolygonizeGraph.getDegreeNonDeleted(toNode) === 1) nodeStack.push(toNode);
      }
    }

    return dangleLines;
  }

}

class HoleAssigner {
  constructor() {
    HoleAssigner.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._shells = null;
    this._shellIndex = null;
    const shells = arguments[0];
    this._shells = shells;
    this.buildIndex();
  }

  static assignHolesToShells(holes, shells) {
    const assigner = new HoleAssigner(shells);
    assigner.assignHolesToShells(holes);
  }

  assignHolesToShells(holeList) {
    for (let i = holeList.iterator(); i.hasNext();) {
      const holeER = i.next();
      this.assignHoleToShell(holeER);
    }
  }

  buildIndex() {
    this._shellIndex = new STRtree();

    for (const shell of this._shells) this._shellIndex.insert(shell.getRing().getEnvelopeInternal(), shell);
  }

  queryOverlappingShells(ringEnv) {
    return this._shellIndex.query(ringEnv);
  }

  findShellContaining(testEr) {
    const testEnv = testEr.getRing().getEnvelopeInternal();
    const candidateShells = this.queryOverlappingShells(testEnv);
    return EdgeRing.findEdgeRingContaining(testEr, candidateShells);
  }

  assignHoleToShell(holeER) {
    const shell = this.findShellContaining(holeER);
    if (shell !== null) shell.addHole(holeER);
  }

}

class Polygonizer {
  constructor() {
    Polygonizer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._lineStringAdder = new LineStringAdder(this);
    this._graph = null;
    this._dangles = new ArrayList();
    this._cutEdges = new ArrayList();
    this._invalidRingLines = new ArrayList();
    this._holeList = null;
    this._shellList = null;
    this._polyList = null;
    this._isCheckingRingsValid = true;
    this._extractOnlyPolygonal = null;
    this._geomFactory = null;

    if (arguments.length === 0) {
      Polygonizer.constructor_.call(this, false);
    } else if (arguments.length === 1) {
      const extractOnlyPolygonal = arguments[0];
      this._extractOnlyPolygonal = extractOnlyPolygonal;
    }
  }

  static extractPolygons(shellList, includeAll) {
    const polyList = new ArrayList();

    for (let i = shellList.iterator(); i.hasNext();) {
      const er = i.next();
      if (includeAll || er.isIncluded()) polyList.add(er.getPolygon());
    }

    return polyList;
  }

  static findOuterShells(shellList) {
    for (let i = shellList.iterator(); i.hasNext();) {
      const er = i.next();
      const outerHoleER = er.getOuterHole();

      if (outerHoleER !== null && !outerHoleER.isProcessed()) {
        er.setIncluded(true);
        outerHoleER.setProcessed(true);
      }
    }
  }

  static findDisjointShells(shellList) {
    Polygonizer.findOuterShells(shellList);
    let isMoreToScan = null;

    do {
      isMoreToScan = false;

      for (let i = shellList.iterator(); i.hasNext();) {
        const er = i.next();
        if (er.isIncludedSet()) continue;
        er.updateIncluded();
        if (!er.isIncludedSet()) isMoreToScan = true;
      }
    } while (isMoreToScan);
  }

  getGeometry() {
    if (this._geomFactory === null) this._geomFactory = new GeometryFactory();
    this.polygonize();
    if (this._extractOnlyPolygonal) return this._geomFactory.buildGeometry(this._polyList);
    return this._geomFactory.createGeometryCollection(GeometryFactory.toGeometryArray(this._polyList));
  }

  getInvalidRingLines() {
    this.polygonize();
    return this._invalidRingLines;
  }

  findValidRings(edgeRingList, validEdgeRingList, invalidRingList) {
    for (let i = edgeRingList.iterator(); i.hasNext();) {
      const er = i.next();
      if (er.isValid()) validEdgeRingList.add(er);else invalidRingList.add(er.getLineString());
    }
  }

  polygonize() {
    if (this._polyList !== null) return null;
    this._polyList = new ArrayList();
    if (this._graph === null) return null;
    this._dangles = this._graph.deleteDangles();
    this._cutEdges = this._graph.deleteCutEdges();

    const edgeRingList = this._graph.getEdgeRings();

    let validEdgeRingList = new ArrayList();
    this._invalidRingLines = new ArrayList();
    if (this._isCheckingRingsValid) this.findValidRings(edgeRingList, validEdgeRingList, this._invalidRingLines);else validEdgeRingList = edgeRingList;
    this.findShellsAndHoles(validEdgeRingList);
    HoleAssigner.assignHolesToShells(this._holeList, this._shellList);
    Collections.sort(this._shellList, new EdgeRing.EnvelopeComparator());
    let includeAll = true;

    if (this._extractOnlyPolygonal) {
      Polygonizer.findDisjointShells(this._shellList);
      includeAll = false;
    }

    this._polyList = Polygonizer.extractPolygons(this._shellList, includeAll);
  }

  getDangles() {
    this.polygonize();
    return this._dangles;
  }

  getCutEdges() {
    this.polygonize();
    return this._cutEdges;
  }

  getPolygons() {
    this.polygonize();
    return this._polyList;
  }

  add() {
    if (hasInterface(arguments[0], Collection)) {
      const geomList = arguments[0];

      for (let i = geomList.iterator(); i.hasNext();) {
        const geometry = i.next();
        this.add(geometry);
      }
    } else if (arguments[0] instanceof LineString) {
      const line = arguments[0];
      this._geomFactory = line.getFactory();
      if (this._graph === null) this._graph = new PolygonizeGraph(this._geomFactory);

      this._graph.addEdge(line);
    } else if (arguments[0] instanceof Geometry) {
      const g = arguments[0];
      g.apply(this._lineStringAdder);
    }
  }

  setCheckRingsValid(isCheckingRingsValid) {
    this._isCheckingRingsValid = isCheckingRingsValid;
  }

  findShellsAndHoles(edgeRingList) {
    this._holeList = new ArrayList();
    this._shellList = new ArrayList();

    for (let i = edgeRingList.iterator(); i.hasNext();) {
      const er = i.next();
      er.computeHole();
      if (er.isHole()) this._holeList.add(er);else this._shellList.add(er);
    }
  }

}

class LineStringAdder {
  constructor() {
    LineStringAdder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.p = null;
    const p = arguments[0];
    this.p = p;
  }

  filter(g) {
    if (g instanceof LineString) this.p.add(g);
  }

  get interfaces_() {
    return [GeometryComponentFilter];
  }

}

Polygonizer.LineStringAdder = LineStringAdder;

var polygonize = /*#__PURE__*/Object.freeze({
  __proto__: null,
  Polygonizer: Polygonizer
});

class RelateComputer {
  constructor() {
    RelateComputer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._li = new RobustLineIntersector();
    this._ptLocator = new PointLocator();
    this._arg = null;
    this._nodes = new NodeMap$1(new RelateNodeFactory());
    this._im = null;
    this._isolatedEdges = new ArrayList();
    this._invalidPoint = null;
    const arg = arguments[0];
    this._arg = arg;
  }

  insertEdgeEnds(ee) {
    for (let i = ee.iterator(); i.hasNext();) {
      const e = i.next();

      this._nodes.add(e);
    }
  }

  computeProperIntersectionIM(intersector, im) {
    const dimA = this._arg[0].getGeometry().getDimension();

    const dimB = this._arg[1].getGeometry().getDimension();

    const hasProper = intersector.hasProperIntersection();
    const hasProperInterior = intersector.hasProperInteriorIntersection();

    if (dimA === 2 && dimB === 2) {
      if (hasProper) im.setAtLeast('212101212');
    } else if (dimA === 2 && dimB === 1) {
      if (hasProper) im.setAtLeast('FFF0FFFF2');
      if (hasProperInterior) im.setAtLeast('1FFFFF1FF');
    } else if (dimA === 1 && dimB === 2) {
      if (hasProper) im.setAtLeast('F0FFFFFF2');
      if (hasProperInterior) im.setAtLeast('1F1FFFFFF');
    } else if (dimA === 1 && dimB === 1) {
      if (hasProperInterior) im.setAtLeast('0FFFFFFFF');
    }
  }

  labelIsolatedEdges(thisIndex, targetIndex) {
    for (let ei = this._arg[thisIndex].getEdgeIterator(); ei.hasNext();) {
      const e = ei.next();

      if (e.isIsolated()) {
        this.labelIsolatedEdge(e, targetIndex, this._arg[targetIndex].getGeometry());

        this._isolatedEdges.add(e);
      }
    }
  }

  labelIsolatedEdge(e, targetIndex, target) {
    if (target.getDimension() > 0) {
      const loc = this._ptLocator.locate(e.getCoordinate(), target);

      e.getLabel().setAllLocations(targetIndex, loc);
    } else {
      e.getLabel().setAllLocations(targetIndex, Location.EXTERIOR);
    }
  }

  computeIM() {
    const im = new IntersectionMatrix();
    im.set(Location.EXTERIOR, Location.EXTERIOR, 2);

    if (!this._arg[0].getGeometry().getEnvelopeInternal().intersects(this._arg[1].getGeometry().getEnvelopeInternal())) {
      this.computeDisjointIM(im);
      return im;
    }

    this._arg[0].computeSelfNodes(this._li, false);

    this._arg[1].computeSelfNodes(this._li, false);

    const intersector = this._arg[0].computeEdgeIntersections(this._arg[1], this._li, false);

    this.computeIntersectionNodes(0);
    this.computeIntersectionNodes(1);
    this.copyNodesAndLabels(0);
    this.copyNodesAndLabels(1);
    this.labelIsolatedNodes();
    this.computeProperIntersectionIM(intersector, im);
    const eeBuilder = new EdgeEndBuilder();
    const ee0 = eeBuilder.computeEdgeEnds(this._arg[0].getEdgeIterator());
    this.insertEdgeEnds(ee0);
    const ee1 = eeBuilder.computeEdgeEnds(this._arg[1].getEdgeIterator());
    this.insertEdgeEnds(ee1);
    this.labelNodeEdges();
    this.labelIsolatedEdges(0, 1);
    this.labelIsolatedEdges(1, 0);
    this.updateIM(im);
    return im;
  }

  labelNodeEdges() {
    for (let ni = this._nodes.iterator(); ni.hasNext();) {
      const node = ni.next();
      node.getEdges().computeLabelling(this._arg);
    }
  }

  copyNodesAndLabels(argIndex) {
    for (let i = this._arg[argIndex].getNodeIterator(); i.hasNext();) {
      const graphNode = i.next();

      const newNode = this._nodes.addNode(graphNode.getCoordinate());

      newNode.setLabel(argIndex, graphNode.getLabel().getLocation(argIndex));
    }
  }

  labelIntersectionNodes(argIndex) {
    for (let i = this._arg[argIndex].getEdgeIterator(); i.hasNext();) {
      const e = i.next();
      const eLoc = e.getLabel().getLocation(argIndex);

      for (let eiIt = e.getEdgeIntersectionList().iterator(); eiIt.hasNext();) {
        const ei = eiIt.next();

        const n = this._nodes.find(ei.coord);

        if (n.getLabel().isNull(argIndex)) if (eLoc === Location.BOUNDARY) n.setLabelBoundary(argIndex);else n.setLabel(argIndex, Location.INTERIOR);
      }
    }
  }

  labelIsolatedNode(n, targetIndex) {
    const loc = this._ptLocator.locate(n.getCoordinate(), this._arg[targetIndex].getGeometry());

    n.getLabel().setAllLocations(targetIndex, loc);
  }

  computeIntersectionNodes(argIndex) {
    for (let i = this._arg[argIndex].getEdgeIterator(); i.hasNext();) {
      const e = i.next();
      const eLoc = e.getLabel().getLocation(argIndex);

      for (let eiIt = e.getEdgeIntersectionList().iterator(); eiIt.hasNext();) {
        const ei = eiIt.next();

        const n = this._nodes.addNode(ei.coord);

        if (eLoc === Location.BOUNDARY) n.setLabelBoundary(argIndex);else if (n.getLabel().isNull(argIndex)) n.setLabel(argIndex, Location.INTERIOR);
      }
    }
  }

  labelIsolatedNodes() {
    for (let ni = this._nodes.iterator(); ni.hasNext();) {
      const n = ni.next();
      const label = n.getLabel();
      Assert.isTrue(label.getGeometryCount() > 0, 'node with empty label found');
      if (n.isIsolated()) if (label.isNull(0)) this.labelIsolatedNode(n, 0);else this.labelIsolatedNode(n, 1);
    }
  }

  updateIM(im) {
    for (let ei = this._isolatedEdges.iterator(); ei.hasNext();) {
      const e = ei.next();
      e.updateIM(im);
    }

    for (let ni = this._nodes.iterator(); ni.hasNext();) {
      const node = ni.next();
      node.updateIM(im);
      node.updateIMFromEdges(im);
    }
  }

  computeDisjointIM(im) {
    const ga = this._arg[0].getGeometry();

    if (!ga.isEmpty()) {
      im.set(Location.INTERIOR, Location.EXTERIOR, ga.getDimension());
      im.set(Location.BOUNDARY, Location.EXTERIOR, ga.getBoundaryDimension());
    }

    const gb = this._arg[1].getGeometry();

    if (!gb.isEmpty()) {
      im.set(Location.EXTERIOR, Location.INTERIOR, gb.getDimension());
      im.set(Location.EXTERIOR, Location.BOUNDARY, gb.getBoundaryDimension());
    }
  }

}

class RectangleContains {
  constructor() {
    RectangleContains.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._rectEnv = null;
    const rectangle = arguments[0];
    this._rectEnv = rectangle.getEnvelopeInternal();
  }

  static contains(rectangle, b) {
    const rc = new RectangleContains(rectangle);
    return rc.contains(b);
  }

  isContainedInBoundary(geom) {
    if (geom instanceof Polygon) return false;
    if (geom instanceof Point) return this.isPointContainedInBoundary(geom);
    if (geom instanceof LineString) return this.isLineStringContainedInBoundary(geom);

    for (let i = 0; i < geom.getNumGeometries(); i++) {
      const comp = geom.getGeometryN(i);
      if (!this.isContainedInBoundary(comp)) return false;
    }

    return true;
  }

  isLineSegmentContainedInBoundary(p0, p1) {
    if (p0.equals(p1)) return this.isPointContainedInBoundary(p0);

    if (p0.x === p1.x) {
      if (p0.x === this._rectEnv.getMinX() || p0.x === this._rectEnv.getMaxX()) return true;
    } else if (p0.y === p1.y) {
      if (p0.y === this._rectEnv.getMinY() || p0.y === this._rectEnv.getMaxY()) return true;
    }

    return false;
  }

  isLineStringContainedInBoundary(line) {
    const seq = line.getCoordinateSequence();
    const p0 = new Coordinate();
    const p1 = new Coordinate();

    for (let i = 0; i < seq.size() - 1; i++) {
      seq.getCoordinate(i, p0);
      seq.getCoordinate(i + 1, p1);
      if (!this.isLineSegmentContainedInBoundary(p0, p1)) return false;
    }

    return true;
  }

  isPointContainedInBoundary() {
    if (arguments[0] instanceof Point) {
      const point = arguments[0];
      return this.isPointContainedInBoundary(point.getCoordinate());
    } else if (arguments[0] instanceof Coordinate) {
      const pt = arguments[0];
      return pt.x === this._rectEnv.getMinX() || pt.x === this._rectEnv.getMaxX() || pt.y === this._rectEnv.getMinY() || pt.y === this._rectEnv.getMaxY();
    }
  }

  contains(geom) {
    if (!this._rectEnv.contains(geom.getEnvelopeInternal())) return false;
    if (this.isContainedInBoundary(geom)) return false;
    return true;
  }

}

class RectangleLineIntersector {
  constructor() {
    RectangleLineIntersector.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._li = new RobustLineIntersector();
    this._rectEnv = null;
    this._diagUp0 = null;
    this._diagUp1 = null;
    this._diagDown0 = null;
    this._diagDown1 = null;
    const rectEnv = arguments[0];
    this._rectEnv = rectEnv;
    this._diagUp0 = new Coordinate(rectEnv.getMinX(), rectEnv.getMinY());
    this._diagUp1 = new Coordinate(rectEnv.getMaxX(), rectEnv.getMaxY());
    this._diagDown0 = new Coordinate(rectEnv.getMinX(), rectEnv.getMaxY());
    this._diagDown1 = new Coordinate(rectEnv.getMaxX(), rectEnv.getMinY());
  }

  intersects(p0, p1) {
    const segEnv = new Envelope(p0, p1);
    if (!this._rectEnv.intersects(segEnv)) return false;
    if (this._rectEnv.intersects(p0)) return true;
    if (this._rectEnv.intersects(p1)) return true;

    if (p0.compareTo(p1) > 0) {
      const tmp = p0;
      p0 = p1;
      p1 = tmp;
    }

    let isSegUpwards = false;
    if (p1.y > p0.y) isSegUpwards = true;
    if (isSegUpwards) this._li.computeIntersection(p0, p1, this._diagDown0, this._diagDown1);else this._li.computeIntersection(p0, p1, this._diagUp0, this._diagUp1);
    if (this._li.hasIntersection()) return true;
    return false;
  }

}

class RectangleIntersects {
  constructor() {
    RectangleIntersects.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._rectangle = null;
    this._rectEnv = null;
    const rectangle = arguments[0];
    this._rectangle = rectangle;
    this._rectEnv = rectangle.getEnvelopeInternal();
  }

  static intersects(rectangle, b) {
    const rp = new RectangleIntersects(rectangle);
    return rp.intersects(b);
  }

  intersects(geom) {
    if (!this._rectEnv.intersects(geom.getEnvelopeInternal())) return false;
    const visitor = new EnvelopeIntersectsVisitor(this._rectEnv);
    visitor.applyTo(geom);
    if (visitor.intersects()) return true;
    const ecpVisitor = new GeometryContainsPointVisitor(this._rectangle);
    ecpVisitor.applyTo(geom);
    if (ecpVisitor.containsPoint()) return true;
    const riVisitor = new RectangleIntersectsSegmentVisitor(this._rectangle);
    riVisitor.applyTo(geom);
    if (riVisitor.intersects()) return true;
    return false;
  }

}

class EnvelopeIntersectsVisitor extends ShortCircuitedGeometryVisitor {
  constructor() {
    super();
    EnvelopeIntersectsVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._rectEnv = null;
    this._intersects = false;
    const rectEnv = arguments[0];
    this._rectEnv = rectEnv;
  }

  isDone() {
    return this._intersects === true;
  }

  visit(element) {
    const elementEnv = element.getEnvelopeInternal();
    if (!this._rectEnv.intersects(elementEnv)) return null;

    if (this._rectEnv.contains(elementEnv)) {
      this._intersects = true;
      return null;
    }

    if (elementEnv.getMinX() >= this._rectEnv.getMinX() && elementEnv.getMaxX() <= this._rectEnv.getMaxX()) {
      this._intersects = true;
      return null;
    }

    if (elementEnv.getMinY() >= this._rectEnv.getMinY() && elementEnv.getMaxY() <= this._rectEnv.getMaxY()) {
      this._intersects = true;
      return null;
    }
  }

  intersects() {
    return this._intersects;
  }

}

class GeometryContainsPointVisitor extends ShortCircuitedGeometryVisitor {
  constructor() {
    super();
    GeometryContainsPointVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._rectSeq = null;
    this._rectEnv = null;
    this._containsPoint = false;
    const rectangle = arguments[0];
    this._rectSeq = rectangle.getExteriorRing().getCoordinateSequence();
    this._rectEnv = rectangle.getEnvelopeInternal();
  }

  isDone() {
    return this._containsPoint === true;
  }

  visit(geom) {
    if (!(geom instanceof Polygon)) return null;
    const elementEnv = geom.getEnvelopeInternal();
    if (!this._rectEnv.intersects(elementEnv)) return null;
    const rectPt = new Coordinate();

    for (let i = 0; i < 4; i++) {
      this._rectSeq.getCoordinate(i, rectPt);

      if (!elementEnv.contains(rectPt)) continue;

      if (SimplePointInAreaLocator.containsPointInPolygon(rectPt, geom)) {
        this._containsPoint = true;
        return null;
      }
    }
  }

  containsPoint() {
    return this._containsPoint;
  }

}

class RectangleIntersectsSegmentVisitor extends ShortCircuitedGeometryVisitor {
  constructor() {
    super();
    RectangleIntersectsSegmentVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._rectEnv = null;
    this._rectIntersector = null;
    this._hasIntersection = false;
    this._p0 = new Coordinate();
    this._p1 = new Coordinate();
    const rectangle = arguments[0];
    this._rectEnv = rectangle.getEnvelopeInternal();
    this._rectIntersector = new RectangleLineIntersector(this._rectEnv);
  }

  intersects() {
    return this._hasIntersection;
  }

  isDone() {
    return this._hasIntersection === true;
  }

  visit(geom) {
    const elementEnv = geom.getEnvelopeInternal();
    if (!this._rectEnv.intersects(elementEnv)) return null;
    const lines = LinearComponentExtracter.getLines(geom);
    this.checkIntersectionWithLineStrings(lines);
  }

  checkIntersectionWithLineStrings(lines) {
    for (let i = lines.iterator(); i.hasNext();) {
      const testLine = i.next();
      this.checkIntersectionWithSegments(testLine);
      if (this._hasIntersection) return null;
    }
  }

  checkIntersectionWithSegments(testLine) {
    const seq1 = testLine.getCoordinateSequence();

    for (let j = 1; j < seq1.size(); j++) {
      seq1.getCoordinate(j - 1, this._p0);
      seq1.getCoordinate(j, this._p1);

      if (this._rectIntersector.intersects(this._p0, this._p1)) {
        this._hasIntersection = true;
        return null;
      }
    }
  }

}

class RelateOp extends GeometryGraphOperation {
  constructor() {
    super();
    RelateOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._relate = null;

    if (arguments.length === 2) {
      const g0 = arguments[0],
            g1 = arguments[1];
      GeometryGraphOperation.constructor_.call(this, g0, g1);
      this._relate = new RelateComputer(this._arg);
    } else if (arguments.length === 3) {
      const g0 = arguments[0],
            g1 = arguments[1],
            boundaryNodeRule = arguments[2];
      GeometryGraphOperation.constructor_.call(this, g0, g1, boundaryNodeRule);
      this._relate = new RelateComputer(this._arg);
    }
  }

  static covers(g1, g2) {
    if (g2.getDimension() === 2 && g1.getDimension() < 2) return false;
    if (g2.getDimension() === 1 && g1.getDimension() < 1 && g2.getLength() > 0.0) return false;
    if (!g1.getEnvelopeInternal().covers(g2.getEnvelopeInternal())) return false;
    if (g1.isRectangle()) return true;
    return new RelateOp(g1, g2).getIntersectionMatrix().isCovers();
  }

  static intersects(g1, g2) {
    if (!g1.getEnvelopeInternal().intersects(g2.getEnvelopeInternal())) return false;
    if (g1.isRectangle()) return RectangleIntersects.intersects(g1, g2);
    if (g2.isRectangle()) return RectangleIntersects.intersects(g2, g1);

    if (g1.isGeometryCollection() || g2.isGeometryCollection()) {

      for (let i = 0; i < g1.getNumGeometries(); i++) for (let j = 0; j < g2.getNumGeometries(); j++) if (g1.getGeometryN(i).intersects(g2.getGeometryN(j))) return true;

      return false;
    }

    return new RelateOp(g1, g2).getIntersectionMatrix().isIntersects();
  }

  static touches(g1, g2) {
    if (!g1.getEnvelopeInternal().intersects(g2.getEnvelopeInternal())) return false;
    return new RelateOp(g1, g2).getIntersectionMatrix().isTouches(g1.getDimension(), g2.getDimension());
  }

  static equalsTopo(g1, g2) {
    if (!g1.getEnvelopeInternal().equals(g2.getEnvelopeInternal())) return false;
    return RelateOp.relate(g1, g2).isEquals(g1.getDimension(), g2.getDimension());
  }

  static relate() {
    if (arguments.length === 2) {
      const a = arguments[0],
            b = arguments[1];
      const relOp = new RelateOp(a, b);
      const im = relOp.getIntersectionMatrix();
      return im;
    } else if (arguments.length === 3) {
      const a = arguments[0],
            b = arguments[1],
            boundaryNodeRule = arguments[2];
      const relOp = new RelateOp(a, b, boundaryNodeRule);
      const im = relOp.getIntersectionMatrix();
      return im;
    }
  }

  static overlaps(g1, g2) {
    if (!g1.getEnvelopeInternal().intersects(g2.getEnvelopeInternal())) return false;
    return new RelateOp(g1, g2).getIntersectionMatrix().isOverlaps(g1.getDimension(), g2.getDimension());
  }

  static crosses(g1, g2) {
    if (!g1.getEnvelopeInternal().intersects(g2.getEnvelopeInternal())) return false;
    return new RelateOp(g1, g2).getIntersectionMatrix().isCrosses(g1.getDimension(), g2.getDimension());
  }

  static contains(g1, g2) {
    if (g2.getDimension() === 2 && g1.getDimension() < 2) return false;
    if (g2.getDimension() === 1 && g1.getDimension() < 1 && g2.getLength() > 0.0) return false;
    if (!g1.getEnvelopeInternal().contains(g2.getEnvelopeInternal())) return false;
    if (g1.isRectangle()) return RectangleContains.contains(g1, g2);
    return new RelateOp(g1, g2).getIntersectionMatrix().isContains();
  }

  getIntersectionMatrix() {
    return this._relate.computeIM();
  }

}

var relate = /*#__PURE__*/Object.freeze({
  __proto__: null,
  RelateOp: RelateOp
});

class PointGeometryUnion {
  constructor() {
    PointGeometryUnion.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pointGeom = null;
    this._otherGeom = null;
    this._geomFact = null;
    const pointGeom = arguments[0],
          otherGeom = arguments[1];
    this._pointGeom = pointGeom;
    this._otherGeom = otherGeom;
    this._geomFact = otherGeom.getFactory();
  }

  static union(pointGeom, otherGeom) {
    const unioner = new PointGeometryUnion(pointGeom, otherGeom);
    return unioner.union();
  }

  union() {
    const locater = new PointLocator();
    const exteriorCoords = new TreeSet();

    for (let i = 0; i < this._pointGeom.getNumGeometries(); i++) {
      const point = this._pointGeom.getGeometryN(i);

      const coord = point.getCoordinate();
      const loc = locater.locate(coord, this._otherGeom);
      if (loc === Location.EXTERIOR) exteriorCoords.add(coord);
    }

    if (exteriorCoords.size() === 0) return this._otherGeom;
    let ptComp = null;
    const coords = CoordinateArrays.toCoordinateArray(exteriorCoords);
    if (coords.length === 1) ptComp = this._geomFact.createPoint(coords[0]);else ptComp = this._geomFact.createMultiPointFromCoords(coords);
    return GeometryCombiner.combine(ptComp, this._otherGeom);
  }

}

class InputExtracter {
  constructor() {
    InputExtracter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geomFactory = null;
    this._polygons = new ArrayList();
    this._lines = new ArrayList();
    this._points = new ArrayList();
    this._dimension = Dimension.FALSE;
  }

  static extract() {
    if (hasInterface(arguments[0], Collection)) {
      const geoms = arguments[0];
      const extracter = new InputExtracter();
      extracter.add(geoms);
      return extracter;
    } else if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];
      const extracter = new InputExtracter();
      extracter.add(geom);
      return extracter;
    }
  }

  getFactory() {
    return this._geomFactory;
  }

  recordDimension(dim) {
    if (dim > this._dimension) this._dimension = dim;
  }

  getDimension() {
    return this._dimension;
  }

  filter(geom) {
    this.recordDimension(geom.getDimension());
    if (geom instanceof GeometryCollection) return null;
    if (geom.isEmpty()) return null;

    if (geom instanceof Polygon) {
      this._polygons.add(geom);

      return null;
    } else if (geom instanceof LineString) {
      this._lines.add(geom);

      return null;
    } else if (geom instanceof Point) {
      this._points.add(geom);

      return null;
    }

    Assert.shouldNeverReachHere('Unhandled geometry type: ' + geom.getGeometryType());
  }

  getExtract(dim) {
    switch (dim) {
      case 0:
        return this._points;

      case 1:
        return this._lines;

      case 2:
        return this._polygons;
    }

    Assert.shouldNeverReachHere('Invalid dimension: ' + dim);
    return null;
  }

  isEmpty() {
    return this._polygons.isEmpty() && this._lines.isEmpty() && this._points.isEmpty();
  }

  add() {
    if (hasInterface(arguments[0], Collection)) {
      const geoms = arguments[0];

      for (const geom of geoms) this.add(geom);
    } else if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];
      if (this._geomFactory === null) this._geomFactory = geom.getFactory();
      geom.apply(this);
    }
  }

  get interfaces_() {
    return [GeometryFilter];
  }

}

class OverlapUnion {
  constructor() {
    OverlapUnion.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geomFactory = null;
    this._g0 = null;
    this._g1 = null;
    this._isUnionSafe = null;
    const g0 = arguments[0],
          g1 = arguments[1];
    this._g0 = g0;
    this._g1 = g1;
    this._geomFactory = g0.getFactory();
  }

  static containsProperly() {
    if (arguments.length === 2) {
      const env = arguments[0],
            p = arguments[1];
      if (env.isNull()) return false;
      return p.getX() > env.getMinX() && p.getX() < env.getMaxX() && p.getY() > env.getMinY() && p.getY() < env.getMaxY();
    } else if (arguments.length === 3) {
      const env = arguments[0],
            p0 = arguments[1],
            p1 = arguments[2];
      return OverlapUnion.containsProperly(env, p0) && OverlapUnion.containsProperly(env, p1);
    }
  }

  static union(g0, g1) {
    const union = new OverlapUnion(g0, g1);
    return union.union();
  }

  static intersects(env, p0, p1) {
    return env.intersects(p0) || env.intersects(p1);
  }

  static overlapEnvelope(g0, g1) {
    const g0Env = g0.getEnvelopeInternal();
    const g1Env = g1.getEnvelopeInternal();
    const overlapEnv = g0Env.intersection(g1Env);
    return overlapEnv;
  }

  static extractBorderSegments(geom, env, segs) {
    geom.apply(new class {
      get interfaces_() {
        return [CoordinateSequenceFilter];
      }

      filter(seq, i) {
        if (i <= 0) return null;
        const p0 = seq.getCoordinate(i - 1);
        const p1 = seq.getCoordinate(i);
        const isBorder = OverlapUnion.intersects(env, p0, p1) && !OverlapUnion.containsProperly(env, p0, p1);

        if (isBorder) {
          const seg = new LineSegment(p0, p1);
          segs.add(seg);
        }
      }

      isDone() {
        return false;
      }

      isGeometryChanged() {
        return false;
      }

    }());
  }

  static unionBuffer(g0, g1) {
    const factory = g0.getFactory();
    const gColl = factory.createGeometryCollection([g0, g1]);
    const union = gColl.buffer(0.0);
    return union;
  }

  isBorderSegmentsSame(result, env) {
    const segsBefore = this.extractBorderSegments(this._g0, this._g1, env);
    const segsAfter = new ArrayList();
    OverlapUnion.extractBorderSegments(result, env, segsAfter);
    return this.isEqual(segsBefore, segsAfter);
  }

  extractByEnvelope(env, geom, disjointGeoms) {
    const intersectingGeoms = new ArrayList();

    for (let i = 0; i < geom.getNumGeometries(); i++) {
      const elem = geom.getGeometryN(i);

      if (elem.getEnvelopeInternal().intersects(env)) {
        intersectingGeoms.add(elem);
      } else {
        const copy = elem.copy();
        disjointGeoms.add(copy);
      }
    }

    return this._geomFactory.buildGeometry(intersectingGeoms);
  }

  isEqual(segs0, segs1) {
    if (segs0.size() !== segs1.size()) return false;
    const segIndex = new HashSet(segs0);

    for (const seg of segs1) if (!segIndex.contains(seg)) return false;

    return true;
  }

  union() {
    const overlapEnv = OverlapUnion.overlapEnvelope(this._g0, this._g1);

    if (overlapEnv.isNull()) {
      const g0Copy = this._g0.copy();

      const g1Copy = this._g1.copy();

      return GeometryCombiner.combine(g0Copy, g1Copy);
    }

    const disjointPolys = new ArrayList();
    const g0Overlap = this.extractByEnvelope(overlapEnv, this._g0, disjointPolys);
    const g1Overlap = this.extractByEnvelope(overlapEnv, this._g1, disjointPolys);
    const unionGeom = this.unionFull(g0Overlap, g1Overlap);
    let result = null;
    this._isUnionSafe = this.isBorderSegmentsSame(unionGeom, overlapEnv);
    if (!this._isUnionSafe) result = this.unionFull(this._g0, this._g1);else result = this.combine(unionGeom, disjointPolys);
    return result;
  }

  combine(unionGeom, disjointPolys) {
    if (disjointPolys.size() <= 0) return unionGeom;
    disjointPolys.add(unionGeom);
    const result = GeometryCombiner.combine(disjointPolys);
    return result;
  }

  unionFull(geom0, geom1) {
    try {
      return geom0.union(geom1);
    } catch (ex) {
      if (ex instanceof TopologyException) return OverlapUnion.unionBuffer(geom0, geom1);else throw ex;
    } finally {}
  }

  extractBorderSegments(geom0, geom1, env) {
    const segs = new ArrayList();
    OverlapUnion.extractBorderSegments(geom0, env, segs);
    if (geom1 !== null) OverlapUnion.extractBorderSegments(geom1, env, segs);
    return segs;
  }

  isUnionOptimized() {
    return this._isUnionSafe;
  }

}

class CascadedPolygonUnion {
  constructor() {
    CascadedPolygonUnion.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputPolys = null;
    this._geomFactory = null;
    const polys = arguments[0];
    this._inputPolys = polys;
    if (this._inputPolys === null) this._inputPolys = new ArrayList();
  }

  static restrictToPolygons(g) {
    if (hasInterface(g, Polygonal)) return g;
    const polygons = PolygonExtracter.getPolygons(g);
    if (polygons.size() === 1) return polygons.get(0);
    return g.getFactory().createMultiPolygon(GeometryFactory.toPolygonArray(polygons));
  }

  static getGeometry(list, index) {
    if (index >= list.size()) return null;
    return list.get(index);
  }

  static union(polys) {
    const op = new CascadedPolygonUnion(polys);
    return op.union();
  }

  reduceToGeometries(geomTree) {
    const geoms = new ArrayList();

    for (let i = geomTree.iterator(); i.hasNext();) {
      const o = i.next();
      let geom = null;
      if (hasInterface(o, List)) geom = this.unionTree(o);else if (o instanceof Geometry) geom = o;
      geoms.add(geom);
    }

    return geoms;
  }

  union() {
    if (this._inputPolys === null) throw new IllegalStateException('union() method cannot be called twice');
    if (this._inputPolys.isEmpty()) return null;
    this._geomFactory = this._inputPolys.iterator().next().getFactory();
    const index = new STRtree(CascadedPolygonUnion.STRTREE_NODE_CAPACITY);

    for (let i = this._inputPolys.iterator(); i.hasNext();) {
      const item = i.next();
      index.insert(item.getEnvelopeInternal(), item);
    }

    this._inputPolys = null;
    const itemTree = index.itemsTree();
    const unionAll = this.unionTree(itemTree);
    return unionAll;
  }

  binaryUnion() {
    if (arguments.length === 1) {
      const geoms = arguments[0];
      return this.binaryUnion(geoms, 0, geoms.size());
    } else if (arguments.length === 3) {
      const geoms = arguments[0],
            start = arguments[1],
            end = arguments[2];

      if (end - start <= 1) {
        const g0 = CascadedPolygonUnion.getGeometry(geoms, start);
        return this.unionSafe(g0, null);
      } else if (end - start === 2) {
        return this.unionSafe(CascadedPolygonUnion.getGeometry(geoms, start), CascadedPolygonUnion.getGeometry(geoms, start + 1));
      } else {
        const mid = Math.trunc((end + start) / 2);
        const g0 = this.binaryUnion(geoms, start, mid);
        const g1 = this.binaryUnion(geoms, mid, end);
        return this.unionSafe(g0, g1);
      }
    }
  }

  repeatedUnion(geoms) {
    let union = null;

    for (let i = geoms.iterator(); i.hasNext();) {
      const g = i.next();
      if (union === null) union = g.copy();else union = union.union(g);
    }

    return union;
  }

  unionSafe(g0, g1) {
    if (g0 === null && g1 === null) return null;
    if (g0 === null) return g1.copy();
    if (g1 === null) return g0.copy();
    return this.unionActual(g0, g1);
  }

  unionActual(g0, g1) {
    const union = OverlapUnion.union(g0, g1);
    return CascadedPolygonUnion.restrictToPolygons(union);
  }

  unionTree(geomTree) {
    const geoms = this.reduceToGeometries(geomTree);
    const union = this.binaryUnion(geoms);
    return union;
  }

  bufferUnion() {
    if (arguments.length === 1) {
      const geoms = arguments[0];
      const factory = geoms.get(0).getFactory();
      const gColl = factory.buildGeometry(geoms);
      const unionAll = gColl.buffer(0.0);
      return unionAll;
    } else if (arguments.length === 2) {
      const g0 = arguments[0],
            g1 = arguments[1];
      const factory = g0.getFactory();
      const gColl = factory.createGeometryCollection([g0, g1]);
      const unionAll = gColl.buffer(0.0);
      return unionAll;
    }
  }

}
CascadedPolygonUnion.STRTREE_NODE_CAPACITY = 4;

class UnaryUnionOp {
  constructor() {
    UnaryUnionOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geomFact = null;
    this._extracter = null;

    if (arguments.length === 1) {
      if (hasInterface(arguments[0], Collection)) {
        const geoms = arguments[0];
        this.extract(geoms);
      } else if (arguments[0] instanceof Geometry) {
        const geom = arguments[0];
        this.extract(geom);
      }
    } else if (arguments.length === 2) {
      const geoms = arguments[0],
            geomFact = arguments[1];
      this._geomFact = geomFact;
      this.extract(geoms);
    }
  }

  static union() {
    if (arguments.length === 1) {
      if (hasInterface(arguments[0], Collection)) {
        const geoms = arguments[0];
        const op = new UnaryUnionOp(geoms);
        return op.union();
      } else if (arguments[0] instanceof Geometry) {
        const geom = arguments[0];
        const op = new UnaryUnionOp(geom);
        return op.union();
      }
    } else if (arguments.length === 2) {
      const geoms = arguments[0],
            geomFact = arguments[1];
      const op = new UnaryUnionOp(geoms, geomFact);
      return op.union();
    }
  }

  unionNoOpt(g0) {
    const empty = this._geomFact.createPoint();

    return SnapIfNeededOverlayOp.overlayOp(g0, empty, OverlayOp.UNION);
  }

  unionWithNull(g0, g1) {
    if (g0 === null && g1 === null) return null;
    if (g1 === null) return g0;
    if (g0 === null) return g1;
    return g0.union(g1);
  }

  extract() {
    if (hasInterface(arguments[0], Collection)) {
      const geoms = arguments[0];
      this._extracter = InputExtracter.extract(geoms);
    } else if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];
      this._extracter = InputExtracter.extract(geom);
    }
  }

  union() {
    if (this._geomFact === null) this._geomFact = this._extracter.getFactory();
    if (this._geomFact === null) return null;
    if (this._extracter.isEmpty()) return this._geomFact.createEmpty(this._extracter.getDimension());

    const points = this._extracter.getExtract(0);

    const lines = this._extracter.getExtract(1);

    const polygons = this._extracter.getExtract(2);

    let unionPoints = null;

    if (points.size() > 0) {
      const ptGeom = this._geomFact.buildGeometry(points);

      unionPoints = this.unionNoOpt(ptGeom);
    }

    let unionLines = null;

    if (lines.size() > 0) {
      const lineGeom = this._geomFact.buildGeometry(lines);

      unionLines = this.unionNoOpt(lineGeom);
    }

    let unionPolygons = null;
    if (polygons.size() > 0) unionPolygons = CascadedPolygonUnion.union(polygons);
    const unionLA = this.unionWithNull(unionLines, unionPolygons);
    let union = null;
    if (unionPoints === null) union = unionLA;else if (unionLA === null) union = unionPoints;else union = PointGeometryUnion.union(unionPoints, unionLA);
    if (union === null) return this._geomFact.createGeometryCollection();
    return union;
  }

}

var union = /*#__PURE__*/Object.freeze({
  __proto__: null,
  UnaryUnionOp: UnaryUnionOp
});

var valid = /*#__PURE__*/Object.freeze({
  __proto__: null,
  IsValidOp: IsValidOp,
  ConsistentAreaTester: ConsistentAreaTester
});

var operation = /*#__PURE__*/Object.freeze({
  __proto__: null,
  BoundaryOp: BoundaryOp,
  IsSimpleOp: IsSimpleOp,
  buffer: buffer,
  distance: distance,
  linemerge: linemerge,
  overlay: overlay,
  polygonize: polygonize,
  relate: relate,
  union: union,
  valid: valid
});

class CommonBitsOp {
  constructor() {
    CommonBitsOp.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._returnToOriginalPrecision = true;
    this._cbr = null;

    if (arguments.length === 0) {
      CommonBitsOp.constructor_.call(this, true);
    } else if (arguments.length === 1) {
      const returnToOriginalPrecision = arguments[0];
      this._returnToOriginalPrecision = returnToOriginalPrecision;
    }
  }

  computeResultPrecision(result) {
    if (this._returnToOriginalPrecision) this._cbr.addCommonBits(result);
    return result;
  }

  union(geom0, geom1) {
    const geom = this.removeCommonBits(geom0, geom1);
    return this.computeResultPrecision(geom[0].union(geom[1]));
  }

  intersection(geom0, geom1) {
    const geom = this.removeCommonBits(geom0, geom1);
    return this.computeResultPrecision(geom[0].intersection(geom[1]));
  }

  removeCommonBits() {
    if (arguments.length === 1) {
      const geom0 = arguments[0];
      this._cbr = new CommonBitsRemover();

      this._cbr.add(geom0);

      const geom = this._cbr.removeCommonBits(geom0.copy());

      return geom;
    } else if (arguments.length === 2) {
      const geom0 = arguments[0],
            geom1 = arguments[1];
      this._cbr = new CommonBitsRemover();

      this._cbr.add(geom0);

      this._cbr.add(geom1);

      const geom = new Array(2).fill(null);
      geom[0] = this._cbr.removeCommonBits(geom0.copy());
      geom[1] = this._cbr.removeCommonBits(geom1.copy());
      return geom;
    }
  }

  buffer(geom0, distance) {
    const geom = this.removeCommonBits(geom0);
    return this.computeResultPrecision(geom.buffer(distance));
  }

  symDifference(geom0, geom1) {
    const geom = this.removeCommonBits(geom0, geom1);
    return this.computeResultPrecision(geom[0].symDifference(geom[1]));
  }

  difference(geom0, geom1) {
    const geom = this.removeCommonBits(geom0, geom1);
    return this.computeResultPrecision(geom[0].difference(geom[1]));
  }

}

class EnhancedPrecisionOp {
  static union(geom0, geom1) {
    let originalEx = null;

    try {
      const result = geom0.union(geom1);
      return result;
    } catch (ex) {
      if (ex instanceof RuntimeException) originalEx = ex;else throw ex;
    } finally {}

    try {
      const cbo = new CommonBitsOp(true);
      const resultEP = cbo.union(geom0, geom1);
      if (!resultEP.isValid()) throw originalEx;
      return resultEP;
    } catch (ex2) {
      if (ex2 instanceof RuntimeException) throw originalEx;else throw ex2;
    } finally {}
  }

  static intersection(geom0, geom1) {
    let originalEx = null;

    try {
      const result = geom0.intersection(geom1);
      return result;
    } catch (ex) {
      if (ex instanceof RuntimeException) originalEx = ex;else throw ex;
    } finally {}

    try {
      const cbo = new CommonBitsOp(true);
      const resultEP = cbo.intersection(geom0, geom1);
      if (!resultEP.isValid()) throw originalEx;
      return resultEP;
    } catch (ex2) {
      if (ex2 instanceof RuntimeException) throw originalEx;else throw ex2;
    } finally {}
  }

  static buffer(geom, distance) {
    let originalEx = null;

    try {
      const result = geom.buffer(distance);
      return result;
    } catch (ex) {
      if (ex instanceof RuntimeException) originalEx = ex;else throw ex;
    } finally {}

    try {
      const cbo = new CommonBitsOp(true);
      const resultEP = cbo.buffer(geom, distance);
      if (!resultEP.isValid()) throw originalEx;
      return resultEP;
    } catch (ex2) {
      if (ex2 instanceof RuntimeException) throw originalEx;else throw ex2;
    } finally {}
  }

  static symDifference(geom0, geom1) {
    let originalEx = null;

    try {
      const result = geom0.symDifference(geom1);
      return result;
    } catch (ex) {
      if (ex instanceof RuntimeException) originalEx = ex;else throw ex;
    } finally {}

    try {
      const cbo = new CommonBitsOp(true);
      const resultEP = cbo.symDifference(geom0, geom1);
      if (!resultEP.isValid()) throw originalEx;
      return resultEP;
    } catch (ex2) {
      if (ex2 instanceof RuntimeException) throw originalEx;else throw ex2;
    } finally {}
  }

  static difference(geom0, geom1) {
    let originalEx = null;

    try {
      const result = geom0.difference(geom1);
      return result;
    } catch (ex) {
      if (ex instanceof RuntimeException) originalEx = ex;else throw ex;
    } finally {}

    try {
      const cbo = new CommonBitsOp(true);
      const resultEP = cbo.difference(geom0, geom1);
      if (!resultEP.isValid()) throw originalEx;
      return resultEP;
    } catch (ex2) {
      if (ex2 instanceof RuntimeException) throw originalEx;else throw ex2;
    } finally {}
  }

}

class PrecisionReducerCoordinateOperation extends GeometryEditor.CoordinateOperation {
  constructor() {
    super();
    PrecisionReducerCoordinateOperation.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._targetPM = null;
    this._removeCollapsed = true;
    const targetPM = arguments[0],
          removeCollapsed = arguments[1];
    this._targetPM = targetPM;
    this._removeCollapsed = removeCollapsed;
  }

  edit() {
    if (arguments.length === 2 && arguments[1] instanceof Geometry && arguments[0] instanceof Array) {
      const coordinates = arguments[0],
            geom = arguments[1];
      if (coordinates.length === 0) return null;
      const reducedCoords = new Array(coordinates.length).fill(null);

      for (let i = 0; i < coordinates.length; i++) {
        const coord = new Coordinate(coordinates[i]);

        this._targetPM.makePrecise(coord);

        reducedCoords[i] = coord;
      }

      const noRepeatedCoordList = new CoordinateList(reducedCoords, false);
      const noRepeatedCoords = noRepeatedCoordList.toCoordinateArray();
      let minLength = 0;
      if (geom instanceof LineString) minLength = 2;
      if (geom instanceof LinearRing) minLength = 4;
      let collapsedCoords = reducedCoords;
      if (this._removeCollapsed) collapsedCoords = null;
      if (noRepeatedCoords.length < minLength) return collapsedCoords;
      return noRepeatedCoords;
    } else {
      return super.edit.apply(this, arguments);
    }
  }

}

class GeometryPrecisionReducer {
  constructor() {
    GeometryPrecisionReducer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._targetPM = null;
    this._removeCollapsed = true;
    this._changePrecisionModel = false;
    this._isPointwise = false;
    const pm = arguments[0];
    this._targetPM = pm;
  }

  static reduce(g, precModel) {
    const reducer = new GeometryPrecisionReducer(precModel);
    return reducer.reduce(g);
  }

  static reducePointwise(g, precModel) {
    const reducer = new GeometryPrecisionReducer(precModel);
    reducer.setPointwise(true);
    return reducer.reduce(g);
  }

  fixPolygonalTopology(geom) {
    let geomToBuffer = geom;
    if (!this._changePrecisionModel) geomToBuffer = this.changePM(geom, this._targetPM);
    const bufGeom = BufferOp.bufferOp(geomToBuffer, 0);
    return bufGeom;
  }

  reducePointwise(geom) {
    let geomEdit = null;

    if (this._changePrecisionModel) {
      const newFactory = this.createFactory(geom.getFactory(), this._targetPM);
      geomEdit = new GeometryEditor(newFactory);
    } else {
      geomEdit = new GeometryEditor();
    }

    let finalRemoveCollapsed = this._removeCollapsed;
    if (geom.getDimension() >= 2) finalRemoveCollapsed = true;
    const reduceGeom = geomEdit.edit(geom, new PrecisionReducerCoordinateOperation(this._targetPM, finalRemoveCollapsed));
    return reduceGeom;
  }

  changePM(geom, newPM) {
    const geomEditor = this.createEditor(geom.getFactory(), newPM);
    return geomEditor.edit(geom, new GeometryEditor.NoOpGeometryOperation());
  }

  setRemoveCollapsedComponents(removeCollapsed) {
    this._removeCollapsed = removeCollapsed;
  }

  createFactory(inputFactory, pm) {
    const newFactory = new GeometryFactory(pm, inputFactory.getSRID(), inputFactory.getCoordinateSequenceFactory());
    return newFactory;
  }

  setChangePrecisionModel(changePrecisionModel) {
    this._changePrecisionModel = changePrecisionModel;
  }

  reduce(geom) {
    const reducePW = this.reducePointwise(geom);
    if (this._isPointwise) return reducePW;
    if (!hasInterface(reducePW, Polygonal)) return reducePW;
    if (IsValidOp.isValid(reducePW)) return reducePW;
    return this.fixPolygonalTopology(reducePW);
  }

  setPointwise(isPointwise) {
    this._isPointwise = isPointwise;
  }

  createEditor(geomFactory, newPM) {
    if (geomFactory.getPrecisionModel() === newPM) return new GeometryEditor();
    const newFactory = this.createFactory(geomFactory, newPM);
    const geomEdit = new GeometryEditor(newFactory);
    return geomEdit;
  }

}

class FacetSequence {
  constructor() {
    FacetSequence.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geom = null;
    this._pts = null;
    this._start = null;
    this._end = null;

    if (arguments.length === 2) {
      const pts = arguments[0],
            start = arguments[1];
      this._pts = pts;
      this._start = start;
      this._end = start + 1;
    } else if (arguments.length === 3) {
      const pts = arguments[0],
            start = arguments[1],
            end = arguments[2];
      this._pts = pts;
      this._start = start;
      this._end = end;
    } else if (arguments.length === 4) {
      const geom = arguments[0],
            pts = arguments[1],
            start = arguments[2],
            end = arguments[3];
      this._geom = geom;
      this._pts = pts;
      this._start = start;
      this._end = end;
    }
  }

  computeDistanceLineLine(facetSeq, locs) {
    let minDistance = Double.MAX_VALUE;

    for (let i = this._start; i < this._end - 1; i++) {
      const p0 = this._pts.getCoordinate(i);

      const p1 = this._pts.getCoordinate(i + 1);

      for (let j = facetSeq._start; j < facetSeq._end - 1; j++) {
        const q0 = facetSeq._pts.getCoordinate(j);

        const q1 = facetSeq._pts.getCoordinate(j + 1);

        const dist = Distance.segmentToSegment(p0, p1, q0, q1);

        if (dist < minDistance) {
          minDistance = dist;
          if (locs !== null) this.updateNearestLocationsLineLine(i, p0, p1, facetSeq, j, q0, q1, locs);
          if (minDistance <= 0.0) return minDistance;
        }
      }
    }

    return minDistance;
  }

  updateNearestLocationsPointLine(pt, facetSeq, i, q0, q1, locs) {
    locs[0] = new GeometryLocation(this._geom, this._start, new Coordinate(pt));
    const seg = new LineSegment(q0, q1);
    const segClosestPoint = seg.closestPoint(pt);
    locs[1] = new GeometryLocation(facetSeq._geom, i, new Coordinate(segClosestPoint));
  }

  size() {
    return this._end - this._start;
  }

  getCoordinate(index) {
    return this._pts.getCoordinate(this._start + index);
  }

  nearestLocations(facetSeq) {
    const isPoint = this.isPoint();
    const isPointOther = facetSeq.isPoint();
    const locs = new Array(2).fill(null);

    if (isPoint && isPointOther) {
      const pt = this._pts.getCoordinate(this._start);

      const seqPt = facetSeq._pts.getCoordinate(facetSeq._start);

      locs[0] = new GeometryLocation(this._geom, this._start, new Coordinate(pt));
      locs[1] = new GeometryLocation(facetSeq._geom, facetSeq._start, new Coordinate(seqPt));
    } else if (isPoint) {
      const pt = this._pts.getCoordinate(this._start);

      this.computeDistancePointLine(pt, facetSeq, locs);
    } else if (isPointOther) {
      const seqPt = facetSeq._pts.getCoordinate(facetSeq._start);

      this.computeDistancePointLine(seqPt, this, locs);
      const tmp = locs[0];
      locs[0] = locs[1];
      locs[1] = tmp;
    } else {
      this.computeDistanceLineLine(facetSeq, locs);
    }

    return locs;
  }

  getEnvelope() {
    const env = new Envelope();

    for (let i = this._start; i < this._end; i++) env.expandToInclude(this._pts.getX(i), this._pts.getY(i));

    return env;
  }

  updateNearestLocationsLineLine(i, p0, p1, facetSeq, j, q0, q1, locs) {
    const seg0 = new LineSegment(p0, p1);
    const seg1 = new LineSegment(q0, q1);
    const closestPt = seg0.closestPoints(seg1);
    locs[0] = new GeometryLocation(this._geom, i, new Coordinate(closestPt[0]));
    locs[1] = new GeometryLocation(facetSeq._geom, j, new Coordinate(closestPt[1]));
  }

  toString() {
    const buf = new StringBuffer();
    buf.append('LINESTRING ( ');
    const p = new Coordinate();

    for (let i = this._start; i < this._end; i++) {
      if (i > this._start) buf.append(', ');

      this._pts.getCoordinate(i, p);

      buf.append(p.x + ' ' + p.y);
    }

    buf.append(' )');
    return buf.toString();
  }

  computeDistancePointLine(pt, facetSeq, locs) {
    let minDistance = Double.MAX_VALUE;

    for (let i = facetSeq._start; i < facetSeq._end - 1; i++) {
      const q0 = facetSeq._pts.getCoordinate(i);

      const q1 = facetSeq._pts.getCoordinate(i + 1);

      const dist = Distance.pointToSegment(pt, q0, q1);

      if (dist < minDistance) {
        minDistance = dist;
        if (locs !== null) this.updateNearestLocationsPointLine(pt, facetSeq, i, q0, q1, locs);
        if (minDistance <= 0.0) return minDistance;
      }
    }

    return minDistance;
  }

  isPoint() {
    return this._end - this._start === 1;
  }

  distance(facetSeq) {
    const isPoint = this.isPoint();
    const isPointOther = facetSeq.isPoint();
    let distance = null;

    if (isPoint && isPointOther) {
      const pt = this._pts.getCoordinate(this._start);

      const seqPt = facetSeq._pts.getCoordinate(facetSeq._start);

      distance = pt.distance(seqPt);
    } else if (isPoint) {
      const pt = this._pts.getCoordinate(this._start);

      distance = this.computeDistancePointLine(pt, facetSeq, null);
    } else if (isPointOther) {
      const seqPt = facetSeq._pts.getCoordinate(facetSeq._start);

      distance = this.computeDistancePointLine(seqPt, this, null);
    } else {
      distance = this.computeDistanceLineLine(facetSeq, null);
    }

    return distance;
  }

}

class FacetSequenceTreeBuilder {
  static addFacetSequences(geom, pts, sections) {
    let i = 0;
    const size = pts.size();

    while (i <= size - 1) {
      let end = i + FacetSequenceTreeBuilder.FACET_SEQUENCE_SIZE + 1;
      if (end >= size - 1) end = size;
      const sect = new FacetSequence(geom, pts, i, end);
      sections.add(sect);
      i = i + FacetSequenceTreeBuilder.FACET_SEQUENCE_SIZE;
    }
  }

  static computeFacetSequences(g) {
    const sections = new ArrayList();
    g.apply(new class {
      get interfaces_() {
        return [GeometryComponentFilter];
      }

      filter(geom) {
        let seq = null;

        if (geom instanceof LineString) {
          seq = geom.getCoordinateSequence();
          FacetSequenceTreeBuilder.addFacetSequences(geom, seq, sections);
        } else if (geom instanceof Point) {
          seq = geom.getCoordinateSequence();
          FacetSequenceTreeBuilder.addFacetSequences(geom, seq, sections);
        }
      }

    }());
    return sections;
  }

  static build(g) {
    const tree = new STRtree(FacetSequenceTreeBuilder.STR_TREE_NODE_CAPACITY);
    const sections = FacetSequenceTreeBuilder.computeFacetSequences(g);

    for (let i = sections.iterator(); i.hasNext();) {
      const section = i.next();
      tree.insert(section.getEnvelope(), section);
    }

    tree.build();
    return tree;
  }

}
FacetSequenceTreeBuilder.FACET_SEQUENCE_SIZE = 6;
FacetSequenceTreeBuilder.STR_TREE_NODE_CAPACITY = 4;

class MinimumClearance {
  constructor() {
    MinimumClearance.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._minClearance = null;
    this._minClearancePts = null;
    const geom = arguments[0];
    this._inputGeom = geom;
  }

  static getLine(g) {
    const rp = new MinimumClearance(g);
    return rp.getLine();
  }

  static getDistance(g) {
    const rp = new MinimumClearance(g);
    return rp.getDistance();
  }

  getLine() {
    this.compute();
    if (this._minClearancePts === null || this._minClearancePts[0] === null) return this._inputGeom.getFactory().createLineString();
    return this._inputGeom.getFactory().createLineString(this._minClearancePts);
  }

  compute() {
    if (this._minClearancePts !== null) return null;
    this._minClearancePts = new Array(2).fill(null);
    this._minClearance = Double.MAX_VALUE;
    if (this._inputGeom.isEmpty()) return null;
    const geomTree = FacetSequenceTreeBuilder.build(this._inputGeom);
    const nearest = geomTree.nearestNeighbour(new MinClearanceDistance());
    const mcd = new MinClearanceDistance();
    this._minClearance = mcd.distance(nearest[0], nearest[1]);
    this._minClearancePts = mcd.getCoordinates();
  }

  getDistance() {
    this.compute();
    return this._minClearance;
  }

}

class MinClearanceDistance {
  constructor() {
    MinClearanceDistance.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._minDist = Double.MAX_VALUE;
    this._minPts = new Array(2).fill(null);
  }

  vertexDistance(fs1, fs2) {
    for (let i1 = 0; i1 < fs1.size(); i1++) for (let i2 = 0; i2 < fs2.size(); i2++) {
      const p1 = fs1.getCoordinate(i1);
      const p2 = fs2.getCoordinate(i2);

      if (!p1.equals2D(p2)) {
        const d = p1.distance(p2);

        if (d < this._minDist) {
          this._minDist = d;
          this._minPts[0] = p1;
          this._minPts[1] = p2;
          if (d === 0.0) return d;
        }
      }
    }

    return this._minDist;
  }

  getCoordinates() {
    return this._minPts;
  }

  segmentDistance(fs1, fs2) {
    for (let i1 = 0; i1 < fs1.size(); i1++) for (let i2 = 1; i2 < fs2.size(); i2++) {
      const p = fs1.getCoordinate(i1);
      const seg0 = fs2.getCoordinate(i2 - 1);
      const seg1 = fs2.getCoordinate(i2);

      if (!(p.equals2D(seg0) || p.equals2D(seg1))) {
        const d = Distance.pointToSegment(p, seg0, seg1);

        if (d < this._minDist) {
          this._minDist = d;
          this.updatePts(p, seg0, seg1);
          if (d === 0.0) return d;
        }
      }
    }

    return this._minDist;
  }

  distance() {
    if (arguments[0] instanceof ItemBoundable && arguments[1] instanceof ItemBoundable) {
      const b1 = arguments[0],
            b2 = arguments[1];
      const fs1 = b1.getItem();
      const fs2 = b2.getItem();
      this._minDist = Double.MAX_VALUE;
      return this.distance(fs1, fs2);
    } else if (arguments[0] instanceof FacetSequence && arguments[1] instanceof FacetSequence) {
      const fs1 = arguments[0],
            fs2 = arguments[1];
      this.vertexDistance(fs1, fs2);
      if (fs1.size() === 1 && fs2.size() === 1) return this._minDist;
      if (this._minDist <= 0.0) return this._minDist;
      this.segmentDistance(fs1, fs2);
      if (this._minDist <= 0.0) return this._minDist;
      this.segmentDistance(fs2, fs1);
      return this._minDist;
    }
  }

  updatePts(p, seg0, seg1) {
    this._minPts[0] = p;
    const seg = new LineSegment(seg0, seg1);
    this._minPts[1] = new Coordinate(seg.closestPoint(p));
  }

  get interfaces_() {
    return [ItemDistance];
  }

}

MinimumClearance.MinClearanceDistance = MinClearanceDistance;

class SimpleMinimumClearance {
  constructor() {
    SimpleMinimumClearance.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._minClearance = null;
    this._minClearancePts = null;
    const geom = arguments[0];
    this._inputGeom = geom;
  }

  static getLine(g) {
    const rp = new SimpleMinimumClearance(g);
    return rp.getLine();
  }

  static getDistance(g) {
    const rp = new SimpleMinimumClearance(g);
    return rp.getDistance();
  }

  getLine() {
    this.compute();
    return this._inputGeom.getFactory().createLineString(this._minClearancePts);
  }

  updateClearance() {
    if (arguments.length === 3) {
      const candidateValue = arguments[0],
            p0 = arguments[1],
            p1 = arguments[2];

      if (candidateValue < this._minClearance) {
        this._minClearance = candidateValue;
        this._minClearancePts[0] = new Coordinate(p0);
        this._minClearancePts[1] = new Coordinate(p1);
      }
    } else if (arguments.length === 4) {
      const candidateValue = arguments[0],
            p = arguments[1],
            seg0 = arguments[2],
            seg1 = arguments[3];

      if (candidateValue < this._minClearance) {
        this._minClearance = candidateValue;
        this._minClearancePts[0] = new Coordinate(p);
        const seg = new LineSegment(seg0, seg1);
        this._minClearancePts[1] = new Coordinate(seg.closestPoint(p));
      }
    }
  }

  compute() {
    if (this._minClearancePts !== null) return null;
    this._minClearancePts = new Array(2).fill(null);
    this._minClearance = Double.MAX_VALUE;

    this._inputGeom.apply(new VertexCoordinateFilter(this));
  }

  getDistance() {
    this.compute();
    return this._minClearance;
  }

}

class VertexCoordinateFilter {
  constructor() {
    VertexCoordinateFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.smc = null;
    const smc = arguments[0];
    this.smc = smc;
  }

  filter(coord) {
    this.smc._inputGeom.apply(new ComputeMCCoordinateSequenceFilter(this.smc, coord));
  }

  get interfaces_() {
    return [CoordinateFilter];
  }

}

class ComputeMCCoordinateSequenceFilter {
  constructor() {
    ComputeMCCoordinateSequenceFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.smc = null;
    this._queryPt = null;
    const smc = arguments[0],
          queryPt = arguments[1];
    this.smc = smc;
    this._queryPt = queryPt;
  }

  isGeometryChanged() {
    return false;
  }

  checkVertexDistance(vertex) {
    const vertexDist = vertex.distance(this._queryPt);
    if (vertexDist > 0) this.smc.updateClearance(vertexDist, this._queryPt, vertex);
  }

  filter(seq, i) {
    this.checkVertexDistance(seq.getCoordinate(i));
    if (i > 0) this.checkSegmentDistance(seq.getCoordinate(i - 1), seq.getCoordinate(i));
  }

  checkSegmentDistance(seg0, seg1) {
    if (this._queryPt.equals2D(seg0) || this._queryPt.equals2D(seg1)) return null;
    const segDist = Distance.pointToSegment(this._queryPt, seg1, seg0);
    if (segDist > 0) this.smc.updateClearance(segDist, this._queryPt, seg1, seg0);
  }

  isDone() {
    return false;
  }

  get interfaces_() {
    return [CoordinateSequenceFilter];
  }

}

SimpleMinimumClearance.VertexCoordinateFilter = VertexCoordinateFilter;
SimpleMinimumClearance.ComputeMCCoordinateSequenceFilter = ComputeMCCoordinateSequenceFilter;

var precision = /*#__PURE__*/Object.freeze({
  __proto__: null,
  CommonBits: CommonBits,
  CommonBitsOp: CommonBitsOp,
  CommonBitsRemover: CommonBitsRemover,
  EnhancedPrecisionOp: EnhancedPrecisionOp,
  GeometryPrecisionReducer: GeometryPrecisionReducer,
  MinimumClearance: MinimumClearance,
  SimpleMinimumClearance: SimpleMinimumClearance
});

class DouglasPeuckerLineSimplifier {
  constructor() {
    DouglasPeuckerLineSimplifier.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pts = null;
    this._usePt = null;
    this._distanceTolerance = null;
    this._seg = new LineSegment();
    const pts = arguments[0];
    this._pts = pts;
  }

  static simplify(pts, distanceTolerance) {
    const simp = new DouglasPeuckerLineSimplifier(pts);
    simp.setDistanceTolerance(distanceTolerance);
    return simp.simplify();
  }

  simplifySection(i, j) {
    if (i + 1 === j) return null;
    this._seg.p0 = this._pts[i];
    this._seg.p1 = this._pts[j];
    let maxDistance = -1.0;
    let maxIndex = i;

    for (let k = i + 1; k < j; k++) {
      const distance = this._seg.distance(this._pts[k]);

      if (distance > maxDistance) {
        maxDistance = distance;
        maxIndex = k;
      }
    }

    if (maxDistance <= this._distanceTolerance) {
      for (let k = i + 1; k < j; k++) this._usePt[k] = false;
    } else {
      this.simplifySection(i, maxIndex);
      this.simplifySection(maxIndex, j);
    }
  }

  setDistanceTolerance(distanceTolerance) {
    this._distanceTolerance = distanceTolerance;
  }

  simplify() {
    this._usePt = new Array(this._pts.length).fill(null);

    for (let i = 0; i < this._pts.length; i++) this._usePt[i] = true;

    this.simplifySection(0, this._pts.length - 1);
    const coordList = new CoordinateList();

    for (let i = 0; i < this._pts.length; i++) if (this._usePt[i]) coordList.add(new Coordinate(this._pts[i]));

    return coordList.toCoordinateArray();
  }

}

class DouglasPeuckerSimplifier {
  constructor() {
    DouglasPeuckerSimplifier.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._distanceTolerance = null;
    this._isEnsureValidTopology = true;
    const inputGeom = arguments[0];
    this._inputGeom = inputGeom;
  }

  static simplify(geom, distanceTolerance) {
    const tss = new DouglasPeuckerSimplifier(geom);
    tss.setDistanceTolerance(distanceTolerance);
    return tss.getResultGeometry();
  }

  setEnsureValid(isEnsureValidTopology) {
    this._isEnsureValidTopology = isEnsureValidTopology;
  }

  getResultGeometry() {
    if (this._inputGeom.isEmpty()) return this._inputGeom.copy();
    return new DPTransformer(this._isEnsureValidTopology, this._distanceTolerance).transform(this._inputGeom);
  }

  setDistanceTolerance(distanceTolerance) {
    if (distanceTolerance < 0.0) throw new IllegalArgumentException('Tolerance must be non-negative');
    this._distanceTolerance = distanceTolerance;
  }

}

class DPTransformer extends GeometryTransformer {
  constructor() {
    super();
    DPTransformer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._isEnsureValidTopology = true;
    this._distanceTolerance = null;
    const isEnsureValidTopology = arguments[0],
          distanceTolerance = arguments[1];
    this._isEnsureValidTopology = isEnsureValidTopology;
    this._distanceTolerance = distanceTolerance;
  }

  transformPolygon(geom, parent) {
    if (geom.isEmpty()) return null;
    const rawGeom = super.transformPolygon.call(this, geom, parent);
    if (parent instanceof MultiPolygon) return rawGeom;
    return this.createValidArea(rawGeom);
  }

  createValidArea(rawAreaGeom) {
    if (this._isEnsureValidTopology) return rawAreaGeom.buffer(0.0);
    return rawAreaGeom;
  }

  transformCoordinates(coords, parent) {
    const inputPts = coords.toCoordinateArray();
    let newPts = null;
    if (inputPts.length === 0) newPts = new Array(0).fill(null);else newPts = DouglasPeuckerLineSimplifier.simplify(inputPts, this._distanceTolerance);
    return this._factory.getCoordinateSequenceFactory().create(newPts);
  }

  transformMultiPolygon(geom, parent) {
    const rawGeom = super.transformMultiPolygon.call(this, geom, parent);
    return this.createValidArea(rawGeom);
  }

  transformLinearRing(geom, parent) {
    const removeDegenerateRings = parent instanceof Polygon;
    const simpResult = super.transformLinearRing.call(this, geom, parent);
    if (removeDegenerateRings && !(simpResult instanceof LinearRing)) return null;
    return simpResult;
  }

}

DouglasPeuckerSimplifier.DPTransformer = DPTransformer;

class TaggedLineSegment extends LineSegment {
  constructor() {
    super();
    TaggedLineSegment.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._parent = null;
    this._index = null;

    if (arguments.length === 2) {
      const p0 = arguments[0],
            p1 = arguments[1];
      TaggedLineSegment.constructor_.call(this, p0, p1, null, -1);
    } else if (arguments.length === 4) {
      const p0 = arguments[0],
            p1 = arguments[1],
            parent = arguments[2],
            index = arguments[3];
      LineSegment.constructor_.call(this, p0, p1);
      this._parent = parent;
      this._index = index;
    }
  }

  getIndex() {
    return this._index;
  }

  getParent() {
    return this._parent;
  }

}

class TaggedLineString {
  constructor() {
    TaggedLineString.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._parentLine = null;
    this._segs = null;
    this._resultSegs = new ArrayList();
    this._minimumSize = null;

    if (arguments.length === 1) {
      const parentLine = arguments[0];
      TaggedLineString.constructor_.call(this, parentLine, 2);
    } else if (arguments.length === 2) {
      const parentLine = arguments[0],
            minimumSize = arguments[1];
      this._parentLine = parentLine;
      this._minimumSize = minimumSize;
      this.init();
    }
  }

  static extractCoordinates(segs) {
    const pts = new Array(segs.size() + 1).fill(null);
    let seg = null;

    for (let i = 0; i < segs.size(); i++) {
      seg = segs.get(i);
      pts[i] = seg.p0;
    }

    pts[pts.length - 1] = seg.p1;
    return pts;
  }

  addToResult(seg) {
    this._resultSegs.add(seg);
  }

  asLineString() {
    return this._parentLine.getFactory().createLineString(TaggedLineString.extractCoordinates(this._resultSegs));
  }

  getResultSize() {
    const resultSegsSize = this._resultSegs.size();

    return resultSegsSize === 0 ? 0 : resultSegsSize + 1;
  }

  getParent() {
    return this._parentLine;
  }

  getSegment(i) {
    return this._segs[i];
  }

  getParentCoordinates() {
    return this._parentLine.getCoordinates();
  }

  getMinimumSize() {
    return this._minimumSize;
  }

  asLinearRing() {
    return this._parentLine.getFactory().createLinearRing(TaggedLineString.extractCoordinates(this._resultSegs));
  }

  getSegments() {
    return this._segs;
  }

  init() {
    const pts = this._parentLine.getCoordinates();

    this._segs = new Array(pts.length - 1).fill(null);

    for (let i = 0; i < pts.length - 1; i++) {
      const seg = new TaggedLineSegment(pts[i], pts[i + 1], this._parentLine, i);
      this._segs[i] = seg;
    }
  }

  getResultCoordinates() {
    return TaggedLineString.extractCoordinates(this._resultSegs);
  }

}

class LineSegmentIndex {
  constructor() {
    LineSegmentIndex.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._index = new Quadtree();
  }

  remove(seg) {
    this._index.remove(new Envelope(seg.p0, seg.p1), seg);
  }

  add() {
    if (arguments[0] instanceof TaggedLineString) {
      const line = arguments[0];
      const segs = line.getSegments();

      for (let i = 0; i < segs.length; i++) {
        const seg = segs[i];
        this.add(seg);
      }
    } else if (arguments[0] instanceof LineSegment) {
      const seg = arguments[0];

      this._index.insert(new Envelope(seg.p0, seg.p1), seg);
    }
  }

  query(querySeg) {
    const env = new Envelope(querySeg.p0, querySeg.p1);
    const visitor = new LineSegmentVisitor(querySeg);

    this._index.query(env, visitor);

    const itemsFound = visitor.getItems();
    return itemsFound;
  }

}

class LineSegmentVisitor {
  constructor() {
    LineSegmentVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._querySeg = null;
    this._items = new ArrayList();
    const querySeg = arguments[0];
    this._querySeg = querySeg;
  }

  visitItem(item) {
    const seg = item;
    if (Envelope.intersects(seg.p0, seg.p1, this._querySeg.p0, this._querySeg.p1)) this._items.add(item);
  }

  getItems() {
    return this._items;
  }

  get interfaces_() {
    return [ItemVisitor];
  }

}

class TaggedLineStringSimplifier {
  constructor() {
    TaggedLineStringSimplifier.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._li = new RobustLineIntersector();
    this._inputIndex = new LineSegmentIndex();
    this._outputIndex = new LineSegmentIndex();
    this._line = null;
    this._linePts = null;
    this._distanceTolerance = 0.0;
    const inputIndex = arguments[0],
          outputIndex = arguments[1];
    this._inputIndex = inputIndex;
    this._outputIndex = outputIndex;
  }

  static isInLineSection(line, sectionIndex, seg) {
    if (seg.getParent() !== line.getParent()) return false;
    const segIndex = seg.getIndex();
    if (segIndex >= sectionIndex[0] && segIndex < sectionIndex[1]) return true;
    return false;
  }

  flatten(start, end) {
    const p0 = this._linePts[start];
    const p1 = this._linePts[end];
    const newSeg = new LineSegment(p0, p1);
    this.remove(this._line, start, end);

    this._outputIndex.add(newSeg);

    return newSeg;
  }

  hasBadIntersection(parentLine, sectionIndex, candidateSeg) {
    if (this.hasBadOutputIntersection(candidateSeg)) return true;
    if (this.hasBadInputIntersection(parentLine, sectionIndex, candidateSeg)) return true;
    return false;
  }

  setDistanceTolerance(distanceTolerance) {
    this._distanceTolerance = distanceTolerance;
  }

  simplifySection(i, j, depth) {
    depth += 1;
    const sectionIndex = new Array(2).fill(null);

    if (i + 1 === j) {
      const newSeg = this._line.getSegment(i);

      this._line.addToResult(newSeg);

      return null;
    }

    let isValidToSimplify = true;

    if (this._line.getResultSize() < this._line.getMinimumSize()) {
      const worstCaseSize = depth + 1;
      if (worstCaseSize < this._line.getMinimumSize()) isValidToSimplify = false;
    }

    const distance = new Array(1).fill(null);
    const furthestPtIndex = this.findFurthestPoint(this._linePts, i, j, distance);
    if (distance[0] > this._distanceTolerance) isValidToSimplify = false;
    const candidateSeg = new LineSegment();
    candidateSeg.p0 = this._linePts[i];
    candidateSeg.p1 = this._linePts[j];
    sectionIndex[0] = i;
    sectionIndex[1] = j;
    if (this.hasBadIntersection(this._line, sectionIndex, candidateSeg)) isValidToSimplify = false;

    if (isValidToSimplify) {
      const newSeg = this.flatten(i, j);

      this._line.addToResult(newSeg);

      return null;
    }

    this.simplifySection(i, furthestPtIndex, depth);
    this.simplifySection(furthestPtIndex, j, depth);
  }

  hasBadOutputIntersection(candidateSeg) {
    const querySegs = this._outputIndex.query(candidateSeg);

    for (let i = querySegs.iterator(); i.hasNext();) {
      const querySeg = i.next();
      if (this.hasInteriorIntersection(querySeg, candidateSeg)) return true;
    }

    return false;
  }

  findFurthestPoint(pts, i, j, maxDistance) {
    const seg = new LineSegment();
    seg.p0 = pts[i];
    seg.p1 = pts[j];
    let maxDist = -1.0;
    let maxIndex = i;

    for (let k = i + 1; k < j; k++) {
      const midPt = pts[k];
      const distance = seg.distance(midPt);

      if (distance > maxDist) {
        maxDist = distance;
        maxIndex = k;
      }
    }

    maxDistance[0] = maxDist;
    return maxIndex;
  }

  simplify(line) {
    this._line = line;
    this._linePts = line.getParentCoordinates();
    this.simplifySection(0, this._linePts.length - 1, 0);
  }

  remove(line, start, end) {
    for (let i = start; i < end; i++) {
      const seg = line.getSegment(i);

      this._inputIndex.remove(seg);
    }
  }

  hasInteriorIntersection(seg0, seg1) {
    this._li.computeIntersection(seg0.p0, seg0.p1, seg1.p0, seg1.p1);

    return this._li.isInteriorIntersection();
  }

  hasBadInputIntersection(parentLine, sectionIndex, candidateSeg) {
    const querySegs = this._inputIndex.query(candidateSeg);

    for (let i = querySegs.iterator(); i.hasNext();) {
      const querySeg = i.next();

      if (this.hasInteriorIntersection(querySeg, candidateSeg)) {
        if (TaggedLineStringSimplifier.isInLineSection(parentLine, sectionIndex, querySeg)) continue;
        return true;
      }
    }

    return false;
  }

}

class TaggedLinesSimplifier {
  constructor() {
    TaggedLinesSimplifier.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputIndex = new LineSegmentIndex();
    this._outputIndex = new LineSegmentIndex();
    this._distanceTolerance = 0.0;
  }

  setDistanceTolerance(distanceTolerance) {
    this._distanceTolerance = distanceTolerance;
  }

  simplify(taggedLines) {
    for (let i = taggedLines.iterator(); i.hasNext();) this._inputIndex.add(i.next());

    for (let i = taggedLines.iterator(); i.hasNext();) {
      const tlss = new TaggedLineStringSimplifier(this._inputIndex, this._outputIndex);
      tlss.setDistanceTolerance(this._distanceTolerance);
      tlss.simplify(i.next());
    }
  }

}

class TopologyPreservingSimplifier {
  constructor() {
    TopologyPreservingSimplifier.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._lineSimplifier = new TaggedLinesSimplifier();
    this._linestringMap = null;
    const inputGeom = arguments[0];
    this._inputGeom = inputGeom;
  }

  static simplify(geom, distanceTolerance) {
    const tss = new TopologyPreservingSimplifier(geom);
    tss.setDistanceTolerance(distanceTolerance);
    return tss.getResultGeometry();
  }

  getResultGeometry() {
    if (this._inputGeom.isEmpty()) return this._inputGeom.copy();
    this._linestringMap = new HashMap();

    this._inputGeom.apply(new LineStringMapBuilderFilter(this));

    this._lineSimplifier.simplify(this._linestringMap.values());

    const result = new LineStringTransformer(this._linestringMap).transform(this._inputGeom);
    return result;
  }

  setDistanceTolerance(distanceTolerance) {
    if (distanceTolerance < 0.0) throw new IllegalArgumentException('Tolerance must be non-negative');

    this._lineSimplifier.setDistanceTolerance(distanceTolerance);
  }

}

class LineStringTransformer extends GeometryTransformer {
  constructor() {
    super();
    LineStringTransformer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._linestringMap = null;
    const linestringMap = arguments[0];
    this._linestringMap = linestringMap;
  }

  transformCoordinates(coords, parent) {
    if (coords.size() === 0) return null;

    if (parent instanceof LineString) {
      const taggedLine = this._linestringMap.get(parent);

      return this.createCoordinateSequence(taggedLine.getResultCoordinates());
    }

    return super.transformCoordinates.call(this, coords, parent);
  }

}

class LineStringMapBuilderFilter {
  constructor() {
    LineStringMapBuilderFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.tps = null;
    const tps = arguments[0];
    this.tps = tps;
  }

  filter(geom) {
    if (geom instanceof LineString) {
      const line = geom;
      if (line.isEmpty()) return null;
      const minSize = line.isClosed() ? 4 : 2;
      const taggedLine = new TaggedLineString(line, minSize);

      this.tps._linestringMap.put(line, taggedLine);
    }
  }

  get interfaces_() {
    return [GeometryComponentFilter];
  }

}

TopologyPreservingSimplifier.LineStringTransformer = LineStringTransformer;
TopologyPreservingSimplifier.LineStringMapBuilderFilter = LineStringMapBuilderFilter;

class VWLineSimplifier {
  constructor() {
    VWLineSimplifier.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pts = null;
    this._tolerance = null;
    const pts = arguments[0],
          distanceTolerance = arguments[1];
    this._pts = pts;
    this._tolerance = distanceTolerance * distanceTolerance;
  }

  static simplify(pts, distanceTolerance) {
    const simp = new VWLineSimplifier(pts, distanceTolerance);
    return simp.simplify();
  }

  simplifyVertex(vwLine) {
    let curr = vwLine;
    let minArea = curr.getArea();
    let minVertex = null;

    while (curr !== null) {
      const area = curr.getArea();

      if (area < minArea) {
        minArea = area;
        minVertex = curr;
      }

      curr = curr._next;
    }

    if (minVertex !== null && minArea < this._tolerance) minVertex.remove();
    if (!vwLine.isLive()) return -1;
    return minArea;
  }

  simplify() {
    const vwLine = VWVertex.buildLine(this._pts);
    let minArea = this._tolerance;

    do minArea = this.simplifyVertex(vwLine); while (minArea < this._tolerance);

    const simp = vwLine.getCoordinates();
    if (simp.length < 2) return [simp[0], new Coordinate(simp[0])];
    return simp;
  }

}

class VWVertex {
  constructor() {
    VWVertex.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pt = null;
    this._prev = null;
    this._next = null;
    this._area = VWVertex.MAX_AREA;
    this._isLive = true;
    const pt = arguments[0];
    this._pt = pt;
  }

  static buildLine(pts) {
    let first = null;
    let prev = null;

    for (let i = 0; i < pts.length; i++) {
      const v = new VWVertex(pts[i]);
      if (first === null) first = v;
      v.setPrev(prev);

      if (prev !== null) {
        prev.setNext(v);
        prev.updateArea();
      }

      prev = v;
    }

    return first;
  }

  getCoordinates() {
    const coords = new CoordinateList();
    let curr = this;

    do {
      coords.add(curr._pt, false);
      curr = curr._next;
    } while (curr !== null);

    return coords.toCoordinateArray();
  }

  getArea() {
    return this._area;
  }

  updateArea() {
    if (this._prev === null || this._next === null) {
      this._area = VWVertex.MAX_AREA;
      return null;
    }

    this._area = Math.abs(Triangle.area(this._prev._pt, this._pt, this._next._pt));
  }

  remove() {
    const tmpPrev = this._prev;
    const tmpNext = this._next;
    let result = null;

    if (this._prev !== null) {
      this._prev.setNext(tmpNext);

      this._prev.updateArea();

      result = this._prev;
    }

    if (this._next !== null) {
      this._next.setPrev(tmpPrev);

      this._next.updateArea();

      if (result === null) result = this._next;
    }

    this._isLive = false;
    return result;
  }

  isLive() {
    return this._isLive;
  }

  setPrev(prev) {
    this._prev = prev;
  }

  setNext(next) {
    this._next = next;
  }

}

VWVertex.MAX_AREA = Double.MAX_VALUE;
VWLineSimplifier.VWVertex = VWVertex;

class VWSimplifier {
  constructor() {
    VWSimplifier.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._inputGeom = null;
    this._distanceTolerance = null;
    this._isEnsureValidTopology = true;
    const inputGeom = arguments[0];
    this._inputGeom = inputGeom;
  }

  static simplify(geom, distanceTolerance) {
    const simp = new VWSimplifier(geom);
    simp.setDistanceTolerance(distanceTolerance);
    return simp.getResultGeometry();
  }

  setEnsureValid(isEnsureValidTopology) {
    this._isEnsureValidTopology = isEnsureValidTopology;
  }

  getResultGeometry() {
    if (this._inputGeom.isEmpty()) return this._inputGeom.copy();
    return new VWTransformer(this._isEnsureValidTopology, this._distanceTolerance).transform(this._inputGeom);
  }

  setDistanceTolerance(distanceTolerance) {
    if (distanceTolerance < 0.0) throw new IllegalArgumentException('Tolerance must be non-negative');
    this._distanceTolerance = distanceTolerance;
  }

}

class VWTransformer extends GeometryTransformer {
  constructor() {
    super();
    VWTransformer.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._isEnsureValidTopology = true;
    this._distanceTolerance = null;
    const isEnsureValidTopology = arguments[0],
          distanceTolerance = arguments[1];
    this._isEnsureValidTopology = isEnsureValidTopology;
    this._distanceTolerance = distanceTolerance;
  }

  transformPolygon(geom, parent) {
    if (geom.isEmpty()) return null;
    const rawGeom = super.transformPolygon.call(this, geom, parent);
    if (parent instanceof MultiPolygon) return rawGeom;
    return this.createValidArea(rawGeom);
  }

  createValidArea(rawAreaGeom) {
    if (this._isEnsureValidTopology) return rawAreaGeom.buffer(0.0);
    return rawAreaGeom;
  }

  transformCoordinates(coords, parent) {
    const inputPts = coords.toCoordinateArray();
    let newPts = null;
    if (inputPts.length === 0) newPts = new Array(0).fill(null);else newPts = VWLineSimplifier.simplify(inputPts, this._distanceTolerance);
    return this._factory.getCoordinateSequenceFactory().create(newPts);
  }

  transformMultiPolygon(geom, parent) {
    const rawGeom = super.transformMultiPolygon.call(this, geom, parent);
    return this.createValidArea(rawGeom);
  }

  transformLinearRing(geom, parent) {
    const removeDegenerateRings = parent instanceof Polygon;
    const simpResult = super.transformLinearRing.call(this, geom, parent);
    if (removeDegenerateRings && !(simpResult instanceof LinearRing)) return null;
    return simpResult;
  }

}

VWSimplifier.VWTransformer = VWTransformer;

var simplify = /*#__PURE__*/Object.freeze({
  __proto__: null,
  DouglasPeuckerSimplifier: DouglasPeuckerSimplifier,
  TopologyPreservingSimplifier: TopologyPreservingSimplifier,
  VWSimplifier: VWSimplifier
});

class SplitSegment {
  constructor() {
    SplitSegment.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._seg = null;
    this._segLen = null;
    this._splitPt = null;
    this._minimumLen = 0.0;
    const seg = arguments[0];
    this._seg = seg;
    this._segLen = seg.getLength();
  }

  static pointAlongReverse(seg, segmentLengthFraction) {
    const coord = new Coordinate();
    coord.x = seg.p1.x - segmentLengthFraction * (seg.p1.x - seg.p0.x);
    coord.y = seg.p1.y - segmentLengthFraction * (seg.p1.y - seg.p0.y);
    return coord;
  }

  splitAt() {
    if (arguments.length === 1) {
      const pt = arguments[0];
      const minFrac = this._minimumLen / this._segLen;

      if (pt.distance(this._seg.p0) < this._minimumLen) {
        this._splitPt = this._seg.pointAlong(minFrac);
        return null;
      }

      if (pt.distance(this._seg.p1) < this._minimumLen) {
        this._splitPt = SplitSegment.pointAlongReverse(this._seg, minFrac);
        return null;
      }

      this._splitPt = pt;
    } else if (arguments.length === 2) {
      const length = arguments[0],
            endPt = arguments[1];
      const actualLen = this.getConstrainedLength(length);
      const frac = actualLen / this._segLen;
      if (endPt.equals2D(this._seg.p0)) this._splitPt = this._seg.pointAlong(frac);else this._splitPt = SplitSegment.pointAlongReverse(this._seg, frac);
    }
  }

  setMinimumLength(minLen) {
    this._minimumLen = minLen;
  }

  getConstrainedLength(len) {
    if (len < this._minimumLen) return this._minimumLen;
    return len;
  }

  getSplitPoint() {
    return this._splitPt;
  }

}

class ConstraintSplitPointFinder {
  findSplitPoint(seg, encroachPt) {}

}

class NonEncroachingSplitPointFinder {
  static projectedSplitPoint(seg, encroachPt) {
    const lineSeg = seg.getLineSegment();
    const projPt = lineSeg.project(encroachPt);
    return projPt;
  }

  findSplitPoint(seg, encroachPt) {
    const lineSeg = seg.getLineSegment();
    const segLen = lineSeg.getLength();
    const midPtLen = segLen / 2;
    const splitSeg = new SplitSegment(lineSeg);
    const projPt = NonEncroachingSplitPointFinder.projectedSplitPoint(seg, encroachPt);
    const nonEncroachDiam = projPt.distance(encroachPt) * 2 * 0.8;
    let maxSplitLen = nonEncroachDiam;
    if (maxSplitLen > midPtLen) maxSplitLen = midPtLen;
    splitSeg.setMinimumLength(maxSplitLen);
    splitSeg.splitAt(projPt);
    return splitSeg.getSplitPoint();
  }

  get interfaces_() {
    return [ConstraintSplitPointFinder];
  }

}

class TrianglePredicate {
  static triArea(a, b, c) {
    return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
  }

  static isInCircleDDNormalized(a, b, c, p) {
    const adx = DD.valueOf(a.x).selfSubtract(p.x);
    const ady = DD.valueOf(a.y).selfSubtract(p.y);
    const bdx = DD.valueOf(b.x).selfSubtract(p.x);
    const bdy = DD.valueOf(b.y).selfSubtract(p.y);
    const cdx = DD.valueOf(c.x).selfSubtract(p.x);
    const cdy = DD.valueOf(c.y).selfSubtract(p.y);
    const abdet = adx.multiply(bdy).selfSubtract(bdx.multiply(ady));
    const bcdet = bdx.multiply(cdy).selfSubtract(cdx.multiply(bdy));
    const cadet = cdx.multiply(ady).selfSubtract(adx.multiply(cdy));
    const alift = adx.multiply(adx).selfAdd(ady.multiply(ady));
    const blift = bdx.multiply(bdx).selfAdd(bdy.multiply(bdy));
    const clift = cdx.multiply(cdx).selfAdd(cdy.multiply(cdy));
    const sum = alift.selfMultiply(bcdet).selfAdd(blift.selfMultiply(cadet)).selfAdd(clift.selfMultiply(abdet));
    const isInCircle = sum.doubleValue() > 0;
    return isInCircle;
  }

  static checkRobustInCircle(a, b, c, p) {
    const nonRobustInCircle = TrianglePredicate.isInCircleNonRobust(a, b, c, p);
    const isInCircleDD = TrianglePredicate.isInCircleDDSlow(a, b, c, p);
    const isInCircleCC = TrianglePredicate.isInCircleCC(a, b, c, p);
    const circumCentre = Triangle.circumcentre(a, b, c);
    System.out.println('p radius diff a = ' + Math.abs(p.distance(circumCentre) - a.distance(circumCentre)) / a.distance(circumCentre));

    if (nonRobustInCircle !== isInCircleDD || nonRobustInCircle !== isInCircleCC) {
      System.out.println('inCircle robustness failure (double result = ' + nonRobustInCircle + ', DD result = ' + isInCircleDD + ', CC result = ' + isInCircleCC + ')');
      System.out.println(WKTWriter.toLineString(new CoordinateArraySequence([a, b, c, p])));
      System.out.println('Circumcentre = ' + WKTWriter.toPoint(circumCentre) + ' radius = ' + a.distance(circumCentre));
      System.out.println('p radius diff a = ' + Math.abs(p.distance(circumCentre) / a.distance(circumCentre) - 1));
      System.out.println('p radius diff b = ' + Math.abs(p.distance(circumCentre) / b.distance(circumCentre) - 1));
      System.out.println('p radius diff c = ' + Math.abs(p.distance(circumCentre) / c.distance(circumCentre) - 1));
      System.out.println();
    }
  }

  static isInCircleDDFast(a, b, c, p) {
    const aTerm = DD.sqr(a.x).selfAdd(DD.sqr(a.y)).selfMultiply(TrianglePredicate.triAreaDDFast(b, c, p));
    const bTerm = DD.sqr(b.x).selfAdd(DD.sqr(b.y)).selfMultiply(TrianglePredicate.triAreaDDFast(a, c, p));
    const cTerm = DD.sqr(c.x).selfAdd(DD.sqr(c.y)).selfMultiply(TrianglePredicate.triAreaDDFast(a, b, p));
    const pTerm = DD.sqr(p.x).selfAdd(DD.sqr(p.y)).selfMultiply(TrianglePredicate.triAreaDDFast(a, b, c));
    const sum = aTerm.selfSubtract(bTerm).selfAdd(cTerm).selfSubtract(pTerm);
    const isInCircle = sum.doubleValue() > 0;
    return isInCircle;
  }

  static isInCircleCC(a, b, c, p) {
    const cc = Triangle.circumcentre(a, b, c);
    const ccRadius = a.distance(cc);
    const pRadiusDiff = p.distance(cc) - ccRadius;
    return pRadiusDiff <= 0;
  }

  static isInCircleNormalized(a, b, c, p) {
    const adx = a.x - p.x;
    const ady = a.y - p.y;
    const bdx = b.x - p.x;
    const bdy = b.y - p.y;
    const cdx = c.x - p.x;
    const cdy = c.y - p.y;
    const abdet = adx * bdy - bdx * ady;
    const bcdet = bdx * cdy - cdx * bdy;
    const cadet = cdx * ady - adx * cdy;
    const alift = adx * adx + ady * ady;
    const blift = bdx * bdx + bdy * bdy;
    const clift = cdx * cdx + cdy * cdy;
    const disc = alift * bcdet + blift * cadet + clift * abdet;
    return disc > 0;
  }

  static isInCircleDDSlow(a, b, c, p) {
    const px = DD.valueOf(p.x);
    const py = DD.valueOf(p.y);
    const ax = DD.valueOf(a.x);
    const ay = DD.valueOf(a.y);
    const bx = DD.valueOf(b.x);
    const by = DD.valueOf(b.y);
    const cx = DD.valueOf(c.x);
    const cy = DD.valueOf(c.y);
    const aTerm = ax.multiply(ax).add(ay.multiply(ay)).multiply(TrianglePredicate.triAreaDDSlow(bx, by, cx, cy, px, py));
    const bTerm = bx.multiply(bx).add(by.multiply(by)).multiply(TrianglePredicate.triAreaDDSlow(ax, ay, cx, cy, px, py));
    const cTerm = cx.multiply(cx).add(cy.multiply(cy)).multiply(TrianglePredicate.triAreaDDSlow(ax, ay, bx, by, px, py));
    const pTerm = px.multiply(px).add(py.multiply(py)).multiply(TrianglePredicate.triAreaDDSlow(ax, ay, bx, by, cx, cy));
    const sum = aTerm.subtract(bTerm).add(cTerm).subtract(pTerm);
    const isInCircle = sum.doubleValue() > 0;
    return isInCircle;
  }

  static isInCircleNonRobust(a, b, c, p) {
    const isInCircle = (a.x * a.x + a.y * a.y) * TrianglePredicate.triArea(b, c, p) - (b.x * b.x + b.y * b.y) * TrianglePredicate.triArea(a, c, p) + (c.x * c.x + c.y * c.y) * TrianglePredicate.triArea(a, b, p) - (p.x * p.x + p.y * p.y) * TrianglePredicate.triArea(a, b, c) > 0;
    return isInCircle;
  }

  static isInCircleRobust(a, b, c, p) {
    return TrianglePredicate.isInCircleNormalized(a, b, c, p);
  }

  static triAreaDDSlow(ax, ay, bx, by, cx, cy) {
    return bx.subtract(ax).multiply(cy.subtract(ay)).subtract(by.subtract(ay).multiply(cx.subtract(ax)));
  }

  static triAreaDDFast(a, b, c) {
    const t1 = DD.valueOf(b.x).selfSubtract(a.x).selfMultiply(DD.valueOf(c.y).selfSubtract(a.y));
    const t2 = DD.valueOf(b.y).selfSubtract(a.y).selfMultiply(DD.valueOf(c.x).selfSubtract(a.x));
    return t1.selfSubtract(t2);
  }

}

class Vertex {
  constructor() {
    Vertex.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._p = null;

    if (arguments.length === 1) {
      const _p = arguments[0];
      this._p = new Coordinate(_p);
    } else if (arguments.length === 2) {
      const _x = arguments[0],
            _y = arguments[1];
      this._p = new Coordinate(_x, _y);
    } else if (arguments.length === 3) {
      const _x = arguments[0],
            _y = arguments[1],
            _z = arguments[2];
      this._p = new Coordinate(_x, _y, _z);
    }
  }

  static interpolateZ() {
    if (arguments.length === 3) {
      const p = arguments[0],
            p0 = arguments[1],
            p1 = arguments[2];
      const segLen = p0.distance(p1);
      const ptLen = p.distance(p0);
      const dz = p1.getZ() - p0.getZ();
      const pz = p0.getZ() + dz * (ptLen / segLen);
      return pz;
    } else if (arguments.length === 4) {
      const p = arguments[0],
            v0 = arguments[1],
            v1 = arguments[2],
            v2 = arguments[3];
      const x0 = v0.x;
      const y0 = v0.y;
      const a = v1.x - x0;
      const b = v2.x - x0;
      const c = v1.y - y0;
      const d = v2.y - y0;
      const det = a * d - b * c;
      const dx = p.x - x0;
      const dy = p.y - y0;
      const t = (d * dx - b * dy) / det;
      const u = (-c * dx + a * dy) / det;
      const z = v0.getZ() + t * (v1.getZ() - v0.getZ()) + u * (v2.getZ() - v0.getZ());
      return z;
    }
  }

  circleCenter(b, c) {
    const a = new Vertex(this.getX(), this.getY());
    const cab = this.bisector(a, b);
    const cbc = this.bisector(b, c);
    const hcc = new HCoordinate(cab, cbc);
    let cc = null;

    try {
      cc = new Vertex(hcc.getX(), hcc.getY());
    } catch (nre) {
      if (nre instanceof NotRepresentableException) {
        System.err.println('a: ' + a + '  b: ' + b + '  c: ' + c);
        System.err.println(nre);
      } else {
        throw nre;
      }
    } finally {}

    return cc;
  }

  dot(v) {
    return this._p.x * v.getX() + this._p.y * v.getY();
  }

  magn() {
    return Math.sqrt(this._p.x * this._p.x + this._p.y * this._p.y);
  }

  getZ() {
    return this._p.getZ();
  }

  bisector(a, b) {
    const dx = b.getX() - a.getX();
    const dy = b.getY() - a.getY();
    const l1 = new HCoordinate(a.getX() + dx / 2.0, a.getY() + dy / 2.0, 1.0);
    const l2 = new HCoordinate(a.getX() - dy + dx / 2.0, a.getY() + dx + dy / 2.0, 1.0);
    return new HCoordinate(l1, l2);
  }

  equals() {
    if (arguments.length === 1) {
      const _x = arguments[0];
      if (this._p.x === _x.getX() && this._p.y === _x.getY()) return true;else return false;
    } else if (arguments.length === 2) {
      const _x = arguments[0],
            tolerance = arguments[1];
      if (this._p.distance(_x.getCoordinate()) < tolerance) return true;else return false;
    }
  }

  getCoordinate() {
    return this._p;
  }

  isInCircle(a, b, c) {
    return TrianglePredicate.isInCircleRobust(a._p, b._p, c._p, this._p);
  }

  interpolateZValue(v0, v1, v2) {
    const x0 = v0.getX();
    const y0 = v0.getY();
    const a = v1.getX() - x0;
    const b = v2.getX() - x0;
    const c = v1.getY() - y0;
    const d = v2.getY() - y0;
    const det = a * d - b * c;
    const dx = this.getX() - x0;
    const dy = this.getY() - y0;
    const t = (d * dx - b * dy) / det;
    const u = (-c * dx + a * dy) / det;
    const z = v0.getZ() + t * (v1.getZ() - v0.getZ()) + u * (v2.getZ() - v0.getZ());
    return z;
  }

  midPoint(a) {
    const xm = (this._p.x + a.getX()) / 2.0;
    const ym = (this._p.y + a.getY()) / 2.0;
    const zm = (this._p.getZ() + a.getZ()) / 2.0;
    return new Vertex(xm, ym, zm);
  }

  rightOf(e) {
    return this.isCCW(e.dest(), e.orig());
  }

  isCCW(b, c) {
    return (b._p.x - this._p.x) * (c._p.y - this._p.y) - (b._p.y - this._p.y) * (c._p.x - this._p.x) > 0;
  }

  getX() {
    return this._p.x;
  }

  crossProduct(v) {
    return this._p.x * v.getY() - this._p.y * v.getX();
  }

  setZ(_z) {
    this._p.setZ(_z);
  }

  times(c) {
    return new Vertex(c * this._p.x, c * this._p.y);
  }

  cross() {
    return new Vertex(this._p.y, -this._p.x);
  }

  leftOf(e) {
    return this.isCCW(e.orig(), e.dest());
  }

  toString() {
    return 'POINT (' + this._p.x + ' ' + this._p.y + ')';
  }

  sub(v) {
    return new Vertex(this._p.x - v.getX(), this._p.y - v.getY());
  }

  getY() {
    return this._p.y;
  }

  classify(p0, p1) {
    const p2 = this;
    const a = p1.sub(p0);
    const b = p2.sub(p0);
    const sa = a.crossProduct(b);
    if (sa > 0.0) return Vertex.LEFT;
    if (sa < 0.0) return Vertex.RIGHT;
    if (a.getX() * b.getX() < 0.0 || a.getY() * b.getY() < 0.0) return Vertex.BEHIND;
    if (a.magn() < b.magn()) return Vertex.BEYOND;
    if (p0.equals(p2)) return Vertex.ORIGIN;
    if (p1.equals(p2)) return Vertex.DESTINATION;
    return Vertex.BETWEEN;
  }

  sum(v) {
    return new Vertex(this._p.x + v.getX(), this._p.y + v.getY());
  }

  distance(v1, v2) {
    return Math.sqrt(Math.pow(v2.getX() - v1.getX(), 2.0) + Math.pow(v2.getY() - v1.getY(), 2.0));
  }

  circumRadiusRatio(b, c) {
    const x = this.circleCenter(b, c);
    const radius = this.distance(x, b);
    let edgeLength = this.distance(this, b);
    let el = this.distance(b, c);
    if (el < edgeLength) edgeLength = el;
    el = this.distance(c, this);
    if (el < edgeLength) edgeLength = el;
    return radius / edgeLength;
  }

}
Vertex.LEFT = 0;
Vertex.RIGHT = 1;
Vertex.BEYOND = 2;
Vertex.BEHIND = 3;
Vertex.BETWEEN = 4;
Vertex.ORIGIN = 5;
Vertex.DESTINATION = 6;

class ConstraintVertex extends Vertex {
  constructor() {
    super();
    ConstraintVertex.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._isOnConstraint = null;
    this._constraint = null;
    const p = arguments[0];
    Vertex.constructor_.call(this, p);
  }

  getConstraint() {
    return this._constraint;
  }

  setOnConstraint(isOnConstraint) {
    this._isOnConstraint = isOnConstraint;
  }

  merge(other) {
    if (other._isOnConstraint) {
      this._isOnConstraint = true;
      this._constraint = other._constraint;
    }
  }

  isOnConstraint() {
    return this._isOnConstraint;
  }

  setConstraint(constraint) {
    this._isOnConstraint = true;
    this._constraint = constraint;
  }

}

class QuadEdge {
  constructor() {
    QuadEdge.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._rot = null;
    this._vertex = null;
    this._next = null;
    this._data = null;
  }

  static makeEdge(o, d) {
    const q0 = new QuadEdge();
    const q1 = new QuadEdge();
    const q2 = new QuadEdge();
    const q3 = new QuadEdge();
    q0._rot = q1;
    q1._rot = q2;
    q2._rot = q3;
    q3._rot = q0;
    q0.setNext(q0);
    q1.setNext(q3);
    q2.setNext(q2);
    q3.setNext(q1);
    const base = q0;
    base.setOrig(o);
    base.setDest(d);
    return base;
  }

  static swap(e) {
    const a = e.oPrev();
    const b = e.sym().oPrev();
    QuadEdge.splice(e, a);
    QuadEdge.splice(e.sym(), b);
    QuadEdge.splice(e, a.lNext());
    QuadEdge.splice(e.sym(), b.lNext());
    e.setOrig(a.dest());
    e.setDest(b.dest());
  }

  static splice(a, b) {
    const alpha = a.oNext().rot();
    const beta = b.oNext().rot();
    const t1 = b.oNext();
    const t2 = a.oNext();
    const t3 = beta.oNext();
    const t4 = alpha.oNext();
    a.setNext(t1);
    b.setNext(t2);
    alpha.setNext(t3);
    beta.setNext(t4);
  }

  static connect(a, b) {
    const e = QuadEdge.makeEdge(a.dest(), b.orig());
    QuadEdge.splice(e, a.lNext());
    QuadEdge.splice(e.sym(), b);
    return e;
  }

  equalsNonOriented(qe) {
    if (this.equalsOriented(qe)) return true;
    if (this.equalsOriented(qe.sym())) return true;
    return false;
  }

  toLineSegment() {
    return new LineSegment(this._vertex.getCoordinate(), this.dest().getCoordinate());
  }

  dest() {
    return this.sym().orig();
  }

  oNext() {
    return this._next;
  }

  equalsOriented(qe) {
    if (this.orig().getCoordinate().equals2D(qe.orig().getCoordinate()) && this.dest().getCoordinate().equals2D(qe.dest().getCoordinate())) return true;
    return false;
  }

  dNext() {
    return this.sym().oNext().sym();
  }

  lPrev() {
    return this._next.sym();
  }

  rPrev() {
    return this.sym().oNext();
  }

  rot() {
    return this._rot;
  }

  oPrev() {
    return this._rot._next._rot;
  }

  sym() {
    return this._rot._rot;
  }

  setOrig(o) {
    this._vertex = o;
  }

  lNext() {
    return this.invRot().oNext().rot();
  }

  getLength() {
    return this.orig().getCoordinate().distance(this.dest().getCoordinate());
  }

  invRot() {
    return this._rot.sym();
  }

  setDest(d) {
    this.sym().setOrig(d);
  }

  setData(data) {
    this._data = data;
  }

  getData() {
    return this._data;
  }

  delete() {
    this._rot = null;
  }

  orig() {
    return this._vertex;
  }

  rNext() {
    return this._rot._next.invRot();
  }

  toString() {
    const p0 = this._vertex.getCoordinate();

    const p1 = this.dest().getCoordinate();
    return WKTWriter.toLineString(p0, p1);
  }

  isLive() {
    return this._rot !== null;
  }

  getPrimary() {
    if (this.orig().getCoordinate().compareTo(this.dest().getCoordinate()) <= 0) return this;else return this.sym();
  }

  dPrev() {
    return this.invRot().oNext().invRot();
  }

  setNext(next) {
    this._next = next;
  }

}

class IncrementalDelaunayTriangulator {
  constructor() {
    IncrementalDelaunayTriangulator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._subdiv = null;
    this._isUsingTolerance = false;
    const subdiv = arguments[0];
    this._subdiv = subdiv;
    this._isUsingTolerance = subdiv.getTolerance() > 0.0;
  }

  insertSite(v) {
    let e = this._subdiv.locate(v);

    if (this._subdiv.isVertexOfEdge(e, v)) {
      return e;
    } else if (this._subdiv.isOnEdge(e, v.getCoordinate())) {
      e = e.oPrev();

      this._subdiv.delete(e.oNext());
    }

    let base = this._subdiv.makeEdge(e.orig(), v);

    QuadEdge.splice(base, e);
    const startEdge = base;

    do {
      base = this._subdiv.connect(e, base.sym());
      e = base.oPrev();
    } while (e.lNext() !== startEdge);

    do {
      const t = e.oPrev();

      if (t.dest().rightOf(e) && v.isInCircle(e.orig(), t.dest(), e.dest())) {
        QuadEdge.swap(e);
        e = e.oPrev();
      } else if (e.oNext() === startEdge) {
        return base;
      } else {
        e = e.oNext().lPrev();
      }
    } while (true);
  }

  insertSites(vertices) {
    for (let i = vertices.iterator(); i.hasNext();) {
      const v = i.next();
      this.insertSite(v);
    }
  }

}

class QuadEdgeLocator {
  locate(v) {}

}

class LastFoundQuadEdgeLocator {
  constructor() {
    LastFoundQuadEdgeLocator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._subdiv = null;
    this._lastEdge = null;
    const subdiv = arguments[0];
    this._subdiv = subdiv;
    this.init();
  }

  init() {
    this._lastEdge = this.findEdge();
  }

  locate(v) {
    if (!this._lastEdge.isLive()) this.init();

    const e = this._subdiv.locateFromEdge(v, this._lastEdge);

    this._lastEdge = e;
    return e;
  }

  findEdge() {
    const edges = this._subdiv.getEdges();

    return edges.iterator().next();
  }

  get interfaces_() {
    return [QuadEdgeLocator];
  }

}

class LocateFailureException extends RuntimeException {
  constructor() {
    super();
    LocateFailureException.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._seg = null;

    if (arguments.length === 1) {
      if (typeof arguments[0] === 'string') {
        const msg = arguments[0];
        RuntimeException.constructor_.call(this, msg);
      } else if (arguments[0] instanceof LineSegment) {
        const seg = arguments[0];
        RuntimeException.constructor_.call(this, 'Locate failed to converge (at edge: ' + seg + ').  Possible causes include invalid Subdivision topology or very close sites');
        this._seg = new LineSegment(seg);
      }
    } else if (arguments.length === 2) {
      const msg = arguments[0],
            seg = arguments[1];
      RuntimeException.constructor_.call(this, LocateFailureException.msgWithSpatial(msg, seg));
      this._seg = new LineSegment(seg);
    }
  }

  static msgWithSpatial(msg, seg) {
    if (seg !== null) return msg + ' [ ' + seg + ' ]';
    return msg;
  }

  getSegment() {
    return this._seg;
  }

}

class TriangleVisitor {
  visit(triEdges) {}

}

class QuadEdgeSubdivision {
  constructor() {
    QuadEdgeSubdivision.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._visitedKey = 0;
    this._quadEdges = new ArrayList();
    this._startingEdge = null;
    this._tolerance = null;
    this._edgeCoincidenceTolerance = null;
    this._frameVertex = new Array(3).fill(null);
    this._frameEnv = null;
    this._locator = null;
    this._seg = new LineSegment();
    this._triEdges = new Array(3).fill(null);
    const env = arguments[0],
          tolerance = arguments[1];
    this._tolerance = tolerance;
    this._edgeCoincidenceTolerance = tolerance / QuadEdgeSubdivision.EDGE_COINCIDENCE_TOL_FACTOR;
    this.createFrame(env);
    this._startingEdge = this.initSubdiv();
    this._locator = new LastFoundQuadEdgeLocator(this);
  }

  static getTriangleEdges(startQE, triEdge) {
    triEdge[0] = startQE;
    triEdge[1] = triEdge[0].lNext();
    triEdge[2] = triEdge[1].lNext();
    if (triEdge[2].lNext() !== triEdge[0]) throw new IllegalArgumentException('Edges do not form a triangle');
  }

  getTriangleVertices(includeFrame) {
    const visitor = new TriangleVertexListVisitor();
    this.visitTriangles(visitor, includeFrame);
    return visitor.getTriangleVertices();
  }

  isFrameVertex(v) {
    if (v.equals(this._frameVertex[0])) return true;
    if (v.equals(this._frameVertex[1])) return true;
    if (v.equals(this._frameVertex[2])) return true;
    return false;
  }

  isVertexOfEdge(e, v) {
    if (v.equals(e.orig(), this._tolerance) || v.equals(e.dest(), this._tolerance)) return true;
    return false;
  }

  connect(a, b) {
    const q = QuadEdge.connect(a, b);

    this._quadEdges.add(q);

    return q;
  }

  getVoronoiCellPolygon(qe, geomFact) {
    const cellPts = new ArrayList();
    const startQE = qe;

    do {
      const cc = qe.rot().orig().getCoordinate();
      cellPts.add(cc);
      qe = qe.oPrev();
    } while (qe !== startQE);

    const coordList = new CoordinateList();
    coordList.addAll(cellPts, false);
    coordList.closeRing();

    if (coordList.size() < 4) {
      System.out.println(coordList);
      coordList.add(coordList.get(coordList.size() - 1), true);
    }

    const pts = coordList.toCoordinateArray();
    const cellPoly = geomFact.createPolygon(geomFact.createLinearRing(pts));
    const v = startQE.orig();
    cellPoly.setUserData(v.getCoordinate());
    return cellPoly;
  }

  setLocator(locator) {
    this._locator = locator;
  }

  initSubdiv() {
    const ea = this.makeEdge(this._frameVertex[0], this._frameVertex[1]);
    const eb = this.makeEdge(this._frameVertex[1], this._frameVertex[2]);
    QuadEdge.splice(ea.sym(), eb);
    const ec = this.makeEdge(this._frameVertex[2], this._frameVertex[0]);
    QuadEdge.splice(eb.sym(), ec);
    QuadEdge.splice(ec.sym(), ea);
    return ea;
  }

  isFrameBorderEdge(e) {
    const leftTri = new Array(3).fill(null);
    QuadEdgeSubdivision.getTriangleEdges(e, leftTri);
    const rightTri = new Array(3).fill(null);
    QuadEdgeSubdivision.getTriangleEdges(e.sym(), rightTri);
    const vLeftTriOther = e.lNext().dest();
    if (this.isFrameVertex(vLeftTriOther)) return true;
    const vRightTriOther = e.sym().lNext().dest();
    if (this.isFrameVertex(vRightTriOther)) return true;
    return false;
  }

  makeEdge(o, d) {
    const q = QuadEdge.makeEdge(o, d);

    this._quadEdges.add(q);

    return q;
  }

  visitTriangles(triVisitor, includeFrame) {
    this._visitedKey++;
    const edgeStack = new Stack();
    edgeStack.push(this._startingEdge);
    const visitedEdges = new HashSet();

    while (!edgeStack.empty()) {
      const edge = edgeStack.pop();

      if (!visitedEdges.contains(edge)) {
        const triEdges = this.fetchTriangleToVisit(edge, edgeStack, includeFrame, visitedEdges);
        if (triEdges !== null) triVisitor.visit(triEdges);
      }
    }
  }

  isFrameEdge(e) {
    if (this.isFrameVertex(e.orig()) || this.isFrameVertex(e.dest())) return true;
    return false;
  }

  isOnEdge(e, p) {
    this._seg.setCoordinates(e.orig().getCoordinate(), e.dest().getCoordinate());

    const dist = this._seg.distance(p);

    return dist < this._edgeCoincidenceTolerance;
  }

  getEnvelope() {
    return new Envelope(this._frameEnv);
  }

  createFrame(env) {
    const deltaX = env.getWidth();
    const deltaY = env.getHeight();
    let offset = 0.0;
    if (deltaX > deltaY) offset = deltaX * 10.0;else offset = deltaY * 10.0;
    this._frameVertex[0] = new Vertex((env.getMaxX() + env.getMinX()) / 2.0, env.getMaxY() + offset);
    this._frameVertex[1] = new Vertex(env.getMinX() - offset, env.getMinY() - offset);
    this._frameVertex[2] = new Vertex(env.getMaxX() + offset, env.getMinY() - offset);
    this._frameEnv = new Envelope(this._frameVertex[0].getCoordinate(), this._frameVertex[1].getCoordinate());

    this._frameEnv.expandToInclude(this._frameVertex[2].getCoordinate());
  }

  getTriangleCoordinates(includeFrame) {
    const visitor = new TriangleCoordinatesVisitor();
    this.visitTriangles(visitor, includeFrame);
    return visitor.getTriangles();
  }

  getVertices(includeFrame) {
    const vertices = new HashSet();

    for (let i = this._quadEdges.iterator(); i.hasNext();) {
      const qe = i.next();
      const v = qe.orig();
      if (includeFrame || !this.isFrameVertex(v)) vertices.add(v);
      const vd = qe.dest();
      if (includeFrame || !this.isFrameVertex(vd)) vertices.add(vd);
    }

    return vertices;
  }

  fetchTriangleToVisit(edge, edgeStack, includeFrame, visitedEdges) {
    let curr = edge;
    let edgeCount = 0;
    let isFrame = false;

    do {
      this._triEdges[edgeCount] = curr;
      if (this.isFrameEdge(curr)) isFrame = true;
      const sym = curr.sym();
      if (!visitedEdges.contains(sym)) edgeStack.push(sym);
      visitedEdges.add(curr);
      edgeCount++;
      curr = curr.lNext();
    } while (curr !== edge);

    if (isFrame && !includeFrame) return null;
    return this._triEdges;
  }

  getEdges() {
    if (arguments.length === 0) {
      return this._quadEdges;
    } else if (arguments.length === 1) {
      const geomFact = arguments[0];
      const quadEdges = this.getPrimaryEdges(false);
      const edges = new Array(quadEdges.size()).fill(null);
      let i = 0;

      for (let it = quadEdges.iterator(); it.hasNext();) {
        const qe = it.next();
        edges[i++] = geomFact.createLineString([qe.orig().getCoordinate(), qe.dest().getCoordinate()]);
      }

      return geomFact.createMultiLineString(edges);
    }
  }

  getVertexUniqueEdges(includeFrame) {
    const edges = new ArrayList();
    const visitedVertices = new HashSet();

    for (let i = this._quadEdges.iterator(); i.hasNext();) {
      const qe = i.next();
      const v = qe.orig();

      if (!visitedVertices.contains(v)) {
        visitedVertices.add(v);
        if (includeFrame || !this.isFrameVertex(v)) edges.add(qe);
      }

      const qd = qe.sym();
      const vd = qd.orig();

      if (!visitedVertices.contains(vd)) {
        visitedVertices.add(vd);
        if (includeFrame || !this.isFrameVertex(vd)) edges.add(qd);
      }
    }

    return edges;
  }

  getTriangleEdges(includeFrame) {
    const visitor = new TriangleEdgesListVisitor();
    this.visitTriangles(visitor, includeFrame);
    return visitor.getTriangleEdges();
  }

  getPrimaryEdges(includeFrame) {
    this._visitedKey++;
    const edges = new ArrayList();
    const edgeStack = new Stack();
    edgeStack.push(this._startingEdge);
    const visitedEdges = new HashSet();

    while (!edgeStack.empty()) {
      const edge = edgeStack.pop();

      if (!visitedEdges.contains(edge)) {
        const priQE = edge.getPrimary();
        if (includeFrame || !this.isFrameEdge(priQE)) edges.add(priQE);
        edgeStack.push(edge.oNext());
        edgeStack.push(edge.sym().oNext());
        visitedEdges.add(edge);
        visitedEdges.add(edge.sym());
      }
    }

    return edges;
  }

  delete(e) {
    QuadEdge.splice(e, e.oPrev());
    QuadEdge.splice(e.sym(), e.sym().oPrev());
    const eSym = e.sym();
    const eRot = e.rot();
    const eRotSym = e.rot().sym();

    this._quadEdges.remove(e);

    this._quadEdges.remove(eSym);

    this._quadEdges.remove(eRot);

    this._quadEdges.remove(eRotSym);

    e.delete();
    eSym.delete();
    eRot.delete();
    eRotSym.delete();
  }

  locateFromEdge(v, startEdge) {
    let iter = 0;

    const maxIter = this._quadEdges.size();

    let e = startEdge;

    while (true) {
      iter++;
      if (iter > maxIter) throw new LocateFailureException(e.toLineSegment());
      if (v.equals(e.orig()) || v.equals(e.dest())) break;else if (v.rightOf(e)) e = e.sym();else if (!v.rightOf(e.oNext())) e = e.oNext();else if (!v.rightOf(e.dPrev())) e = e.dPrev();else break;
    }

    return e;
  }

  getTolerance() {
    return this._tolerance;
  }

  getVoronoiCellPolygons(geomFact) {
    this.visitTriangles(new TriangleCircumcentreVisitor(), true);
    const cells = new ArrayList();
    const edges = this.getVertexUniqueEdges(false);

    for (let i = edges.iterator(); i.hasNext();) {
      const qe = i.next();
      cells.add(this.getVoronoiCellPolygon(qe, geomFact));
    }

    return cells;
  }

  getVoronoiDiagram(geomFact) {
    const vorCells = this.getVoronoiCellPolygons(geomFact);
    return geomFact.createGeometryCollection(GeometryFactory.toGeometryArray(vorCells));
  }

  getTriangles(geomFact) {
    const triPtsList = this.getTriangleCoordinates(false);
    const tris = new Array(triPtsList.size()).fill(null);
    let i = 0;

    for (let it = triPtsList.iterator(); it.hasNext();) {
      const triPt = it.next();
      tris[i++] = geomFact.createPolygon(geomFact.createLinearRing(triPt));
    }

    return geomFact.createGeometryCollection(tris);
  }

  insertSite(v) {
    let e = this.locate(v);
    if (v.equals(e.orig(), this._tolerance) || v.equals(e.dest(), this._tolerance)) return e;
    let base = this.makeEdge(e.orig(), v);
    QuadEdge.splice(base, e);
    const startEdge = base;

    do {
      base = this.connect(e, base.sym());
      e = base.oPrev();
    } while (e.lNext() !== startEdge);

    return startEdge;
  }

  locate() {
    if (arguments.length === 1) {
      if (arguments[0] instanceof Vertex) {
        const v = arguments[0];
        return this._locator.locate(v);
      } else if (arguments[0] instanceof Coordinate) {
        const p = arguments[0];
        return this._locator.locate(new Vertex(p));
      }
    } else if (arguments.length === 2) {
      const p0 = arguments[0],
            p1 = arguments[1];

      const e = this._locator.locate(new Vertex(p0));

      if (e === null) return null;
      let base = e;
      if (e.dest().getCoordinate().equals2D(p0)) base = e.sym();
      let locEdge = base;

      do {
        if (locEdge.dest().getCoordinate().equals2D(p1)) return locEdge;
        locEdge = locEdge.oNext();
      } while (locEdge !== base);

      return null;
    }
  }

}

class TriangleCircumcentreVisitor {
  visit(triEdges) {
    const a = triEdges[0].orig().getCoordinate();
    const b = triEdges[1].orig().getCoordinate();
    const c = triEdges[2].orig().getCoordinate();
    const cc = Triangle.circumcentreDD(a, b, c);
    const ccVertex = new Vertex(cc);

    for (let i = 0; i < 3; i++) triEdges[i].rot().setOrig(ccVertex);
  }

  get interfaces_() {
    return [TriangleVisitor];
  }

}

class TriangleEdgesListVisitor {
  constructor() {
    TriangleEdgesListVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._triList = new ArrayList();
  }

  getTriangleEdges() {
    return this._triList;
  }

  visit(triEdges) {
    this._triList.add(triEdges);
  }

  get interfaces_() {
    return [TriangleVisitor];
  }

}

class TriangleVertexListVisitor {
  constructor() {
    TriangleVertexListVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._triList = new ArrayList();
  }

  visit(triEdges) {
    this._triList.add([triEdges[0].orig(), triEdges[1].orig(), triEdges[2].orig()]);
  }

  getTriangleVertices() {
    return this._triList;
  }

  get interfaces_() {
    return [TriangleVisitor];
  }

}

class TriangleCoordinatesVisitor {
  constructor() {
    TriangleCoordinatesVisitor.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._coordList = new CoordinateList();
    this._triCoords = new ArrayList();
  }

  checkTriangleSize(pts) {
    if (pts.length >= 2) WKTWriter.toLineString(pts[0], pts[1]);else if (pts.length >= 1) WKTWriter.toPoint(pts[0]);
  }

  visit(triEdges) {
    this._coordList.clear();

    for (let i = 0; i < 3; i++) {
      const v = triEdges[i].orig();

      this._coordList.add(v.getCoordinate());
    }

    if (this._coordList.size() > 0) {
      this._coordList.closeRing();

      const pts = this._coordList.toCoordinateArray();

      if (pts.length !== 4) return null;

      this._triCoords.add(pts);
    }
  }

  getTriangles() {
    return this._triCoords;
  }

  get interfaces_() {
    return [TriangleVisitor];
  }

}

QuadEdgeSubdivision.TriangleCircumcentreVisitor = TriangleCircumcentreVisitor;
QuadEdgeSubdivision.TriangleEdgesListVisitor = TriangleEdgesListVisitor;
QuadEdgeSubdivision.TriangleVertexListVisitor = TriangleVertexListVisitor;
QuadEdgeSubdivision.TriangleCoordinatesVisitor = TriangleCoordinatesVisitor;
QuadEdgeSubdivision.EDGE_COINCIDENCE_TOL_FACTOR = 1000;

class Segment {
  constructor() {
    Segment.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._ls = null;
    this._data = null;

    if (arguments.length === 2) {
      const p0 = arguments[0],
            p1 = arguments[1];
      this._ls = new LineSegment(p0, p1);
    } else if (arguments.length === 3) {
      const p0 = arguments[0],
            p1 = arguments[1],
            data = arguments[2];
      this._ls = new LineSegment(p0, p1);
      this._data = data;
    } else if (arguments.length === 6) {
      const x1 = arguments[0],
            y1 = arguments[1],
            z1 = arguments[2],
            x2 = arguments[3],
            y2 = arguments[4],
            z2 = arguments[5];
      Segment.constructor_.call(this, new Coordinate(x1, y1, z1), new Coordinate(x2, y2, z2));
    } else if (arguments.length === 7) {
      const x1 = arguments[0],
            y1 = arguments[1],
            z1 = arguments[2],
            x2 = arguments[3],
            y2 = arguments[4],
            z2 = arguments[5],
            data = arguments[6];
      Segment.constructor_.call(this, new Coordinate(x1, y1, z1), new Coordinate(x2, y2, z2), data);
    }
  }

  getLineSegment() {
    return this._ls;
  }

  getEndZ() {
    const p = this._ls.getCoordinate(1);

    return p.getZ();
  }

  getStartZ() {
    const p = this._ls.getCoordinate(0);

    return p.getZ();
  }

  intersection(s) {
    return this._ls.intersection(s.getLineSegment());
  }

  getStart() {
    return this._ls.getCoordinate(0);
  }

  getEnd() {
    return this._ls.getCoordinate(1);
  }

  getEndY() {
    const p = this._ls.getCoordinate(1);

    return p.y;
  }

  getStartX() {
    const p = this._ls.getCoordinate(0);

    return p.x;
  }

  equalsTopo(s) {
    return this._ls.equalsTopo(s.getLineSegment());
  }

  getStartY() {
    const p = this._ls.getCoordinate(0);

    return p.y;
  }

  setData(data) {
    this._data = data;
  }

  getData() {
    return this._data;
  }

  getEndX() {
    const p = this._ls.getCoordinate(1);

    return p.x;
  }

  toString() {
    return this._ls.toString();
  }

}

class ConstraintEnforcementException extends RuntimeException {
  constructor() {
    super();
    ConstraintEnforcementException.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._pt = null;

    if (arguments.length === 1) {
      const msg = arguments[0];
      RuntimeException.constructor_.call(this, msg);
    } else if (arguments.length === 2) {
      const msg = arguments[0],
            pt = arguments[1];
      RuntimeException.constructor_.call(this, ConstraintEnforcementException.msgWithCoord(msg, pt));
      this._pt = new Coordinate(pt);
    }
  }

  static msgWithCoord(msg, pt) {
    if (pt !== null) return msg + ' [ ' + WKTWriter.toPoint(pt) + ' ]';
    return msg;
  }

  getCoordinate() {
    return this._pt;
  }

}

class ConformingDelaunayTriangulator {
  constructor() {
    ConformingDelaunayTriangulator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._initialVertices = null;
    this._segVertices = null;
    this._segments = new ArrayList();
    this._subdiv = null;
    this._incDel = null;
    this._convexHull = null;
    this._splitFinder = new NonEncroachingSplitPointFinder();
    this._kdt = null;
    this._vertexFactory = null;
    this._computeAreaEnv = null;
    this._splitPt = null;
    this._tolerance = null;
    const initialVertices = arguments[0],
          tolerance = arguments[1];
    this._initialVertices = new ArrayList(initialVertices);
    this._tolerance = tolerance;
    this._kdt = new KdTree(tolerance);
  }

  static computeVertexEnvelope(vertices) {
    const env = new Envelope();

    for (let i = vertices.iterator(); i.hasNext();) {
      const v = i.next();
      env.expandToInclude(v.getCoordinate());
    }

    return env;
  }

  getInitialVertices() {
    return this._initialVertices;
  }

  getKDT() {
    return this._kdt;
  }

  enforceConstraints() {
    this.addConstraintVertices();
    let count = 0;
    let splits = 0;

    do {
      splits = this.enforceGabriel(this._segments);
      count++;
    } while (splits > 0 && count < ConformingDelaunayTriangulator.MAX_SPLIT_ITER);

    if (count === ConformingDelaunayTriangulator.MAX_SPLIT_ITER) throw new ConstraintEnforcementException('Too many splitting iterations while enforcing constraints.  Last split point was at: ', this._splitPt);
  }

  insertSites(vertices) {
    for (let i = vertices.iterator(); i.hasNext();) {
      const v = i.next();
      this.insertSite(v);
    }
  }

  getVertexFactory() {
    return this._vertexFactory;
  }

  getPointArray() {
    const pts = new Array(this._initialVertices.size() + this._segVertices.size()).fill(null);
    let index = 0;

    for (let i = this._initialVertices.iterator(); i.hasNext();) {
      const v = i.next();
      pts[index++] = v.getCoordinate();
    }

    for (let i2 = this._segVertices.iterator(); i2.hasNext();) {
      const v = i2.next();
      pts[index++] = v.getCoordinate();
    }

    return pts;
  }

  setConstraints(segments, segVertices) {
    this._segments = segments;
    this._segVertices = segVertices;
  }

  computeConvexHull() {
    const fact = new GeometryFactory();
    const coords = this.getPointArray();
    const hull = new ConvexHull(coords, fact);
    this._convexHull = hull.getConvexHull();
  }

  addConstraintVertices() {
    this.computeConvexHull();
    this.insertSites(this._segVertices);
  }

  findNonGabrielPoint(seg) {
    const p = seg.getStart();
    const q = seg.getEnd();
    const midPt = new Coordinate((p.x + q.x) / 2.0, (p.y + q.y) / 2.0);
    const segRadius = p.distance(midPt);
    const env = new Envelope(midPt);
    env.expandBy(segRadius);

    const result = this._kdt.query(env);

    let closestNonGabriel = null;
    let minDist = Double.MAX_VALUE;

    for (let i = result.iterator(); i.hasNext();) {
      const nextNode = i.next();
      const testPt = nextNode.getCoordinate();
      if (testPt.equals2D(p) || testPt.equals2D(q)) continue;
      const testRadius = midPt.distance(testPt);

      if (testRadius < segRadius) {
        const testDist = testRadius;

        if (closestNonGabriel === null || testDist < minDist) {
          closestNonGabriel = testPt;
          minDist = testDist;
        }
      }
    }

    return closestNonGabriel;
  }

  getConstraintSegments() {
    return this._segments;
  }

  setSplitPointFinder(splitFinder) {
    this._splitFinder = splitFinder;
  }

  getConvexHull() {
    return this._convexHull;
  }

  getTolerance() {
    return this._tolerance;
  }

  enforceGabriel(segsToInsert) {
    const newSegments = new ArrayList();
    let splits = 0;
    const segsToRemove = new ArrayList();

    for (let i = segsToInsert.iterator(); i.hasNext();) {
      const seg = i.next();
      const encroachPt = this.findNonGabrielPoint(seg);
      if (encroachPt === null) continue;
      this._splitPt = this._splitFinder.findSplitPoint(seg, encroachPt);
      const splitVertex = this.createVertex(this._splitPt, seg);
      const insertedVertex = this.insertSite(splitVertex);

      if (!insertedVertex.getCoordinate().equals2D(this._splitPt)) ;

      const s1 = new Segment(seg.getStartX(), seg.getStartY(), seg.getStartZ(), splitVertex.getX(), splitVertex.getY(), splitVertex.getZ(), seg.getData());
      const s2 = new Segment(splitVertex.getX(), splitVertex.getY(), splitVertex.getZ(), seg.getEndX(), seg.getEndY(), seg.getEndZ(), seg.getData());
      newSegments.add(s1);
      newSegments.add(s2);
      segsToRemove.add(seg);
      splits = splits + 1;
    }

    segsToInsert.removeAll(segsToRemove);
    segsToInsert.addAll(newSegments);
    return splits;
  }

  createVertex() {
    if (arguments.length === 1) {
      const p = arguments[0];
      let v = null;
      if (this._vertexFactory !== null) v = this._vertexFactory.createVertex(p, null);else v = new ConstraintVertex(p);
      return v;
    } else if (arguments.length === 2) {
      const p = arguments[0],
            seg = arguments[1];
      let v = null;
      if (this._vertexFactory !== null) v = this._vertexFactory.createVertex(p, seg);else v = new ConstraintVertex(p);
      v.setOnConstraint(true);
      return v;
    }
  }

  getSubdivision() {
    return this._subdiv;
  }

  computeBoundingBox() {
    const vertexEnv = ConformingDelaunayTriangulator.computeVertexEnvelope(this._initialVertices);
    const segEnv = ConformingDelaunayTriangulator.computeVertexEnvelope(this._segVertices);
    const allPointsEnv = new Envelope(vertexEnv);
    allPointsEnv.expandToInclude(segEnv);
    const deltaX = allPointsEnv.getWidth() * 0.2;
    const deltaY = allPointsEnv.getHeight() * 0.2;
    const delta = Math.max(deltaX, deltaY);
    this._computeAreaEnv = new Envelope(allPointsEnv);

    this._computeAreaEnv.expandBy(delta);
  }

  setVertexFactory(vertexFactory) {
    this._vertexFactory = vertexFactory;
  }

  formInitialDelaunay() {
    this.computeBoundingBox();
    this._subdiv = new QuadEdgeSubdivision(this._computeAreaEnv, this._tolerance);

    this._subdiv.setLocator(new LastFoundQuadEdgeLocator(this._subdiv));

    this._incDel = new IncrementalDelaunayTriangulator(this._subdiv);
    this.insertSites(this._initialVertices);
  }

  insertSite() {
    if (arguments[0] instanceof ConstraintVertex) {
      const v = arguments[0];

      const kdnode = this._kdt.insert(v.getCoordinate(), v);

      if (!kdnode.isRepeated()) {
        this._incDel.insertSite(v);
      } else {
        const snappedV = kdnode.getData();
        snappedV.merge(v);
        return snappedV;
      }

      return v;
    } else if (arguments[0] instanceof Coordinate) {
      const p = arguments[0];
      this.insertSite(this.createVertex(p));
    }
  }

}
ConformingDelaunayTriangulator.MAX_SPLIT_ITER = 99;

class DelaunayTriangulationBuilder {
  constructor() {
    DelaunayTriangulationBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._siteCoords = null;
    this._tolerance = 0.0;
    this._subdiv = null;
  }

  static extractUniqueCoordinates(geom) {
    if (geom === null) return new CoordinateList();
    const coords = geom.getCoordinates();
    return DelaunayTriangulationBuilder.unique(coords);
  }

  static envelope(coords) {
    const env = new Envelope();

    for (let i = coords.iterator(); i.hasNext();) {
      const coord = i.next();
      env.expandToInclude(coord);
    }

    return env;
  }

  static unique(coords) {
    const coordsCopy = CoordinateArrays.copyDeep(coords);
    Arrays.sort(coordsCopy);
    const coordList = new CoordinateList(coordsCopy, false);
    return coordList;
  }

  static toVertices(coords) {
    const verts = new ArrayList();

    for (let i = coords.iterator(); i.hasNext();) {
      const coord = i.next();
      verts.add(new Vertex(coord));
    }

    return verts;
  }

  create() {
    if (this._subdiv !== null) return null;
    const siteEnv = DelaunayTriangulationBuilder.envelope(this._siteCoords);
    const vertices = DelaunayTriangulationBuilder.toVertices(this._siteCoords);
    this._subdiv = new QuadEdgeSubdivision(siteEnv, this._tolerance);
    const triangulator = new IncrementalDelaunayTriangulator(this._subdiv);
    triangulator.insertSites(vertices);
  }

  setTolerance(tolerance) {
    this._tolerance = tolerance;
  }

  setSites() {
    if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];
      this._siteCoords = DelaunayTriangulationBuilder.extractUniqueCoordinates(geom);
    } else if (hasInterface(arguments[0], Collection)) {
      const coords = arguments[0];
      this._siteCoords = DelaunayTriangulationBuilder.unique(CoordinateArrays.toCoordinateArray(coords));
    }
  }

  getEdges(geomFact) {
    this.create();
    return this._subdiv.getEdges(geomFact);
  }

  getSubdivision() {
    this.create();
    return this._subdiv;
  }

  getTriangles(geomFact) {
    this.create();
    return this._subdiv.getTriangles(geomFact);
  }

}

class ConformingDelaunayTriangulationBuilder {
  constructor() {
    ConformingDelaunayTriangulationBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._siteCoords = null;
    this._constraintLines = null;
    this._tolerance = 0.0;
    this._subdiv = null;
    this._constraintVertexMap = new TreeMap();
  }

  static createConstraintSegments() {
    if (arguments.length === 1) {
      const geom = arguments[0];
      const lines = LinearComponentExtracter.getLines(geom);
      const constraintSegs = new ArrayList();

      for (let i = lines.iterator(); i.hasNext();) {
        const line = i.next();
        ConformingDelaunayTriangulationBuilder.createConstraintSegments(line, constraintSegs);
      }

      return constraintSegs;
    } else if (arguments.length === 2) {
      const line = arguments[0],
            constraintSegs = arguments[1];
      const coords = line.getCoordinates();

      for (let i = 1; i < coords.length; i++) constraintSegs.add(new Segment(coords[i - 1], coords[i]));
    }
  }

  createSiteVertices(coords) {
    const verts = new ArrayList();

    for (let i = coords.iterator(); i.hasNext();) {
      const coord = i.next();
      if (this._constraintVertexMap.containsKey(coord)) continue;
      verts.add(new ConstraintVertex(coord));
    }

    return verts;
  }

  create() {
    if (this._subdiv !== null) return null;
    const siteEnv = DelaunayTriangulationBuilder.envelope(this._siteCoords);
    let segments = new ArrayList();

    if (this._constraintLines !== null) {
      siteEnv.expandToInclude(this._constraintLines.getEnvelopeInternal());
      this.createVertices(this._constraintLines);
      segments = ConformingDelaunayTriangulationBuilder.createConstraintSegments(this._constraintLines);
    }

    const sites = this.createSiteVertices(this._siteCoords);
    const cdt = new ConformingDelaunayTriangulator(sites, this._tolerance);
    cdt.setConstraints(segments, new ArrayList(this._constraintVertexMap.values()));
    cdt.formInitialDelaunay();
    cdt.enforceConstraints();
    this._subdiv = cdt.getSubdivision();
  }

  setTolerance(tolerance) {
    this._tolerance = tolerance;
  }

  setConstraints(constraintLines) {
    this._constraintLines = constraintLines;
  }

  setSites(geom) {
    this._siteCoords = DelaunayTriangulationBuilder.extractUniqueCoordinates(geom);
  }

  getEdges(geomFact) {
    this.create();
    return this._subdiv.getEdges(geomFact);
  }

  getSubdivision() {
    this.create();
    return this._subdiv;
  }

  getTriangles(geomFact) {
    this.create();
    return this._subdiv.getTriangles(geomFact);
  }

  createVertices(geom) {
    const coords = geom.getCoordinates();

    for (let i = 0; i < coords.length; i++) {
      const v = new ConstraintVertex(coords[i]);

      this._constraintVertexMap.put(coords[i], v);
    }
  }

}

class VoronoiDiagramBuilder {
  constructor() {
    VoronoiDiagramBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._siteCoords = null;
    this._tolerance = 0.0;
    this._subdiv = null;
    this._clipEnv = null;
    this._diagramEnv = null;
  }

  static clipGeometryCollection(geom, clipEnv) {
    const clipPoly = geom.getFactory().toGeometry(clipEnv);
    const clipped = new ArrayList();

    for (let i = 0; i < geom.getNumGeometries(); i++) {
      const g = geom.getGeometryN(i);
      let result = null;

      if (clipEnv.contains(g.getEnvelopeInternal())) {
        result = g;
      } else if (clipEnv.intersects(g.getEnvelopeInternal())) {
        result = OverlayOp.intersection(clipPoly, g);
        result.setUserData(g.getUserData());
      }

      if (result !== null && !result.isEmpty()) clipped.add(result);
    }

    return geom.getFactory().createGeometryCollection(GeometryFactory.toGeometryArray(clipped));
  }

  create() {
    if (this._subdiv !== null) return null;
    const siteEnv = DelaunayTriangulationBuilder.envelope(this._siteCoords);
    this._diagramEnv = this._clipEnv;

    if (this._diagramEnv === null) {
      this._diagramEnv = siteEnv;

      const expandBy = this._diagramEnv.getDiameter();

      this._diagramEnv.expandBy(expandBy);
    }

    const vertices = DelaunayTriangulationBuilder.toVertices(this._siteCoords);
    this._subdiv = new QuadEdgeSubdivision(siteEnv, this._tolerance);
    const triangulator = new IncrementalDelaunayTriangulator(this._subdiv);
    triangulator.insertSites(vertices);
  }

  getDiagram(geomFact) {
    this.create();

    const polys = this._subdiv.getVoronoiDiagram(geomFact);

    return VoronoiDiagramBuilder.clipGeometryCollection(polys, this._diagramEnv);
  }

  setTolerance(tolerance) {
    this._tolerance = tolerance;
  }

  setSites() {
    if (arguments[0] instanceof Geometry) {
      const geom = arguments[0];
      this._siteCoords = DelaunayTriangulationBuilder.extractUniqueCoordinates(geom);
    } else if (hasInterface(arguments[0], Collection)) {
      const coords = arguments[0];
      this._siteCoords = DelaunayTriangulationBuilder.unique(CoordinateArrays.toCoordinateArray(coords));
    }
  }

  setClipEnvelope(clipEnv) {
    this._clipEnv = clipEnv;
  }

  getSubdivision() {
    this.create();
    return this._subdiv;
  }

}

var quadedge = /*#__PURE__*/Object.freeze({
  __proto__: null,
  Vertex: Vertex
});

var triangulate = /*#__PURE__*/Object.freeze({
  __proto__: null,
  ConformingDelaunayTriangulationBuilder: ConformingDelaunayTriangulationBuilder,
  DelaunayTriangulationBuilder: DelaunayTriangulationBuilder,
  VoronoiDiagramBuilder: VoronoiDiagramBuilder,
  quadedge: quadedge
});

class LinearIterator {
  constructor() {
    LinearIterator.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._linearGeom = null;
    this._numLines = null;
    this._currentLine = null;
    this._componentIndex = 0;
    this._vertexIndex = 0;

    if (arguments.length === 1) {
      const linear = arguments[0];
      LinearIterator.constructor_.call(this, linear, 0, 0);
    } else if (arguments.length === 2) {
      const linear = arguments[0],
            start = arguments[1];
      LinearIterator.constructor_.call(this, linear, start.getComponentIndex(), LinearIterator.segmentEndVertexIndex(start));
    } else if (arguments.length === 3) {
      const linearGeom = arguments[0],
            componentIndex = arguments[1],
            vertexIndex = arguments[2];
      if (!hasInterface(linearGeom, Lineal)) throw new IllegalArgumentException('Lineal geometry is required');
      this._linearGeom = linearGeom;
      this._numLines = linearGeom.getNumGeometries();
      this._componentIndex = componentIndex;
      this._vertexIndex = vertexIndex;
      this.loadCurrentLine();
    }
  }

  static segmentEndVertexIndex(loc) {
    if (loc.getSegmentFraction() > 0.0) return loc.getSegmentIndex() + 1;
    return loc.getSegmentIndex();
  }

  getComponentIndex() {
    return this._componentIndex;
  }

  getLine() {
    return this._currentLine;
  }

  getVertexIndex() {
    return this._vertexIndex;
  }

  getSegmentEnd() {
    if (this._vertexIndex < this.getLine().getNumPoints() - 1) return this._currentLine.getCoordinateN(this._vertexIndex + 1);
    return null;
  }

  next() {
    if (!this.hasNext()) return null;
    this._vertexIndex++;

    if (this._vertexIndex >= this._currentLine.getNumPoints()) {
      this._componentIndex++;
      this.loadCurrentLine();
      this._vertexIndex = 0;
    }
  }

  loadCurrentLine() {
    if (this._componentIndex >= this._numLines) {
      this._currentLine = null;
      return null;
    }

    this._currentLine = this._linearGeom.getGeometryN(this._componentIndex);
  }

  getSegmentStart() {
    return this._currentLine.getCoordinateN(this._vertexIndex);
  }

  isEndOfLine() {
    if (this._componentIndex >= this._numLines) return false;
    if (this._vertexIndex < this._currentLine.getNumPoints() - 1) return false;
    return true;
  }

  hasNext() {
    if (this._componentIndex >= this._numLines) return false;
    if (this._componentIndex === this._numLines - 1 && this._vertexIndex >= this._currentLine.getNumPoints()) return false;
    return true;
  }

}

class LengthIndexOfPoint {
  constructor() {
    LengthIndexOfPoint.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._linearGeom = null;
    const linearGeom = arguments[0];
    this._linearGeom = linearGeom;
  }

  static indexOf(linearGeom, inputPt) {
    const locater = new LengthIndexOfPoint(linearGeom);
    return locater.indexOf(inputPt);
  }

  static indexOfAfter(linearGeom, inputPt, minIndex) {
    const locater = new LengthIndexOfPoint(linearGeom);
    return locater.indexOfAfter(inputPt, minIndex);
  }

  indexOf(inputPt) {
    return this.indexOfFromStart(inputPt, -1.0);
  }

  indexOfFromStart(inputPt, minIndex) {
    let minDistance = Double.MAX_VALUE;
    let ptMeasure = minIndex;
    let segmentStartMeasure = 0.0;
    const seg = new LineSegment();
    const it = new LinearIterator(this._linearGeom);

    while (it.hasNext()) {
      if (!it.isEndOfLine()) {
        seg.p0 = it.getSegmentStart();
        seg.p1 = it.getSegmentEnd();
        const segDistance = seg.distance(inputPt);
        const segMeasureToPt = this.segmentNearestMeasure(seg, inputPt, segmentStartMeasure);

        if (segDistance < minDistance && segMeasureToPt > minIndex) {
          ptMeasure = segMeasureToPt;
          minDistance = segDistance;
        }

        segmentStartMeasure += seg.getLength();
      }

      it.next();
    }

    return ptMeasure;
  }

  indexOfAfter(inputPt, minIndex) {
    if (minIndex < 0.0) return this.indexOf(inputPt);

    const endIndex = this._linearGeom.getLength();

    if (endIndex < minIndex) return endIndex;
    const closestAfter = this.indexOfFromStart(inputPt, minIndex);
    Assert.isTrue(closestAfter >= minIndex, 'computed index is before specified minimum index');
    return closestAfter;
  }

  segmentNearestMeasure(seg, inputPt, segmentStartMeasure) {
    const projFactor = seg.projectionFactor(inputPt);
    if (projFactor <= 0.0) return segmentStartMeasure;
    if (projFactor <= 1.0) return segmentStartMeasure + projFactor * seg.getLength();
    return segmentStartMeasure + seg.getLength();
  }

}

class LinearLocation {
  constructor() {
    LinearLocation.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._componentIndex = 0;
    this._segmentIndex = 0;
    this._segmentFraction = 0.0;

    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const loc = arguments[0];
      this._componentIndex = loc._componentIndex;
      this._segmentIndex = loc._segmentIndex;
      this._segmentFraction = loc._segmentFraction;
    } else if (arguments.length === 2) {
      const segmentIndex = arguments[0],
            segmentFraction = arguments[1];
      LinearLocation.constructor_.call(this, 0, segmentIndex, segmentFraction);
    } else if (arguments.length === 3) {
      const componentIndex = arguments[0],
            segmentIndex = arguments[1],
            segmentFraction = arguments[2];
      this._componentIndex = componentIndex;
      this._segmentIndex = segmentIndex;
      this._segmentFraction = segmentFraction;
      this.normalize();
    } else if (arguments.length === 4) {
      const componentIndex = arguments[0],
            segmentIndex = arguments[1],
            segmentFraction = arguments[2],
            doNormalize = arguments[3];
      this._componentIndex = componentIndex;
      this._segmentIndex = segmentIndex;
      this._segmentFraction = segmentFraction;
      if (doNormalize) this.normalize();
    }
  }

  static getEndLocation(linear) {
    const loc = new LinearLocation();
    loc.setToEnd(linear);
    return loc;
  }

  static pointAlongSegmentByFraction(p0, p1, frac) {
    if (frac <= 0.0) return p0;
    if (frac >= 1.0) return p1;
    const x = (p1.x - p0.x) * frac + p0.x;
    const y = (p1.y - p0.y) * frac + p0.y;
    const z = (p1.getZ() - p0.getZ()) * frac + p0.getZ();
    return new Coordinate(x, y, z);
  }

  static compareLocationValues(componentIndex0, segmentIndex0, segmentFraction0, componentIndex1, segmentIndex1, segmentFraction1) {
    if (componentIndex0 < componentIndex1) return -1;
    if (componentIndex0 > componentIndex1) return 1;
    if (segmentIndex0 < segmentIndex1) return -1;
    if (segmentIndex0 > segmentIndex1) return 1;
    if (segmentFraction0 < segmentFraction1) return -1;
    if (segmentFraction0 > segmentFraction1) return 1;
    return 0;
  }

  static numSegments(line) {
    const npts = line.getNumPoints();
    if (npts <= 1) return 0;
    return npts - 1;
  }

  getSegmentIndex() {
    return this._segmentIndex;
  }

  getComponentIndex() {
    return this._componentIndex;
  }

  isEndpoint(linearGeom) {
    const lineComp = linearGeom.getGeometryN(this._componentIndex);
    const nseg = LinearLocation.numSegments(lineComp);
    return this._segmentIndex >= nseg || this._segmentIndex === nseg - 1 && this._segmentFraction >= 1.0;
  }

  isValid(linearGeom) {
    if (this._componentIndex < 0 || this._componentIndex >= linearGeom.getNumGeometries()) return false;
    const lineComp = linearGeom.getGeometryN(this._componentIndex);
    if (this._segmentIndex < 0 || this._segmentIndex > lineComp.getNumPoints()) return false;
    if (this._segmentIndex === lineComp.getNumPoints() && this._segmentFraction !== 0.0) return false;
    if (this._segmentFraction < 0.0 || this._segmentFraction > 1.0) return false;
    return true;
  }

  normalize() {
    if (this._segmentFraction < 0.0) this._segmentFraction = 0.0;
    if (this._segmentFraction > 1.0) this._segmentFraction = 1.0;

    if (this._componentIndex < 0) {
      this._componentIndex = 0;
      this._segmentIndex = 0;
      this._segmentFraction = 0.0;
    }

    if (this._segmentIndex < 0) {
      this._segmentIndex = 0;
      this._segmentFraction = 0.0;
    }

    if (this._segmentFraction === 1.0) {
      this._segmentFraction = 0.0;
      this._segmentIndex += 1;
    }
  }

  toLowest(linearGeom) {
    const lineComp = linearGeom.getGeometryN(this._componentIndex);
    const nseg = LinearLocation.numSegments(lineComp);
    if (this._segmentIndex < nseg) return this;
    return new LinearLocation(this._componentIndex, nseg - 1, 1.0, false);
  }

  getCoordinate(linearGeom) {
    const lineComp = linearGeom.getGeometryN(this._componentIndex);
    const p0 = lineComp.getCoordinateN(this._segmentIndex);
    if (this._segmentIndex >= LinearLocation.numSegments(lineComp)) return p0;
    const p1 = lineComp.getCoordinateN(this._segmentIndex + 1);
    return LinearLocation.pointAlongSegmentByFraction(p0, p1, this._segmentFraction);
  }

  getSegmentFraction() {
    return this._segmentFraction;
  }

  getSegment(linearGeom) {
    const lineComp = linearGeom.getGeometryN(this._componentIndex);
    const p0 = lineComp.getCoordinateN(this._segmentIndex);

    if (this._segmentIndex >= LinearLocation.numSegments(lineComp)) {
      const prev = lineComp.getCoordinateN(lineComp.getNumPoints() - 2);
      return new LineSegment(prev, p0);
    }

    const p1 = lineComp.getCoordinateN(this._segmentIndex + 1);
    return new LineSegment(p0, p1);
  }

  clamp(linear) {
    if (this._componentIndex >= linear.getNumGeometries()) {
      this.setToEnd(linear);
      return null;
    }

    if (this._segmentIndex >= linear.getNumPoints()) {
      const line = linear.getGeometryN(this._componentIndex);
      this._segmentIndex = LinearLocation.numSegments(line);
      this._segmentFraction = 1.0;
    }
  }

  setToEnd(linear) {
    this._componentIndex = linear.getNumGeometries() - 1;
    const lastLine = linear.getGeometryN(this._componentIndex);
    this._segmentIndex = LinearLocation.numSegments(lastLine);
    this._segmentFraction = 0.0;
  }

  compareTo(o) {
    const other = o;
    if (this._componentIndex < other._componentIndex) return -1;
    if (this._componentIndex > other._componentIndex) return 1;
    if (this._segmentIndex < other._segmentIndex) return -1;
    if (this._segmentIndex > other._segmentIndex) return 1;
    if (this._segmentFraction < other._segmentFraction) return -1;
    if (this._segmentFraction > other._segmentFraction) return 1;
    return 0;
  }

  copy() {
    return new LinearLocation(this._componentIndex, this._segmentIndex, this._segmentFraction);
  }

  toString() {
    return 'LinearLoc[' + this._componentIndex + ', ' + this._segmentIndex + ', ' + this._segmentFraction + ']';
  }

  isOnSameSegment(loc) {
    if (this._componentIndex !== loc._componentIndex) return false;
    if (this._segmentIndex === loc._segmentIndex) return true;
    if (loc._segmentIndex - this._segmentIndex === 1 && loc._segmentFraction === 0.0) return true;
    if (this._segmentIndex - loc._segmentIndex === 1 && this._segmentFraction === 0.0) return true;
    return false;
  }

  snapToVertex(linearGeom, minDistance) {
    if (this._segmentFraction <= 0.0 || this._segmentFraction >= 1.0) return null;
    const segLen = this.getSegmentLength(linearGeom);
    const lenToStart = this._segmentFraction * segLen;
    const lenToEnd = segLen - lenToStart;
    if (lenToStart <= lenToEnd && lenToStart < minDistance) this._segmentFraction = 0.0;else if (lenToEnd <= lenToStart && lenToEnd < minDistance) this._segmentFraction = 1.0;
  }

  compareLocationValues(componentIndex1, segmentIndex1, segmentFraction1) {
    if (this._componentIndex < componentIndex1) return -1;
    if (this._componentIndex > componentIndex1) return 1;
    if (this._segmentIndex < segmentIndex1) return -1;
    if (this._segmentIndex > segmentIndex1) return 1;
    if (this._segmentFraction < segmentFraction1) return -1;
    if (this._segmentFraction > segmentFraction1) return 1;
    return 0;
  }

  getSegmentLength(linearGeom) {
    const lineComp = linearGeom.getGeometryN(this._componentIndex);
    let segIndex = this._segmentIndex;
    if (this._segmentIndex >= LinearLocation.numSegments(lineComp)) segIndex = lineComp.getNumPoints() - 2;
    const p0 = lineComp.getCoordinateN(segIndex);
    const p1 = lineComp.getCoordinateN(segIndex + 1);
    return p0.distance(p1);
  }

  isVertex() {
    return this._segmentFraction <= 0.0 || this._segmentFraction >= 1.0;
  }

  get interfaces_() {
    return [Comparable];
  }

}

class LocationIndexOfPoint {
  constructor() {
    LocationIndexOfPoint.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._linearGeom = null;
    const linearGeom = arguments[0];
    this._linearGeom = linearGeom;
  }

  static indexOf(linearGeom, inputPt) {
    const locater = new LocationIndexOfPoint(linearGeom);
    return locater.indexOf(inputPt);
  }

  static indexOfAfter(linearGeom, inputPt, minIndex) {
    const locater = new LocationIndexOfPoint(linearGeom);
    return locater.indexOfAfter(inputPt, minIndex);
  }

  indexOf(inputPt) {
    return this.indexOfFromStart(inputPt, null);
  }

  indexOfFromStart(inputPt, minIndex) {
    let minDistance = Double.MAX_VALUE;
    let minComponentIndex = 0;
    let minSegmentIndex = 0;
    let minFrac = -1.0;
    const seg = new LineSegment();

    for (let it = new LinearIterator(this._linearGeom); it.hasNext(); it.next()) if (!it.isEndOfLine()) {
      seg.p0 = it.getSegmentStart();
      seg.p1 = it.getSegmentEnd();
      const segDistance = seg.distance(inputPt);
      const segFrac = seg.segmentFraction(inputPt);
      const candidateComponentIndex = it.getComponentIndex();
      const candidateSegmentIndex = it.getVertexIndex();
      if (segDistance < minDistance) if (minIndex === null || minIndex.compareLocationValues(candidateComponentIndex, candidateSegmentIndex, segFrac) < 0) {
        minComponentIndex = candidateComponentIndex;
        minSegmentIndex = candidateSegmentIndex;
        minFrac = segFrac;
        minDistance = segDistance;
      }
    }

    if (minDistance === Double.MAX_VALUE) return new LinearLocation(minIndex);
    const loc = new LinearLocation(minComponentIndex, minSegmentIndex, minFrac);
    return loc;
  }

  indexOfAfter(inputPt, minIndex) {
    if (minIndex === null) return this.indexOf(inputPt);
    const endLoc = LinearLocation.getEndLocation(this._linearGeom);
    if (endLoc.compareTo(minIndex) <= 0) return endLoc;
    const closestAfter = this.indexOfFromStart(inputPt, minIndex);
    Assert.isTrue(closestAfter.compareTo(minIndex) >= 0, 'computed location is before specified minimum location');
    return closestAfter;
  }

}

class LocationIndexOfLine {
  constructor() {
    LocationIndexOfLine.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._linearGeom = null;
    const linearGeom = arguments[0];
    this._linearGeom = linearGeom;
  }

  static indicesOf(linearGeom, subLine) {
    const locater = new LocationIndexOfLine(linearGeom);
    return locater.indicesOf(subLine);
  }

  indicesOf(subLine) {
    const startPt = subLine.getGeometryN(0).getCoordinateN(0);
    const lastLine = subLine.getGeometryN(subLine.getNumGeometries() - 1);
    const endPt = lastLine.getCoordinateN(lastLine.getNumPoints() - 1);
    const locPt = new LocationIndexOfPoint(this._linearGeom);
    const subLineLoc = new Array(2).fill(null);
    subLineLoc[0] = locPt.indexOf(startPt);
    if (subLine.getLength() === 0.0) subLineLoc[1] = subLineLoc[0].copy();else subLineLoc[1] = locPt.indexOfAfter(endPt, subLineLoc[0]);
    return subLineLoc;
  }

}

class LengthLocationMap {
  constructor() {
    LengthLocationMap.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._linearGeom = null;
    const linearGeom = arguments[0];
    this._linearGeom = linearGeom;
  }

  static getLength(linearGeom, loc) {
    const locater = new LengthLocationMap(linearGeom);
    return locater.getLength(loc);
  }

  static getLocation() {
    if (arguments.length === 2) {
      const linearGeom = arguments[0],
            length = arguments[1];
      const locater = new LengthLocationMap(linearGeom);
      return locater.getLocation(length);
    } else if (arguments.length === 3) {
      const linearGeom = arguments[0],
            length = arguments[1],
            resolveLower = arguments[2];
      const locater = new LengthLocationMap(linearGeom);
      return locater.getLocation(length, resolveLower);
    }
  }

  getLength(loc) {
    let totalLength = 0.0;
    const it = new LinearIterator(this._linearGeom);

    while (it.hasNext()) {
      if (!it.isEndOfLine()) {
        const p0 = it.getSegmentStart();
        const p1 = it.getSegmentEnd();
        const segLen = p1.distance(p0);
        if (loc.getComponentIndex() === it.getComponentIndex() && loc.getSegmentIndex() === it.getVertexIndex()) return totalLength + segLen * loc.getSegmentFraction();
        totalLength += segLen;
      }

      it.next();
    }

    return totalLength;
  }

  resolveHigher(loc) {
    if (!loc.isEndpoint(this._linearGeom)) return loc;
    let compIndex = loc.getComponentIndex();
    if (compIndex >= this._linearGeom.getNumGeometries() - 1) return loc;

    do compIndex++; while (compIndex < this._linearGeom.getNumGeometries() - 1 && this._linearGeom.getGeometryN(compIndex).getLength() === 0);

    return new LinearLocation(compIndex, 0, 0.0);
  }

  getLocation() {
    if (arguments.length === 1) {
      const length = arguments[0];
      return this.getLocation(length, true);
    } else if (arguments.length === 2) {
      const length = arguments[0],
            resolveLower = arguments[1];
      let forwardLength = length;

      if (length < 0.0) {
        const lineLen = this._linearGeom.getLength();

        forwardLength = lineLen + length;
      }

      const loc = this.getLocationForward(forwardLength);
      if (resolveLower) return loc;
      return this.resolveHigher(loc);
    }
  }

  getLocationForward(length) {
    if (length <= 0.0) return new LinearLocation();
    let totalLength = 0.0;
    const it = new LinearIterator(this._linearGeom);

    while (it.hasNext()) {
      if (it.isEndOfLine()) {
        if (totalLength === length) {
          const compIndex = it.getComponentIndex();
          const segIndex = it.getVertexIndex();
          return new LinearLocation(compIndex, segIndex, 0.0);
        }
      } else {
        const p0 = it.getSegmentStart();
        const p1 = it.getSegmentEnd();
        const segLen = p1.distance(p0);

        if (totalLength + segLen > length) {
          const frac = (length - totalLength) / segLen;
          const compIndex = it.getComponentIndex();
          const segIndex = it.getVertexIndex();
          return new LinearLocation(compIndex, segIndex, frac);
        }

        totalLength += segLen;
      }

      it.next();
    }

    return LinearLocation.getEndLocation(this._linearGeom);
  }

}

class LinearGeometryBuilder {
  constructor() {
    LinearGeometryBuilder.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._geomFact = null;
    this._lines = new ArrayList();
    this._coordList = null;
    this._ignoreInvalidLines = false;
    this._fixInvalidLines = false;
    this._lastPt = null;
    const geomFact = arguments[0];
    this._geomFact = geomFact;
  }

  getGeometry() {
    this.endLine();
    return this._geomFact.buildGeometry(this._lines);
  }

  getLastCoordinate() {
    return this._lastPt;
  }

  endLine() {
    if (this._coordList === null) return null;

    if (this._ignoreInvalidLines && this._coordList.size() < 2) {
      this._coordList = null;
      return null;
    }

    const rawPts = this._coordList.toCoordinateArray();

    let pts = rawPts;
    if (this._fixInvalidLines) pts = this.validCoordinateSequence(rawPts);
    this._coordList = null;
    let line = null;

    try {
      line = this._geomFact.createLineString(pts);
    } catch (ex) {
      if (ex instanceof IllegalArgumentException) {
        if (!this._ignoreInvalidLines) throw ex;
      } else {
        throw ex;
      }
    } finally {}

    if (line !== null) this._lines.add(line);
  }

  setFixInvalidLines(fixInvalidLines) {
    this._fixInvalidLines = fixInvalidLines;
  }

  add() {
    if (arguments.length === 1) {
      const pt = arguments[0];
      this.add(pt, true);
    } else if (arguments.length === 2) {
      const pt = arguments[0],
            allowRepeatedPoints = arguments[1];
      if (this._coordList === null) this._coordList = new CoordinateList();

      this._coordList.add(pt, allowRepeatedPoints);

      this._lastPt = pt;
    }
  }

  setIgnoreInvalidLines(ignoreInvalidLines) {
    this._ignoreInvalidLines = ignoreInvalidLines;
  }

  validCoordinateSequence(pts) {
    if (pts.length >= 2) return pts;
    const validPts = [pts[0], pts[0]];
    return validPts;
  }

}

class ExtractLineByLocation {
  constructor() {
    ExtractLineByLocation.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._line = null;
    const line = arguments[0];
    this._line = line;
  }

  static extract(line, start, end) {
    const ls = new ExtractLineByLocation(line);
    return ls.extract(start, end);
  }

  computeLinear(start, end) {
    const builder = new LinearGeometryBuilder(this._line.getFactory());
    builder.setFixInvalidLines(true);
    if (!start.isVertex()) builder.add(start.getCoordinate(this._line));

    for (let it = new LinearIterator(this._line, start); it.hasNext(); it.next()) {
      if (end.compareLocationValues(it.getComponentIndex(), it.getVertexIndex(), 0.0) < 0) break;
      const pt = it.getSegmentStart();
      builder.add(pt);
      if (it.isEndOfLine()) builder.endLine();
    }

    if (!end.isVertex()) builder.add(end.getCoordinate(this._line));
    return builder.getGeometry();
  }

  computeLine(start, end) {
    const coordinates = this._line.getCoordinates();

    const newCoordinates = new CoordinateList();
    let startSegmentIndex = start.getSegmentIndex();
    if (start.getSegmentFraction() > 0.0) startSegmentIndex += 1;
    let lastSegmentIndex = end.getSegmentIndex();
    if (end.getSegmentFraction() === 1.0) lastSegmentIndex += 1;
    if (lastSegmentIndex >= coordinates.length) lastSegmentIndex = coordinates.length - 1;
    if (!start.isVertex()) newCoordinates.add(start.getCoordinate(this._line));

    for (let i = startSegmentIndex; i <= lastSegmentIndex; i++) newCoordinates.add(coordinates[i]);

    if (!end.isVertex()) newCoordinates.add(end.getCoordinate(this._line));
    if (newCoordinates.size() <= 0) newCoordinates.add(start.getCoordinate(this._line));
    let newCoordinateArray = newCoordinates.toCoordinateArray();
    if (newCoordinateArray.length <= 1) newCoordinateArray = [newCoordinateArray[0], newCoordinateArray[0]];
    return this._line.getFactory().createLineString(newCoordinateArray);
  }

  extract(start, end) {
    if (end.compareTo(start) < 0) return this.reverse(this.computeLinear(end, start));
    return this.computeLinear(start, end);
  }

  reverse(linear) {
    if (hasInterface(linear, Lineal)) return linear.reverse();
    Assert.shouldNeverReachHere('non-linear geometry encountered');
    return null;
  }

}

class LengthIndexedLine {
  constructor() {
    LengthIndexedLine.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._linearGeom = null;
    const linearGeom = arguments[0];
    this._linearGeom = linearGeom;
  }

  clampIndex(index) {
    const posIndex = this.positiveIndex(index);
    const startIndex = this.getStartIndex();
    if (posIndex < startIndex) return startIndex;
    const endIndex = this.getEndIndex();
    if (posIndex > endIndex) return endIndex;
    return posIndex;
  }

  locationOf() {
    if (arguments.length === 1) {
      const index = arguments[0];
      return LengthLocationMap.getLocation(this._linearGeom, index);
    } else if (arguments.length === 2) {
      const index = arguments[0],
            resolveLower = arguments[1];
      return LengthLocationMap.getLocation(this._linearGeom, index, resolveLower);
    }
  }

  project(pt) {
    return LengthIndexOfPoint.indexOf(this._linearGeom, pt);
  }

  positiveIndex(index) {
    if (index >= 0.0) return index;
    return this._linearGeom.getLength() + index;
  }

  extractPoint() {
    if (arguments.length === 1) {
      const index = arguments[0];
      const loc = LengthLocationMap.getLocation(this._linearGeom, index);
      return loc.getCoordinate(this._linearGeom);
    } else if (arguments.length === 2) {
      const index = arguments[0],
            offsetDistance = arguments[1];
      const loc = LengthLocationMap.getLocation(this._linearGeom, index);
      const locLow = loc.toLowest(this._linearGeom);
      return locLow.getSegment(this._linearGeom).pointAlongOffset(locLow.getSegmentFraction(), offsetDistance);
    }
  }

  isValidIndex(index) {
    return index >= this.getStartIndex() && index <= this.getEndIndex();
  }

  getEndIndex() {
    return this._linearGeom.getLength();
  }

  getStartIndex() {
    return 0.0;
  }

  indexOfAfter(pt, minIndex) {
    return LengthIndexOfPoint.indexOfAfter(this._linearGeom, pt, minIndex);
  }

  extractLine(startIndex, endIndex) {
    const startIndex2 = this.clampIndex(startIndex);
    const endIndex2 = this.clampIndex(endIndex);
    const resolveStartLower = startIndex2 === endIndex2;
    const startLoc = this.locationOf(startIndex2, resolveStartLower);
    const endLoc = this.locationOf(endIndex2);
    return ExtractLineByLocation.extract(this._linearGeom, startLoc, endLoc);
  }

  indexOf(pt) {
    return LengthIndexOfPoint.indexOf(this._linearGeom, pt);
  }

  indicesOf(subLine) {
    const locIndex = LocationIndexOfLine.indicesOf(this._linearGeom, subLine);
    const index = [LengthLocationMap.getLength(this._linearGeom, locIndex[0]), LengthLocationMap.getLength(this._linearGeom, locIndex[1])];
    return index;
  }

}

class LocationIndexedLine {
  constructor() {
    LocationIndexedLine.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._linearGeom = null;
    const linearGeom = arguments[0];
    this._linearGeom = linearGeom;
    this.checkGeometryType();
  }

  clampIndex(index) {
    const loc = index.copy();
    loc.clamp(this._linearGeom);
    return loc;
  }

  project(pt) {
    return LocationIndexOfPoint.indexOf(this._linearGeom, pt);
  }

  checkGeometryType() {
    if (!(this._linearGeom instanceof LineString || this._linearGeom instanceof MultiLineString)) throw new IllegalArgumentException('Input geometry must be linear');
  }

  extractPoint() {
    if (arguments.length === 1) {
      const index = arguments[0];
      return index.getCoordinate(this._linearGeom);
    } else if (arguments.length === 2) {
      const index = arguments[0],
            offsetDistance = arguments[1];
      const indexLow = index.toLowest(this._linearGeom);
      return indexLow.getSegment(this._linearGeom).pointAlongOffset(indexLow.getSegmentFraction(), offsetDistance);
    }
  }

  isValidIndex(index) {
    return index.isValid(this._linearGeom);
  }

  getEndIndex() {
    return LinearLocation.getEndLocation(this._linearGeom);
  }

  getStartIndex() {
    return new LinearLocation();
  }

  indexOfAfter(pt, minIndex) {
    return LocationIndexOfPoint.indexOfAfter(this._linearGeom, pt, minIndex);
  }

  extractLine(startIndex, endIndex) {
    return ExtractLineByLocation.extract(this._linearGeom, startIndex, endIndex);
  }

  indexOf(pt) {
    return LocationIndexOfPoint.indexOf(this._linearGeom, pt);
  }

  indicesOf(subLine) {
    return LocationIndexOfLine.indicesOf(this._linearGeom, subLine);
  }

}

var linearref = /*#__PURE__*/Object.freeze({
  __proto__: null,
  LengthIndexedLine: LengthIndexedLine,
  LengthLocationMap: LengthLocationMap,
  LinearGeometryBuilder: LinearGeometryBuilder,
  LinearIterator: LinearIterator,
  LinearLocation: LinearLocation,
  LocationIndexedLine: LocationIndexedLine
});

class CollectionUtil {
  static transform(coll, func) {
    const result = new ArrayList();

    for (let i = coll.iterator(); i.hasNext();) result.add(func.execute(i.next()));

    return result;
  }

  static select(collection, func) {
    const result = new ArrayList();

    for (let i = collection.iterator(); i.hasNext();) {
      const item = i.next();
      if (Boolean.TRUE.equals(func.execute(item))) result.add(item);
    }

    return result;
  }

  static apply(coll, func) {
    for (let i = coll.iterator(); i.hasNext();) func.execute(i.next());
  }

}

function Function() {}

CollectionUtil.Function = Function;

class CoordinateArrayFilter {
  constructor() {
    CoordinateArrayFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.pts = null;
    this.n = 0;
    const size = arguments[0];
    this.pts = new Array(size).fill(null);
  }

  filter(coord) {
    this.pts[this.n++] = coord;
  }

  getCoordinates() {
    return this.pts;
  }

  get interfaces_() {
    return [CoordinateFilter];
  }

}

class CoordinateCountFilter {
  constructor() {
    CoordinateCountFilter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._n = 0;
  }

  filter(coord) {
    this._n++;
  }

  getCount() {
    return this._n;
  }

  get interfaces_() {
    return [CoordinateFilter];
  }

}

class ObjectCounter {
  constructor() {
    ObjectCounter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this._counts = new HashMap();
  }

  count(o) {
    const counter = this._counts.get(o);

    if (counter === null) return 0;else return counter.count();
  }

  add(o) {
    const counter = this._counts.get(o);

    if (counter === null) this._counts.put(o, new Counter(1));else counter.increment();
  }

}

class Counter {
  constructor() {
    Counter.constructor_.apply(this, arguments);
  }

  static constructor_() {
    this.count = 0;

    if (arguments.length === 0) ; else if (arguments.length === 1) {
      const count = arguments[0];
      this.count = count;
    }
  }

  count() {
    return this.count;
  }

  increment() {
    this.count++;
  }

}

ObjectCounter.Counter = Counter;

function PrintStream() {}

function StringReader() {}

function ByteArrayOutputStream() {}

class IOException extends Exception {}

function LineNumberReader() {}

class StringUtil {
  static chars(c, n) {
    const ch = new Array(n).fill(null);

    for (let i = 0; i < n; i++) ch[i] = c;

    return new String(ch);
  }

  static getStackTrace() {
    if (arguments.length === 1) {
      const t = arguments[0];
      const os = new ByteArrayOutputStream();
      const ps = new PrintStream(os);
      t.printStackTrace(ps);
      return os.toString();
    } else if (arguments.length === 2) {
      const t = arguments[0],
            depth = arguments[1];
      let stackTrace = '';
      const stringReader = new StringReader(StringUtil.getStackTrace(t));
      const lineNumberReader = new LineNumberReader(stringReader);

      for (let i = 0; i < depth; i++) try {
        stackTrace += lineNumberReader.readLine() + StringUtil.NEWLINE;
      } catch (e) {
        if (e instanceof IOException) Assert.shouldNeverReachHere();else throw e;
      } finally {}

      return stackTrace;
    }
  }

  static spaces(n) {
    return StringUtil.chars(' ', n);
  }

  static split(s, separator) {
    const separatorlen = separator.length;
    const tokenList = new ArrayList();
    let tmpString = '' + s;
    let pos = tmpString.indexOf(separator);

    while (pos >= 0) {
      const token = tmpString.substring(0, pos);
      tokenList.add(token);
      tmpString = tmpString.substring(pos + separatorlen);
      pos = tmpString.indexOf(separator);
    }

    if (tmpString.length > 0) tokenList.add(tmpString);
    const res = new Array(tokenList.size()).fill(null);

    for (let i = 0; i < res.length; i++) res[i] = tokenList.get(i);

    return res;
  }

}
StringUtil.NEWLINE = System.getProperty('line.separator');

var util = /*#__PURE__*/Object.freeze({
  __proto__: null,
  CollectionUtil: CollectionUtil,
  CoordinateArrayFilter: CoordinateArrayFilter,
  CoordinateCountFilter: CoordinateCountFilter,
  GeometricShapeFactory: GeometricShapeFactory,
  NumberUtil: NumberUtil,
  ObjectCounter: ObjectCounter,
  PriorityQueue: PriorityQueue,
  StringUtil: StringUtil,
  UniqueCoordinateArrayFilter: UniqueCoordinateArrayFilter
});

const version = '2.7.1 (16652a2)';

export { algorithm, densify, dissolve, geom, geomgraph, index, io, linearref, noding, operation, precision, simplify, triangulate, util, version };
//# sourceMappingURL=jsts.es6.js.map
