/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved. * This code is licensed under the GPL 2.0 license, availible at the root * application directory. */ package org.vfny.geoserver.wms.responses.map.htmlimagemap; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import org.geotools.data.DataSourceException; import org.geotools.feature.Feature; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.feature.FeatureType; import org.geotools.filter.Filter; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.operation.DefaultMathTransformFactory; import org.geotools.referencing.operation.matrix.GeneralMatrix; import org.geotools.referencing.operation.transform.LinearTransform1D; import org.geotools.renderer.lite.RendererUtilities; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.LineSymbolizer; import org.geotools.styling.Mark; import org.geotools.styling.PointSymbolizer; import org.geotools.styling.Rule; import org.geotools.styling.SLD; import org.geotools.styling.Style; import org.geotools.styling.Symbolizer; import org.geotools.styling.TextSymbolizer; import org.opengis.filter.expression.Expression; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.TransformException; import org.vfny.geoserver.wms.WMSMapContext; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.logging.Logger; /** * Encodes a layer in HTMLImageMap format. * * @author Mauro Bartolomeoli */ public class HTMLImageMapWriter extends OutputStreamWriter { private static final Logger LOGGER = Logger.getLogger(HTMLImageMapWriter.class.getPackage().getName()); /** map of geometry class to writer */ private HashMap writers; WMSMapContext mapContext=null; /** rect representing screen coordinates space **/ Rectangle mapArea=null; /** * Transformation from layer (world) coordinates to "screen" coordinates. */ private AffineTransform worldToScreen=null; /** * Creates a new HTMLImageMapWriter object. * * @param out stream to encode the layer to * @param config current wms context */ public HTMLImageMapWriter(OutputStream out, WMSMapContext mapContext) { super(out); this.mapContext=mapContext; ReferencedEnvelope space = mapContext.getAreaOfInterest(); mapArea=new Rectangle(mapContext.getMapWidth(),mapContext.getMapHeight()); worldToScreen=RendererUtilities.worldToScreenTransform(space, mapArea); initWriters(); } /** * Initializes every type of writer (one for every kind of geometry). * */ private void initWriters() { writers = new HashMap(); writers.put(Point.class, new PointWriter()); writers.put(LineString.class, new LineStringWriter()); writers.put(LinearRing.class, new LineStringWriter()); writers.put(Polygon.class, new PolygonWriter()); writers.put(MultiPoint.class, new MultiPointWriter()); writers.put(MultiLineString.class, new MultiLineStringWriter()); writers.put(MultiPolygon.class, new MultiPolygonWriter()); writers.put(GeometryCollection.class, new GeometryCollectionWriter()); } /** * Encodes a newline * * @throws IOException if an error occurs during encoding */ public void newline() throws IOException { super.write('\n'); } /** * Encodes a single layer (FeatureCollection) using the supplied style. * * @param fColl layer to encode * @param style style to use for encoding * @throws IOException if an error occurs during encoding * @throws AbortedException if the operation is aborted */ public void writeFeatures(FeatureCollection fColl, Style style,FeatureTypeStyle[] ftsList) throws IOException, AbortedException { Feature ft; FeatureIterator iter=null; try { FeatureType featureType = fColl.getSchema(); Class gtype = featureType.getDefaultGeometry().getType(); // retrieves the right feature writer (based on the geometry type of the feature) HTMLImageMapFeatureWriter featureWriter = (HTMLImageMapFeatureWriter) writers.get(gtype); // iterates through the single features iter=fColl.features(); while (iter.hasNext()) { ft = iter.next(); // encodes a single feature, using the supplied style and the current featureWriter featureWriter.writeFeature(ft,style,ftsList); ft = null; } LOGGER.fine("encoded " + featureType.getTypeName()); } catch (NoSuchElementException ex) { throw new DataSourceException(ex.getMessage(), ex); } finally { //make sure we always close fColl.close(iter); } } /** * Evaluates if the supplied scaleDenominator is congruent with a rule defined scale range. * @param r current rule * @param scaleDenominator current value to verify * @return true if scaleDenominator is in the rule defined range */ boolean isWithInScale(Rule r,double scaleDenominator) { return ((r.getMinScaleDenominator() ) <= scaleDenominator) && ((r.getMaxScaleDenominator()) > scaleDenominator); } /** * Filters the rules of featureTypeStyle returnting only * those that apply to feature. *

* This method returns rules for which: *

    *
  1. rule.getFilter() matches feature, or: *
  2. the rule defines an "ElseFilter", and the feature matches no * other rules. *
* This method returns an empty array in the case of which no rules * match. *

* @param featureTypeStyle The feature type style containing the rules. * @param feature The feature being filtered against. * */ Rule[] filterRules(FeatureTypeStyle featureTypeStyle, Feature feature) { Rule[] rules = featureTypeStyle.getRules(); if ((rules == null) || (rules.length == 0)) { return new Rule[0]; } ArrayList filtered = new ArrayList(rules.length); //process the rules, keep track of the need to apply an else filters boolean match = false; boolean hasElseFilter = false; for (int i = 0; i < rules.length; i++) { Rule rule = rules[i]; LOGGER.finer(new StringBuffer("Applying rule: ").append(rule.toString()).toString()); //does this rule have an else filter if (rule.hasElseFilter()) { hasElseFilter = true; continue; } double scaleDenominator; try { scaleDenominator = RendererUtilities.calculateScale(mapContext.getAreaOfInterest(), mapContext.getMapWidth(), mapContext.getMapHeight(),100); //is this rule within scale? if ( !isWithInScale(rule,scaleDenominator)) { continue; } } catch (TransformException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (FactoryException e) { // TODO Auto-generated catch block e.printStackTrace(); } //does this rule have a filter which applies to the feature Filter filter = rule.getFilter(); if ((filter == null) || filter.evaluate(feature)) { match = true; filtered.add(rule); } } //if no rules mached the feautre, re-run through the rules applying // any else filters if (!match && hasElseFilter) { //loop through again and apply all the else rules for (int i = 0; i < rules.length; i++) { Rule rule = rules[i]; if (rule.hasElseFilter()) { filtered.add(rule); } } } return (Rule[]) filtered.toArray(new Rule[filtered.size()]); } /** * Base Class for all the feature writers. * An implementation is defined for every geometry type. * * @author Mauro Bartolomeoli */ private abstract class HTMLImageMapFeatureWriter { // stores a series of attributes to append to the feature tag definition Map extraAttributes=new HashMap(); /** * Encodes a single feature. * Default implementation. * The encoding is accomplished through many phases: * 1) reset writer state * 2) process supplied style and apply filters to decide if the feature has to be included * in output. If the feature has to be included, proceed with the following phases, else go to the * next feature. * 3) start feature encoding * 4) pre geometry encoding * 5) actual geometry encoding * 6) post geometry encoding * 7) end feature encoding * @param ft feature to encode * @param style style to use for the encoding * @param fts "cached" ftss matching the FeatureType of the feature * @throws IOException if an error occurs during encoding */ protected void writeFeature(Feature ft,Style style,FeatureTypeStyle[] fts) throws IOException { // a new feature begins, reset accumulated info, such as extraAttributes reset(ft); // process the supplied style and store rendering info for the following phases // the style processing applies filters to the feature to decide if it has to be included // in output if(processStyle(ft,style,fts)) { // encodes starting element startElement(ft,""); // pre geometry encoding phase startGeometry(ft.getDefaultGeometry()); // actual geometry encoding phase writeGeometry(ft.getDefaultGeometry()); // post geometry encoding phase endGeometry(ft.getDefaultGeometry()); // encodes ending element endElement(ft); } } /** * Encodes a "MultiFeature", a feature with multiple geometries. * Default implementation. * The encoding is accomplished through many phases: * 1) reset writer state * 2) process supplied style and apply filters to decide if the feature has to be included * in output. If the feature has to be included, proceed with the following phases, else go to the * next feature. * 3) loops for all the geometries, with the following phases for every single geometry. * a) start feature encoding * b) pre geometry encoding * c) actual geometry encoding * d) post geometry encoding * e) end feature encoding * @param ft feature to encode * @param style style to use for the encoding * @param fts "cached" ftss matching the FeatureType of the feature * @throws IOException if an error occurs during encoding */ protected void writeMultiFeature(Feature ft,Style style,FeatureTypeStyle[] fts) throws IOException { reset(ft); if(processStyle(ft,style,fts)) { GeometryCollection geomCollection = (GeometryCollection) ft.getDefaultGeometry(); for (int i = 0; i < geomCollection.getNumGeometries(); i++) { startElement(ft,"."+i); startGeometry(geomCollection.getGeometryN(i)); writeGeometry(geomCollection.getGeometryN(i)); endGeometry(geomCollection.getGeometryN(i)); endElement(ft); } } } /** * Encodes the feature starting tag (area). * * @param feature feature to encode * @param suffix (optional) suffix to append to the tag id (useful to have different ids in * the multigeometry scenario. * * @throws IOException if an error occures during encoding */ protected void startElement(Feature feature,String suffix) throws IOException { // each feature (multi geometry ones are an exception) is represented by an tag // each area tag has an id, equal to the feature id, and a shape (rect, poly or circle) write("\n"); } /** * Resets writer status. * extraAttributes is emptied * @param ft current feature to encode */ protected void reset(Feature ft) { extraAttributes=new HashMap(); } /** * Analyze the supplied style and process any matching rule. * * @param ft feature to which the style is going to be applied * @param style style to process * @param ftsList cached fts matching the feature * @return true if the supplied feature has to be included in the output according to * style filters. * @throws IOException if an error occurs during the process */ protected boolean processStyle(Feature ft,Style style,FeatureTypeStyle[] ftsList) throws IOException { int total=0; for(int i=0;icoords Area attribute

* * @param coords coordinates to encode * * @throws IOException if an error occurs during encoding */ protected void writePathContent(Coordinate[] coords) throws IOException { StringBuffer buf=new StringBuffer(); int nCoords = coords.length; for(int i=0;i0) write(buf.substring(1)); } /** * Simplifies a geometry to exclude duplicated points. When translating from world * to screen coordinates it's possible that many world points collapse to a single screen point. * Those colliding points are simplified to a single point. * @param geom * @return */ Geometry decimate(Geometry geom) { DefaultMathTransformFactory f= new DefaultMathTransformFactory(); MathTransform xform=null; try { xform = f.createAffineTransform(new GeneralMatrix(worldToScreen.createInverse())); Decimator decimator=new Decimator(xform,mapArea); geom=decimator.decimate(geom); } catch (FactoryException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } catch (NoninvertibleTransformException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } catch (Exception e1) { // TODO Auto-generated catch block e1.printStackTrace(); } return geom; } } /** * FeatureWriter for point geometry features. * Currently supports circle WellKnownName Marks. */ private class PointWriter extends HTMLImageMapFeatureWriter { // encodes as a circle shape? boolean asCircle=true; // encodes as a different shape? (currently not supported--> empty rendering) String symbol=null; // radius of the circle double size=2; /** * Creates a new PointWriter object. */ public PointWriter() { } /** * The shape for points is a circle. */ protected String getShape() throws IOException { return "circle"; } /** * Uses the supplied style to define point rendering. * Currently it gets WellKnownName from a Mark (circle is the only value correctly rendered by now). * It also uses the Size parameter to define circle radius. */ protected void processSymbolizer(Feature ft, Rule rule,Symbolizer symbolizer) throws IOException{ super.processSymbolizer(ft, rule,symbolizer); if(symbolizer instanceof PointSymbolizer) { Mark mark=SLD.mark((PointSymbolizer)symbolizer); if(mark!=null) { size=SLD.size(mark); asCircle=SLD.wellKnownName(mark).toLowerCase().equals("circle"); if(!asCircle) symbol=SLD.wellKnownName(mark).toLowerCase(); } } } /** * Actually encodes the point. * * @param geom point to encode * * @throws IOException if an error occures during encoding */ protected void writeGeometry(Geometry geom) throws IOException { Point p = (Point) geom; if(asCircle) { write(getPoint(p.getCoordinate())+","+(int)Math.round(size)); } else{ //TODO: manage different shapes } } } /** * FeatureWriter for multipoint geometry features. */ private class MultiPointWriter extends PointWriter { /** * Creates a new MultiPointWriter object. */ public MultiPointWriter() { } /** * Uses writeMultiFeature. */ protected void writeFeature(Feature ft,Style style,FeatureTypeStyle[] fts) throws IOException { writeMultiFeature(ft, style, fts); } } /** * FeatureWriter for LineString geometry features. * A buffer is applied to the linear geometry to transform it to a Polygon. * The result polygon is then encoded. * @author Mauro Bartolomeoli * */ private class LineStringWriter extends HTMLImageMapFeatureWriter { // default buffer size (in screen coordinates) int buffer=2; /** * Creates a new LineStringWriter object. */ public LineStringWriter() { } /** * The shape for lines is a poly. */ protected String getShape() throws IOException { return "poly"; } /** * Uses the supplied style to define line rendering. * Currently it gets stroke-width to define the buffer around the linestring. */ protected void processSymbolizer(Feature ft, Rule rule,Symbolizer symbolizer) throws IOException{ super.processSymbolizer(ft, rule,symbolizer); if(symbolizer instanceof LineSymbolizer) { buffer=SLD.width((LineSymbolizer)symbolizer); } } /** * Actually encodes the linestring. * * @param geom line to encode * * @throws IOException if an error occures during encoding */ protected void writeGeometry(Geometry geom) throws IOException { LineString l = (LineString) geom; try { // transform buffer dimension to world coordinates double bufferMultiplier=worldToScreen.createInverse().getScaleX(); // gets buffered linestring Geometry buffered=l.buffer(buffer*bufferMultiplier); if(buffered instanceof Polygon) { Polygon poly=(Polygon)decimate(buffered); LineString shell = poly.getExteriorRing(); writePathContent(shell.getCoordinates()); } else { //TODO: what kind of geometry can a buffer operation // return? } } catch (NoninvertibleTransformException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * FeatureWriter for multiline geometry features. */ private class MultiLineStringWriter extends LineStringWriter { /** * Creates a new MultiLineStringWriter object. */ public MultiLineStringWriter() { } /** * Uses writeMultiFeature. */ protected void writeFeature(Feature ft,Style style,FeatureTypeStyle[] fts) throws IOException { writeMultiFeature(ft, style, fts); } } /** * FeatureWriter for Polygon geometry features. */ private class PolygonWriter extends HTMLImageMapFeatureWriter { /** * Creates a new PolygonWriter object. */ public PolygonWriter() { } /** * The shape for polygons is a poly. */ protected String getShape() throws IOException { return "poly"; } /** * Actually encodes the polygon. * * @param geom the polygon to encode * * @throws IOException if an error occures during encoding */ protected void writeGeometry(Geometry geom) throws IOException { Polygon poly = (Polygon) decimate(geom); LineString shell = poly.getExteriorRing(); writePathContent(shell.getCoordinates()); //TODO: polygons with holes? } } /** * FeatureWriter for multipolygon geometry features. */ private class MultiPolygonWriter extends PolygonWriter { /** * Creates a new MultiPolygonWriter object. */ public MultiPolygonWriter() { } /** * Uses writeMultiFeature. */ protected void writeFeature(Feature ft,Style style,FeatureTypeStyle[] fts) throws IOException { writeMultiFeature(ft, style, fts); } } /** * FeatureWriter for multipolygon geometry features. */ private class GeometryCollectionWriter extends HTMLImageMapFeatureWriter { HTMLImageMapFeatureWriter delegateWriter=null; /** * Creates a new MultiPolygonWriter object. */ public GeometryCollectionWriter() { } /** * Encodes the GeometryCollection. * * The encoding is accomplished through many phases: * 1) reset writer state * 2) loops for all the geometries, with the following phases for every single geometry. * a) process supplied style * b) start feature encoding * c) pre geometry encoding * d) actual geometry encoding * e) post geometry encoding * f) end feature encoding * A delegate is used for many of these phases. The delegate is a specific FeatureWriter for the * single geometry, during the loop. * @param ft feature to encode * @param style style to use for the encoding * @param fts "cached" ftss matching the FeatureType of the feature * @throws IOException if an error occurs during encoding */ protected void writeFeature(Feature ft,Style style,FeatureTypeStyle[] fts) throws IOException { reset(ft); GeometryCollection geomCollection = (GeometryCollection) ft.getDefaultGeometry(); for (int i = 0; i < geomCollection.getNumGeometries(); i++) { Geometry geom=geomCollection.getGeometryN(i); Class gtype = geom.getClass(); // retrieves the right feature writer (based on the current geometry type) delegateWriter = (HTMLImageMapFeatureWriter) writers.get(gtype); if(processStyle(ft,style,fts)) { startElement(ft,"."+i); startGeometry(geom); writeGeometry(geom); endGeometry(geom); endElement(ft); } } } protected String getShape() throws IOException { return delegateWriter.getShape(); } /** * Analyze the supplied style and process any matching rule. * * @param ft feature to which the style is going to be applied * @param style style to process * @param ftsList cached fts matching the feature * @return true if the style filters "accept" the feature * @throws IOException if an error occurs during the process */ protected boolean processStyle(Feature ft,Style style,FeatureTypeStyle[] ftsList) throws IOException { if(delegateWriter.processStyle(ft, style, ftsList)) { Iterator iter=delegateWriter.extraAttributes.keySet().iterator(); while(iter.hasNext()) { String attrName=(String)iter.next(); extraAttributes.put(attrName,delegateWriter.extraAttributes.get(attrName)); } return true; } else return false; } /** * Actually write the geometry (through the delegate). */ protected void writeGeometry(Geometry geom) throws IOException { delegateWriter.writeGeometry(geom); } } }