/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2014-2016, 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.windbarbs;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.renderer.style.MarkFactory;
import org.geotools.renderer.windbarbs.WindBarb.WindBarbDefinition;
import org.geotools.util.SoftValueHashMap;
import org.opengis.feature.Feature;
import org.opengis.filter.expression.Expression;
/**
* Factory to produce WindBarbs. Urls for wind barbs are in the form: windbarbs://default(speed_value)[units_of_measure]
*
* TODO: We may consider adding a FLAG to say whether the arrows are toward wind (meteo convention) or against wind (ocean convention)
*
* @author Daniele Romagnoli, GeoSolutions SAS
*/
public class WindBarbsFactory implements MarkFactory {
static final int MAX_SPEED = 300;
private static final int NUMBER_OF_ITEMS_IN_CACHE = MAX_SPEED / 5;
/** WINDBARB_DEFINITION */
private static final String WINDBARB_DEFINITION = "windbarbs://.*\\(.{1,}\\)\\[.{1,5}\\]\\??.*";
/** SOUTHERN_EMISPHERE_FLIP */
public static final AffineTransform SOUTHERN_EMISPHERE_FLIP = new AffineTransform2D(
AffineTransform.getScaleInstance(-1, 1));
/** The loggermodule. */
private static final Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(WindBarbsFactory.class);
public static final String WINDBARBS_PREFIX = "windbarbs://";
private static final String DEFAULT_NAME = "default";
private static Pattern SPEED_PATTERN = Pattern.compile("(.*?)\\((.{1,})\\)(.*)");// Pattern.compile("(.*?)(\\d+\\.?\\d*)(.*)");
private static Pattern WINDBARB_SET_PATTERN = Pattern.compile("(.*?)://(.*)\\((.*)");
private static Pattern UNIT_PATTERN = Pattern.compile("(.*?)\\[(.*)\\](.*)");
private static final SoftValueHashMap<WindBarbDefinition, Map<Integer, Shape>> CACHE;
static {
CACHE = new SoftValueHashMap<WindBarb.WindBarbDefinition, Map<Integer, Shape>>(1);// make room for the default definition
CACHE.put(WindBarb.DEFAULT_WINDBARB_DEFINITION, createWindBarbs(WindBarb.DEFAULT_WINDBARB_DEFINITION));
}
/**
* 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) {
// CHECKS
// cannot handle a null url
if (symbolUrl == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Provided null symbol to the WindBarbs Factory");
}
return null;
}
// cannot handle a null feature
if (feature == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Provided null feature to the WindBarbs Factory");
}
return null;
}
//
// START PARSING CODE
//
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Trying to resolve symbol:" + symbolUrl.toString());
}
// evaluate string from feature to extract all values
final String wellKnownName = symbolUrl.evaluate(feature, String.class);
if (wellKnownName == null || wellKnownName.length() <= 0) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Unable to resolve symbol provided to WindBarbs Factory");
}
return null;
}
// //
//
// Basic Syntax
//
// //
if (!wellKnownName.matches(WindBarbsFactory.WINDBARB_DEFINITION)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Unable to resolve symbol: " + wellKnownName);
}
return null;
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Resolved symbol " + wellKnownName);
}
// ok from now on we should have a real windbarb, let's lower the log level
// //
//
// WindBarbs set
//
// //
String windBarbName = null;
Matcher matcher = WINDBARB_SET_PATTERN.matcher(wellKnownName);
if (matcher.matches()) {
try {
windBarbName = matcher.group(2);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "Unable to parse windbarb set from string: "
+ wellKnownName, e);
}
return null;
}
}
if (windBarbName == null || windBarbName.length() <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.fine("Unable to parse windBarbName from string: " + wellKnownName);
}
return null;
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Resolved windBarbName " + windBarbName);
}
// //
//
// Looking for speed
//
// //
matcher = SPEED_PATTERN.matcher(wellKnownName);
double speed = Double.NaN;
if (matcher.matches()) {
String speedString = "";
try {
speedString = matcher.group(2);
speed = Double.parseDouble(speedString);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "Unable to parse speed from string: " + speedString, e);
}
return null;
}
} else {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.fine("Unable to parse speed from string: " + wellKnownName);
}
return null;
}
// //
//
// Looking for unit value
//
// //
String uom = null;// no default
matcher = UNIT_PATTERN.matcher(wellKnownName);
if (matcher.matches()) {
uom = matcher.group(2);
}
if (uom == null || uom.length() <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Unable to parse UoM from " + wellKnownName);
}
return null;
}
// so far so good
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("WindBarbs name " + windBarbName + "with Speed " + speed + "[" + uom + "]");
}
// //
//
// Params
//
// //
int index = wellKnownName.indexOf('?');
if (index > 0) {
final Map<String, String> params = new HashMap<String, String>();
final String kvp = wellKnownName.substring(index + 1);
String[] pairs = kvp.split("&");
if (pairs != null && pairs.length > 0) {
for (String pair : pairs) {
// split
String[] splitPair = pair.split("=");
if (splitPair != null && splitPair.length > 0) {
params.put(splitPair[0].toLowerCase(), splitPair[1]);
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Skipping pair " + pair);
}
}
}
// checks
if (!params.isEmpty()) {
return getWindBarb(windBarbName, speed, uom, params);
}
}
} else {
// make sure we close with ] and nothing else after
if (!wellKnownName.endsWith("]")) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("The provided symbol may be missing a ? before the KVP part.");
}
return null;
}
}
// //
//
// Get shape if possible
//
// //
return getWindBarb(windBarbName, speed, uom);
}
private static Map<Integer, Shape> createWindBarbs(WindBarbDefinition definition) {
final Map<Integer, Shape> windBarbsMapping = new HashMap<Integer, Shape>();
for (int i = 0; i <= NUMBER_OF_ITEMS_IN_CACHE; i++) {
windBarbsMapping.put(i, new WindBarb(definition, i * 5).build()); // pass over the knots definition
}
// no module x----- symbol
windBarbsMapping.put(-1, new WindBarb(definition, -1).build());
return windBarbsMapping;
}
/**
* @param windBarbName
* @param speed
* @param units
* @param params
* @return
*/
private Shape getWindBarb(String windBarbName, double speed, String units,
Map<String, String> params) {
// speed
try {
double knots = SpeedConverter.toKnots(speed, units);
// shape
return getWindBarbForKnots(windBarbName, knots, params);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
}
return null;
}
}
/**
* Get the proper WindBarb related to the referred speed
*
* @param speed
* @param units
* @return
*/
private Shape getWindBarb(final String windBarbName, final double speed, final String units) {
return getWindBarb(windBarbName, speed, units, null);
}
private Shape getWindBarbForKnots(final String windBarbName, final double knots,
Map<String, String> params) {
// No module is signaled by NaN
// checking the barbs using our own limits
int index = -1;// no wind module is -1
if (!Double.isNaN(knots)) {
if (knots < 3) {
index = 0;
} else {
index = (int) ((knots - 3.0) / 5.0 + 1);
}
}
// get the barb
if (windBarbName.equalsIgnoreCase(DEFAULT_NAME)) {
WindBarbDefinition definition = parseWindBarbsDefinition(params);
Map<Integer, Shape> windbarbs = null;
synchronized (CACHE) {
windbarbs = CACHE.get(definition);
if (windbarbs == null) {
windbarbs = createWindBarbs(definition);
CACHE.put(definition, windbarbs);
}
}
// get shape from cached definitions.
Shape shp = windbarbs.get(index);
if (shp == null) {
// No definition available. build it on the fly without caching it
// (supposing it's a rare barb since we are caching up to MAX_SPEED)
shp = new WindBarb(definition, (int) knots).build();
}
if (params == null || params.isEmpty()) {
return shp;
}
if (params.containsKey("emisphere") && params.get("emisphere").equalsIgnoreCase("s")) {
// flip shape on Y axis
return SOUTHERN_EMISPHERE_FLIP.createTransformedShape(shp);
}
if (params.containsKey("hemisphere") && params.get("hemisphere").equalsIgnoreCase("s")) {
// flip shape on Y axis
return SOUTHERN_EMISPHERE_FLIP.createTransformedShape(shp);
}
return shp;
}
throw new IllegalArgumentException("Wrong windbard name:" + windBarbName);
}
/**
* @param params
* @return a {@link WindBarbDefinition} for the provided params
*/
private WindBarbDefinition parseWindBarbsDefinition(Map<String, String> params) {
final WindBarbDefinition retValue = WindBarb.DEFAULT_WINDBARB_DEFINITION;
if (params == null || params.size() <= 0) {
return retValue;
}
// parse
String temp = null;
// //
//
// vectorLength
//
// //
int vectorLength = retValue.vectorLength;
if (params.containsKey("vectorlength")) {
// get value
temp = params.get("vectorlength");
// check and parse
if (temp == null || temp.length() <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong vectorLength provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
try {
vectorLength = Integer.parseInt(temp);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong vectorLength provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
if (vectorLength <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong vectorLength provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
}
// //
//
// basePennantLength
//
// //
int basePennantLength = retValue.basePennantLength;
if (params.containsKey("basepennantlength")) {
// get value
temp = params.get("basepennantlength");
// check and parse
if (temp == null || temp.length() <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong basePennantLength provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
try {
basePennantLength = Integer.parseInt(temp);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong basePennantLength provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
if (basePennantLength <= 0 || basePennantLength >= vectorLength) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong basePennantLength provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
}
// //
//
// elementsSpacing
//
// //
int elementsSpacing = retValue.elementsSpacing;
if (params.containsKey("elementsspacing")) {
// get value
temp = params.get("elementsspacing");
// check and parse
if (temp == null || temp.length() <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong elementsSpacing provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
try {
elementsSpacing = Integer.parseInt(temp);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong elementsSpacing provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
if (elementsSpacing <= 0 || elementsSpacing >= vectorLength
|| elementsSpacing + basePennantLength >= vectorLength) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong elementsSpacing provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
}
// //
//
// longBarbLength
//
// //
int longBarbLength = retValue.longBarbLength;
if (params.containsKey("longbarblength")) {
// get value
temp = params.get("longbarblength");
// check and parse
if (temp == null || temp.length() <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong longBarbLength provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
try {
longBarbLength = Integer.parseInt(temp);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong longBarbLength provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
if (longBarbLength <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong longBarbLength provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
}
// //
//
// zeroWindRadius
//
// //
int zeroWindRadius = retValue.zeroWindRadius;
if (params.containsKey("zerowindradius")) {
// get value
temp = params.get("zerowindradius");
// check and parse
if (temp == null || temp.length() <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong zeroWindRadius provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
try {
zeroWindRadius = Integer.parseInt(temp);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong zeroWindRadius provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
if (zeroWindRadius <= 0) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Wrong zeroWindRadius provided: " + temp
+ " resorting to default wind barb definition");
}
return retValue;// default
}
}
// new definition
return new WindBarbDefinition(vectorLength, basePennantLength, elementsSpacing,
longBarbLength, zeroWindRadius);
}
}