/* 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:
*
* rule.getFilter() matches feature, or:
* - 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);
}
}
}