View Javadoc
1   package nl.tudelft.simulation.dsol.animation.gis.esri;
2   
3   import java.awt.Color;
4   import java.awt.Dimension;
5   import java.io.IOException;
6   import java.net.URL;
7   import java.util.ArrayList;
8   import java.util.List;
9   
10  import javax.xml.parsers.DocumentBuilder;
11  import javax.xml.parsers.DocumentBuilderFactory;
12  import javax.xml.parsers.ParserConfigurationException;
13  
14  import org.djutils.draw.bounds.Bounds2d;
15  import org.djutils.io.URLResource;
16  import org.w3c.dom.DOMException;
17  import org.w3c.dom.Document;
18  import org.w3c.dom.Element;
19  import org.w3c.dom.Node;
20  import org.w3c.dom.NodeList;
21  import org.xml.sax.SAXException;
22  
23  import nl.tudelft.simulation.dsol.animation.gis.GisMapInterface;
24  import nl.tudelft.simulation.dsol.animation.gis.LayerInterface;
25  import nl.tudelft.simulation.dsol.animation.gis.MapImageInterface;
26  import nl.tudelft.simulation.dsol.animation.gis.MapUnits;
27  import nl.tudelft.simulation.dsol.animation.gis.map.Feature;
28  import nl.tudelft.simulation.dsol.animation.gis.map.GisMap;
29  import nl.tudelft.simulation.dsol.animation.gis.map.Layer;
30  import nl.tudelft.simulation.dsol.animation.gis.map.MapImage;
31  import nl.tudelft.simulation.dsol.animation.gis.transform.CoordinateTransform;
32  
33  /**
34   * This class parses an XML file that defines which elements of shape file(s) need to be drawn and what format to use.
35   * <p>
36   * Copyright (c) 2020-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
37   * for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
38   * project is distributed under a three-clause BSD-style license, which can be found at
39   * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
40   * </p>
41   * <p>
42   * The dsol-animation-gis project is based on the gisbeans project that has been part of DSOL since 2002, originally by Peter
43   * Jacobs and Paul Jacobs.
44   * </p>
45   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
46   */
47  public final class EsriFileXmlParser
48  {
49      /** the default mapfile. */
50      public static final URL MAPFILE_SCHEMA = URLResource.getResource("/resources/mapfile.xsd");
51  
52      /** Utility class, no constructor. */
53      private EsriFileXmlParser()
54      {
55          // Utility class
56      }
57  
58      /**
59       * parses a Mapfile URL to a mapFile.
60       * @param url URL; the mapfile url.
61       * @return MapInterface the parsed mapfile.
62       * @throws IOException on failure
63       */
64      public static GisMapInterface parseMapFile(final URL url) throws IOException
65      {
66          return parseMapFile(url, new CoordinateTransform.NoTransform());
67      }
68  
69      /**
70       * parses a Mapfile URL to a mapFile.
71       * @param url URL; the mapfile url.
72       * @param coordinateTransform CoordinateTransform; the transformation of (x, y) coordinates to (x', y') coordinates.
73       * @return MapInterface the parsed mapfile.
74       * @throws IOException on failure
75       */
76      public static GisMapInterface parseMapFile(final URL url, final CoordinateTransform coordinateTransform) throws IOException
77      {
78          try
79          {
80              DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
81              DocumentBuilder builder = factory.newDocumentBuilder();
82              Document document = builder.parse(url.openStream());
83              document.getDocumentElement().normalize();
84              Element root = document.getDocumentElement();
85  
86              GisMapInterface map = new GisMap();
87  
88              // map.name
89              map.setName(nodeText(root, "name"));
90  
91              // map.units
92              if (nodeTagExists(root, "units"))
93              {
94                  map.setUnits(parseUnits(nodeText(root, "units")));
95              }
96  
97              // map.extent
98              map.setExtent(parseExtent(nodeTagItem(root, "extent", 0), coordinateTransform));
99  
100             // map.image
101             if (nodeTagExists(root, "image"))
102             {
103                 map.setImage(parseImage(nodeTagItem(root, "image", 0)));
104             }
105 
106             // map.layer
107             map.setLayers(parseLayers(root.getElementsByTagName("layer"), coordinateTransform));
108 
109             return map;
110         }
111         catch (DOMException | SAXException | ParserConfigurationException exception)
112         {
113             throw new IOException(exception);
114         }
115     }
116 
117     /**
118      * Parses a xml-element representing the units for the map.
119      * @param units String; the string representation of the units
120      * @return MapUnits enum
121      */
122     @SuppressWarnings("checkstyle:needbraces")
123     private static MapUnits parseUnits(final String units)
124     {
125         if (units.equals("feet"))
126             return MapUnits.FEET;
127         if (units.equals("dd"))
128             return MapUnits.DECIMAL_DEGREES;
129         if (units.equals("inches"))
130             return MapUnits.INCHES;
131         if (units.equals("kilometers"))
132             return MapUnits.KILOMETERS;
133         if (units.equals("meters"))
134             return MapUnits.METERS;
135         if (units.equals("miles"))
136             return MapUnits.MILES;
137         return MapUnits.METERS;
138     }
139 
140     /**
141      * Creates the extent for the map, in transformed units.
142      * @param node Node; the dom node
143      * @param coordinateTransform CoordinateTransform; the transformation to apply on the coordinates
144      * @return Bounds2d; the extent for the map, in transformed units
145      * @throws IOException on parsing error
146      */
147     private static Bounds2d parseExtent(final Node node, final CoordinateTransform coordinateTransform) throws IOException
148     {
149         try
150         {
151             double minX = nodeDouble(node, "minX");
152             double minY = nodeDouble(node, "minY");
153             double maxX = nodeDouble(node, "maxX");
154             double maxY = nodeDouble(node, "maxY");
155 
156             double[] p = coordinateTransform.doubleTransform(minX, minY);
157             double[] q = coordinateTransform.doubleTransform(maxX, maxY);
158             minX = Math.min(p[0], q[0]);
159             minY = Math.min(p[1], q[1]);
160             maxX = Math.max(p[0], q[0]);
161             maxY = Math.max(p[1], q[1]);
162             return new Bounds2d(minX, maxX, minY, maxY);
163         }
164         catch (Exception exception)
165         {
166             throw new IOException(exception);
167         }
168     }
169 
170     /**
171      * parses a xml-element representing the Image.
172      * @param node Node; the map.image dom node
173      * @return information about the image
174      * @throws IOException on parsing error
175      */
176     @SuppressWarnings("checkstyle:needbraces")
177     private static MapImageInterface parseImage(final Node node) throws IOException
178     {
179         Element element = (Element) node;
180         MapImageInterface mapImage = new MapImage();
181         try
182         {
183             if (nodeTagExists(node, "backgroundColor"))
184                 mapImage.setBackgroundColor(parseColor(nodeTagItem(element, "backgroundColor", 0)));
185             if (nodeTagExists(node, "size"))
186                 mapImage.setSize(parseDimension(nodeTagItem(element, "size", 0)));
187             return mapImage;
188         }
189         catch (Exception exception)
190         {
191             throw new IOException(exception);
192         }
193     }
194 
195     /**
196      * parses a xml-element representing a Color.
197      * @param node Node; the node to parse for the color
198      * @return Color of element
199      * @throws IOException on parsing error
200      */
201     private static Color parseColor(final Node node) throws IOException
202     {
203         try
204         {
205             int r = nodeInt(node, "r");
206             int g = nodeInt(node, "g");
207             int b = nodeInt(node, "b");
208             if (nodeTagExists(node, "a"))
209             {
210                 int a = nodeInt(node, "a");
211                 return new Color(r, g, b, a);
212             }
213             return new Color(r, g, b);
214         }
215         catch (Exception exception)
216         {
217             throw new IOException(exception.getMessage());
218         }
219     }
220 
221     /**
222      * Parse an xml-element representing a Dimension.
223      * @param node Node; the dom node with the dimension information
224      * @return Dimension of element
225      * @throws IOException on parsing error
226      */
227     private static Dimension parseDimension(final Node node) throws IOException
228     {
229         try
230         {
231             int width = nodeInt(node, "width");
232             int height = nodeInt(node, "height");
233             return new Dimension(width, height);
234         }
235         catch (Exception exception)
236         {
237             throw new IOException(exception.getMessage());
238         }
239     }
240 
241     /**
242      * Parse an xml-element representing a Layer.
243      * @param layerNodeList NodeList; the list of layer tags in the map
244      * @param coordinateTransform CoordinateTransform; the transformation to apply to the layer
245      * @return List&lt;LayerInterface&gt;; the list of parsed layers
246      * @throws IOException on parsing error
247      */
248     private static List<LayerInterface> parseLayers(final NodeList layerNodeList, final CoordinateTransform coordinateTransform)
249             throws IOException
250     {
251         try
252         {
253             List<LayerInterface> layerList = new ArrayList<>();
254             for (int i = 0; i < layerNodeList.getLength(); i++)
255             {
256                 Node layerNode = layerNodeList.item(i);
257                 LayerInterface layer = new Layer();
258                 layerList.add(layer);
259                 layer.setName(nodeText(layerNode, "name"));
260                 Feature feature = new Feature();
261                 layer.addFeature(feature); // key and value remain at * and *
262 
263                 Node dataNode = nodeTagItem(layerNode, "data", 0);
264                 if (nodeTagExists(dataNode, "shapeFile"))
265                 {
266                     String resourceName = nodeText(dataNode, "shapeFile");
267                     URL resource = URLResource.getResource(resourceName);
268                     if (resource == null)
269                     {
270                         throw new IOException("Cannot locate shapeFile: " + resourceName);
271                     }
272                     ShapeFileReader dataSource = new ShapeFileReader(resource, coordinateTransform, layer.getFeatures());
273                     dataSource.populateShapes();
274                 }
275 
276                 /*-
277                 if (nodeTagExists(layerNode, "minScale"))
278                 {
279                     layer.setMinScale(nodeInt(layerNode, "minscale"));
280                 }
281                 if (nodeTagExists(layerNode, "maxScale"))
282                 {
283                     layer.setMaxScale(nodeInt(layerNode, "maxscale"));
284                 }
285                 */
286                 if (nodeTagExists(layerNode, "fillColor"))
287                 {
288                     feature.setFillColor(parseColor(nodeTagItem(layerNode, "fillColor", 0)));
289                 }
290                 if (nodeTagExists(layerNode, "outlineColor"))
291                 {
292                     feature.setOutlineColor(parseColor(nodeTagItem(layerNode, "outlineColor", 0)));
293                 }
294                 if (nodeTagExists(layerNode, "display"))
295                 {
296                     layer.setDisplay(nodeBoolean(layerNode, "display"));
297                 }
298                 if (nodeTagExists(layerNode, "transform"))
299                 {
300                     layer.setTransform(nodeBoolean(layerNode, "transform"));
301                 }
302             }
303             return layerList;
304         }
305         catch (Exception exception)
306         {
307             throw new IOException(exception.getMessage());
308         }
309     }
310 
311     /**
312      * Check if one or more nodes with the tag name exist, e.g. from: &lt;node&gt;&lt;tag&gt;text&lt;/tag&gt; ... &lt;/node&gt;.
313      * @param node Node; the node to check
314      * @param tag String; the name of the tag for which we check one copy exists
315      * @return boolean; whether the tag count is larger than zero
316      * @throws IOException on parse error
317      */
318     private static boolean nodeTagExists(final Node node, final String tag) throws IOException
319     {
320         try
321         {
322             Element element = (Element) node;
323             return element.getElementsByTagName(tag).getLength() > 0;
324         }
325         catch (Exception exception)
326         {
327             throw new IOException(exception);
328         }
329     }
330 
331     /**
332      * Return the i-th node with the tag name in the element, e.g. from: &lt;node&gt;&lt;tag&gt;text&lt;/tag&gt; ...
333      * &lt;/node&gt;.
334      * @param node Node; the node to check
335      * @param tag String; the name of the tag for which we check one copy exists
336      * @param item int; the number in the list to look up
337      * @return Node; the i-th node with the tag name in the element
338      * @throws IOException on parse error
339      */
340     private static Node nodeTagItem(final Node node, final String tag, final int item) throws IOException
341     {
342         try
343         {
344             Element element = (Element) node;
345             return element.getElementsByTagName(tag).item(item);
346         }
347         catch (Exception exception)
348         {
349             throw new IOException(exception);
350         }
351     }
352 
353     /**
354      * Get the text of a node with the tag name, e.g. from: &lt;node&gt;&lt;tag&gt;text&lt;/tag&gt; ... &lt;/node&gt;.
355      * @param node Node; the node to get the text from
356      * @param tag String; the name of the tag that contains the text
357      * @return String; text enclosed in the tag
358      * @throws IOException on parse error
359      */
360     private static String nodeText(final Node node, final String tag) throws IOException
361     {
362         try
363         {
364             Element element = (Element) node;
365             return element.getElementsByTagName(tag).item(0).getTextContent();
366         }
367         catch (Exception exception)
368         {
369             throw new IOException(exception);
370         }
371     }
372 
373     /**
374      * Get the double value of a node with the tag name, e.g. from: &lt;node&gt;&lt;tag&gt;123.45&lt;/tag&gt; ... &lt;/node&gt;.
375      * @param node Node; the node to get the value from
376      * @param tag String; the name of the tag that contains the value
377      * @return double; value enclosed in the tag
378      * @throws IOException on parse error
379      */
380     private static double nodeDouble(final Node node, final String tag) throws IOException
381     {
382         try
383         {
384             return Double.parseDouble(nodeText(node, tag));
385         }
386         catch (Exception exception)
387         {
388             throw new IOException(exception);
389         }
390     }
391 
392     /**
393      * Get the int value of a node with the tag name, e.g. from: &lt;node&gt;&lt;tag&gt;123&lt;/tag&gt; ... &lt;/node&gt;.
394      * @param node Node; the node to get the value from
395      * @param tag String; the name of the tag that contains the value
396      * @return int; value enclosed in the tag
397      * @throws IOException on parse error
398      */
399     private static int nodeInt(final Node node, final String tag) throws IOException
400     {
401         try
402         {
403             return Integer.parseInt(nodeText(node, tag));
404         }
405         catch (Exception exception)
406         {
407             throw new IOException(exception);
408         }
409     }
410 
411     /**
412      * Get the boolean value of a node with the tag name, e.g. from: &lt;node&gt;&lt;tag&gt;true&lt;/tag&gt; ... &lt;/node&gt;.
413      * @param node Node; the node to get the value from
414      * @param tag String; the name of the tag that contains the value
415      * @return boolean; value enclosed in the tag
416      * @throws IOException on parse error
417      */
418     private static boolean nodeBoolean(final Node node, final String tag) throws IOException
419     {
420         try
421         {
422             String b = nodeText(node, tag).toLowerCase();
423             return b.startsWith("t") || b.startsWith("y") || b.equals("1");
424         }
425         catch (Exception exception)
426         {
427             throw new IOException(exception);
428         }
429     }
430 }