kopia lustrzana https://github.com/onthegomap/planetiler
				
				
				
			feat: `--polygon` argument to constrain mbtiles to a poly shape (#280)
							rodzic
							
								
									d1d68cf753
								
							
						
					
					
						commit
						7818634774
					
				| 
						 | 
				
			
			@ -23,6 +23,7 @@ The `planetiler-core` module includes the following software:
 | 
			
		|||
  - org.openstreetmap.osmosis:osmosis-osm-binary (LGPL 3.0)
 | 
			
		||||
  - com.carrotsearch:hppc (Apache license)
 | 
			
		||||
  - com.github.jnr:jnr-ffi (Apache license)
 | 
			
		||||
  - org.roaringbitmap:RoaringBitmap (Apache license)
 | 
			
		||||
- Adapted code:
 | 
			
		||||
  - `DouglasPeuckerSimplifier` from [JTS](https://github.com/locationtech/jts) (EDL)
 | 
			
		||||
  - `OsmMultipolygon` from [imposm3](https://github.com/omniscale/imposm3) (Apache license)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,6 +27,11 @@
 | 
			
		|||
      <artifactId>hppc</artifactId>
 | 
			
		||||
      <version>0.9.1</version>
 | 
			
		||||
    </dependency>
 | 
			
		||||
    <dependency>
 | 
			
		||||
      <groupId>org.roaringbitmap</groupId>
 | 
			
		||||
      <artifactId>RoaringBitmap</artifactId>
 | 
			
		||||
      <version>0.9.30</version>
 | 
			
		||||
    </dependency>
 | 
			
		||||
    <dependency>
 | 
			
		||||
      <groupId>org.openstreetmap.osmosis</groupId>
 | 
			
		||||
      <artifactId>osmosis-osm-binary</artifactId>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,99 +1,84 @@
 | 
			
		|||
package com.onthegomap.planetiler.collection;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.Range;
 | 
			
		||||
import com.google.common.collect.TreeRangeSet;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.PrimitiveIterator;
 | 
			
		||||
import javax.annotation.concurrent.NotThreadSafe;
 | 
			
		||||
import org.roaringbitmap.PeekableIntIterator;
 | 
			
		||||
import org.roaringbitmap.RoaringBitmap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A set of ints backed by a {@link TreeRangeSet} to efficiently represent large continuous ranges.
 | 
			
		||||
 * A set of ints backed by a {@link RoaringBitmap} to efficiently represent large continuous ranges.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This makes iterating through tile coordinates inside ocean polygons significantly faster.
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("UnstableApiUsage")
 | 
			
		||||
@NotThreadSafe
 | 
			
		||||
public class IntRangeSet implements Iterable<Integer> {
 | 
			
		||||
 | 
			
		||||
  private final TreeRangeSet<Integer> rangeSet = TreeRangeSet.create();
 | 
			
		||||
  private final RoaringBitmap bitmap = new RoaringBitmap();
 | 
			
		||||
 | 
			
		||||
  /** Mutates and returns this range set, adding all elements in {@code other} to it. */
 | 
			
		||||
  public IntRangeSet addAll(IntRangeSet other) {
 | 
			
		||||
    rangeSet.addAll(other.rangeSet);
 | 
			
		||||
    bitmap.or(other.bitmap);
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Mutates and returns this range set, removing all elements in {@code other} from it. */
 | 
			
		||||
  public IntRangeSet removeAll(IntRangeSet other) {
 | 
			
		||||
    rangeSet.removeAll(other.rangeSet);
 | 
			
		||||
    bitmap.andNot(other.bitmap);
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public static void main(String[] args) {
 | 
			
		||||
    var set = new IntRangeSet();
 | 
			
		||||
    set.add(0, 100000);
 | 
			
		||||
    set.remove(10000);
 | 
			
		||||
    System.err.println(set.bitmap.getSizeInBytes());
 | 
			
		||||
    set.bitmap.runOptimize();
 | 
			
		||||
    System.err.println(set.bitmap.getSizeInBytes());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public PrimitiveIterator.OfInt iterator() {
 | 
			
		||||
    return new Iter(rangeSet.asRanges().iterator());
 | 
			
		||||
    return new Iter(bitmap.getIntIterator());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Mutates and returns this range set, with range {@code a} to {@code b} (inclusive) added. */
 | 
			
		||||
  public IntRangeSet add(int a, int b) {
 | 
			
		||||
    rangeSet.add(Range.closedOpen(a, b + 1));
 | 
			
		||||
    bitmap.add(a, (long) b + 1);
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Mutates and returns this range set, with {@code a} removed. */
 | 
			
		||||
  public IntRangeSet remove(int a) {
 | 
			
		||||
    rangeSet.remove(Range.closedOpen(a, a + 1));
 | 
			
		||||
    bitmap.remove(a);
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public boolean contains(int y) {
 | 
			
		||||
    return rangeSet.contains(y);
 | 
			
		||||
    return bitmap.contains(y);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Returns the underlying {@link RoaringBitmap} for this int range. */
 | 
			
		||||
  public RoaringBitmap bitmap() {
 | 
			
		||||
    return bitmap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Mutates and returns this range set to remove all elements not in {@code other} */
 | 
			
		||||
  public IntRangeSet intersect(IntRangeSet other) {
 | 
			
		||||
    rangeSet.removeAll(other.rangeSet.complement());
 | 
			
		||||
    bitmap.and(other.bitmap);
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Iterate through all ints in this range */
 | 
			
		||||
  private static class Iter implements PrimitiveIterator.OfInt {
 | 
			
		||||
  private record Iter(PeekableIntIterator iter) implements PrimitiveIterator.OfInt {
 | 
			
		||||
 | 
			
		||||
    private final Iterator<Range<Integer>> rangeIter;
 | 
			
		||||
    Range<Integer> range;
 | 
			
		||||
    int cur;
 | 
			
		||||
    boolean hasNext = true;
 | 
			
		||||
 | 
			
		||||
    private Iter(Iterator<Range<Integer>> rangeIter) {
 | 
			
		||||
      this.rangeIter = rangeIter;
 | 
			
		||||
      advance();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void advance() {
 | 
			
		||||
      while (true) {
 | 
			
		||||
        if (range != null && cur < range.upperEndpoint() - 1) {
 | 
			
		||||
          cur++;
 | 
			
		||||
          return;
 | 
			
		||||
        } else if (rangeIter.hasNext()) {
 | 
			
		||||
          range = rangeIter.next();
 | 
			
		||||
          cur = range.lowerEndpoint() - 1;
 | 
			
		||||
        } else {
 | 
			
		||||
          hasNext = false;
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean hasNext() {
 | 
			
		||||
      return iter.hasNext();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int nextInt() {
 | 
			
		||||
      int result = cur;
 | 
			
		||||
      advance();
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean hasNext() {
 | 
			
		||||
      return hasNext;
 | 
			
		||||
      return iter.next();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ import com.onthegomap.planetiler.geo.GeoUtils;
 | 
			
		|||
import com.onthegomap.planetiler.geo.TileExtents;
 | 
			
		||||
import com.onthegomap.planetiler.reader.osm.OsmInputFile;
 | 
			
		||||
import org.locationtech.jts.geom.Envelope;
 | 
			
		||||
import org.locationtech.jts.geom.Geometry;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +22,8 @@ public class Bounds {
 | 
			
		|||
  private Envelope world;
 | 
			
		||||
  private TileExtents tileExtents;
 | 
			
		||||
 | 
			
		||||
  private Geometry shape;
 | 
			
		||||
 | 
			
		||||
  Bounds(Envelope latLon) {
 | 
			
		||||
    set(latLon);
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -35,7 +38,7 @@ public class Bounds {
 | 
			
		|||
 | 
			
		||||
  public TileExtents tileExtents() {
 | 
			
		||||
    if (tileExtents == null) {
 | 
			
		||||
      tileExtents = TileExtents.computeFromWorldBounds(PlanetilerConfig.MAX_MAXZOOM, world());
 | 
			
		||||
      tileExtents = TileExtents.computeFromWorldBounds(PlanetilerConfig.MAX_MAXZOOM, world(), shape);
 | 
			
		||||
    }
 | 
			
		||||
    return tileExtents;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -52,11 +55,20 @@ public class Bounds {
 | 
			
		|||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Planetiler will emit any tile that intersects {@code shape}. */
 | 
			
		||||
  public Bounds setShape(Geometry shape) {
 | 
			
		||||
    this.shape = shape;
 | 
			
		||||
    if (latLon == null) {
 | 
			
		||||
      set(shape.getEnvelopeInternal());
 | 
			
		||||
    }
 | 
			
		||||
    return this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void set(Envelope latLon) {
 | 
			
		||||
    if (latLon != null) {
 | 
			
		||||
      this.latLon = latLon;
 | 
			
		||||
      this.world = GeoUtils.toWorldBounds(latLon);
 | 
			
		||||
      this.tileExtents = TileExtents.computeFromWorldBounds(PlanetilerConfig.MAX_MAXZOOM, world);
 | 
			
		||||
      this.tileExtents = TileExtents.computeFromWorldBounds(PlanetilerConfig.MAX_MAXZOOM, world, shape);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,10 @@ package com.onthegomap.planetiler.config;
 | 
			
		|||
 | 
			
		||||
import com.onthegomap.planetiler.collection.LongLongMap;
 | 
			
		||||
import com.onthegomap.planetiler.collection.Storage;
 | 
			
		||||
import com.onthegomap.planetiler.reader.osm.PolyFileReader;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.UncheckedIOException;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -90,9 +94,19 @@ public record PlanetilerConfig(
 | 
			
		|||
    int featureProcessThreads =
 | 
			
		||||
      arguments.getInteger("process_threads", "number of threads to use when processing input features",
 | 
			
		||||
        Math.max(threads < 4 ? threads : (threads - featureWriteThreads), 1));
 | 
			
		||||
    Bounds bounds = new Bounds(arguments.bounds("bounds", "bounds"));
 | 
			
		||||
    Path polygonFile =
 | 
			
		||||
      arguments.file("polygon", "a .poly file that limits output to tiles intersecting the shape", null);
 | 
			
		||||
    if (polygonFile != null) {
 | 
			
		||||
      try {
 | 
			
		||||
        bounds.setShape(PolyFileReader.parsePolyFile(polygonFile));
 | 
			
		||||
      } catch (IOException e) {
 | 
			
		||||
        throw new UncheckedIOException(e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return new PlanetilerConfig(
 | 
			
		||||
      arguments,
 | 
			
		||||
      new Bounds(arguments.bounds("bounds", "bounds")),
 | 
			
		||||
      bounds,
 | 
			
		||||
      threads,
 | 
			
		||||
      featureWriteThreads,
 | 
			
		||||
      featureProcessThreads,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -329,13 +329,13 @@ public class GeoUtils {
 | 
			
		|||
   *
 | 
			
		||||
   * @param tilesAtZoom       the tile width of the world at this zoom level
 | 
			
		||||
   * @param labelGridTileSize the tile width of each grid square
 | 
			
		||||
   * @param coord             the coordinate
 | 
			
		||||
   * @param coord             the coordinate, scaled to this zoom level
 | 
			
		||||
   * @return an ID representing the grid square that {@code coord} falls into.
 | 
			
		||||
   */
 | 
			
		||||
  public static long labelGridId(int tilesAtZoom, double labelGridTileSize, Coordinate coord) {
 | 
			
		||||
    return GeoUtils.longPair(
 | 
			
		||||
      (int) Math.floor(wrapDouble(coord.getX() * tilesAtZoom, tilesAtZoom) / labelGridTileSize),
 | 
			
		||||
      (int) Math.floor((coord.getY() * tilesAtZoom) / labelGridTileSize)
 | 
			
		||||
      (int) Math.floor(wrapDouble(coord.getX(), tilesAtZoom) / labelGridTileSize),
 | 
			
		||||
      (int) Math.floor((coord.getY()) / labelGridTileSize)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -118,6 +118,7 @@ public record TileCoord(int encoded, int x, int y, int z) implements Comparable<
 | 
			
		|||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  /** Returns a URL that displays the openstreetmap data for this tile. */
 | 
			
		||||
  public String getDebugUrl() {
 | 
			
		||||
    Coordinate coord = getLatLon();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,19 @@
 | 
			
		|||
package com.onthegomap.planetiler.geo;
 | 
			
		||||
 | 
			
		||||
import com.onthegomap.planetiler.render.TiledGeometry;
 | 
			
		||||
import java.util.function.Predicate;
 | 
			
		||||
import org.locationtech.jts.geom.Envelope;
 | 
			
		||||
import org.locationtech.jts.geom.Geometry;
 | 
			
		||||
import org.locationtech.jts.geom.util.AffineTransformation;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A function that filters to only tile coordinates that overlap a given {@link Envelope}.
 | 
			
		||||
 */
 | 
			
		||||
public class TileExtents implements Predicate<TileCoord> {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(TileExtents.class);
 | 
			
		||||
  private final ForZoom[] zoomExtents;
 | 
			
		||||
 | 
			
		||||
  private TileExtents(ForZoom[] zoomExtents) {
 | 
			
		||||
| 
						 | 
				
			
			@ -24,15 +30,34 @@ public class TileExtents implements Predicate<TileCoord> {
 | 
			
		|||
 | 
			
		||||
  /** Returns a filter to tiles that intersect {@code worldBounds} (specified in world web mercator coordinates). */
 | 
			
		||||
  public static TileExtents computeFromWorldBounds(int maxzoom, Envelope worldBounds) {
 | 
			
		||||
    return computeFromWorldBounds(maxzoom, worldBounds, null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  /** Returns a filter to tiles that intersect {@code worldBounds} (specified in world web mercator coordinates). */
 | 
			
		||||
  public static TileExtents computeFromWorldBounds(int maxzoom, Envelope worldBounds, Geometry shape) {
 | 
			
		||||
    ForZoom[] zoomExtents = new ForZoom[maxzoom + 1];
 | 
			
		||||
    var mercator = shape == null ? null : GeoUtils.latLonToWorldCoords(shape);
 | 
			
		||||
    for (int zoom = 0; zoom <= maxzoom; zoom++) {
 | 
			
		||||
      int max = 1 << zoom;
 | 
			
		||||
      zoomExtents[zoom] = new ForZoom(
 | 
			
		||||
 | 
			
		||||
      var forZoom = new ForZoom(
 | 
			
		||||
        zoom,
 | 
			
		||||
        quantizeDown(worldBounds.getMinX(), max),
 | 
			
		||||
        quantizeDown(worldBounds.getMinY(), max),
 | 
			
		||||
        quantizeUp(worldBounds.getMaxX(), max),
 | 
			
		||||
        quantizeUp(worldBounds.getMaxY(), max)
 | 
			
		||||
        quantizeUp(worldBounds.getMaxY(), max),
 | 
			
		||||
        null
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (mercator != null) {
 | 
			
		||||
        Geometry scaled = AffineTransformation.scaleInstance(1 << zoom, 1 << zoom).transform(mercator);
 | 
			
		||||
        TiledGeometry.CoveredTiles covered = TiledGeometry.getCoveredTiles(scaled, zoom, forZoom);
 | 
			
		||||
        forZoom = forZoom.withShape(covered);
 | 
			
		||||
        LOGGER.info("prepareShapeForZoom z{} {}", zoom, covered);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      zoomExtents[zoom] = forZoom;
 | 
			
		||||
    }
 | 
			
		||||
    return new TileExtents(zoomExtents);
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -52,12 +77,25 @@ public class TileExtents implements Predicate<TileCoord> {
 | 
			
		|||
 | 
			
		||||
  /**
 | 
			
		||||
   * X/Y extents within a given zoom level. {@code minX} and {@code minY} are inclusive and {@code maxX} and {@code
 | 
			
		||||
   * maxY} are exclusive.
 | 
			
		||||
   * maxY} are exclusive. shape is an optional polygon defining a more refine shape
 | 
			
		||||
   */
 | 
			
		||||
  public record ForZoom(int minX, int minY, int maxX, int maxY) {
 | 
			
		||||
  public record ForZoom(int z, int minX, int minY, int maxX, int maxY, TilePredicate shapeFilter)
 | 
			
		||||
    implements TilePredicate {
 | 
			
		||||
 | 
			
		||||
    public ForZoom withShape(TilePredicate shape) {
 | 
			
		||||
      return new ForZoom(z, minX, minY, maxX, maxY, shape);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean test(int x, int y) {
 | 
			
		||||
      return testX(x) && testY(y);
 | 
			
		||||
      return testX(x) && testY(y) && testOverShape(x, y);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean testOverShape(int x, int y) {
 | 
			
		||||
      if (shapeFilter != null) {
 | 
			
		||||
        return shapeFilter.test(x, y);
 | 
			
		||||
      }
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean testX(int x) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
package com.onthegomap.planetiler.geo;
 | 
			
		||||
 | 
			
		||||
public interface TilePredicate {
 | 
			
		||||
  boolean test(int x, int y);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,103 @@
 | 
			
		|||
package com.onthegomap.planetiler.reader.osm;
 | 
			
		||||
 | 
			
		||||
import static com.onthegomap.planetiler.geo.GeoUtils.JTS_FACTORY;
 | 
			
		||||
 | 
			
		||||
import com.onthegomap.planetiler.geo.GeoUtils;
 | 
			
		||||
import com.onthegomap.planetiler.geo.MutableCoordinateSequence;
 | 
			
		||||
import com.onthegomap.planetiler.reader.FileFormatException;
 | 
			
		||||
import java.io.BufferedReader;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.Reader;
 | 
			
		||||
import java.io.StringReader;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import org.locationtech.jts.geom.Geometry;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Parse a polygon file used for filtering planetiler output to a specific shape.
 | 
			
		||||
 *
 | 
			
		||||
 * @see <a href="https://wiki.openstreetmap.org/wiki/Osmosis/Polygon_Filter_File_Format">Osmosis/Polygon Filter File
 | 
			
		||||
 *      Format</a>
 | 
			
		||||
 */
 | 
			
		||||
public class PolyFileReader {
 | 
			
		||||
 | 
			
		||||
  private PolyFileReader() {
 | 
			
		||||
    throw new IllegalStateException("Utility class");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Reads a polygon from a file.
 | 
			
		||||
   */
 | 
			
		||||
  public static Geometry parsePolyFile(Path polyFile) throws IOException {
 | 
			
		||||
    try (BufferedReader reader = Files.newBufferedReader(polyFile)) {
 | 
			
		||||
      return parsePolyFile(reader);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Reads a polygon from a string.
 | 
			
		||||
   */
 | 
			
		||||
  public static Geometry parsePolyFile(String polyFile) throws IOException {
 | 
			
		||||
    try (Reader reader = new StringReader(polyFile)) {
 | 
			
		||||
      return parsePolyFile(reader);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Reads a polygon from a {@link Reader} {@code input}.
 | 
			
		||||
   */
 | 
			
		||||
  public static Geometry parsePolyFile(Reader input) throws IOException {
 | 
			
		||||
    Geometry result = GeoUtils.EMPTY_POLYGON;
 | 
			
		||||
    try (BufferedReader reader = input instanceof BufferedReader br ? br : new BufferedReader(input)) {
 | 
			
		||||
      String line;
 | 
			
		||||
      MutableCoordinateSequence currentRing = null;
 | 
			
		||||
      boolean firstLine = true, inRing = false, inPolygon = true, hole = false;
 | 
			
		||||
      while ((line = reader.readLine()) != null) {
 | 
			
		||||
        if (line.isBlank()) {
 | 
			
		||||
          // ingore line
 | 
			
		||||
        } else if (!inPolygon) {
 | 
			
		||||
          throw new FileFormatException("File continues after end of polygon");
 | 
			
		||||
        } else if (firstLine) {
 | 
			
		||||
          firstLine = false;
 | 
			
		||||
          // first line is junk.
 | 
			
		||||
        } else if (inRing) {
 | 
			
		||||
          if (line.strip().equals("END")) {
 | 
			
		||||
            // we are at the end of a ring, perhaps with more to come.
 | 
			
		||||
            currentRing.closeRing();
 | 
			
		||||
            var polygon = JTS_FACTORY.createPolygon(JTS_FACTORY.createLinearRing(currentRing), null);
 | 
			
		||||
            if (hole) {
 | 
			
		||||
              result = result.difference(polygon);
 | 
			
		||||
            } else {
 | 
			
		||||
              result = result.union(polygon);
 | 
			
		||||
            }
 | 
			
		||||
            currentRing = null;
 | 
			
		||||
            inRing = false;
 | 
			
		||||
          } else {
 | 
			
		||||
            // we are in a ring and picking up new coordinates.
 | 
			
		||||
            String[] splitted = line.trim().split("\s+");
 | 
			
		||||
            currentRing.addPoint(Double.parseDouble(splitted[0]), Double.parseDouble(splitted[1]));
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          if (line.strip().equals("END")) {
 | 
			
		||||
            // we are at the end of the whole polygon.
 | 
			
		||||
            inPolygon = false;
 | 
			
		||||
          } else {
 | 
			
		||||
            // we are at the start of a polygon part.
 | 
			
		||||
            currentRing = new MutableCoordinateSequence();
 | 
			
		||||
            hole = line.strip().charAt(0) == '!';
 | 
			
		||||
            inRing = true;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (inRing) {
 | 
			
		||||
        throw new FileFormatException("Unclosed ring");
 | 
			
		||||
      }
 | 
			
		||||
      if (inPolygon) {
 | 
			
		||||
        throw new FileFormatException("File ends before end of polygon");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -94,13 +94,24 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void renderPoint(FeatureCollector.Feature feature, Coordinate... coords) {
 | 
			
		||||
  private void renderPoint(FeatureCollector.Feature feature, Coordinate... origCoords) {
 | 
			
		||||
    long id = idGenerator.incrementAndGet();
 | 
			
		||||
    boolean hasLabelGrid = feature.hasLabelGrid();
 | 
			
		||||
    Coordinate[] coords = new Coordinate[origCoords.length];
 | 
			
		||||
    for (int i = 0; i < origCoords.length; i++) {
 | 
			
		||||
      coords[i] = origCoords[i].copy();
 | 
			
		||||
    }
 | 
			
		||||
    for (int zoom = feature.getMaxZoom(); zoom >= feature.getMinZoom(); zoom--) {
 | 
			
		||||
      Map<String, Object> attrs = feature.getAttrsAtZoom(zoom);
 | 
			
		||||
      double buffer = feature.getBufferPixelsAtZoom(zoom) / 256;
 | 
			
		||||
      int tilesAtZoom = 1 << zoom;
 | 
			
		||||
      // scale coordinates for this zoom
 | 
			
		||||
      for (int i = 0; i < coords.length; i++) {
 | 
			
		||||
        var orig = origCoords[i];
 | 
			
		||||
        coords[i].setX(orig.x * tilesAtZoom);
 | 
			
		||||
        coords[i].setY(orig.y * tilesAtZoom);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      // for "label grid" point density limiting, compute the grid square that this point sits in
 | 
			
		||||
      // only valid if not a multipoint
 | 
			
		||||
| 
						 | 
				
			
			@ -115,9 +126,9 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
 | 
			
		|||
 | 
			
		||||
      // compute the tile coordinate of every tile these points should show up in at the given buffer size
 | 
			
		||||
      TileExtents.ForZoom extents = config.bounds().tileExtents().getForZoom(zoom);
 | 
			
		||||
      TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords, feature.getSourceId());
 | 
			
		||||
      TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords);
 | 
			
		||||
      int emitted = 0;
 | 
			
		||||
      for (var entry : tiled.getTileData()) {
 | 
			
		||||
      for (var entry : tiled.getTileData().entrySet()) {
 | 
			
		||||
        TileCoord tile = entry.getKey();
 | 
			
		||||
        List<List<CoordinateSequence>> result = entry.getValue();
 | 
			
		||||
        Geometry geom = GeometryCoordinateSequences.reassemblePoints(result);
 | 
			
		||||
| 
						 | 
				
			
			@ -186,7 +197,7 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
 | 
			
		|||
      List<List<CoordinateSequence>> groups = GeometryCoordinateSequences.extractGroups(geom, minSize);
 | 
			
		||||
      double buffer = feature.getBufferPixelsAtZoom(z) / 256;
 | 
			
		||||
      TileExtents.ForZoom extents = config.bounds().tileExtents().getForZoom(z);
 | 
			
		||||
      TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents, feature.getSourceId());
 | 
			
		||||
      TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
 | 
			
		||||
      Map<String, Object> attrs = feature.getAttrsAtZoom(sliced.zoomLevel());
 | 
			
		||||
      if (numPointsAttr != null) {
 | 
			
		||||
        // if profile wants the original number of points that the simplified but untiled geometry started with
 | 
			
		||||
| 
						 | 
				
			
			@ -202,7 +213,7 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Clos
 | 
			
		|||
  private void writeTileFeatures(int zoom, long id, FeatureCollector.Feature feature, TiledGeometry sliced,
 | 
			
		||||
    Map<String, Object> attrs) {
 | 
			
		||||
    int emitted = 0;
 | 
			
		||||
    for (var entry : sliced.getTileData()) {
 | 
			
		||||
    for (var entry : sliced.getTileData().entrySet()) {
 | 
			
		||||
      TileCoord tile = entry.getKey();
 | 
			
		||||
      try {
 | 
			
		||||
        List<List<CoordinateSequence>> geoms = entry.getValue();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,20 +26,33 @@ import com.onthegomap.planetiler.geo.GeoUtils;
 | 
			
		|||
import com.onthegomap.planetiler.geo.MutableCoordinateSequence;
 | 
			
		||||
import com.onthegomap.planetiler.geo.TileCoord;
 | 
			
		||||
import com.onthegomap.planetiler.geo.TileExtents;
 | 
			
		||||
import com.onthegomap.planetiler.geo.TilePredicate;
 | 
			
		||||
import com.onthegomap.planetiler.util.Format;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.EnumSet;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.TreeSet;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
import javax.annotation.concurrent.NotThreadSafe;
 | 
			
		||||
import org.locationtech.jts.geom.Coordinate;
 | 
			
		||||
import org.locationtech.jts.geom.CoordinateSequence;
 | 
			
		||||
import org.locationtech.jts.geom.Geometry;
 | 
			
		||||
import org.locationtech.jts.geom.GeometryCollection;
 | 
			
		||||
import org.locationtech.jts.geom.LineString;
 | 
			
		||||
import org.locationtech.jts.geom.Lineal;
 | 
			
		||||
import org.locationtech.jts.geom.MultiLineString;
 | 
			
		||||
import org.locationtech.jts.geom.MultiPoint;
 | 
			
		||||
import org.locationtech.jts.geom.MultiPolygon;
 | 
			
		||||
import org.locationtech.jts.geom.Point;
 | 
			
		||||
import org.locationtech.jts.geom.Polygon;
 | 
			
		||||
import org.locationtech.jts.geom.Polygonal;
 | 
			
		||||
import org.locationtech.jts.geom.Puntal;
 | 
			
		||||
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
import org.roaringbitmap.RoaringBitmap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Splits geometries represented by lists of {@link CoordinateSequence CoordinateSequences} into the geometries that
 | 
			
		||||
| 
						 | 
				
			
			@ -48,28 +61,27 @@ import org.slf4j.LoggerFactory;
 | 
			
		|||
 * {@link GeometryCoordinateSequences} converts between JTS {@link Geometry} instances and {@link CoordinateSequence}
 | 
			
		||||
 * lists for this utility.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This class is adapted from the stripe clipping algorithm in https://github.com/mapbox/geojson-vt/ and modified so
 | 
			
		||||
 * that it eagerly produces all sliced tiles at a zoom level for each input geometry.
 | 
			
		||||
 * This class is adapted from the stripe clipping algorithm in
 | 
			
		||||
 * <a href="https://github.com/mapbox/geojson-vt/">geojson-vt</a> and modified so that it eagerly produces all sliced
 | 
			
		||||
 * tiles at a zoom level for each input geometry.
 | 
			
		||||
 */
 | 
			
		||||
@NotThreadSafe
 | 
			
		||||
class TiledGeometry {
 | 
			
		||||
public class TiledGeometry {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(TiledGeometry.class);
 | 
			
		||||
  private static final Format FORMAT = Format.defaultInstance();
 | 
			
		||||
  private static final double NEIGHBOR_BUFFER_EPS = 0.1d / 4096;
 | 
			
		||||
 | 
			
		||||
  private final long featureId;
 | 
			
		||||
  private final Map<TileCoord, List<List<CoordinateSequence>>> tileContents = new HashMap<>();
 | 
			
		||||
  /** Map from X coordinate to range of Y coordinates that contain filled tiles inside this geometry */
 | 
			
		||||
  private Map<Integer, IntRangeSet> filledRanges = null;
 | 
			
		||||
  private final TileExtents.ForZoom extents;
 | 
			
		||||
  private final double buffer;
 | 
			
		||||
  private final double neighborBuffer;
 | 
			
		||||
  private final int z;
 | 
			
		||||
  private final boolean area;
 | 
			
		||||
  private final int maxTilesAtThisZoom;
 | 
			
		||||
  /** Map from X coordinate to range of Y coordinates that contain filled tiles inside this geometry */
 | 
			
		||||
  private Map<Integer, IntRangeSet> filledRanges = null;
 | 
			
		||||
 | 
			
		||||
  private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area, long featureId) {
 | 
			
		||||
    this.featureId = featureId;
 | 
			
		||||
  private TiledGeometry(TileExtents.ForZoom extents, double buffer, int z, boolean area) {
 | 
			
		||||
    this.extents = extents;
 | 
			
		||||
    this.buffer = buffer;
 | 
			
		||||
    // make sure we inspect neighboring tiles when a line runs along an edge
 | 
			
		||||
| 
						 | 
				
			
			@ -87,12 +99,11 @@ class TiledGeometry {
 | 
			
		|||
   * @param z       zoom level
 | 
			
		||||
   * @param coords  the world web mercator coordinates of each point to emit at this zoom level where (0,0) is the
 | 
			
		||||
   *                northwest and (2^z,2^z) is the southeast corner of the planet
 | 
			
		||||
   * @param id      feature ID
 | 
			
		||||
   * @return each tile this feature touches, and the points that appear on each
 | 
			
		||||
   */
 | 
			
		||||
  public static TiledGeometry slicePointsIntoTiles(TileExtents.ForZoom extents, double buffer, int z,
 | 
			
		||||
    Coordinate[] coords, long id) {
 | 
			
		||||
    TiledGeometry result = new TiledGeometry(extents, buffer, z, false, id);
 | 
			
		||||
  static TiledGeometry slicePointsIntoTiles(TileExtents.ForZoom extents, double buffer, int z,
 | 
			
		||||
    Coordinate[] coords) {
 | 
			
		||||
    TiledGeometry result = new TiledGeometry(extents, buffer, z, false);
 | 
			
		||||
    for (Coordinate coord : coords) {
 | 
			
		||||
      result.slicePoint(coord);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -107,35 +118,69 @@ class TiledGeometry {
 | 
			
		|||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void slicePoint(Coordinate coord) {
 | 
			
		||||
    double worldX = coord.getX() * maxTilesAtThisZoom;
 | 
			
		||||
    double worldY = coord.getY() * maxTilesAtThisZoom;
 | 
			
		||||
    int minX = (int) Math.floor(worldX - neighborBuffer);
 | 
			
		||||
    int maxX = (int) Math.floor(worldX + neighborBuffer);
 | 
			
		||||
    int minY = Math.max(extents.minY(), (int) Math.floor(worldY - neighborBuffer));
 | 
			
		||||
    int maxY = Math.min(extents.maxY() - 1, (int) Math.floor(worldY + neighborBuffer));
 | 
			
		||||
    for (int x = minX; x <= maxX; x++) {
 | 
			
		||||
      double tileX = worldX - x;
 | 
			
		||||
      int wrappedX = wrapInt(x, maxTilesAtThisZoom);
 | 
			
		||||
      // point may end up inside bounds after wrapping
 | 
			
		||||
      if (extents.testX(wrappedX)) {
 | 
			
		||||
        for (int y = minY; y <= maxY; y++) {
 | 
			
		||||
          TileCoord tile = TileCoord.ofXYZ(wrappedX, y, z);
 | 
			
		||||
          double tileY = worldY - y;
 | 
			
		||||
          tileContents.computeIfAbsent(tile, t -> List.of(new ArrayList<>()))
 | 
			
		||||
            .get(0)
 | 
			
		||||
            .add(GeoUtils.coordinateSequence(tileX * 256, tileY * 256));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns all the points that appear on tiles representing points at {@code coords}.
 | 
			
		||||
   *
 | 
			
		||||
   * @param scaledGeom the scaled geometry to slice into tiles, in world web mercator coordinates where (0,0) is the
 | 
			
		||||
   *                   northwest and (1,1) is the southeast corner of the planet
 | 
			
		||||
   * @param minSize    the minimum length of a line or area of a polygon to emit
 | 
			
		||||
   * @param buffer     how far detail should be included beyond the edge of each tile (0=none, 1=a full tile width)
 | 
			
		||||
   * @param z          zoom level
 | 
			
		||||
   * @param extents    range of tile coordinates within the bounds of the map to generate
 | 
			
		||||
   * @return each tile this feature touches, and the points that appear on each
 | 
			
		||||
   */
 | 
			
		||||
  public static TiledGeometry sliceIntoTiles(Geometry scaledGeom, double minSize, double buffer, int z,
 | 
			
		||||
    TileExtents.ForZoom extents) {
 | 
			
		||||
 | 
			
		||||
    if (scaledGeom.isEmpty()) {
 | 
			
		||||
      // ignore
 | 
			
		||||
      return new TiledGeometry(extents, buffer, z, false);
 | 
			
		||||
    } else if (scaledGeom instanceof Point point) {
 | 
			
		||||
      return slicePointsIntoTiles(extents, buffer, z, point.getCoordinates());
 | 
			
		||||
    } else if (scaledGeom instanceof MultiPoint points) {
 | 
			
		||||
      return slicePointsIntoTiles(extents, buffer, z, points.getCoordinates());
 | 
			
		||||
    } else if (scaledGeom instanceof Polygon || scaledGeom instanceof MultiPolygon ||
 | 
			
		||||
      scaledGeom instanceof LineString ||
 | 
			
		||||
      scaledGeom instanceof MultiLineString) {
 | 
			
		||||
      var coordinateSequences = GeometryCoordinateSequences.extractGroups(scaledGeom, minSize);
 | 
			
		||||
      boolean area = scaledGeom instanceof Polygonal;
 | 
			
		||||
      return sliceIntoTiles(coordinateSequences, buffer, area, z, extents);
 | 
			
		||||
    } else {
 | 
			
		||||
      throw new UnsupportedOperationException(
 | 
			
		||||
        "Unsupported JTS geometry type " + scaledGeom.getClass().getSimpleName() + " " +
 | 
			
		||||
          scaledGeom.getGeometryType());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public int zoomLevel() {
 | 
			
		||||
    return z;
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the set of tiles that {@code scaledGeom} touches at a zoom level.
 | 
			
		||||
   *
 | 
			
		||||
   * @param scaledGeom The geometry in scaled web mercator coordinates where northwest is (0,0) and southeast is
 | 
			
		||||
   *                   (2^z,2^z)
 | 
			
		||||
   * @param zoom       The zoom level
 | 
			
		||||
   * @param extents    The tile extents for this zoom level.
 | 
			
		||||
   * @return A {@link CoveredTiles} instance for the tiles that are covered by this geometry.
 | 
			
		||||
   */
 | 
			
		||||
  public static CoveredTiles getCoveredTiles(Geometry scaledGeom, int zoom, TileExtents.ForZoom extents) {
 | 
			
		||||
    if (scaledGeom.isEmpty()) {
 | 
			
		||||
      return new CoveredTiles(new RoaringBitmap(), zoom);
 | 
			
		||||
    } else if (scaledGeom instanceof Puntal || scaledGeom instanceof Polygonal || scaledGeom instanceof Lineal) {
 | 
			
		||||
      return sliceIntoTiles(scaledGeom, 0, 0, zoom, extents).getCoveredTiles();
 | 
			
		||||
    } else if (scaledGeom instanceof GeometryCollection gc) {
 | 
			
		||||
      CoveredTiles result = new CoveredTiles(new RoaringBitmap(), zoom);
 | 
			
		||||
      for (int i = 0; i < gc.getNumGeometries(); i++) {
 | 
			
		||||
        result = CoveredTiles.merge(getCoveredTiles(gc.getGeometryN(i), zoom, extents), result);
 | 
			
		||||
      }
 | 
			
		||||
      return result;
 | 
			
		||||
    } else {
 | 
			
		||||
      throw new UnsupportedOperationException(
 | 
			
		||||
        "Unsupported JTS geometry type " + scaledGeom.getClass().getSimpleName() + " " +
 | 
			
		||||
          scaledGeom.getGeometryType());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns all the points that appear on tiles representing points at {@code coords}.
 | 
			
		||||
   * Returns the tiles that this geometry touches, and the contents of those tiles for this geometry.
 | 
			
		||||
   *
 | 
			
		||||
   * @param groups  the list of linestrings or polygon rings extracted using {@link GeometryCoordinateSequences} in
 | 
			
		||||
   *                world web mercator coordinates where (0,0) is the northwest and (2^z,2^z) is the southeast corner of
 | 
			
		||||
| 
						 | 
				
			
			@ -144,12 +189,11 @@ class TiledGeometry {
 | 
			
		|||
   * @param area    {@code true} if this is a polygon {@code false} if this is a linestring
 | 
			
		||||
   * @param z       zoom level
 | 
			
		||||
   * @param extents range of tile coordinates within the bounds of the map to generate
 | 
			
		||||
   * @param id      feature ID
 | 
			
		||||
   * @return each tile this feature touches, and the points that appear on each
 | 
			
		||||
   */
 | 
			
		||||
  public static TiledGeometry sliceIntoTiles(List<List<CoordinateSequence>> groups, double buffer, boolean area, int z,
 | 
			
		||||
    TileExtents.ForZoom extents, long id) {
 | 
			
		||||
    TiledGeometry result = new TiledGeometry(extents, buffer, z, area, id);
 | 
			
		||||
  static TiledGeometry sliceIntoTiles(List<List<CoordinateSequence>> groups, double buffer, boolean area, int z,
 | 
			
		||||
    TileExtents.ForZoom extents) {
 | 
			
		||||
    TiledGeometry result = new TiledGeometry(extents, buffer, z, area);
 | 
			
		||||
    EnumSet<Direction> wrapResult = result.sliceWorldCopy(groups, 0);
 | 
			
		||||
    if (wrapResult.contains(Direction.RIGHT)) {
 | 
			
		||||
      result.sliceWorldCopy(groups, -result.maxTilesAtThisZoom);
 | 
			
		||||
| 
						 | 
				
			
			@ -160,32 +204,6 @@ class TiledGeometry {
 | 
			
		|||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an iterator over the coordinates of every tile that is completely filled within this polygon at this zoom
 | 
			
		||||
   * level, ordered by x ascending, y ascending.
 | 
			
		||||
   */
 | 
			
		||||
  public Iterable<TileCoord> getFilledTiles() {
 | 
			
		||||
    return filledRanges == null ? Collections.emptyList() :
 | 
			
		||||
      () -> filledRanges.entrySet().stream()
 | 
			
		||||
        .<TileCoord>mapMulti((entry, next) -> {
 | 
			
		||||
          int x = entry.getKey();
 | 
			
		||||
          for (int y : entry.getValue()) {
 | 
			
		||||
            TileCoord coord = TileCoord.ofXYZ(x, y, z);
 | 
			
		||||
            if (!tileContents.containsKey(coord)) {
 | 
			
		||||
              next.accept(coord);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }).iterator();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns every tile that this geometry touches, and the partial geometry contained on that tile that can be
 | 
			
		||||
   * reassembled using {@link GeometryCoordinateSequences}.
 | 
			
		||||
   */
 | 
			
		||||
  public Iterable<Map.Entry<TileCoord, List<List<CoordinateSequence>>>> getTileData() {
 | 
			
		||||
    return tileContents.entrySet();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static int wrapX(int x, int max) {
 | 
			
		||||
    x %= max;
 | 
			
		||||
    if (x < 0) {
 | 
			
		||||
| 
						 | 
				
			
			@ -220,6 +238,79 @@ class TiledGeometry {
 | 
			
		|||
    }, 2, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void slicePoint(Coordinate coord) {
 | 
			
		||||
    double worldX = coord.getX();
 | 
			
		||||
    double worldY = coord.getY();
 | 
			
		||||
    int minX = (int) Math.floor(worldX - neighborBuffer);
 | 
			
		||||
    int maxX = (int) Math.floor(worldX + neighborBuffer);
 | 
			
		||||
    int minY = Math.max(extents.minY(), (int) Math.floor(worldY - neighborBuffer));
 | 
			
		||||
    int maxY = Math.min(extents.maxY() - 1, (int) Math.floor(worldY + neighborBuffer));
 | 
			
		||||
    for (int x = minX; x <= maxX; x++) {
 | 
			
		||||
      double tileX = worldX - x;
 | 
			
		||||
      int wrappedX = wrapInt(x, maxTilesAtThisZoom);
 | 
			
		||||
      // point may end up inside bounds after wrapping
 | 
			
		||||
      if (extents.testX(wrappedX)) {
 | 
			
		||||
        for (int y = minY; y <= maxY; y++) {
 | 
			
		||||
          if (extents.test(wrappedX, y)) {
 | 
			
		||||
            TileCoord tile = TileCoord.ofXYZ(wrappedX, y, z);
 | 
			
		||||
            double tileY = worldY - y;
 | 
			
		||||
            tileContents.computeIfAbsent(tile, t -> List.of(new ArrayList<>()))
 | 
			
		||||
              .get(0)
 | 
			
		||||
              .add(GeoUtils.coordinateSequence(tileX * 256, tileY * 256));
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public int zoomLevel() {
 | 
			
		||||
    return z;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an iterator over the coordinates of every tile that is completely filled within this polygon at this zoom
 | 
			
		||||
   * level, ordered by x ascending, y ascending.
 | 
			
		||||
   */
 | 
			
		||||
  public Iterable<TileCoord> getFilledTiles() {
 | 
			
		||||
    return filledRanges == null ? Collections.emptyList() :
 | 
			
		||||
      () -> filledRanges.entrySet().stream()
 | 
			
		||||
        .<TileCoord>mapMulti((entry, next) -> {
 | 
			
		||||
          int x = entry.getKey();
 | 
			
		||||
          for (int y : entry.getValue()) {
 | 
			
		||||
            if (extents.test(x, y)) {
 | 
			
		||||
              TileCoord coord = TileCoord.ofXYZ(x, y, z);
 | 
			
		||||
              if (!tileContents.containsKey(coord)) {
 | 
			
		||||
                next.accept(coord);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }).iterator();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Returns the tiles touched by this geometry. */
 | 
			
		||||
  public CoveredTiles getCoveredTiles() {
 | 
			
		||||
    RoaringBitmap bitmap = new RoaringBitmap();
 | 
			
		||||
    for (TileCoord coord : tileContents.keySet()) {
 | 
			
		||||
      bitmap.add(maxTilesAtThisZoom * coord.x() + coord.y());
 | 
			
		||||
    }
 | 
			
		||||
    if (filledRanges != null) {
 | 
			
		||||
      for (var entry : filledRanges.entrySet()) {
 | 
			
		||||
        long colStart = (long) entry.getKey() * maxTilesAtThisZoom;
 | 
			
		||||
        var yRanges = entry.getValue();
 | 
			
		||||
        bitmap.or(RoaringBitmap.addOffset(yRanges.bitmap(), colStart));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return new CoveredTiles(bitmap, z);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns every tile that this geometry touches, and the partial geometry contained on that tile that can be
 | 
			
		||||
   * reassembled using {@link GeometryCoordinateSequences}.
 | 
			
		||||
   */
 | 
			
		||||
  public Map<TileCoord, List<List<CoordinateSequence>>> getTileData() {
 | 
			
		||||
    return tileContents;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Slices a geometry into tiles and stores in member fields for a single "copy" of the world.
 | 
			
		||||
   * <p>
 | 
			
		||||
| 
						 | 
				
			
			@ -251,9 +342,6 @@ class TiledGeometry {
 | 
			
		|||
         *  | | | | | |
 | 
			
		||||
         */
 | 
			
		||||
        IntObjectMap<List<MutableCoordinateSequence>> xSlices = sliceX(segment);
 | 
			
		||||
        if (z >= 6 && xSlices.size() >= Math.pow(2, z) - 1) {
 | 
			
		||||
          LOGGER.warn("Feature " + featureId + " crosses world at z" + z + ": " + xSlices.size());
 | 
			
		||||
        }
 | 
			
		||||
        for (IntObjectCursor<List<MutableCoordinateSequence>> xCursor : xSlices) {
 | 
			
		||||
          int x = xCursor.key + xOffset;
 | 
			
		||||
          // skip processing content past the edge of the world, but return that we saw it
 | 
			
		||||
| 
						 | 
				
			
			@ -299,7 +387,7 @@ class TiledGeometry {
 | 
			
		|||
      List<CoordinateSequence> outSeqs = inSeqs.stream()
 | 
			
		||||
        .filter(seq -> seq.size() >= minPoints)
 | 
			
		||||
        .toList();
 | 
			
		||||
      if (!outSeqs.isEmpty()) {
 | 
			
		||||
      if (!outSeqs.isEmpty() && extents.test(tileID.x(), tileID.y())) {
 | 
			
		||||
        tileContents.computeIfAbsent(tileID, tile -> new ArrayList<>()).add(outSeqs);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -448,7 +536,6 @@ class TiledGeometry {
 | 
			
		|||
      boolean onLeftEdge = area && ax == bx && ax == leftEdge && by < ay;
 | 
			
		||||
 | 
			
		||||
      for (int y = startY; y <= endY; y++) {
 | 
			
		||||
 | 
			
		||||
        // skip over filled tiles until we get to the next tile that already has detail on it
 | 
			
		||||
        if (area && y > endStartY && y < startEndY) {
 | 
			
		||||
          if (onRightEdge || onLeftEdge) {
 | 
			
		||||
| 
						 | 
				
			
			@ -609,4 +696,51 @@ class TiledGeometry {
 | 
			
		|||
    RIGHT,
 | 
			
		||||
    LEFT
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A set of tiles touched by a geometry.
 | 
			
		||||
   */
 | 
			
		||||
  public static class CoveredTiles implements TilePredicate, Iterable<TileCoord> {
 | 
			
		||||
    private final RoaringBitmap bitmap;
 | 
			
		||||
    private final int maxTilesAtZoom;
 | 
			
		||||
    private final int z;
 | 
			
		||||
 | 
			
		||||
    private CoveredTiles(RoaringBitmap bitmap, int z) {
 | 
			
		||||
      this.bitmap = bitmap;
 | 
			
		||||
      this.maxTilesAtZoom = 1 << z;
 | 
			
		||||
      this.z = z;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the union of tiles covered by {@code a} and {@code b}.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws IllegalArgumentException if {@code a} and {@code b} have different zoom levels.
 | 
			
		||||
     */
 | 
			
		||||
    public static CoveredTiles merge(CoveredTiles a, CoveredTiles b) {
 | 
			
		||||
      if (a.z != b.z) {
 | 
			
		||||
        throw new IllegalArgumentException("Cannot combine CoveredTiles with different zoom levels ");
 | 
			
		||||
      }
 | 
			
		||||
      return new CoveredTiles(RoaringBitmap.or(a.bitmap, b.bitmap), a.z);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean test(int x, int y) {
 | 
			
		||||
      return bitmap.contains(x * maxTilesAtZoom + y);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String toString() {
 | 
			
		||||
      return "CoveredTiles{z=" + z + ", tiles=" + FORMAT.integer(bitmap.getCardinality()) + ", storage=" +
 | 
			
		||||
        FORMAT.storage(bitmap.getSizeInBytes()) + "B}";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Stream<TileCoord> stream() {
 | 
			
		||||
      return bitmap.stream().mapToObj(i -> TileCoord.ofXYZ(i / maxTilesAtZoom, i % maxTilesAtZoom, z));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Iterator<TileCoord> iterator() {
 | 
			
		||||
      return stream().iterator();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1791,4 +1791,54 @@ class PlanetilerTests {
 | 
			
		|||
    assertTrue(renderMaxzoomResult.tiles.containsKey(z8Tile));
 | 
			
		||||
    assertFalse(maxzoomResult.tiles.containsKey(z8Tile));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private PlanetilerResults runForBoundsTest(int minzoom, int maxzoom, String key, String value) throws Exception {
 | 
			
		||||
    return runWithReaderFeatures(
 | 
			
		||||
      Map.of("threads", "1", key, value),
 | 
			
		||||
      List.of(
 | 
			
		||||
        newReaderFeature(WORLD_POLYGON, Map.of())
 | 
			
		||||
      ),
 | 
			
		||||
      (in, features) -> features.polygon("layer")
 | 
			
		||||
        .setZoomRange(minzoom, maxzoom)
 | 
			
		||||
        .setBufferPixels(0)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testBoundFilters() throws Exception {
 | 
			
		||||
    var origResult = runForBoundsTest(0, 2, "", "");
 | 
			
		||||
    var bboxResult = runForBoundsTest(0, 2, "bounds", "1,-85.05113,180,-1");
 | 
			
		||||
    var polyResult = runForBoundsTest(0, 2, "polygon", TestUtils.pathToResource("bottomrightearth.poly").toString());
 | 
			
		||||
 | 
			
		||||
    assertEquals(1 + 4 + 16, origResult.tiles.size());
 | 
			
		||||
    assertEquals(Set.of(
 | 
			
		||||
      TileCoord.ofXYZ(0, 0, 0),
 | 
			
		||||
      TileCoord.ofXYZ(1, 1, 1),
 | 
			
		||||
      TileCoord.ofXYZ(2, 2, 2),
 | 
			
		||||
      TileCoord.ofXYZ(3, 2, 2),
 | 
			
		||||
      TileCoord.ofXYZ(2, 3, 2),
 | 
			
		||||
      TileCoord.ofXYZ(3, 3, 2)
 | 
			
		||||
    ), bboxResult.tiles.keySet());
 | 
			
		||||
    assertEquals(Set.of(
 | 
			
		||||
      TileCoord.ofXYZ(0, 0, 0),
 | 
			
		||||
      TileCoord.ofXYZ(1, 1, 1),
 | 
			
		||||
      // TileCoord.ofXYZ(2, 2, 2),  - omit since this one is outside of triangle
 | 
			
		||||
      TileCoord.ofXYZ(3, 2, 2),
 | 
			
		||||
      TileCoord.ofXYZ(2, 3, 2),
 | 
			
		||||
      TileCoord.ofXYZ(3, 3, 2)
 | 
			
		||||
    ), polyResult.tiles.keySet());
 | 
			
		||||
 | 
			
		||||
    // but besides the omitted tile, the rest should be the same
 | 
			
		||||
    bboxResult.tiles.remove(TileCoord.ofXYZ(2, 2, 2));
 | 
			
		||||
    assertEquals(bboxResult.tiles, polyResult.tiles);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testBoundFiltersFill() throws Exception {
 | 
			
		||||
    var polyResultz8 = runForBoundsTest(8, 8, "polygon", TestUtils.pathToResource("bottomrightearth.poly").toString());
 | 
			
		||||
 | 
			
		||||
    int z8tiles = 1 << 8;
 | 
			
		||||
    assertFalse(polyResultz8.tiles.containsKey(TileCoord.ofXYZ(z8tiles * 3 / 4, z8tiles * 5 / 8, 8)));
 | 
			
		||||
    assertTrue(polyResultz8.tiles.containsKey(TileCoord.ofXYZ(z8tiles * 3 / 4, z8tiles * 7 / 8, 8)));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,10 @@
 | 
			
		|||
package com.onthegomap.planetiler.geo;
 | 
			
		||||
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertFalse;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertTrue;
 | 
			
		||||
 | 
			
		||||
import com.onthegomap.planetiler.TestUtils;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.locationtech.jts.geom.Envelope;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -56,4 +59,29 @@ class TileExtentsTest {
 | 
			
		|||
      assertEquals(1 << z, extents.getForZoom(z).maxY(), "z" + z + " maxY");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testShape() {
 | 
			
		||||
    var size = Math.pow(2, -14);
 | 
			
		||||
    var shape = GeoUtils.worldToLatLonCoords(TestUtils.newPolygon(
 | 
			
		||||
      0.5, 0.5 - size * 5,
 | 
			
		||||
      0.5 + size * 5, 0.5,
 | 
			
		||||
      0.5, 0.5 + size * 5,
 | 
			
		||||
      0.5 - size * 5, 0.5,
 | 
			
		||||
      0.5, 0.5 - size * 5
 | 
			
		||||
    ));
 | 
			
		||||
    TileExtents extents = TileExtents
 | 
			
		||||
      .computeFromWorldBounds(14, new Envelope(0.5 - size * 4, 0.5 + size * 4, 0.5 - size * 4, 0.5 + size * 4),
 | 
			
		||||
        shape);
 | 
			
		||||
    for (int z = 0; z <= 14; z++) {
 | 
			
		||||
      int middle = (1 << z) / 2;
 | 
			
		||||
      assertTrue(extents.test(middle, middle, z), "z" + z);
 | 
			
		||||
    }
 | 
			
		||||
    // inside shape and bounds
 | 
			
		||||
    assertTrue(extents.test((1 << 13) + 3, (1 << 13), 14));
 | 
			
		||||
    // inside shape but outside bounds
 | 
			
		||||
    assertFalse(extents.test((1 << 13) + 4, (1 << 13), 14));
 | 
			
		||||
    // inside bounds, outside shape
 | 
			
		||||
    assertFalse(extents.test((1 << 13) + 3, (1 << 13) + 3, 14));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,169 @@
 | 
			
		|||
package com.onthegomap.planetiler.reader.osm;
 | 
			
		||||
 | 
			
		||||
import static com.onthegomap.planetiler.reader.osm.PolyFileReader.parsePolyFile;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertThrows;
 | 
			
		||||
 | 
			
		||||
import com.onthegomap.planetiler.reader.FileFormatException;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
class PolyFileReaderTest {
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testParseAustralia() throws Exception {
 | 
			
		||||
    var poly = parsePolyFile("""
 | 
			
		||||
      australia_v
 | 
			
		||||
      first_area
 | 
			
		||||
           0.1446693E+03    -0.3826255E+02
 | 
			
		||||
           0.1446627E+03    -0.3825661E+02
 | 
			
		||||
           0.1446763E+03    -0.3824465E+02
 | 
			
		||||
           0.1446813E+03    -0.3824343E+02
 | 
			
		||||
           0.1446824E+03    -0.3824484E+02
 | 
			
		||||
           0.1446826E+03    -0.3825356E+02
 | 
			
		||||
           0.1446876E+03    -0.3825210E+02
 | 
			
		||||
           0.1446919E+03    -0.3824719E+02
 | 
			
		||||
           0.1447006E+03    -0.3824723E+02
 | 
			
		||||
           0.1447042E+03    -0.3825078E+02
 | 
			
		||||
           0.1446758E+03    -0.3826229E+02
 | 
			
		||||
           0.1446693E+03    -0.3826255E+02
 | 
			
		||||
      END
 | 
			
		||||
      second_area
 | 
			
		||||
           0.1422436E+03    -0.3839315E+02
 | 
			
		||||
           0.1422496E+03    -0.3839070E+02
 | 
			
		||||
           0.1422543E+03    -0.3839025E+02
 | 
			
		||||
           0.1422574E+03    -0.3839155E+02
 | 
			
		||||
           0.1422467E+03    -0.3840065E+02
 | 
			
		||||
           0.1422433E+03    -0.3840048E+02
 | 
			
		||||
           0.1422420E+03    -0.3839857E+02
 | 
			
		||||
           0.1422436E+03    -0.3839315E+02
 | 
			
		||||
      END
 | 
			
		||||
      END
 | 
			
		||||
      """);
 | 
			
		||||
    assertEquals(2, poly.getNumGeometries());
 | 
			
		||||
    assertEquals(4.60252e-4, poly.getArea(), 1e-10);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testParseAustraliaOceana() throws IOException {
 | 
			
		||||
    var poly = parsePolyFile("""
 | 
			
		||||
      australia-oceania
 | 
			
		||||
      1
 | 
			
		||||
         -107.863281   11.780702
 | 
			
		||||
         -104.171875   -28.082042
 | 
			
		||||
         -179.999999   -45.652740
 | 
			
		||||
         -179.999999   4.082818
 | 
			
		||||
         -107.863281   11.780702
 | 
			
		||||
      END
 | 
			
		||||
      0
 | 
			
		||||
         89.512500   -11.143360
 | 
			
		||||
         61.663780   -9.177713
 | 
			
		||||
         44.655470   -57.087780
 | 
			
		||||
         180.000000   -57.164820
 | 
			
		||||
         180.000000   26.277810
 | 
			
		||||
         141.547997   22.628320
 | 
			
		||||
         130.145100   3.640314
 | 
			
		||||
         129.953200   -0.535293
 | 
			
		||||
         131.061600   -3.784815
 | 
			
		||||
         130.266900   -10.043780
 | 
			
		||||
         118.255700   -13.011650
 | 
			
		||||
         102.800900   -8.390453
 | 
			
		||||
         89.512500   -11.143360
 | 
			
		||||
      END
 | 
			
		||||
      END
 | 
			
		||||
      """);
 | 
			
		||||
    assertEquals(2, poly.getNumGeometries());
 | 
			
		||||
    assertEquals(10876.51613, poly.getArea(), 1e-4);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testParseInvalid() throws IOException {
 | 
			
		||||
    assertThrows(FileFormatException.class, () -> parsePolyFile("""
 | 
			
		||||
      name
 | 
			
		||||
      section
 | 
			
		||||
         1 2
 | 
			
		||||
         3 4
 | 
			
		||||
         5 6
 | 
			
		||||
         7 8
 | 
			
		||||
      """));
 | 
			
		||||
    assertThrows(FileFormatException.class, () -> parsePolyFile("""
 | 
			
		||||
      name
 | 
			
		||||
      section
 | 
			
		||||
         1 2
 | 
			
		||||
         3 4
 | 
			
		||||
         5 6
 | 
			
		||||
         7 8
 | 
			
		||||
      END
 | 
			
		||||
      """));
 | 
			
		||||
    parsePolyFile("""
 | 
			
		||||
      name
 | 
			
		||||
      section
 | 
			
		||||
         1 2
 | 
			
		||||
         3 4
 | 
			
		||||
         5 6
 | 
			
		||||
         7 8
 | 
			
		||||
      END
 | 
			
		||||
      END
 | 
			
		||||
      """);
 | 
			
		||||
    parsePolyFile("""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      name
 | 
			
		||||
 | 
			
		||||
      section
 | 
			
		||||
 | 
			
		||||
         1 2
 | 
			
		||||
         3 4
 | 
			
		||||
         5 6
 | 
			
		||||
         7 8
 | 
			
		||||
 | 
			
		||||
      END
 | 
			
		||||
 | 
			
		||||
      END
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      """);
 | 
			
		||||
    assertThrows(FileFormatException.class, () -> parsePolyFile("""
 | 
			
		||||
      name
 | 
			
		||||
      section
 | 
			
		||||
         1 2
 | 
			
		||||
         3 4
 | 
			
		||||
         5 6
 | 
			
		||||
         7 8
 | 
			
		||||
      END
 | 
			
		||||
      END
 | 
			
		||||
      name
 | 
			
		||||
      section
 | 
			
		||||
         1 2
 | 
			
		||||
         3 4
 | 
			
		||||
         5 6
 | 
			
		||||
         7 8
 | 
			
		||||
      END
 | 
			
		||||
      END
 | 
			
		||||
      """));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testParseHole() throws IOException {
 | 
			
		||||
    var poly = parsePolyFile("""
 | 
			
		||||
      poly
 | 
			
		||||
      outer
 | 
			
		||||
         0 0
 | 
			
		||||
         0 10
 | 
			
		||||
         10 10
 | 
			
		||||
         10 0
 | 
			
		||||
         0 0
 | 
			
		||||
      END
 | 
			
		||||
      !inner
 | 
			
		||||
         1 1
 | 
			
		||||
         1 9
 | 
			
		||||
         9 9
 | 
			
		||||
         9 1
 | 
			
		||||
         1 1
 | 
			
		||||
      END
 | 
			
		||||
      END
 | 
			
		||||
      """);
 | 
			
		||||
    assertEquals(1, poly.getNumGeometries());
 | 
			
		||||
    assertEquals(10 * 10 - 8 * 8, poly.getArea(), 1e-4);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -137,6 +137,20 @@ class FeatureRendererTest {
 | 
			
		|||
    ), renderGeometry(feature));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testEmitPointsRespectShape() {
 | 
			
		||||
    config = PlanetilerConfig.from(Arguments.of(
 | 
			
		||||
      "polygon", TestUtils.pathToResource("bottomrightearth.poly")
 | 
			
		||||
    ));
 | 
			
		||||
    var feature = pointFeature(newPoint(0.5 + 1d / 512, 0.5 + 1d / 512))
 | 
			
		||||
      .setZoomRange(0, 2)
 | 
			
		||||
      .setBufferPixels(2);
 | 
			
		||||
    assertSameNormalizedFeatures(Map.of(
 | 
			
		||||
      TileCoord.ofXYZ(0, 0, 0), List.of(newPoint(128.5, 128.5)),
 | 
			
		||||
      TileCoord.ofXYZ(1, 1, 1), List.of(newPoint(1, 1))
 | 
			
		||||
    ), renderGeometry(feature));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @TestFactory
 | 
			
		||||
  List<DynamicTest> testProcessPointsNearInternationalDateLineAndPoles() {
 | 
			
		||||
    double d = 1d / 512;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,149 @@
 | 
			
		|||
package com.onthegomap.planetiler.render;
 | 
			
		||||
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertFalse;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertTrue;
 | 
			
		||||
 | 
			
		||||
import com.onthegomap.planetiler.TestUtils;
 | 
			
		||||
import com.onthegomap.planetiler.geo.TileCoord;
 | 
			
		||||
import com.onthegomap.planetiler.geo.TileExtents;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
class TiledGeometryTest {
 | 
			
		||||
  private static final int Z14_TILES = 1 << 14;
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testPoint() {
 | 
			
		||||
    var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newPoint(0.5, 0.5), 14,
 | 
			
		||||
      new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
 | 
			
		||||
    assertTrue(tiledGeom.test(0, 0));
 | 
			
		||||
    assertFalse(tiledGeom.test(0, 1));
 | 
			
		||||
    assertFalse(tiledGeom.test(1, 0));
 | 
			
		||||
    assertFalse(tiledGeom.test(1, 1));
 | 
			
		||||
    assertEquals(Set.of(TileCoord.ofXYZ(0, 0, 14)), tiledGeom.stream().collect(Collectors.toSet()));
 | 
			
		||||
 | 
			
		||||
    tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newPoint(Z14_TILES - 0.5, Z14_TILES - 0.5), 14,
 | 
			
		||||
      new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
 | 
			
		||||
    assertTrue(tiledGeom.test(Z14_TILES - 1, Z14_TILES - 1));
 | 
			
		||||
    assertFalse(tiledGeom.test(Z14_TILES - 2, Z14_TILES - 1));
 | 
			
		||||
    assertFalse(tiledGeom.test(Z14_TILES - 1, Z14_TILES - 2));
 | 
			
		||||
    assertFalse(tiledGeom.test(Z14_TILES - 2, Z14_TILES - 2));
 | 
			
		||||
    assertEquals(Set.of(TileCoord.ofXYZ(Z14_TILES - 1, Z14_TILES - 1, 14)),
 | 
			
		||||
      tiledGeom.stream().collect(Collectors.toSet()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testMultiPoint() {
 | 
			
		||||
    var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newMultiPoint(
 | 
			
		||||
      TestUtils.newPoint(0.5, 0.5),
 | 
			
		||||
      TestUtils.newPoint(2.5, 1.5)
 | 
			
		||||
    ), 14,
 | 
			
		||||
      new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
 | 
			
		||||
    assertEquals(Set.of(
 | 
			
		||||
      TileCoord.ofXYZ(0, 0, 14),
 | 
			
		||||
      TileCoord.ofXYZ(2, 1, 14)
 | 
			
		||||
    ), tiledGeom.stream().collect(Collectors.toSet()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testLine() {
 | 
			
		||||
    var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newLineString(
 | 
			
		||||
      0.5, 0.5,
 | 
			
		||||
      1.5, 0.5
 | 
			
		||||
    ), 14, new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
 | 
			
		||||
    assertEquals(Set.of(
 | 
			
		||||
      TileCoord.ofXYZ(0, 0, 14),
 | 
			
		||||
      TileCoord.ofXYZ(1, 0, 14)
 | 
			
		||||
    ), tiledGeom.stream().collect(Collectors.toSet()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testMultiLine() {
 | 
			
		||||
    var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newMultiLineString(
 | 
			
		||||
      TestUtils.newLineString(
 | 
			
		||||
        0.5, 0.5,
 | 
			
		||||
        1.5, 0.5
 | 
			
		||||
      ),
 | 
			
		||||
      TestUtils.newLineString(
 | 
			
		||||
        3.5, 1.5,
 | 
			
		||||
        4.5, 1.5
 | 
			
		||||
      )
 | 
			
		||||
    ), 14, new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
 | 
			
		||||
    assertEquals(Set.of(
 | 
			
		||||
      TileCoord.ofXYZ(0, 0, 14),
 | 
			
		||||
      TileCoord.ofXYZ(1, 0, 14),
 | 
			
		||||
      TileCoord.ofXYZ(3, 1, 14),
 | 
			
		||||
      TileCoord.ofXYZ(4, 1, 14)
 | 
			
		||||
    ), tiledGeom.stream().collect(Collectors.toSet()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testPolygon() {
 | 
			
		||||
    var tiledGeom =
 | 
			
		||||
      TiledGeometry.getCoveredTiles(TestUtils.newPolygon(
 | 
			
		||||
        TestUtils.rectangleCoordList(25.5, 27.5),
 | 
			
		||||
        List.of(TestUtils.rectangleCoordList(25.9, 27.1))
 | 
			
		||||
      ), 14, new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
 | 
			
		||||
    assertEquals(Set.of(
 | 
			
		||||
      TileCoord.ofXYZ(25, 25, 14),
 | 
			
		||||
      TileCoord.ofXYZ(26, 25, 14),
 | 
			
		||||
      TileCoord.ofXYZ(27, 25, 14),
 | 
			
		||||
 | 
			
		||||
      TileCoord.ofXYZ(25, 26, 14),
 | 
			
		||||
      //      TileCoord.ofXYZ(26, 26, 14), skipped because of hole!
 | 
			
		||||
      TileCoord.ofXYZ(27, 26, 14),
 | 
			
		||||
 | 
			
		||||
      TileCoord.ofXYZ(25, 27, 14),
 | 
			
		||||
      TileCoord.ofXYZ(26, 27, 14),
 | 
			
		||||
      TileCoord.ofXYZ(27, 27, 14)
 | 
			
		||||
    ), tiledGeom.stream().collect(Collectors.toSet()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testMultiPolygon() {
 | 
			
		||||
    var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newMultiPolygon(
 | 
			
		||||
      TestUtils.rectangle(25.5, 26.5),
 | 
			
		||||
      TestUtils.rectangle(30.1, 30.9)
 | 
			
		||||
    ), 14,
 | 
			
		||||
      new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
 | 
			
		||||
    assertEquals(Set.of(
 | 
			
		||||
      TileCoord.ofXYZ(25, 25, 14),
 | 
			
		||||
      TileCoord.ofXYZ(25, 26, 14),
 | 
			
		||||
      TileCoord.ofXYZ(26, 25, 14),
 | 
			
		||||
      TileCoord.ofXYZ(26, 26, 14),
 | 
			
		||||
 | 
			
		||||
      TileCoord.ofXYZ(30, 30, 14)
 | 
			
		||||
    ), tiledGeom.stream().collect(Collectors.toSet()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testEmpty() {
 | 
			
		||||
    var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newGeometryCollection(), 14,
 | 
			
		||||
      new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
 | 
			
		||||
    assertEquals(Set.of(), tiledGeom.stream().collect(Collectors.toSet()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void testGeometryCollection() {
 | 
			
		||||
    var tiledGeom = TiledGeometry.getCoveredTiles(TestUtils.newGeometryCollection(
 | 
			
		||||
      TestUtils.rectangle(0.1, 0.9),
 | 
			
		||||
      TestUtils.newPoint(1.5, 1.5),
 | 
			
		||||
      TestUtils.newGeometryCollection(TestUtils.newLineString(3.5, 10.5, 4.5, 10.5))
 | 
			
		||||
    ), 14,
 | 
			
		||||
      new TileExtents.ForZoom(14, 0, 0, Z14_TILES, Z14_TILES, null));
 | 
			
		||||
    assertEquals(Set.of(
 | 
			
		||||
      // rectangle
 | 
			
		||||
      TileCoord.ofXYZ(0, 0, 14),
 | 
			
		||||
 | 
			
		||||
      // point
 | 
			
		||||
      TileCoord.ofXYZ(1, 1, 14),
 | 
			
		||||
 | 
			
		||||
      // linestring
 | 
			
		||||
      TileCoord.ofXYZ(3, 10, 14),
 | 
			
		||||
      TileCoord.ofXYZ(4, 10, 14)
 | 
			
		||||
    ), tiledGeom.stream().collect(Collectors.toSet()));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
bottom-right
 | 
			
		||||
1
 | 
			
		||||
   180 -1
 | 
			
		||||
   180 -85
 | 
			
		||||
   1 -85
 | 
			
		||||
   180 -1
 | 
			
		||||
END
 | 
			
		||||
END
 | 
			
		||||
		Ładowanie…
	
		Reference in New Issue