/* (c) 2014-2016 Open Source Geospatial Foundation - all rights reserved * (c) 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.kml.sequence; import java.util.List; import org.geoserver.kml.KmlEncodingContext; import org.geoserver.platform.ServiceException; import org.geoserver.wms.WMSMapContent; import org.geoserver.wms.WMSRequests; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.FeatureLayer; import org.geotools.map.Layer; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import de.micromata.opengis.kml.v_2_2_0.Feature; import de.micromata.opengis.kml.v_2_2_0.Folder; import de.micromata.opengis.kml.v_2_2_0.GroundOverlay; import de.micromata.opengis.kml.v_2_2_0.Icon; import de.micromata.opengis.kml.v_2_2_0.LatLonBox; import de.micromata.opengis.kml.v_2_2_0.ViewRefreshMode; /** * Creates a sequence of folders mapping the layers in the map content, using either kml dumps * or ground overlays (the classic approach, that is) * * @author Andrea Aime - GeoSolutions */ public class PlainFolderSequenceFactory extends AbstractFolderSequenceFactory { public PlainFolderSequenceFactory(KmlEncodingContext context) { super(context); } @Override public Sequence<Feature> newSequence() { return new PlainFolderGenerator(); } public class PlainFolderGenerator extends AbstractFolderGenerator { protected void encodeFolderContents(Layer layer, Folder folder) { // now encode the contents (dynamic bit, it may use the Sequence construct) if (layer instanceof FeatureLayer) { // do we use a KML placemark dump, or a ground overlay? if (useVectorOutput(context)) { List<Feature> features = new SequenceList<Feature>( new FeatureSequenceFactory(context, (FeatureLayer) layer)); context.addFeatures(folder, features); } else { addGroundOverlay(folder, layer); // in case of ground overlays we might still want to output placemarks // for the if(context.isPlacemarkForced()) { addFeatureCentroids(layer, folder); } } } else { addGroundOverlay(folder, layer); } } /** * Adds the feature centroids to the output features, without actually adding the full * geometry (used when doing raster overlays of vector data with a desire to retain the * popups) * @param layer * @param folder */ private void addFeatureCentroids(Layer layer, Folder folder) { SimpleFeatureCollection centroids = new KMLCentroidFeatureCollection(context.getCurrentFeatureCollection()); context.setCurrentFeatureCollection(centroids); FeatureLayer centroidsLayer = new FeatureLayer(centroids, layer.getStyle(), layer.getTitle()); List<Feature> features = new SequenceList<Feature>( new FeatureSequenceFactory(context, centroidsLayer)); context.addFeatures(folder, features); } /** * Encodes the ground overlay for the specified layer * * @param folder * @param layer */ private void addGroundOverlay(Folder folder, Layer layer) { int mapLayerOrder = context.getMapContent().layers().indexOf(layer); GroundOverlay go = folder.createAndAddGroundOverlay(); go.setName(layer.getTitle()); go.setDrawOrder(mapLayerOrder); Icon icon = go.createAndSetIcon(); icon.setHref(getGroundOverlayHRef(layer)); icon.setViewRefreshMode(ViewRefreshMode.NEVER); icon.setViewBoundScale(0.75); ReferencedEnvelope box = new ReferencedEnvelope(context.getMapContent() .getRenderingArea()); boolean reprojectBBox = (box.getCoordinateReferenceSystem() != null) && !CRS.equalsIgnoreMetadata(box.getCoordinateReferenceSystem(), DefaultGeographicCRS.WGS84); if (reprojectBBox) { try { box = box.transform(DefaultGeographicCRS.WGS84, true); } catch (Exception e) { throw new ServiceException("Could not transform bbox to WGS84", e, "ReprojectionError", ""); } } LatLonBox gobox = go.createAndSetLatLonBox(); gobox.setEast(box.getMinX()); gobox.setWest(box.getMaxX()); gobox.setNorth(box.getMaxY()); gobox.setSouth(box.getMinY()); } String getGroundOverlayHRef(Layer layer) { WMSMapContent mapContent = context.getMapContent(); if (context.isKmz()) { // embed the ground overlay in the kmz archive int mapLayerOrder = mapContent.layers().indexOf(layer); String href = "images/layers_" + mapLayerOrder + ".png"; context.addKmzGroundOverlay(href, layer); return href; } else { // refer to a GetMap request return WMSRequests.getGetMapUrl(mapContent.getRequest(), layer, 0, mapContent.getRenderingArea(), new String[] { "format", "image/png", "transparent", "true" }); } } /** * Determines whether to return a vector (KML) result of the data or to return an image * instead. If the kmscore is 100, then the output should always be vector. If the kmscore * is 0, it should always be raster. In between, the number of features is weighed against * the kmscore value. kmscore determines whether to return the features as vectors, or as * one raster image. It is the point, determined by the user, where X number of features is * "too many" and the result should be returned as an image instead. * * kmscore is logarithmic. The higher the value, the more features it takes to make the * algorithm return an image. The lower the kmscore, the fewer features it takes to force an * image to be returned. (in use, the formula is exponential: as you increase the KMScore * value, the number of features required increases exponentially). * * @param kmscore the score, between 0 and 100, use to determine what output to use * @param numFeatures how many features are being rendered * @return true: use just kml vectors, false: use raster result */ boolean useVectorOutput(KmlEncodingContext context) { // are we in download mode? String mode = context.getMode(); if("refresh".equalsIgnoreCase(mode)) { // calculate kmscore to determine if we should write as vectors // or pre-render int kmscore = context.getKmScore(); if (kmscore == 100) { return true; // vector KML } if (kmscore == 0) { return false; // raster KMZ } // For numbers in between, determine exponentionally based on kmscore value: // 10^(kmscore/15) // This results in exponential growth. // The lowest bound is 1 feature and the highest bound is 3.98 million features // The most useful kmscore values are between 20 and 70 (21 and 46000 features // respectively) // A good default kmscore value is around 40 (464 features) double magic = Math.pow(10, kmscore / 15); int currentSize = context.getCurrentFeatureCollection().size(); if (currentSize > magic) { return false; // return raster } else { return true; // return vector } } else { // download or superoverlay return true; } } } }