Skip to content

Commit

Permalink
GEOS-8033: Adding the "kmcentroid" option to KML.
Browse files Browse the repository at this point in the history
The option provides the capability to specify options that control how
the placement of a KML place mark occurs. Namely to clip the geometry
before calculating the centroid, and the ability to generate a centroid
that falls within a polygon.
  • Loading branch information
jdeolive committed Jul 11, 2017
1 parent c396ab9 commit b143eb0
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import com.vividsolutions.jts.geom.Envelope;
import org.geoserver.kml.KmlEncodingContext;
import org.geoserver.kml.utils.KmlCentroidBuilder;
import org.geoserver.kml.utils.KmlCentroidOptions;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.WMSInfo;
import org.geoserver.wms.featureinfo.FeatureTemplate;
Expand Down Expand Up @@ -53,7 +55,9 @@ public KmlDecorator getDecorator(Class<? extends Feature> featureClass,
if (Placemark.class.isAssignableFrom(featureClass)) {
boolean hasHeightTemplate = hasHeightTemplate(context);
boolean isExtrudeEnabled = isExtrudeEnabled(context);
return new PlacemarkGeometryDecorator(hasHeightTemplate, isExtrudeEnabled);
KmlCentroidOptions centroidOpts = KmlCentroidOptions.create(context);

return new PlacemarkGeometryDecorator(hasHeightTemplate, isExtrudeEnabled, centroidOpts);
} else {
return null;
}
Expand Down Expand Up @@ -95,10 +99,12 @@ static class PlacemarkGeometryDecorator implements KmlDecorator {
static final Logger LOGGER = Logging.getLogger(PlacemarkGeometryDecorator.class);
private boolean hasHeightTemplate;
private boolean extrudeEnabled;
private KmlCentroidOptions centroidOpts;

public PlacemarkGeometryDecorator(boolean hasHeightTemplate, boolean extrudeEnabled) {
public PlacemarkGeometryDecorator(boolean hasHeightTemplate, boolean extrudeEnabled, KmlCentroidOptions centroidOpts) {
this.hasHeightTemplate = hasHeightTemplate;
this.extrudeEnabled = extrudeEnabled;
this.centroidOpts = centroidOpts;
}

@Override
Expand Down Expand Up @@ -159,7 +165,7 @@ private de.micromata.opengis.kml.v_2_2_0.Geometry encodeGeometry(Geometry geomet
MultiGeometry mg = new MultiGeometry();

// centroid + full geometry
Coordinate c = CENTROIDS.geometryCentroid(geometry);
Coordinate c = CENTROIDS.geometryCentroid(geometry, context.getRequest().getBbox(), centroidOpts);
if(!Double.isNaN(height)) {
c.setOrdinate(2, height);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import java.util.NoSuchElementException;

import org.geoserver.kml.KmlEncodingContext;
import org.geoserver.kml.utils.KmlCentroidBuilder;
import org.geoserver.kml.utils.KmlCentroidOptions;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.collection.DecoratingSimpleFeatureCollection;
Expand All @@ -33,10 +35,12 @@ class KMLCentroidFeatureCollection extends DecoratingSimpleFeatureCollection {


private SimpleFeatureType schema;
private KmlEncodingContext context;

protected KMLCentroidFeatureCollection(
FeatureCollection<SimpleFeatureType, SimpleFeature> delegate) {
FeatureCollection<SimpleFeatureType, SimpleFeature> delegate, KmlEncodingContext context) {
super(delegate);
this.context = context;

// build the centroid collection schema
SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
Expand All @@ -62,19 +66,23 @@ protected KMLCentroidFeatureCollection(

@Override
public SimpleFeatureIterator features() {
return new KMLCentroidFeatureIterator(delegate.features(), this.schema);
return new KMLCentroidFeatureIterator(delegate.features(), this.schema, this.context);
}

static class KMLCentroidFeatureIterator implements SimpleFeatureIterator {

private SimpleFeatureIterator delegate;
private SimpleFeatureBuilder builder;
private KmlCentroidBuilder centroids;
private KmlEncodingContext context;
private KmlCentroidOptions centroidOpts;

public KMLCentroidFeatureIterator(SimpleFeatureIterator features, SimpleFeatureType schema) {
public KMLCentroidFeatureIterator(SimpleFeatureIterator features, SimpleFeatureType schema, KmlEncodingContext context) {
this.delegate = features;
this.builder = new SimpleFeatureBuilder(schema);
this.centroids = new KmlCentroidBuilder();
this.context = context;
this.centroidOpts = KmlCentroidOptions.create(context);
}

@Override
Expand All @@ -89,7 +97,7 @@ public SimpleFeature next() throws NoSuchElementException {
if ((attribute instanceof Geometry) &&
!(attribute instanceof Point)) {
Geometry geom = (Geometry) attribute;
Coordinate point = centroids.geometryCentroid(geom);
Coordinate point = centroids.geometryCentroid(geom, context.getRequest().getBbox(), centroidOpts);
attribute = geom.getFactory().createPoint(point);
}
builder.add(attribute);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ protected void encodeFolderContents(Layer layer, Folder folder) {
* @param folder
*/
private void addFeatureCentroids(Layer layer, Folder folder) {
SimpleFeatureCollection centroids = new KMLCentroidFeatureCollection(context.getCurrentFeatureCollection());
SimpleFeatureCollection centroids = new KMLCentroidFeatureCollection(context.getCurrentFeatureCollection(), context);
context.setCurrentFeatureCollection(centroids);
FeatureLayer centroidsLayer = new FeatureLayer(centroids, layer.getStyle(), layer.getTitle());
List<Feature> features = new SequenceList<Feature>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,28 @@
package org.geoserver.kml.utils;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.linearref.LengthIndexedLine;
import org.geotools.geometry.jts.GeometryClipper;
import org.geotools.renderer.lite.RendererUtilities;
import org.geotools.util.logging.Logging;

import java.util.logging.Level;
import java.util.logging.Logger;

/**
* A KML specific geometry centroid extractor
*/
public class KmlCentroidBuilder {

static Logger LOG = Logging.getLogger(KmlCentroidBuilder.class);

/**
* Returns the centroid of the geometry, handling a geometry collection.
* <p>
Expand All @@ -26,6 +36,37 @@ public class KmlCentroidBuilder {
* </p>
*/
public Coordinate geometryCentroid(Geometry g) {
return geometryCentroid(g, null,null);
}

/**
* Returns the centroid of the geometry, handling a geometry collection.
* <p>
* In the case of a collection a multi point containing the centroid of each geometry in the
* collection is calculated. The first point in the multi point is returned as the controid.
* </p>
* <p>
* The <tt>opts</tt> parameter is used to provide additional options controlling how the
* centroid is computed.
* </p>
* @param g The geometry to compute the centroid.
* @param bbox The request bbox, used to potentially clip the geometry before computting the centroid.
* @param opts The centroid options controlling whether clipping/sampling/etc... are used.
*/
public Coordinate geometryCentroid(Geometry g, Envelope bbox, KmlCentroidOptions opts) {
if (opts == null) {
opts = KmlCentroidOptions.DEFAULT;
}

// clip?
if (opts.isClip()) {
if (bbox != null) {
g = clipGeometry(g, bbox);
} else {
LOG.warning("Clip option specified for kml centroids, but no bbox available");
}
}

if (g instanceof GeometryCollection) {
g = selectRepresentativeGeometry((GeometryCollection) g);
}
Expand All @@ -40,10 +81,23 @@ public Coordinate geometryCentroid(Geometry g) {
LineString line = (LineString) g;
LengthIndexedLine lil = new LengthIndexedLine(line);
return lil.extractPoint(line.getLength() / 2.0);
} else {
// return the actual centroid
return g.getCentroid().getCoordinate();
}
} else if (g instanceof Polygon) {
if (opts.isContain()) {
try {
Point p = RendererUtilities.sampleForInternalPoint(
(Polygon)g, null, null, null, -1, opts.getSamples());
if (p != null && !p.isEmpty()) {
return p.getCoordinate();
}
}
catch(Exception e) {
LOG.log(Level.WARNING, "Unable to calculate central point for polygon", e);
}
}
}

// return the actual centroid
return g.getCentroid().getCoordinate();
}

/**
Expand Down Expand Up @@ -81,4 +135,7 @@ private Geometry selectRepresentativeGeometry(GeometryCollection g) {
}
}

private Geometry clipGeometry(Geometry g, Envelope bbox) {
return new GeometryClipper(bbox).clipSafe(g, true, 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.kml.utils;

import org.geoserver.kml.KmlEncodingContext;
import org.geoserver.ows.util.CaseInsensitiveMap;
import org.geoserver.ows.util.KvpMap;

import java.util.Collections;
import java.util.Map;

/**
* Options used for computing geometry centroids by {@link KmlCentroidBuilder}.
*/
public class KmlCentroidOptions {

public static final String PREFIX = "kmcentroid";
public static final String CONTAIN = PREFIX + "_contain";
public static final String SAMPLE = PREFIX + "_sample";
public static final String CLIP = PREFIX + "_clip";

public static final KmlCentroidOptions DEFAULT = new KmlCentroidOptions(new KvpMap());

static final int DEFAULT_SAMPLES = 5;

/**
* Creates centroid options from the specified encoding context.
*/
public static KmlCentroidOptions create(KmlEncodingContext context) {
return create(context != null && context.getRequest() != null ?
context.getRequest().getFormatOptions() : Collections.EMPTY_MAP);
}

/**
* Creates centroid options from the specified format options.
*/
public static KmlCentroidOptions create(Map formatOptions) {
if (formatOptions != null) {
for (Object key : formatOptions.keySet()) {
if (key.toString().toLowerCase().startsWith(PREFIX)) {
return new KmlCentroidOptions(CaseInsensitiveMap.wrap(formatOptions));
}
}
}
return KmlCentroidOptions.DEFAULT;
}

Map raw;

public KmlCentroidOptions(Map raw) {
this.raw = raw;
}

/**
* Determines if the "contain" option is set.
* <p>
* This option causes the centroid builder to find a point (via sampling if necessary) that is
* contained within a polygon geometry.
* </p>
* @see #getSamples()
*/
public boolean isContain() {
return Boolean.valueOf(raw.getOrDefault(CONTAIN, "false").toString());
}

/**
* Determines if the "clip" option is set.
* <p>
* This option causes the centroid builder to clip geometries by the request bounding box before
* computing the centroid.
* </p>
*/
public boolean isClip() {
return Boolean.valueOf(raw.getOrDefault(CLIP, "false").toString());
}

/**
* The number of samples to try when computing a centroid when {@link #isContain()} is set.
* <p>
* When unset this falls back to
* </p>
*/
public int getSamples() {
try {
return Integer.parseInt(raw.getOrDefault(SAMPLE, String.valueOf(DEFAULT_SAMPLES)).toString());
}
catch(NumberFormatException e) {
return DEFAULT_SAMPLES;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.kml.utils;

import com.google.common.collect.ImmutableMap;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.WKTReader;
import org.junit.Before;
import org.junit.Test;

import static org.geoserver.kml.utils.KmlCentroidOptions.CLIP;
import static org.geoserver.kml.utils.KmlCentroidOptions.CONTAIN;
import static org.geoserver.kml.utils.KmlCentroidOptions.SAMPLE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class KmlCentroidBuilderTest {

Geometry cShapeGeom;

@Before
public void setUp() throws Exception {
cShapeGeom = new WKTReader().read("POLYGON ((-112.534433451864 43.8706532611928,-112.499157652296 44.7878240499628,-99.6587666095152 44.7878240499628,-99.7242788087131 43.2155312692142,-111.085391877449 43.099601544023,-110.744593363875 36.1862602686501,-98.6760836215473 35.9436771582516,-98.7415958207452 33.5197257879307,-111.77852346112 33.9783111823157,-111.758573671673 34.6566040234952,-113.088767445077 34.7644575726901,-113.023255245879 43.8706532611928,-112.534433451864 43.8706532611928))");
}

@Test
public void testSampleForPoint() throws Exception {
Geometry g = cShapeGeom;

KmlCentroidOptions opts1 = KmlCentroidOptions.create(ImmutableMap.of(CONTAIN, "true", SAMPLE, "2"));
KmlCentroidOptions opts2 = KmlCentroidOptions.create(ImmutableMap.of(CONTAIN, "true", SAMPLE, "10"));

KmlCentroidBuilder builder = new KmlCentroidBuilder();

Coordinate c = builder.geometryCentroid(g, null, opts1);
assertFalse(g.contains(g.getFactory().createPoint(c)));

c = builder.geometryCentroid(g, null, opts2);
assertTrue(g.contains(g.getFactory().createPoint(c)));
}

@Test
public void testClip() {
Geometry g = cShapeGeom;
KmlCentroidOptions opts1 = KmlCentroidOptions.create(ImmutableMap.of());
KmlCentroidOptions opts2 = KmlCentroidOptions.create(ImmutableMap.of(CLIP, "true"));
opts2.isClip();

KmlCentroidBuilder builder = new KmlCentroidBuilder();

Coordinate c = builder.geometryCentroid(g, null, opts1);
assertFalse(g.contains(g.getFactory().createPoint(c)));

Envelope bbox = new Envelope(
-106.603059724489, -103.655010760585, 34.6334331742943, 36.9918723454173);
c = builder.geometryCentroid(g, bbox, opts2);
assertTrue(g.contains(g.getFactory().createPoint(c)));
}

@Test
public void testCaseInsensitivity() {
KmlCentroidOptions opts = KmlCentroidOptions.create(ImmutableMap.of(
CONTAIN.toUpperCase(), "true", CLIP.toUpperCase(), "true", SAMPLE.toUpperCase(), "12"));
assertTrue(opts.isContain());
assertTrue(opts.isClip());
assertEquals(12, opts.getSamples());
}
}

0 comments on commit b143eb0

Please sign in to comment.