/* (c) 2016 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.geogig.geoserver.gwc; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.ResourceInfo; import org.geoserver.gwc.layer.GeoServerTileLayer; import org.geotools.geometry.jts.JTS; import org.geotools.referencing.CRS; import org.geowebcache.GeoWebCacheException; import org.geowebcache.filter.parameters.ParameterFilter; import org.geowebcache.grid.Grid; import org.geowebcache.grid.GridSubset; import org.geowebcache.grid.SRS; import org.geowebcache.layer.TileLayer; import org.geowebcache.mime.MimeType; import org.geowebcache.seed.GWCTask; import org.geowebcache.seed.GWCTask.TYPE; import org.geowebcache.seed.TileBreeder; import org.geowebcache.storage.DiscontinuousTileRange; import org.geowebcache.storage.TileRange; import org.geowebcache.storage.TileRangeMask; import org.locationtech.geogig.model.ObjectId; import org.locationtech.geogig.model.Ref; import org.locationtech.geogig.repository.Context; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.google.common.base.Stopwatch; import com.google.common.base.Throwables; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.io.WKTWriter; import com.vividsolutions.jts.operation.buffer.BufferOp; import com.vividsolutions.jts.operation.buffer.BufferParameters; import com.vividsolutions.jts.simplify.TopologyPreservingSimplifier; class TruncateHelper { private static final Logger LOGGER = LoggerFactory.getLogger(TruncateHelper.class); public static void issueTruncateTasks(Context context, Optional<Ref> oldRef, Optional<Ref> newRef, GeoServerTileLayer tileLayer, TileBreeder breeder) { final ObjectId oldCommit = oldRef.isPresent() ? oldRef.get().getObjectId() : ObjectId.NULL; final ObjectId newCommit = newRef.isPresent() ? newRef.get().getObjectId() : ObjectId.NULL; final String tileLayerName = tileLayer.getName(); final String layerTreeName = tileLayer.getLayerInfo().getResource().getNativeName(); LOGGER.debug(String.format( "Computing minimal bounds geometry on layer '%s' (tree '%s') for change %s...%s ", tileLayerName, layerTreeName, oldCommit, newCommit)); final Geometry minimalBounds; Stopwatch sw = Stopwatch.createStarted(); try { MinimalDiffBounds geomBuildCommand = context.command(MinimalDiffBounds.class) .setOldVersion(oldCommit.toString()).setNewVersion(newCommit.toString()); geomBuildCommand.setTreeNameFilter(layerTreeName); minimalBounds = geomBuildCommand.call(); sw.stop(); if (minimalBounds.isEmpty()) { LOGGER.debug( String.format("Feature tree '%s' not affected by change %s...%s (took %s)", layerTreeName, oldCommit, newCommit, sw)); return; } if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Minimal bounds on layer '%s' computed in %s: %s", tileLayerName, sw, formattedWKT(minimalBounds))); } } catch (Exception e) { sw.stop(); LOGGER.error(String.format( "Error computing minimal bounds for %s...%s on layer '%s' after %s", oldCommit, newCommit, tileLayerName, sw)); throw Throwables.propagate(e); } final Set<String> gridSubsets = tileLayer.getGridSubsets(); LayerInfo layerInfo = tileLayer.getLayerInfo(); ResourceInfo resource = layerInfo.getResource(); final CoordinateReferenceSystem sourceCrs; { CoordinateReferenceSystem nativeCrs = resource.getNativeCRS(); if (nativeCrs == null) { // no native CRS specified, layer must have been configured with an overriding one sourceCrs = resource.getCRS(); } else { sourceCrs = nativeCrs; } } for (String gridsetId : gridSubsets) { GridSubset gridSubset = tileLayer.getGridSubset(gridsetId); final CoordinateReferenceSystem gridSetCrs = getGridsetCrs(gridSubset); LOGGER.debug("Reprojecting geometry mask to gridset {}", gridsetId); Geometry geomInGridsetCrs = transformToGridsetCrs(minimalBounds, sourceCrs, gridSetCrs); if (LOGGER.isDebugEnabled()) { LOGGER.debug("geometry mask reprojected to gridset {}: {}", gridsetId, formattedWKT(geomInGridsetCrs)); } geomInGridsetCrs = bufferAndSimplifyBySizeOfSmallerTile(geomInGridsetCrs, gridSetCrs, gridSubset); try { truncate(tileLayer, gridsetId, geomInGridsetCrs, breeder); } catch (Exception e) { e.printStackTrace(); } } } private static Geometry bufferAndSimplifyBySizeOfSmallerTile(Geometry geomInGridsetCrs, CoordinateReferenceSystem gridSetCrs, GridSubset gridSubset) { double bufferRatio; // try { // Unit<?> axisUnit = gridSetCrs.getCoordinateSystem().getAxis(0).getUnit(); // bufferRatio = toMeters(axisUnit, 1); // } catch (RuntimeException e) { // } // Integer zoomStop = gridSubset.getMaxCachedZoom(); if (zoomStop == null) { zoomStop = gridSubset.getGridSet().getNumLevels() - 1; } // we know some degenerate gridsets have like 30+ zoom levels and nobody could really seed // them to that level where the resolution is sub-millimetric. 18 is usually a safe option zoomStop = Math.min(18, zoomStop); Grid grid = gridSubset.getGridSet().getGrid(zoomStop); double width = grid.getResolution() * gridSubset.getTileWidth(); double height = grid.getResolution() * gridSubset.getTileHeight(); // buffer by the length of two tiles at the finest zoom level bufferRatio = 2 * Math.max(width, height); // create a buffer with no rounded joins BufferParameters bp = new BufferParameters(); bp.setEndCapStyle(BufferParameters.CAP_SQUARE); bp.setJoinStyle(BufferParameters.JOIN_MITRE); BufferOp bufferOp = new BufferOp(geomInGridsetCrs, bp); Geometry geometry = bufferOp.getResultGeometry(bufferRatio); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format( "Geometry buffered by the size of a tile at zoom level %s (%s units): %s", zoomStop, bufferRatio, formattedWKT(geometry))); } TopologyPreservingSimplifier simplifier = new TopologyPreservingSimplifier(geometry); simplifier.setDistanceTolerance(bufferRatio / 2); geometry = simplifier.getResultGeometry(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Simplified geometry: {}", formattedWKT(geometry)); } return geometry; } /** * A one-line WKT may be too large, this function returns a multiline formatted one */ private static Object formattedWKT(Geometry geometry) { WKTWriter w = new WKTWriter(); w.setFormatted(true); w.setMaxCoordinatesPerLine(100); return w.write(geometry); } private static void truncate(final GeoServerTileLayer tileLayer, final String gridsetId, final Geometry geomInGridsetCrs, final TileBreeder breeder) { final List<MimeType> mimeTypes = tileLayer.getMimeTypes(); final Set<String> cachedStyles = getCachedStyles(tileLayer); final String defaultStyle = tileLayer.getStyles(); GridSubset gridSubset = tileLayer.getGridSubset(gridsetId); for (String style : cachedStyles) { Map<String, String> parameters; if (style.isEmpty() || style.equals(defaultStyle)) { parameters = null; } else { parameters = Collections.singletonMap("STYLES", style); } for (MimeType mime : mimeTypes) { truncate(breeder, tileLayer, gridSubset, mime, parameters, geomInGridsetCrs); } } } private static void truncate(TileBreeder breeder, GeoServerTileLayer tileLayer, GridSubset gridSubset, MimeType mimeType, Map<String, String> parameters, Geometry geomInGridsetCrs) { Integer zoomStart = gridSubset.getMinCachedZoom(); Integer zoomStop = gridSubset.getMaxCachedZoom(); if (zoomStart == null) { zoomStart = 0; } if (zoomStop == null) { zoomStop = gridSubset.getGridSet().getNumLevels() - 1; } TileRangeMask rasterMask = GeometryTileRangeMask.build(tileLayer, gridSubset, geomInGridsetCrs); String layerName = tileLayer.getName(); String gridSetId = gridSubset.getName(); TileRange tileRange = new DiscontinuousTileRange(layerName, gridSetId, zoomStart, zoomStop, rasterMask, mimeType, parameters); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Truncating layer %s#%s#%s with geom mask %s", layerName, gridSetId, mimeType.getFormat(), formattedWKT(geomInGridsetCrs))); } try { GWCTask[] tasks = breeder.createTasks(tileRange, TYPE.TRUNCATE, 1, false); breeder.dispatchTasks(tasks); } catch (GeoWebCacheException e) { throw Throwables.propagate(e); } } private static Geometry transformToGridsetCrs(Geometry minimalBounds, CoordinateReferenceSystem defaultCrs, CoordinateReferenceSystem gridSetCrs) { Geometry geomInGridsetCrs; try { MathTransform transform = CRS.findMathTransform(defaultCrs, gridSetCrs); geomInGridsetCrs = JTS.transform(minimalBounds, transform); } catch (Exception e) { throw Throwables.propagate(e); } return geomInGridsetCrs; } private static CoordinateReferenceSystem getGridsetCrs(GridSubset gridSubset) { final CoordinateReferenceSystem gridSetCrs; SRS srs = gridSubset.getGridSet().getSrs(); try { int epsgCode = srs.getNumber(); String epsgId = "EPSG:" + epsgCode; boolean longitudeFirst = true;// as used by geoserver gridSetCrs = CRS.decode(epsgId, longitudeFirst); } catch (Exception e) { throw new RuntimeException("Can't decode SRS ESPG:" + srs.getNumber()); } return gridSetCrs; } private static Set<String> getCachedStyles(final TileLayer l) { Set<String> cachedStyles = new HashSet<String>(); String defaultStyle = l.getStyles(); if (defaultStyle != null) { cachedStyles.add(defaultStyle); } List<ParameterFilter> parameterFilters = l.getParameterFilters(); if (parameterFilters != null) { for (ParameterFilter pf : parameterFilters) { if (!"STYLES".equalsIgnoreCase(pf.getKey())) { continue; } cachedStyles.add(pf.getDefaultValue()); cachedStyles.addAll(pf.getLegalValues()); break; } } if (cachedStyles.isEmpty()) { cachedStyles.add(""); } return cachedStyles; } }