/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2011, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.renderer.markwkt;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.renderer.style.MarkFactory;
import org.geotools.renderer.style.shape.ExplicitBoundsShape;
import org.opengis.feature.Feature;
import org.opengis.filter.expression.Expression;
/**
* Adds to the well-known shapes some symbols the weathermen may find useful.
*
* @author Luca Morandini lmorandini@ieee.org
* @version $Id$
*/
public class MeteoMarkFactory implements MarkFactory {
public static final String SHAPE_PREFIX = "extshape://";
/** The logger for the rendering module. */
private static final Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(MeteoMarkFactory.class);
/**
* Key for the extshape://arrow height ratio (between 0 and 1000). Default value is 2, twice as
* high as large
*/
public static final String ARROW_HEIGHT_RATIO_KEY = "hr";
/**
* Key for the extshape://arrow base line thickness (must be between 0, just border, and 1,
* which turns the arrow into a irregular pentagon, "little house" like). Default value is 0.2
*/
public static final String ARROW_THICKNESS_KEY = "t";
/**
* Key for the extshape://arrow location of the arrowhead base, a value of 0 turns the shape
* into a triangle, a value of 1 into a rectangle. Default value is 0.5
*/
public static final String ARROWHEAD_BASE_KEY = "ab";
protected final static Map<String, Shape> WELLKNOWN_SHAPES = new HashMap<String, Shape>();
static {
GeneralPath gp = new GeneralPath();
gp = new GeneralPath();
ExplicitBoundsShape bnd = null;
gp.moveTo(-0.145f, 0.000f);
gp.lineTo(0.000f, 0.175f);
gp.lineTo(0.105f, 0.000f);
gp.closePath();
bnd = new ExplicitBoundsShape(gp);
bnd.setBounds(new Rectangle2D.Double(-0.5, -0.5, 0.5, 0.5));
WELLKNOWN_SHAPES.put("triangle", bnd);
gp = new GeneralPath();
gp.moveTo(-0.125f, 0.000f);
gp.curveTo(-0.125f, 0.000f, 0.000f, 0.250f, 0.125f, 0.000f);
gp.closePath();
bnd = new ExplicitBoundsShape(gp);
bnd.setBounds(new Rectangle2D.Double(-0.5, -0.5, 0.5, 0.5));
WELLKNOWN_SHAPES.put("emicircle", bnd);
gp = new GeneralPath();
gp.moveTo(-0.395f, 0.000f);
gp.lineTo(-0.250f, -0.175f);
gp.lineTo(-0.145f, 0.000f);
gp.moveTo(0.125f, 0.000f);
gp.curveTo(0.125f, 0.000f, 0.250f, 0.250f, 0.375f, 0.000f);
gp.closePath();
bnd = new ExplicitBoundsShape(gp);
bnd.setBounds(new Rectangle2D.Double(-0.5, -0.5, 1.0, 1.0));
WELLKNOWN_SHAPES.put("triangleemicircle", bnd);
gp = new GeneralPath();
gp.moveTo(0f, 1f);
gp.lineTo(0.5, 0f);
gp.lineTo(0.1f, 0f);
gp.lineTo(0.1f, -1f);
gp.lineTo(-0.1f, -1f);
gp.lineTo(-0.1f, 0f);
gp.lineTo(-0.5f, 0f);
gp.closePath();
WELLKNOWN_SHAPES.put("narrow", gp);
// South Arrow
AffineTransform at = AffineTransform.getQuadrantRotateInstance(2);
gp = new GeneralPath();
gp.moveTo(0f, 1f);
gp.lineTo(0.5, 0f);
gp.lineTo(0.1f, 0f);
gp.lineTo(0.1f, -1.0f);
gp.lineTo(-0.1f, -1.0f);
gp.lineTo(-0.1f, 0f);
gp.lineTo(-0.5f, 0f);
gp.closePath();
gp.transform(at);
WELLKNOWN_SHAPES.put("sarrow", gp);
// a sane arrow, centered in its middle, with its actual size
WELLKNOWN_SHAPES.put("arrow", buildDynamicArrow(2, 0.2f, 0.5f));
}
/*
* Return a shape with the given url.
*
* @see org.geotools.renderer.style.MarkFactory#getShape(java.awt.Graphics2D,
* org.opengis.filter.expression.Expression, org.opengis.feature.Feature)
*/
public Shape getShape(Graphics2D graphics, Expression symbolUrl, Feature feature)
throws Exception {
// cannot handle a null url
if (symbolUrl == null)
return null;
// see if it's a shape
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Trying to resolve symbol:" + symbolUrl.toString());
}
String wellKnownName = symbolUrl.evaluate(feature, String.class);
if (wellKnownName == null || !wellKnownName.startsWith(SHAPE_PREFIX)) {
// see if it's a shape
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Unable to resolve symbol");
}
return null;
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Resolved symbol");
}
String localName = wellKnownName.substring(SHAPE_PREFIX.length());
if (localName.startsWith("arrow?")) {
return buildDynamicArrow(localName);
} else {
return WELLKNOWN_SHAPES.get(localName);
}
}
private Shape buildDynamicArrow(String name) {
Map<String, String> params = getParams(name);
float height = 1;
float thickness = 0.2f;
float arrowBase = 0.5f;
for (Map.Entry<String, String> entry : params.entrySet()) {
String key = entry.getKey();
String svalue = entry.getValue();
float value = 0;
try {
value = Float.valueOf(svalue);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid numerical value " + svalue);
}
switch (key) {
case ARROW_HEIGHT_RATIO_KEY:
validateRange(key, value, 0, 1000);
height = value;
break;
case ARROW_THICKNESS_KEY:
validateRange(key, value, 0, 1);
thickness = value;
break;
case ARROWHEAD_BASE_KEY:
validateRange(key, value, 0, 1);
arrowBase = value;
break;
default:
LOGGER.warning("Unexpected key value pair " + key + "=" + svalue
+ " for extshape://arrow");
}
}
return buildDynamicArrow(height, thickness, arrowBase);
}
private static Shape buildDynamicArrow(float height, float thickness, float arrowBase) {
GeneralPath gp = new GeneralPath();
// start from the point of the arrow
gp.moveTo(0f, height / 2);
// the right base of the arrow
float arrowBaseHeight = height / 2 - height * (1 - arrowBase);
gp.lineTo(0.5, arrowBaseHeight);
// back to the center
float t2 = thickness / 2;
if (t2 < 0.5) {
gp.lineTo(t2, arrowBaseHeight);
}
// down to the base
gp.lineTo(t2, -height / 2);
if (t2 > 0) {
// go the the other side of the base
gp.lineTo(-t2, -height / 2);
}
// back up to the arrow base
if (t2 < 0.5) {
gp.lineTo(-t2, arrowBaseHeight);
}
gp.lineTo(-0.5f, arrowBaseHeight);
// and finish
gp.closePath();
return gp;
}
private void validateRange(String key, double value, double min, double max) {
if (value < min || value > max) {
throw new IllegalArgumentException("Invalid value " + value + " for key " + key
+ ", should have been between " + min + " and " + max + " (extreme excluded)");
}
}
private Map<String, String> getParams(String name) {
Map<String, String> params = new HashMap<>();
String kvpPart = name.substring(name.indexOf('?') + 1);
String[] keyValues = kvpPart.split("&");
for (String keyValue : keyValues) {
String[] kv = keyValue.split("=");
if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()) {
LOGGER.fine("Skipping invalid kvp pair " + keyValue);
}
params.put(kv[0], kv[1]);
}
return params;
}
}