/**
   * Reads the shape specification as defined in the class javadocs. If the first character is a
   * letter but it doesn't complete out "Circle" or "CIRCLE" then this method returns null, offering
   * the caller the opportunity to potentially try additional parsing. If the first character is not
   * a letter then it's assumed to be a point or rectangle. If that doesn't work out then an {@link
   * org.locationtech.spatial4j.exception.InvalidShapeException} is thrown.
   */
  public static Shape readShapeOrNull(String str, SpatialContext ctx) throws InvalidShapeException {
    if (str == null || str.length() == 0) {
      throw new InvalidShapeException(str);
    }

    if (Character.isLetter(str.charAt(0))) {
      if (str.startsWith("Circle(") || str.startsWith("CIRCLE(")) {
        int idx = str.lastIndexOf(')');
        if (idx > 0) {
          String body = str.substring("Circle(".length(), idx);
          StringTokenizer st = new StringTokenizer(body, " ");
          String token = st.nextToken();
          Point pt;
          if (token.indexOf(',') != -1) {
            pt = readLatCommaLonPoint(token, ctx);
          } else {
            double x = Double.parseDouble(token);
            double y = Double.parseDouble(st.nextToken());
            pt = ctx.makePoint(x, y);
          }
          Double d = null;

          String arg = st.nextToken();
          idx = arg.indexOf('=');
          if (idx > 0) {
            String k = arg.substring(0, idx);
            if (k.equals("d") || k.equals("distance")) {
              d = Double.parseDouble(arg.substring(idx + 1));
            } else {
              throw new InvalidShapeException("unknown arg: " + k + " :: " + str);
            }
          } else {
            d = Double.parseDouble(arg);
          }
          if (st.hasMoreTokens()) {
            throw new InvalidShapeException("Extra arguments: " + st.nextToken() + " :: " + str);
          }
          if (d == null) {
            throw new InvalidShapeException("Missing Distance: " + str);
          }
          // NOTE: we are assuming the units of 'd' is the same as that of the spatial context.
          return ctx.makeCircle(pt, d);
        }
      }
      return null; // caller has opportunity to try other parsing
    }

    if (str.indexOf(',') != -1) return readLatCommaLonPoint(str, ctx);
    StringTokenizer st = new StringTokenizer(str, " ");
    double p0 = Double.parseDouble(st.nextToken());
    double p1 = Double.parseDouble(st.nextToken());
    if (st.hasMoreTokens()) {
      double p2 = Double.parseDouble(st.nextToken());
      double p3 = Double.parseDouble(st.nextToken());
      if (st.hasMoreTokens())
        throw new InvalidShapeException("Only 4 numbers supported (rect) but found more: " + str);
      return ctx.makeRectangle(p0, p2, p1, p3);
    }
    return ctx.makePoint(p0, p1);
  }
 @Test
 public void testRectangle() throws IOException {
   Shape s = read("-10 -20 10 20");
   assertEquals(ctx.makeRectangle(-10, 10, -20, 20), s);
   assertEquals(s, writeThenRead(s));
   assertTrue(s.hasArea());
 }
 @Test
 public void testCircle() throws IOException {
   Shape s = read("Circle(1.23 4.56 distance=7.89)");
   assertEquals(ctx.makeCircle(1.23, 4.56, 7.89), s);
   assertEquals(s, writeThenRead(s));
   assertEquals(s, read("CIRCLE( 4.56,1.23 d=7.89 )")); // use lat,lon and use 'd' abbreviation
   assertTrue(s.hasArea());
 }
 @Test
 public void testPoint() throws IOException {
   Shape s = read("10 20");
   assertEquals(ctx.makePoint(10, 20), s);
   assertEquals(s, writeThenRead(s));
   assertEquals(s, read("20,10")); // check comma for y,x format
   assertEquals(s, read("20, 10")); // test space
   assertFalse(s.hasArea());
 }
  protected Rectangle randomRectangle(Rectangle bounds) {
    double[] xNewStartAndWidth = randomSubRange(bounds.getMinX(), bounds.getWidth());
    double xMin = xNewStartAndWidth[0];
    double xMax = xMin + xNewStartAndWidth[1];
    if (bounds.getCrossesDateLine()) {
      xMin = DistanceUtils.normLonDEG(xMin);
      xMax = DistanceUtils.normLonDEG(xMax);
    }

    double[] yNewStartAndHeight = randomSubRange(bounds.getMinY(), bounds.getHeight());
    double yMin = yNewStartAndHeight[0];
    double yMax = yMin + yNewStartAndHeight[1];

    return ctx.makeRectangle(xMin, xMax, yMin, yMax);
  }
 protected Rectangle randomRectangle() {
   return randomRectangle(ctx.getWorldBounds());
 }
 protected Point randomPoint() {
   final Rectangle WB = ctx.getWorldBounds();
   return ctx.makePoint(
       randomIntBetween((int) WB.getMinX(), (int) WB.getMaxX()),
       randomIntBetween((int) WB.getMinY(), (int) WB.getMaxY()));
 }
 /** Reads geospatial latitude then a comma then longitude. */
 private static Point readLatCommaLonPoint(String value, SpatialContext ctx)
     throws InvalidShapeException {
   double[] latLon = ParseUtils.parseLatitudeLongitude(value);
   return ctx.makePoint(latLon[1], latLon[0]);
 }