package org.geoserver.geosearch; import static org.geoserver.ows.util.ResponseUtils.*; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.config.GeoServerInfo; import org.geoserver.ows.URLMangler.URLType; import org.geoserver.ows.util.RequestUtils; import org.geoserver.wms.MapLayerInfo; import org.geotools.data.DefaultQuery; import org.geotools.data.jdbc.JDBCUtils; import org.geotools.feature.FeatureCollection; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.geotools.util.logging.Logging; import org.jdom.Document; import org.jdom.Element; import org.jdom.Namespace; import org.opengis.feature.simple.SimpleFeature; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.restlet.data.MediaType; import org.restlet.data.Method; import org.restlet.data.Request; import org.restlet.data.Response; import org.restlet.data.Status; import org.restlet.resource.StringRepresentation; import org.springframework.jdbc.support.incrementer.H2SequenceMaxValueIncrementer; import com.vividsolutions.jts.geom.Envelope; public class LayerSiteMapRestlet extends GeoServerProxyAwareRestlet{ private static Logger LOGGER = Logging.getLogger("org.geoserver.geosearch"); private Catalog myCatalog; private String GEOSERVER_URL; private Namespace SITEMAP = Namespace.getNamespace("http://www.sitemaps.org/schemas/sitemap/0.9"); private static Namespace GEOSITEMAP = Namespace.getNamespace("geo","http://www.google.com/geo/schemas/sitemap/1.0"); static final CoordinateReferenceSystem WGS84; static final ReferencedEnvelope WORLD_BOUNDS; static final double MAX_TILE_WIDTH; static { try { // Common geographic info WGS84 = CRS.decode("EPSG:4326"); WORLD_BOUNDS = new ReferencedEnvelope( new Envelope(180.0, -180.0,90.0, -90.0), WGS84); MAX_TILE_WIDTH = WORLD_BOUNDS.getWidth() / 2.0; // Make sure that H2 is around Class.forName("org.h2.Driver"); } catch (Exception e) { throw new RuntimeException( "Could not initialize the class constants", e); } } public void setCatalog(Catalog c){ myCatalog = c; } public Catalog getCatalog(){ return myCatalog; } public LayerSiteMapRestlet() { } public void handle(Request request, Response response){ GEOSERVER_URL = getBaseURL(request); if (request.getMethod().equals(Method.GET)){ doGet(request, response); } else { response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST); } } /** * Performs basic checks, making sure that this layer * does indeed support regionating and is supposed to be searchable. * * Then kicks off the process that creates a document, filling * in the details from the H2 database. * * @param request * @param response */ public void doGet(Request request, Response response){ String layerName = (String)request.getAttributes().get("layer"); String page = request.getAttributes().containsKey("page") ? (String) request.getAttributes().get("page") : null; // Check that layer exists, and that we allow people to index it FeatureTypeInfo fti = getCatalog().getFeatureTypeByName(layerName); if(fti == null || ! (Boolean)fti.getMetadata().get("indexingEnabled")) { response.setStatus(Status.CLIENT_ERROR_FORBIDDEN); LOGGER.log(Level.FINE, "not allowed to publish layername: " + layerName); //TODO nice error message return; } // // Do we have a regionating strategy ? // if(fti.getRegionateAttribute() == null // || fti.getRegionateAttribute().length() == 0) { // response.setStatus(Status.CLIENT_ERROR_NOT_FOUND); // //TODO nice error message // return; // } if (page != null) { try { Integer i = Integer.valueOf(page); Document d = buildPagedSitemap(layerName, fti, i); response.setEntity(new JDOMRepresentation(d)); } catch (NumberFormatException e) { response.setEntity( new StringRepresentation(e.toString(), MediaType.TEXT_PLAIN ) ); response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST); } } else { // All good, we're finally here: Document d = buildSitemap(layerName, fti); response.setEntity(new JDOMRepresentation(d)); } } /** * Wrapper function that constructs document, which is then passed * on to the H2 database which goes and looks for tiles to link to. * * @param layerName * @param fti * @return */ private Document buildSitemap(String layerName, FeatureTypeInfo fti) { final Document d = new Document(); Element sitemapindex = new Element("sitemapindex", SiteMapIndexRestlet.SITEMAP); d.setRootElement(sitemapindex); try { int featurecount = fti.getFeatureSource(null, null).getFeatures().size(); int pagecount = featurecount / 50000; // 50000 features per page pagecount += featurecount % 50000 == 0 ? 0 : 1; for (int i = 1; i <= pagecount; i++) { SiteMapIndexRestlet.addSitemap( sitemapindex, buildURL(GEOSERVER_URL, "rest/layers/" + layerName + "/sitemap-" + i + ".xml", null, URLType.SERVICE) ); } } catch (IOException ioe) { //TODO log } return d; } private Document buildPagedSitemap(String layername, FeatureTypeInfo fti, int page){ final Document d = new Document(); Element urlSet = new Element("urlset", SITEMAP); urlSet.addNamespaceDeclaration(GEOSITEMAP); d.setRootElement(urlSet); try { DefaultQuery q = new DefaultQuery(); if (fti.getFeatureSource(null, null).getQueryCapabilities().isOffsetSupported()){ // surely no one would use a shapefile for more than 50000 features, right? q.setStartIndex(1 + 50000 * (page - 1)); q.setMaxFeatures(50000); } FeatureCollection col = fti.getFeatureSource(null, null).getFeatures(q); Iterator fi = col.iterator(); while (fi.hasNext()){ try { SimpleFeature f = (SimpleFeature)fi.next(); encodeFeatureLink(urlSet, layername, f); } catch (Exception e) { e.printStackTrace(); // TODO: Log } } col.close(fi); } catch (IOException ioe) { ioe.printStackTrace(); // TODO log } return d; } private void encodeFeatureLink(Element urlSet, String layername, SimpleFeature f){ Element urlElement = new Element("url", SITEMAP); Element loc = new Element("loc", SITEMAP); loc.setText(buildURL(GEOSERVER_URL, "rest/layers/" + layername + "/" + f.getID() + ".kml", null, URLType.SERVICE)); urlElement.addContent(loc); Element geo = new Element("geo", GEOSITEMAP); Element geoformat = new Element("format", GEOSITEMAP); geoformat.setText("kml"); geo.addContent(geoformat); urlElement.addContent(geo); urlSet.addContent(urlElement); } /** * Just adds one URL to the sitemap, with geo tags * * @param urlSet * @param url */ private void addTile(Element urlSet, String url) { Element urlElement = new Element("url", SITEMAP); Element loc = new Element("loc", SITEMAP); loc.setText(url); urlElement.addContent(loc); Element geo = new Element("geo",GEOSITEMAP); Element geoformat = new Element("format",GEOSITEMAP); geoformat.setText("kml"); geo.addContent(geoformat); urlElement.addContent(geo); urlSet.addContent(urlElement); } /** * This method has a bit too much content at the moment, but the trouble * is really elsewhere. We need a nice way to build the entire hierarchy. * * This code extracts all the tiles from the H2 database. * * If it finds none, it will link to the topmost tile. * * Afterwards, it links to all the tiles one zoomlevel below. The crawler * will then add them to the H2 database, thereby expanding the index. * * @param urlSet * @param fti * @throws IOException */ private void getTilesFromDatababase(Element urlSet, FeatureTypeInfo fti) throws IOException { String dataDir = getCatalog().getResourceLoader() .findOrCreateDirectory("geosearch") .getCanonicalPath(); String tableName = fti.getFeatureType().getName() + "_" + fti.getMetadata().get("kml.regionateAttribute"); Connection conn = null; Statement st = null; ResultSet rs = null; long[] maxCoords = {0,0,0,0,-1}; try { conn = DriverManager.getConnection("jdbc:h2:file:" + dataDir + "/h2cache_" + tableName, "geoserver", "geopass"); st = conn.createStatement(); rs = st.executeQuery("SELECT x,y,z FROM TILECACHE WHERE FID IS NOT NULL GROUP BY x,y,z ORDER BY z ASC"); while(rs.next()) { long[] coords = new long[3]; coords[0] = rs.getLong(1); coords[1] = rs.getLong(2); coords[2] = rs.getLong(3); updateMaxCoords(maxCoords, coords); addTile(urlSet, makeUrl(coords, fti)); } rs.close(); } catch (SQLException se) { LOGGER.severe(se.getMessage()); se.printStackTrace(); } finally { JDBCUtils.close(st); JDBCUtils.close(conn, null, null); // Check that we got something ? if(maxCoords[4] < 0) { // Nope. We start from the top. long[][] coordss = zoomedOut(fti); for(int i=0; i<coordss.length; i++) { addTile(urlSet, makeUrl(coordss[i], fti)); updateMaxCoords(maxCoords, coordss[i]); } } expandHierarchy(urlSet, fti, maxCoords); } } private long[][] zoomedOut(FeatureTypeInfo fti) { try { Envelope env = fti.getLatLonBoundingBox(); //double[] coords = {env.getMinX(), env.getMinY(), env.getMaxX(), env.getMaxY()}; // World wide case if(env.getMinX() < 0.0 && env.getMaxX() > 0.0) { long[][] ret = {{0,0,0},{1,0,0}}; return ret; } long[] nextQuad = new long[3]; if(env.getMinX() < 0.0) { nextQuad[0] = 0; nextQuad[1] = 0; nextQuad[2] = 0; } else { nextQuad[0] = 1; nextQuad[1] = 0; nextQuad[2] = 0; } long[] prevQuad = null; while(nextQuad != null) { // Try each of the quadrants long[][] quads = { { nextQuad[0] * 2, nextQuad[1] * 2, nextQuad[2] + 1}, { nextQuad[0] * 2 + 1, nextQuad[1] * 2, nextQuad[2] + 1}, { nextQuad[0] * 2, nextQuad[1] * 2 + 1 , nextQuad[2] + 1}, { nextQuad[0] * 2 + 1, nextQuad[1] * 2 + 1, nextQuad[2] + 1} }; prevQuad = nextQuad; nextQuad = null; for(int i=0; i<quads.length; i++) { ReferencedEnvelope testEnv = envelope(quads[i][0], quads[i][1], quads[i][2]); if(testEnv.contains(env)) { nextQuad = quads[i]; } } } long[][] ret = {prevQuad}; return ret; } catch (Exception e) { e.printStackTrace(); } return null; } /** * The maxCoords describe as far as the hierarchy is built. * * This expands one level further, which may lead to linking to * some 204s, but unless the data is distributed perfectly evenly * it will only be a few tiles. * * @param urlSet * @param maxCoords */ private void expandHierarchy(Element urlSet, FeatureTypeInfo fti, long[] maxCoords) { long z = maxCoords[4] + 1; for(long x=maxCoords[0]; x<=maxCoords[2]; x++) { for(long y=maxCoords[1]; y<=maxCoords[3]; y++) { long[] bl = {x * 2, y * 2, z}; long[] br = {x * 2 + 1, y * 2, z}; long[] tl = {x * 2, y * 2 + 1 , z}; long[] tr = {x * 2 + 1, y * 2 + 1, z}; addTile(urlSet, makeUrl(bl, fti)); addTile(urlSet, makeUrl(br, fti)); addTile(urlSet, makeUrl(tl, fti)); addTile(urlSet, makeUrl(tr, fti)); } } } /** * Converts x,y,z into an envelope, to be used in the WMS URL * * @param x * @param y * @param z * @return */ private ReferencedEnvelope envelope(long x, long y, long z) { double tileSize = MAX_TILE_WIDTH / Math.pow(2, z); double xMin = x * tileSize + WORLD_BOUNDS.getMinX(); double yMin = y * tileSize + WORLD_BOUNDS.getMinY(); return new ReferencedEnvelope(xMin, xMin + tileSize, yMin, yMin + tileSize, WGS84); } /** * This keeps track of the bounds and resets every time * the zoomlevel changes * * @param maxCoords * @param coords */ private void updateMaxCoords(long[] maxCoords, long[] coords) { if(coords[2] > maxCoords[4]) { maxCoords[0] = Long.MAX_VALUE; maxCoords[1] = Long.MAX_VALUE; maxCoords[2] = Long.MIN_VALUE; maxCoords[3] = Long.MIN_VALUE; maxCoords[4] = coords[2]; } if(maxCoords[0] > coords[0]) { maxCoords[0] = coords[0]; } if(maxCoords[1] > coords[1]) { maxCoords[1] = coords[1]; } if(maxCoords[2] < coords[0]) { maxCoords[2] = coords[0]; } if(maxCoords[3] < coords[1]) { maxCoords[3] = coords[1]; } } /** * Constructs a WMS URL for the given coordinates * * @param coords * @param fti * @return */ private String makeUrl(long[] coords, FeatureTypeInfo fti) { // Ok we have the coordinates, now we turn that into a bbox for a WMS query ReferencedEnvelope env = envelope(coords[0],coords[1],coords[2]); Map<String, String> params = params("service", "wms", "version", "1.1.0", "request", "GetMap", "format", "application/vnd.google-earth.kml+xml", "exceptions", "application/vnd.ogc.se_inimage", "bbox", env.getMinX() + "," + env.getMinY() + "," + env.getMaxX() + "," + env.getMaxY(), "srs", "EPSG:4326", "layers", fti.getName(), "width", "256", "height", "256"); return buildURL(GEOSERVER_URL, "wms", params, URLType.SERVICE); } }