Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.locationtech.jts.algorithm.locate.SimplePointInAreaLocator;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import scala.Tuple2;
Expand Down Expand Up @@ -375,35 +377,22 @@ public static List<Tuple2<Pixel, Double>> FindPixelCoordinates(
Envelope datasetBoundary,
Polygon spatialObject,
boolean reverseSpatialCoordinate) {
List<Tuple2<Pixel, Double>> result = new ArrayList<Tuple2<Pixel, Double>>();
for (int i = 0; i < spatialObject.getCoordinates().length - 1; i++) {
Tuple2<Integer, Integer> pixelCoordinate1 = null;
Tuple2<Integer, Integer> pixelCoordinate2 = null;
try {
pixelCoordinate1 =
FindOnePixelCoordinate(
resolutionX,
resolutionY,
datasetBoundary,
spatialObject.getCoordinates()[i],
reverseSpatialCoordinate);
pixelCoordinate2 =
FindOnePixelCoordinate(
resolutionX,
resolutionY,
datasetBoundary,
spatialObject.getCoordinates()[i + 1],
reverseSpatialCoordinate);
} catch (Exception e) {
// This pixel is out of boundary. Should be ignored.
continue;
}
// Trace each ring on its own. Polygon.getCoordinates() concatenates the rings, so tracing it
// directly also draws a segment from the end of one ring to the start of the next.
List<Tuple2<Pixel, Double>> result =
FindPixelCoordinates(
resolutionX,
resolutionY,
datasetBoundary,
spatialObject.getExteriorRing(),
reverseSpatialCoordinate);
for (int i = 0; i < spatialObject.getNumInteriorRing(); i++) {
result.addAll(
FindPixelCoordinates(
resolutionX,
resolutionY,
pixelCoordinate1,
pixelCoordinate2,
datasetBoundary,
spatialObject.getInteriorRingN(i),
reverseSpatialCoordinate));
}
return result;
Expand All @@ -428,34 +417,41 @@ public static List<Tuple2<Pixel, Double>> FindPixelCoordinates(
boolean reverseSpatialCoordinate,
Double objectWeight) {
List<Tuple2<Pixel, Double>> result = new ArrayList<Tuple2<Pixel, Double>>();
if (spatialObject.isEmpty()) {
return result;
}
GeometryFactory geometryfactory = new GeometryFactory();
ArrayList<Coordinate> coordinatesList = new ArrayList<Coordinate>();
LinearRing linear;
for (int i = 0; i < spatialObject.getCoordinates().length; i++) {
Tuple2<Integer, Integer> pixelCoordinate = null;
pixelCoordinate =
FindOnePixelCoordinate(
// Convert each ring to pixel space on its own, keeping the holes as holes. Converting
// Polygon.getCoordinates() as one ring would join the rings into a single self-crossing ring.
LinearRing shell =
toPixelRing(
spatialObject.getExteriorRing(),
resolutionX,
resolutionY,
datasetBoundary,
reverseSpatialCoordinate,
geometryfactory);
LinearRing[] holes = new LinearRing[spatialObject.getNumInteriorRing()];
for (int i = 0; i < holes.length; i++) {
holes[i] =
toPixelRing(
spatialObject.getInteriorRingN(i),
resolutionX,
resolutionY,
datasetBoundary,
spatialObject.getCoordinates()[i],
reverseSpatialCoordinate);

coordinatesList.add(new Coordinate(pixelCoordinate._1, pixelCoordinate._2));
reverseSpatialCoordinate,
geometryfactory);
}

coordinatesList.add(coordinatesList.get(0));
linear =
geometryfactory.createLinearRing(
coordinatesList.toArray(new Coordinate[coordinatesList.size()]));
Polygon polygon = new Polygon(linear, null, geometryfactory);
Polygon polygon = geometryfactory.createPolygon(shell, holes);
int minPixelX = (int) polygon.getEnvelopeInternal().getMinX();
int maxPixelX = (int) polygon.getEnvelopeInternal().getMaxX();
int minPixelY = (int) polygon.getEnvelopeInternal().getMinY();
int maxPixelY = (int) polygon.getEnvelopeInternal().getMaxY();
for (int j = minPixelY; j <= maxPixelY; j++) {
for (int i = minPixelX; i <= maxPixelX; i++) {
if (polygon.contains(geometryfactory.createPoint(new Coordinate(i, j)))) {
// Snapping to pixels can make a hole share an edge with the shell or another hole.
// Polygon.contains throws a TopologyException on such rings; the point locator does not.
if (SimplePointInAreaLocator.locate(new Coordinate(i, j), polygon) == Location.INTERIOR) {
try {
Pixel newPixel = new Pixel(i, j, resolutionX, resolutionY);
result.add(new Tuple2<Pixel, Double>(newPixel, new Double(objectWeight)));
Expand All @@ -470,6 +466,25 @@ public static List<Tuple2<Pixel, Double>> FindPixelCoordinates(
return result;
}

/** Converts a ring's coordinates to pixel coordinates, as a closed ring in pixel space. */
private static LinearRing toPixelRing(
LineString ring,
int resolutionX,
int resolutionY,
Envelope datasetBoundary,
boolean reverseSpatialCoordinate,
GeometryFactory geometryfactory) {
Coordinate[] coordinates = ring.getCoordinates();
Coordinate[] pixelCoordinates = new Coordinate[coordinates.length];
for (int i = 0; i < coordinates.length; i++) {
Tuple2<Integer, Integer> pixelCoordinate =
FindOnePixelCoordinate(
resolutionX, resolutionY, datasetBoundary, coordinates[i], reverseSpatialCoordinate);
pixelCoordinates[i] = new Coordinate(pixelCoordinate._1, pixelCoordinate._2);
}
return geometryfactory.createLinearRing(pixelCoordinates);
}

/**
* Find pixel coordinates.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sedona.viz.utils;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.junit.Test;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import scala.Tuple2;

/** Pixel coordinates of polygons on a 10 x 10 grid over (0, 0, 10, 10). */
public class RasterizationUtilsTest {

private static final Envelope BOUNDARY = new Envelope(0, 10, 0, 10);

private static final String EXTERIOR = "(0.5 0.5, 9.5 0.5, 9.5 9.5, 0.5 9.5, 0.5 0.5)";
private static final String HOLE = "(4.5 4.5, 6.5 4.5, 6.5 6.5, 4.5 6.5, 4.5 4.5)";
private static final String SECOND_HOLE = "(1.5 1.5, 2.5 1.5, 2.5 2.5, 1.5 2.5, 1.5 1.5)";

private static Polygon polygon(String wkt) throws ParseException {
return (Polygon) new WKTReader().read(wkt);
}

/** The outline pixels as sorted "x,y" strings, keeping duplicates. */
private static List<String> outline(Polygon polygon) {
return pixels(RasterizationUtils.FindPixelCoordinates(10, 10, BOUNDARY, polygon, false));
}

/** The filled pixels as sorted "x,y" strings, keeping duplicates. */
private static List<String> fill(Polygon polygon) {
return pixels(RasterizationUtils.FindPixelCoordinates(10, 10, BOUNDARY, polygon, false, 1.0));
}

private static List<String> pixels(List<Tuple2<Pixel, Double>> pixels) {
List<String> result = new ArrayList<>();
for (Tuple2<Pixel, Double> pixel : pixels) {
result.add((int) pixel._1().getX() + "," + (int) pixel._1().getY());
}
Collections.sort(result);
return result;
}

/** Every pixel (x, y) with lo <= x, y <= hi, as "x,y" strings. */
private static Set<String> block(int lo, int hi) {
Set<String> result = new TreeSet<>();
for (int x = lo; x <= hi; x++) {
for (int y = lo; y <= hi; y++) {
result.add(x + "," + y);
}
}
return result;
}

/** Every pixel (x, y) with x0 <= x <= x1 and y0 <= y <= y1, as "x,y" strings. */
private static Set<String> rectangle(int x0, int x1, int y0, int y1) {
Set<String> result = new TreeSet<>();
for (int x = x0; x <= x1; x++) {
for (int y = y0; y <= y1; y++) {
result.add(x + "," + y);
}
}
return result;
}

private static List<String> concat(List<String> first, List<String> second) {
List<String> result = new ArrayList<>(first);
result.addAll(second);
Collections.sort(result);
return result;
}

@Test
public void outlineOfPolygonWithoutHolesIsItsExteriorRing() throws ParseException {
Set<String> expected = block(0, 9);
expected.removeAll(block(1, 8));
List<String> pixels = outline(polygon("POLYGON (" + EXTERIOR + ")"));
assertEquals(36, pixels.size());
assertEquals(expected, new TreeSet<>(pixels));
}

@Test
public void outlineOfPolygonWithHoleTracesEachRingSeparately() throws ParseException {
List<String> exterior = outline(polygon("POLYGON (" + EXTERIOR + ")"));
List<String> hole = outline(polygon("POLYGON (" + HOLE + ")"));
List<String> pixels = outline(polygon("POLYGON (" + EXTERIOR + ", " + HOLE + ")"));

assertEquals(concat(exterior, hole), pixels);
assertEquals(44, pixels.size());
// No segment joining the exterior ring's last vertex to the hole's first vertex
assertFalse(pixels.contains("1,1"));
assertFalse(pixels.contains("2,2"));
assertFalse(pixels.contains("3,3"));
}

@Test
public void outlineOfPolygonWithSeveralHolesTracesEachRingSeparately() throws ParseException {
List<String> exterior = outline(polygon("POLYGON (" + EXTERIOR + ")"));
List<String> hole = outline(polygon("POLYGON (" + HOLE + ")"));
List<String> secondHole = outline(polygon("POLYGON (" + SECOND_HOLE + ")"));

List<String> pixels =
outline(polygon("POLYGON (" + EXTERIOR + ", " + HOLE + ", " + SECOND_HOLE + ")"));

assertEquals(concat(concat(exterior, hole), secondHole), pixels);
}

@Test
public void fillOfPolygonWithoutHolesCoversItsInterior() throws ParseException {
assertEquals(new ArrayList<>(block(1, 8)), fill(polygon("POLYGON (" + EXTERIOR + ")")));
}

@Test
public void fillOfPolygonWithHoleLeavesTheHoleOut() throws ParseException {
Set<String> expected = block(1, 8);
expected.removeAll(block(4, 6));

List<String> pixels = fill(polygon("POLYGON (" + EXTERIOR + ", " + HOLE + ")"));

assertEquals(new ArrayList<>(expected), pixels);
// The interior on the line between the exterior ring and the hole is still filled
assertTrue(pixels.contains("1,1"));
assertTrue(pixels.contains("2,2"));
assertTrue(pixels.contains("3,3"));
}

@Test
public void fillOfPolygonWithSeveralHolesLeavesEachHoleOut() throws ParseException {
Set<String> expected = block(1, 8);
expected.removeAll(block(4, 6));
expected.removeAll(block(1, 2));

List<String> pixels =
fill(polygon("POLYGON (" + EXTERIOR + ", " + HOLE + ", " + SECOND_HOLE + ")"));

assertEquals(new ArrayList<>(expected), pixels);
}

@Test
public void fillOfPolygonWithHoleSnappedOntoTheExteriorRing() throws ParseException {
// In pixels the hole runs from (0, 2) to (4, 4), so its left edge lies on the exterior ring
Set<String> expected = block(1, 8);
expected.removeAll(rectangle(1, 4, 2, 4));

List<String> pixels =
fill(polygon("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (0.5 2, 4 2, 4 4, 0.5 4, 0.5 2))"));

assertEquals(new ArrayList<>(expected), pixels);
}

@Test
public void fillOfPolygonWithHolesSnappedOntoEachOther() throws ParseException {
// In pixels the holes run from x = 2 to 3 and from x = 3 to 5, sharing the edge x = 3
Set<String> expected = block(1, 8);
expected.removeAll(rectangle(2, 5, 2, 3));

List<String> pixels =
fill(
polygon(
"POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0),"
+ " (2.1 2.1, 3.5 2.1, 3.5 3.5, 2.1 3.5, 2.1 2.1),"
+ " (3.6 2.1, 5.5 2.1, 5.5 3.5, 3.6 3.5, 3.6 2.1))"));

assertEquals(new ArrayList<>(expected), pixels);
}

@Test
public void emptyPolygonHasNoPixels() throws ParseException {
Polygon empty = polygon("POLYGON EMPTY");
assertTrue(outline(empty).isEmpty());
assertTrue(fill(empty).isEmpty());
}
}
Loading