EsriFileXmlParser.java
package nl.tudelft.simulation.dsol.animation.gis.esri;
import java.awt.Color;
import java.awt.Dimension;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.djutils.draw.bounds.Bounds2d;
import org.djutils.io.URLResource;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import nl.tudelft.simulation.dsol.animation.gis.GisMapInterface;
import nl.tudelft.simulation.dsol.animation.gis.LayerInterface;
import nl.tudelft.simulation.dsol.animation.gis.MapImageInterface;
import nl.tudelft.simulation.dsol.animation.gis.MapUnits;
import nl.tudelft.simulation.dsol.animation.gis.map.Feature;
import nl.tudelft.simulation.dsol.animation.gis.map.GisMap;
import nl.tudelft.simulation.dsol.animation.gis.map.Layer;
import nl.tudelft.simulation.dsol.animation.gis.map.MapImage;
import nl.tudelft.simulation.dsol.animation.gis.transform.CoordinateTransform;
/**
* This class parses an XML file that defines which elements of shape file(s) need to be drawn and what format to use.
* <p>
* Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
* project is distributed under a three-clause BSD-style license, which can be found at
* <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
* </p>
* <p>
* The dsol-animation-gis project is based on the gisbeans project that has been part of DSOL since 2002, originally by Peter
* Jacobs and Paul Jacobs.
* </p>
* @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
*/
public final class EsriFileXmlParser
{
/** the default mapfile. */
public static final URL MAPFILE_SCHEMA = URLResource.getResource("/resources/mapfile.xsd");
/** Utility class, no constructor. */
private EsriFileXmlParser()
{
// Utility class
}
/**
* parses a Mapfile URL to a mapFile.
* @param url URL; the mapfile url.
* @return MapInterface the parsed mapfile.
* @throws IOException on failure
*/
public static GisMapInterface parseMapFile(final URL url) throws IOException
{
return parseMapFile(url, new CoordinateTransform.NoTransform());
}
/**
* parses a Mapfile URL to a mapFile.
* @param url URL; the mapfile url.
* @param coordinateTransform CoordinateTransform; the transformation of (x, y) coordinates to (x', y') coordinates.
* @return MapInterface the parsed mapfile.
* @throws IOException on failure
*/
public static GisMapInterface parseMapFile(final URL url, final CoordinateTransform coordinateTransform) throws IOException
{
try
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(url.openStream());
document.getDocumentElement().normalize();
Element root = document.getDocumentElement();
GisMapInterface map = new GisMap();
// map.name
map.setName(nodeText(root, "name"));
// map.units
if (nodeTagExists(root, "units"))
{
map.setUnits(parseUnits(nodeText(root, "units")));
}
// map.extent
map.setExtent(parseExtent(nodeTagItem(root, "extent", 0), coordinateTransform));
// map.image
if (nodeTagExists(root, "image"))
{
map.setImage(parseImage(nodeTagItem(root, "image", 0)));
}
// map.layer
map.setLayers(parseLayers(root.getElementsByTagName("layer"), coordinateTransform));
return map;
}
catch (DOMException | SAXException | ParserConfigurationException exception)
{
throw new IOException(exception);
}
}
/**
* Parses a xml-element representing the units for the map.
* @param units String; the string representation of the units
* @return MapUnits enum
*/
@SuppressWarnings("checkstyle:needbraces")
private static MapUnits parseUnits(final String units)
{
if (units.equals("feet"))
return MapUnits.FEET;
if (units.equals("dd"))
return MapUnits.DECIMAL_DEGREES;
if (units.equals("inches"))
return MapUnits.INCHES;
if (units.equals("kilometers"))
return MapUnits.KILOMETERS;
if (units.equals("meters"))
return MapUnits.METERS;
if (units.equals("miles"))
return MapUnits.MILES;
return MapUnits.METERS;
}
/**
* Creates the extent for the map, in transformed units.
* @param node Node; the dom node
* @param coordinateTransform CoordinateTransform; the transformation to apply on the coordinates
* @return Bounds2d; the extent for the map, in transformed units
* @throws IOException on parsing error
*/
private static Bounds2d parseExtent(final Node node, final CoordinateTransform coordinateTransform) throws IOException
{
try
{
double minX = nodeDouble(node, "minX");
double minY = nodeDouble(node, "minY");
double maxX = nodeDouble(node, "maxX");
double maxY = nodeDouble(node, "maxY");
double[] p = coordinateTransform.doubleTransform(minX, minY);
double[] q = coordinateTransform.doubleTransform(maxX, maxY);
minX = Math.min(p[0], q[0]);
minY = Math.min(p[1], q[1]);
maxX = Math.max(p[0], q[0]);
maxY = Math.max(p[1], q[1]);
return new Bounds2d(minX, maxX, minY, maxY);
}
catch (Exception exception)
{
throw new IOException(exception);
}
}
/**
* parses a xml-element representing the Image.
* @param node Node; the map.image dom node
* @return information about the image
* @throws IOException on parsing error
*/
@SuppressWarnings("checkstyle:needbraces")
private static MapImageInterface parseImage(final Node node) throws IOException
{
Element element = (Element) node;
MapImageInterface mapImage = new MapImage();
try
{
if (nodeTagExists(node, "backgroundColor"))
mapImage.setBackgroundColor(parseColor(nodeTagItem(element, "backgroundColor", 0)));
if (nodeTagExists(node, "size"))
mapImage.setSize(parseDimension(nodeTagItem(element, "size", 0)));
return mapImage;
}
catch (Exception exception)
{
throw new IOException(exception);
}
}
/**
* parses a xml-element representing a Color.
* @param node Node; the node to parse for the color
* @return Color of element
* @throws IOException on parsing error
*/
private static Color parseColor(final Node node) throws IOException
{
try
{
int r = nodeInt(node, "r");
int g = nodeInt(node, "g");
int b = nodeInt(node, "b");
if (nodeTagExists(node, "a"))
{
int a = nodeInt(node, "a");
return new Color(r, g, b, a);
}
return new Color(r, g, b);
}
catch (Exception exception)
{
throw new IOException(exception.getMessage());
}
}
/**
* Parse an xml-element representing a Dimension.
* @param node Node; the dom node with the dimension information
* @return Dimension of element
* @throws IOException on parsing error
*/
private static Dimension parseDimension(final Node node) throws IOException
{
try
{
int width = nodeInt(node, "width");
int height = nodeInt(node, "height");
return new Dimension(width, height);
}
catch (Exception exception)
{
throw new IOException(exception.getMessage());
}
}
/**
* Parse an xml-element representing a Layer.
* @param layerNodeList NodeList; the list of layer tags in the map
* @param coordinateTransform CoordinateTransform; the transformation to apply to the layer
* @return List<LayerInterface>; the list of parsed layers
* @throws IOException on parsing error
*/
private static List<LayerInterface> parseLayers(final NodeList layerNodeList, final CoordinateTransform coordinateTransform)
throws IOException
{
try
{
List<LayerInterface> layerList = new ArrayList<>();
for (int i = 0; i < layerNodeList.getLength(); i++)
{
Node layerNode = layerNodeList.item(i);
LayerInterface layer = new Layer();
layerList.add(layer);
layer.setName(nodeText(layerNode, "name"));
Feature feature = new Feature();
layer.addFeature(feature); // key and value remain at * and *
Node dataNode = nodeTagItem(layerNode, "data", 0);
if (nodeTagExists(dataNode, "shapeFile"))
{
String resourceName = nodeText(dataNode, "shapeFile");
URL resource = URLResource.getResource(resourceName);
if (resource == null)
{
throw new IOException("Cannot locate shapeFile: " + resourceName);
}
ShapeFileReader dataSource = new ShapeFileReader(resource, coordinateTransform, layer.getFeatures());
dataSource.populateShapes();
}
/*-
if (nodeTagExists(layerNode, "minScale"))
{
layer.setMinScale(nodeInt(layerNode, "minscale"));
}
if (nodeTagExists(layerNode, "maxScale"))
{
layer.setMaxScale(nodeInt(layerNode, "maxscale"));
}
*/
if (nodeTagExists(layerNode, "fillColor"))
{
feature.setFillColor(parseColor(nodeTagItem(layerNode, "fillColor", 0)));
}
if (nodeTagExists(layerNode, "outlineColor"))
{
feature.setOutlineColor(parseColor(nodeTagItem(layerNode, "outlineColor", 0)));
}
if (nodeTagExists(layerNode, "display"))
{
layer.setDisplay(nodeBoolean(layerNode, "display"));
}
if (nodeTagExists(layerNode, "transform"))
{
layer.setTransform(nodeBoolean(layerNode, "transform"));
}
}
return layerList;
}
catch (Exception exception)
{
throw new IOException(exception.getMessage());
}
}
/**
* Check if one or more nodes with the tag name exist, e.g. from: <node><tag>text</tag> ... </node>.
* @param node Node; the node to check
* @param tag String; the name of the tag for which we check one copy exists
* @return boolean; whether the tag count is larger than zero
* @throws IOException on parse error
*/
private static boolean nodeTagExists(final Node node, final String tag) throws IOException
{
try
{
Element element = (Element) node;
return element.getElementsByTagName(tag).getLength() > 0;
}
catch (Exception exception)
{
throw new IOException(exception);
}
}
/**
* Return the i-th node with the tag name in the element, e.g. from: <node><tag>text</tag> ...
* </node>.
* @param node Node; the node to check
* @param tag String; the name of the tag for which we check one copy exists
* @param item int; the number in the list to look up
* @return Node; the i-th node with the tag name in the element
* @throws IOException on parse error
*/
private static Node nodeTagItem(final Node node, final String tag, final int item) throws IOException
{
try
{
Element element = (Element) node;
return element.getElementsByTagName(tag).item(item);
}
catch (Exception exception)
{
throw new IOException(exception);
}
}
/**
* Get the text of a node with the tag name, e.g. from: <node><tag>text</tag> ... </node>.
* @param node Node; the node to get the text from
* @param tag String; the name of the tag that contains the text
* @return String; text enclosed in the tag
* @throws IOException on parse error
*/
private static String nodeText(final Node node, final String tag) throws IOException
{
try
{
Element element = (Element) node;
return element.getElementsByTagName(tag).item(0).getTextContent();
}
catch (Exception exception)
{
throw new IOException(exception);
}
}
/**
* Get the double value of a node with the tag name, e.g. from: <node><tag>123.45</tag> ... </node>.
* @param node Node; the node to get the value from
* @param tag String; the name of the tag that contains the value
* @return double; value enclosed in the tag
* @throws IOException on parse error
*/
private static double nodeDouble(final Node node, final String tag) throws IOException
{
try
{
return Double.parseDouble(nodeText(node, tag));
}
catch (Exception exception)
{
throw new IOException(exception);
}
}
/**
* Get the int value of a node with the tag name, e.g. from: <node><tag>123</tag> ... </node>.
* @param node Node; the node to get the value from
* @param tag String; the name of the tag that contains the value
* @return int; value enclosed in the tag
* @throws IOException on parse error
*/
private static int nodeInt(final Node node, final String tag) throws IOException
{
try
{
return Integer.parseInt(nodeText(node, tag));
}
catch (Exception exception)
{
throw new IOException(exception);
}
}
/**
* Get the boolean value of a node with the tag name, e.g. from: <node><tag>true</tag> ... </node>.
* @param node Node; the node to get the value from
* @param tag String; the name of the tag that contains the value
* @return boolean; value enclosed in the tag
* @throws IOException on parse error
*/
private static boolean nodeBoolean(final Node node, final String tag) throws IOException
{
try
{
String b = nodeText(node, tag).toLowerCase();
return b.startsWith("t") || b.startsWith("y") || b.equals("1");
}
catch (Exception exception)
{
throw new IOException(exception);
}
}
}