├── .gitattributes ├── .gitignore ├── README.md ├── nvgReader.py ├── nvgWriter.py └── toolbox ├── NVG.WriteNVG.pyt.xml ├── NVG.loadNVG.pyt.xml ├── NVG.pyt ├── NVG.pyt.xml ├── nvgReader.py ├── nvgWriter.py └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[cod] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stories in Ready](https://badge.waffle.io/daveb1034/NVGTools.png?label=ready&title=Ready)](https://waffle.io/daveb1034/NVGTools) 2 | [![Stories in Progress](https://badge.waffle.io/daveb1034/NVGTools.png?label=in%20progress&title=In%20Progress)](https://waffle.io/daveb1034/NVGTools) 3 | 4 | # No Longer Maintained # 5 | NVGTools 6 | ======== 7 | 8 | The NATO Vector Graphics format was developed as a means for NATO systems to share and use overlays. The format is based on SVG. 9 | 10 | ## Update ## 11 | 12 | Apologies for the lack of progress on these tools. I have been diverted to other projects at work and also have a dissertation to finish. I am planning on getting back onto this late July so will provide updates then. 13 | 14 | ## nvgReader.py ## 15 | 16 | Provides a means to read a nvg file and output feature classes to a file geodatabase. 17 | 18 | The reader currently returns 4 lists which contain the geometries and atributes for supported features from an NVG 1.4.0 document. 19 | Each item in the list can be inserted into a feature class with the relevant fields directly using an insert cursor. 20 | 21 | Examples of usage will be provided with a sample python toolbox. 22 | 23 | The default NVG namespaces are supported for reading versions 1.4.0, 1.5.0 and 2.0.0. The reader suports version 1.4.0 of the schema. 24 | The code has been tested agains versions 1.5.0 and 2.0.0 however there are additional features added in 2.0.0 that have not yet been added. 25 | These are the rect and orbit elements. Due to the limited use of version 2.0.0 at present the focus of the project will be adding write support for version 1.4.0 then full support for 1.5.0. 26 | 27 | ## Usage 28 | 29 | Reading NVG files is done using an instance of the Reader class. The optional namespace tag in the Reader class is not yet implemented and should be left to the default value None. 30 | 31 | ```python 32 | import nvgReader as NVG 33 | 34 | nvgFile = r'e:\mydata\nvg\sample.nvg' 35 | 36 | reader = NVG.Reader(nvgFile,namespaces=None) 37 | points, polylines, polygons, multipoints = reader.read() 38 | ``` 39 | The read method returns a tuple of 4 lists: 40 | ```python 41 | >>> [points, polylines, polygons, multipoints] 42 | ``` 43 | 44 | Each feature is returned as a list with the geomerty objct at position 0 and the common attributes. If an attribute is not provided in the NVG file then None is returned. 45 | 46 | The list returns the following attributes 47 | ```python 48 | >>> [, 'uri', 'style', 'label', 'symbol', 'modifiers', 'course', 'speed', 'width', 'min_altitude', 'max_altitude', 'parenNode'] 49 | ``` 50 | ## nvgWriter.py ## 51 | 52 | The writer requires the use of a layer pack that provides the correct values for writing the style tags. Further details are provided in the toolbox directory. Point features 53 | are not currently supported due to the need to implement APP6A and Mil2525B SIDCs in the layer packs to ensure a valid symbol tag. In addition a standard list of icons used by different C2 systems is 54 | required to enable the use on non military symbols. 55 | 56 | Each item will have one or more NVG features in a form ready to load into a feature class. 57 | ## Contributing ## 58 | 59 | Please feel free to contribute to the code. I am happy to include ideas people may have for additional functionality. The best way to do this is to either use the fork and pull workflow or raise an issue and I will attempt to add the required functionality. 60 | -------------------------------------------------------------------------------- /nvgReader.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: nvgReader.py 3 | # Purpose: Provide a way to import data in NVG format into ArcGIS File 4 | # Geodatabase format. 5 | # 6 | # Author: Dave Barrett 7 | # 8 | # Created: 16/09/2014 9 | # Copyright: (c) Dave 2014 10 | # Licence: 11 | #------------------------------------------------------------------------------- 12 | 13 | """ 14 | This module provides a number of functions and classes to read a NVG file into 15 | an ESRI ArcGIS file geodatabase. This module requires a licensed copy of ArcGIS 16 | in order to have access to the functions found in the arcpy site package. 17 | 18 | The implementation proivded here attempts to read all mandatory properties from 19 | the NVG specification for version 1.4, future versions of these tools will 20 | include support for future versions as required. 21 | """ 22 | import xml.dom.minidom 23 | import arcpy 24 | import math 25 | 26 | # , and features not yet implemented 27 | 28 | def geo2arithetic(inAngle): 29 | """converts a bearing to aritmetic angle. 30 | """ 31 | outAngle = -1.0 32 | # Force input angle into 0 to 360 range 33 | if inAngle > 360.0: 34 | inAngle = math.fmod(inAngle,360.0) 35 | 36 | # if anlge is 360 make it 0 37 | if inAngle == 360.0: 38 | inAngle = 0.0 39 | 40 | #0 to 90 41 | if (inAngle >= 0.0 and inAngle <= 90.0): 42 | outAngle = math.fabs(inAngle - 90.0) 43 | 44 | #90 to 360 45 | if (inAngle > 90.0 and inAngle < 360.0): 46 | outAngle = 360.0 - (inAngle - 90.0) 47 | 48 | return outAngle 49 | 50 | class Reader(object): 51 | """NATO Vector Graphic Reader instance. Reads and processes a NATO Vector 52 | Graphic to ESRI Geometry. 53 | """ 54 | def __init__(self,nvgFile): 55 | """Initiate the object and set the basic attributes 56 | """ 57 | self.nvgFile = nvgFile 58 | # namespace based on the version of the NVG document. 59 | self.namespaces = {'1.4.0': 'http://tide.act.nato.int/schemas/2008/10/nvg', 60 | '1.5.0': 'http://tide.act.nato.int/schemas/2009/10/nvg', 61 | '2.0.0': 'https://tide.act.nato.int/schemas/2012/10/nvg'} 62 | 63 | # parse the nvgFile 64 | self.dom = xml.dom.minidom.parse(self.nvgFile) 65 | 66 | # get the nvg version 67 | # consider moving this to a seperate method 68 | self.version = self.dom.documentElement.getAttribute("version") 69 | 70 | # update the namespace based on the version of the document 71 | if self.version == '1.4.0': 72 | self.namespace = self.namespaces['1.4.0'] 73 | elif self.version == '1.5.0': 74 | self.namespace = self.namespaces['1.5.0'] 75 | elif self.version == '2.0.0': 76 | self.namespace = self.namespaces['2.0.0'] 77 | 78 | # need to define the outputs based on the datatypes in the nvg 79 | self.esriPolygon = [] 80 | self.esriPolyline = [] 81 | self.esriPoint = [] 82 | self.esriMultiPoint = [] 83 | 84 | # define Spatial Reference Objects 85 | self.wgs84 = arcpy.SpatialReference(4326) 86 | self.world_merc = arcpy.SpatialReference(3395) 87 | 88 | return 89 | 90 | 91 | def _getElement(self,tag): 92 | """Return all elements with given tag with the correct namespace for the 93 | version of NVG. 94 | """ 95 | return self.dom.getElementsByTagNameNS(self.namespace,tag) 96 | 97 | def _cleanPoints(self,points): 98 | """Cleans a string of point coordinate pairs and returns a list of 99 | numerical coordinate pairs 100 | """ 101 | # get a list of coord pairs as strings from points 102 | # [['x1','y1'],['x2','y2'],[...]] 103 | listPoints = [p.split(",") for p in points.strip().split(" ")] 104 | 105 | # convert each coord to float 106 | 107 | fPoints = [[float(x) for x in row] for row in listPoints] 108 | 109 | return fPoints 110 | 111 | def _projectGeometry(self,geometry, spatial_refernce): 112 | """Projects the input geometry into the spatial_reference 113 | 114 | THis is used to ensure the maths works on the geometry functions 115 | """ 116 | projected = geometry.projectAs(spatial_refernce) 117 | 118 | return projected 119 | 120 | def _buildPoint(self,x,y): 121 | """build a point geometry from x,y coordinates. 122 | """ 123 | # construct the geometry 124 | pnt = arcpy.Point(x,y) 125 | pGeom = arcpy.PointGeometry(pnt,self.wgs84) 126 | 127 | return pGeom 128 | 129 | def _buildGeometry(self,points,geometry_type,spatial_reference): 130 | """Builds the relevant geometry from a string of points based on the 131 | geometry_type and spatial_reference. 132 | 133 | Valid Geometyr Types are: 134 | POLYGON 135 | POLYLINE 136 | MULTIPOINT 137 | 138 | Valid Spatial References are 139 | self.wgs84 - used for all default shapes 140 | self.world_merc - used for ellipses, arcs, and arcbands 141 | 142 | the returned geometry will always be projected as wgs84 143 | """ 144 | # clean the point string 145 | cPoints = self._cleanPoints(points) 146 | 147 | # array to hold point objects 148 | array = arcpy.Array() 149 | 150 | for point in cPoints: 151 | pnt = arcpy.Point() 152 | pnt.X = point[0] 153 | pnt.Y = point[1] 154 | array.add(pnt) 155 | 156 | if geometry_type == 'POLYGON': 157 | geom = arcpy.Polygon(array,spatial_reference) 158 | elif geometry_type == 'POLYLINE': 159 | geom = arcpy.Polyline(array,spatial_reference) 160 | elif geometry_type == 'MULTIPOINT': 161 | geom = arcpy.Multipoint(array,spatial_reference) 162 | 163 | # ensure final geom is returned in wgs84 164 | if geom.spatialReference.factoryCode != 4326: 165 | geom = self._projectGeometry(geom,self.wgs84) 166 | return geom 167 | 168 | def _pointString(self,points): 169 | """Returns a string in the format required by NVG for point coordinates. 170 | 171 | This method is used to parse the output of _buildElliptical and _buildCircular 172 | into a format used by the geometry construction methods. 173 | """ 174 | s = '' 175 | for pnt in points: 176 | pnt = str(pnt).strip('[]').replace(" ","") 177 | s = s + pnt + " " 178 | 179 | return s 180 | 181 | def _buildElliptical(self,cx,cy,rx,ry,rotation,startangle=0,endangle=360): 182 | """Generates a set of point cordinates that describe an ellipse or an arc. 183 | 184 | Coordinates need to be projected before using the tools. 185 | """ 186 | points = [] 187 | # projects the cx,cy to world mercator 188 | pGeom = arcpy.PointGeometry(arcpy.Point(cx,cy),self.wgs84) 189 | centrePnt = self._projectGeometry(pGeom,self.world_merc) 190 | cX = centrePnt.firstPoint.X 191 | cY = centrePnt.firstPoint.Y 192 | rotation = math.radians(float(rotation)) 193 | step = 1 194 | startangle = float(startangle) 195 | endangle = float(endangle) 196 | 197 | if startangle > endangle: 198 | endangle=endangle + 360 199 | 200 | # generate points and rotate 201 | for theata in range(int(startangle),int(endangle),step): 202 | #caclulate points on the ellipse 203 | theata = math.radians(float(theata)) 204 | X = float(rx) * math.cos(theata) 205 | Y = float(ry) * math.sin(theata) 206 | 207 | # rotate point around the centre 208 | rotX = cX + X * math.cos(rotation) + Y * math.sin(rotation) 209 | rotY = cY - X * math.sin(rotation) + Y * math.cos(rotation) 210 | 211 | points.append([rotX,rotY]) 212 | 213 | # build the geometry 214 | if startangle != 0 or endangle != 360: 215 | geom = self._buildGeometry(self._pointString(points),"POLYLINE",self.world_merc) 216 | else: 217 | geom = self._buildGeometry(self._pointString(points),"POLYGON",self.world_merc) 218 | 219 | return geom 220 | 221 | def _buildCircle(self,cx,cy,r): 222 | """Returns arcpy.Polygon circle from the cx, cy and radius. 223 | 224 | The radius needs to be in the same units as the cx,cy location. 225 | """ 226 | # may need to move the projection code to a seperate method as it will 227 | # need to be done multiple times 228 | 229 | # project the point to world mercator 230 | pGeom_wgs84 = arcpy.PointGeometry(arcpy.Point(cx,cy),self.wgs84) 231 | pGeom_WM = self._projectGeometry(pGeom_wgs84,self.world_merc) 232 | 233 | # buffer the point by the radius 234 | polygon_wm = pGeom_WM.buffer(float(r)) 235 | # return the polygon in wgs84 geographics 236 | polygon = self._projectGeometry(polygon_wm,self.wgs84) 237 | 238 | return polygon 239 | 240 | def _buildArcband(self,cx,cy,minr,maxr,start,end): 241 | """Builds a wedge describing an area between two concentric circles. 242 | """ 243 | # project the point to metres 244 | pGeom = arcpy.PointGeometry(arcpy.Point(cx,cy),self.wgs84) 245 | centrePnt = self._projectGeometry(pGeom,self.world_merc) 246 | cx = centrePnt.firstPoint.X 247 | cy = centrePnt.firstPoint.Y 248 | 249 | # convert values to float 250 | r1 = float(minr) 251 | r2 = float(maxr) 252 | start = float(start) 253 | end = float(end) 254 | 255 | # convert the bearings from north to maths for use in the coordinate calculations 256 | #start = math.radians(90 - start) 257 | if start > end: 258 | end = end + 360 259 | end = math.radians(90 - end) 260 | else: 261 | end = math.radians(90 - end) 262 | start = math.radians(90 - start) 263 | #Calculate the end x,y for the wedge 264 | x_end = cx + r2*math.cos(start) 265 | y_end = cy + r2*math.sin(start) 266 | 267 | #Set the step value for the x,y coordiantes 268 | i = math.radians(0.1) 269 | 270 | points = [] 271 | #Calculate the outer edge of the wedge 272 | a = start 273 | 274 | #If r1 == 0 then create a wedge from the centre point 275 | if r1 == 0: 276 | #Add the start point to the array 277 | points.append([cx,cy]) 278 | #Calculate the rest of the wedge 279 | while a >= end: 280 | X = cx + r2*math.cos(a) 281 | Y = cy + r2*math.sin(a) 282 | 283 | points.append([X,Y]) 284 | a -= i 285 | #Close the polygon 286 | X = cx 287 | Y = cy 288 | 289 | points.append([X,Y]) 290 | 291 | else: 292 | while a >= end: 293 | X = cx + r2*math.cos(a) 294 | Y = cy + r2*math.sin(a) 295 | a -= i 296 | points.append([X,Y]) 297 | 298 | 299 | #Caluclate the inner edge of the wedge 300 | a = end 301 | while a <= start: 302 | a += i ## should this be bofore the calc or after? 303 | X = cx + r1*math.cos(a) 304 | Y = cy + r1*math.sin(a) 305 | 306 | points.append([X,Y]) 307 | 308 | 309 | 310 | #Close the polygon by adding the end point 311 | points.append([x_end,y_end]) 312 | 313 | # build the geom 314 | geom = self._buildGeometry(self._pointString(points),"POLYGON",self.world_merc) 315 | 316 | return geom 317 | 318 | def _readAttributes(self,element): 319 | """reads attrbiutes from 320 | """ 321 | # get all the attributes for the element 322 | attributes = element.attributes 323 | data = [] 324 | # collect all the attributes that could be present for all features 325 | # any not present will be returned as None 326 | # uri 327 | if attributes.get('uri'): 328 | data.append(attributes.get('uri').value) 329 | else: 330 | data.append(None) 331 | # style 332 | if attributes.get('style'): 333 | data.append(attributes.get('style').value) 334 | else: 335 | data.append(None) 336 | # label 337 | # this wil need an addiitonal check to get the content tag for text elements 338 | # as this will be loaded into the label field 339 | if attributes.get('label'): 340 | data.append(attributes.get('label').value) 341 | # reads the contents of any content tags and appends to the text variable 342 | # this may not be the best way to extract data from content and further work 343 | # is needed. 344 | elif element.getElementsByTagName('content'): 345 | content = element.getElementsByTagName('content') 346 | text = '' 347 | for node in content: 348 | text += node.firstChild.data 349 | text += ' ' 350 | data.append(text) 351 | else: 352 | data.append(None) 353 | # symbol 354 | if attributes.get('symbol'): 355 | data.append(attributes.get('symbol').value) 356 | else: 357 | data.append(None) 358 | # modifier(s) not correctly specified in version 1.4 359 | if attributes.get('modifier'): 360 | data.append(attributes.get('modifier').value) 361 | elif attributes.get('modifiers'): 362 | data.append(attributes.get('modifiers').value) 363 | else: 364 | data.append(None) 365 | # course 366 | if attributes.get('course'): 367 | data.append(attributes.get('course').value) 368 | else: 369 | data.append(None) 370 | # speed 371 | if attributes.get('speed'): 372 | data.append(attributes.get('speed').value) 373 | else: 374 | data.append(None) 375 | # width 376 | if attributes.get('width'): 377 | data.append(attributes.get('width').value) 378 | else: 379 | data.append(None) 380 | # minaltitude 381 | if attributes.get('minaltitude'): 382 | data.append(attributes.get('minaltitude').value) 383 | else: 384 | data.append(None) 385 | # maxaltitude 386 | if attributes.get('maxaltitude'): 387 | data.append(attributes.get('maxaltitude').value) 388 | else: 389 | data.append(None) 390 | # parent node 391 | data.append(element.parentNode.nodeName) 392 | return data 393 | 394 | def read(self): 395 | """reads all elements in an NVG into the relevant esri feature types. 396 | 397 | Returns a tuple of 4 lists: points, polylines, polygons, multipoints. 398 | These contain the geometry and atributes for the extracted NVG features. 399 | Each list contains a list for each feature in the form: 400 | [geom,attr1,attr2,...] 401 | This is can be directly inserted into a feature class with the correct schema. 402 | """ 403 | # works through each element type and creates the geometry and extracts 404 | # attributes. The final ouput of this is list of geometries with associated 405 | # attributes. 406 | 407 | # lists for the results 408 | points = [] 409 | polylines = [] 410 | polygons = [] 411 | multipoints = [] 412 | 413 | # read point features 414 | pElems = self._getElement('point') 415 | 416 | # build geometries and get the aributes for each point element 417 | for pElem in pElems: 418 | pGeom = self._buildPoint(pElem.attributes.get('x').value, 419 | pElem.attributes.get('y').value) 420 | pAttrs = self._readAttributes(pElem) 421 | pAttrs.insert(0,pGeom) 422 | points.append(pAttrs) 423 | 424 | # text 425 | tElems = self._getElement('text') 426 | 427 | # build geometries and get the aributes for each text element 428 | for tElem in tElems: 429 | tGeom = self._buildPoint(tElem.attributes.get('x').value, 430 | tElem.attributes.get('y').value) 431 | tAttrs = self._readAttributes(tElem) 432 | tAttrs.insert(0,tGeom) 433 | points.append(tAttrs) 434 | 435 | # polyline 436 | lines = ['polyline','corridor','arc'] 437 | for line in lines: 438 | if line == 'arc': 439 | lnElems = self._getElement(line) 440 | for lnElem in lnElems: 441 | lnGeom = self._buildElliptical(lnElem.attributes.get('cx').value, 442 | lnElem.attributes.get('cy').value, 443 | lnElem.attributes.get('rx').value, 444 | lnElem.attributes.get('ry').value, 445 | lnElem.attributes.get('rotation').value, 446 | lnElem.attributes.get('startangle').value, 447 | lnElem.attributes.get('endangle').value) 448 | lnAttrs = self._readAttributes(lnElem) 449 | lnAttrs.insert(0,lnGeom) 450 | polylines.append(lnAttrs) 451 | 452 | else: 453 | # builds gemetries and reads attributes for polyines and corridor 454 | lnElems = self._getElement(line) 455 | 456 | # build geometries and get the aributes for each text element 457 | for lnElem in lnElems: 458 | lnGeom = self._buildGeometry(lnElem.attributes.get('points').value, 459 | 'POLYLINE',self.wgs84) 460 | lnAttrs = self._readAttributes(lnElem) 461 | lnAttrs.insert(0,lnGeom) 462 | polylines.append(lnAttrs) 463 | 464 | # get polygons, circles, ellipses and arcbands 465 | for polygon in ['polygon','circle','ellipse','arcband']: 466 | if polygon == 'polygon': 467 | polyElems = self._getElement('polygon') 468 | for polyElem in polyElems: 469 | polyGeom = self._buildGeometry(polyElem.attributes.get('points').value, 470 | 'POLYGON',self.wgs84) 471 | polyAttrs = self._readAttributes(polyElem) 472 | polyAttrs.insert(0,polyGeom) 473 | polygons.append(polyAttrs) 474 | elif polygon == 'circle': 475 | circleElems = self._getElement('circle') 476 | for circleElem in circleElems: 477 | circleGeom = self._buildCircle(circleElem.attributes.get('cx').value, 478 | circleElem.attributes.get('cy').value, 479 | circleElem.attributes.get('r').value,) 480 | circleAttrs = self._readAttributes(circleElem) 481 | circleAttrs.insert(0,circleGeom) 482 | polygons.append(circleAttrs) 483 | 484 | elif polygon == 'ellipse': 485 | ellipseElems = self._getElement('ellipse') 486 | for ellipseElem in ellipseElems: 487 | ellipseGeom = self._buildElliptical(ellipseElem.attributes.get('cx').value, 488 | ellipseElem.attributes.get('cy').value, 489 | ellipseElem.attributes.get('rx').value, 490 | ellipseElem.attributes.get('ry').value, 491 | ellipseElem.attributes.get('rotation').value) 492 | ellipseAttrs = self._readAttributes(ellipseElem) 493 | ellipseAttrs.insert(0,ellipseGeom) 494 | polygons.append(ellipseAttrs) 495 | 496 | elif polygon == 'arcband': 497 | arcElems = self._getElement('arcband') 498 | for arcElem in arcElems: 499 | arcGeom = self._buildArcband(arcElem.attributes.get('cx').value, 500 | arcElem.attributes.get('cy').value, 501 | arcElem.attributes.get('minr').value, 502 | arcElem.attributes.get('maxr').value, 503 | arcElem.attributes.get('startangle').value, 504 | arcElem.attributes.get('endangle').value) 505 | arcAttrs = self._readAttributes(arcElem) 506 | arcAttrs.insert(0,arcGeom) 507 | polygons.append(arcAttrs) 508 | # build geometries and get the aributes for each multipoint element 509 | mpElems = self._getElement('multipoint') 510 | for mpElem in mpElems: 511 | mpGeom = self._buildGeometry(mpElem.attributes.get('points').value, 512 | 'MULTIPOINT',self.wgs84) 513 | mpAttrs = self._readAttributes(mpElem) 514 | mpAttrs.insert(0,mpGeom) 515 | multipoints.append(mpAttrs) 516 | 517 | return points, polylines, polygons, multipoints 518 | 519 | if __name__ =="__main__": 520 | # need to handle file paths passed to the scrip that have \t etc in the path 521 | # by default this is not handled correctly 522 | nvg = r"C:\Users\dave\Documents\NVGData\nvg_1_4\APP6A_SAMPLE.nvg" 523 | reader = Reader(nvg,namespaces) 524 | test = reader.read() 525 | print test[3] 526 | 527 | 528 | 529 | 530 | 531 | -------------------------------------------------------------------------------- /nvgWriter.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: nvgWriter.py 3 | # Purpose: Provide a way to import data in NVG format into ArcGIS File 4 | # Geodatabase format. 5 | # 6 | # Author: Dave Barrett 7 | # 8 | # Created: 02/09/2014 9 | # Copyright: (c) Dave 2014 10 | # Licence: 11 | #------------------------------------------------------------------------------- 12 | 13 | """ 14 | This module provides a means to create NVG files from ArcGIS geodatabase feature 15 | classes. 16 | """ 17 | 18 | from xml.etree.ElementTree import ElementTree, Element, SubElement, Comment, tostring 19 | import sys 20 | from xml.dom import minidom 21 | import arcpy 22 | 23 | def prettify(elem): 24 | """Return a pretty-printed XML string for the element 25 | """ 26 | rough_string = tostring(elem, 'utf-8') 27 | reparsed = minidom.parseString(rough_string) 28 | return reparsed.toprettyxml(indent="\t") 29 | 30 | nvg = Element('nvg') 31 | nvg.set('version', '1.4.0') 32 | nvg.set('xmlns','http://tide.act.nato.int/schemas/2008/10/nvg') 33 | ## 34 | ##points = [Element('point',num=str(i)) for i in xrange(100)] 35 | ## 36 | ##nvg.extend(points) 37 | ## 38 | ##print prettify(nvg) 39 | 40 | class Writer(object): 41 | """NATO Vector Graphic Writer instance. Reads ESRI Feature Class into NVG. 42 | """ 43 | 44 | def __init__(self): 45 | """Create the main NVG document element. 46 | """ 47 | # setup the NVG document elments with basic attributes. All features 48 | # will be appended to this. 49 | self.nvg = Element('nvg') 50 | self.nvg.set('version', '1.4.0') 51 | self.nvg.set('xmlns', 'http://tide.act.nato.int/schemas/2008/10/nvg') 52 | #self.nvg.append(Comment('NVG generated by nvgWriter.py')) 53 | 54 | return 55 | 56 | def _generateStyle(self,geometryType,colour=None,width=None,fill=None): 57 | """generates a style string based on the colour, width and fill 58 | parameters. 59 | """ 60 | 61 | # ComBAT specific 62 | # these parameters are used to specify the style information for nvg features 63 | # on the ComBAT system. 64 | 65 | colours = {1: '#000000', 2: '#993333', 3: '#000066', 4: '#336600', 5: '#009900', 66 | 6: '#ccffff', 7: '#c9c9c9', 8: '#ccffcc', 9: '#ffffcc', 10: '#3333cc', 67 | 11: '#868686', 12: '#ff6600', 13: '#ffcccc', 14: '#ff0000', 68 | 15: '#e6e6e6', 16: '#cc9966', 17: '#ffffff', 18: '#ffff00'} 69 | 70 | fills = {1: "fill:none;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};", 71 | 2: "fill:{0};fill-opacity:0.333333;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};", 72 | 3: "fill:{0};fill-opacity:0.686275;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};", 73 | 4: "fill:{0};fill-opacity:0.333333;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};", 74 | 5: "fill:{0};fill-opacity:1.000000;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};"} 75 | 76 | 77 | if geometryType == "Polyline": 78 | try: 79 | style = "stroke:{0};stroke-opacity:1.000000;stroke-width:{1};".format(colours[colour],str(width)) 80 | except: 81 | # default to 1 pt black stroke 82 | style = "stroke:#000000;stroke-opacity:1.000000;stroke-width:1;" 83 | elif geometryType == "Polygon": 84 | try: 85 | # build styles based on the fill pattern selected 86 | style = fills[fill].format(colours[colour],str(width)) 87 | except: 88 | # default to 1 pt black stroke with clear fill 89 | style = "fill:none;stroke:#000000;stroke-opacity:1.000000;stroke-width:1;" 90 | 91 | return style 92 | 93 | def _describe(self,fc): 94 | """Returns an arcpy Describe object. 95 | """ 96 | return arcpy.Describe(fc) 97 | 98 | def _pointString(self,points): 99 | """Returns a string in the format required by NVG for point coordinates. 100 | 101 | This method is used to parse the coordinates from each geometry into a 102 | string sutiable for writing into the NVG file. 103 | """ 104 | s = '' 105 | for pnt in points: 106 | pnt = str(pnt).strip('[]').replace(" ","") 107 | s = s + pnt + " " 108 | 109 | return s.rstrip() 110 | 111 | def _fieldCheck(self,fc): 112 | """Returns True if the required fields are present in the feature class. 113 | 114 | The Label, Colour, Width and Fill fields are required to generate an NVG 115 | file that can be read by ComBAT. 116 | """ 117 | result = False 118 | shapeType = self._describe(fc).shapeType 119 | fields = arcpy.ListFields(fc) 120 | fieldNames = [field.name for field in fields] 121 | 122 | # check required fileds are present 123 | if shapeType == 'Point': 124 | if 'LABEL' in fieldNames: 125 | result = True 126 | elif shapeType == 'Polyline': 127 | lineFields = ['LABEL', 'COLOUR', 'WIDTH'] 128 | setFields = set(lineFields) 129 | intersect = set(fieldNames).intersection(setFields) 130 | if lineFields.sort() == list(intersect).sort(): 131 | result = True 132 | elif shapeType == 'Polygon': 133 | polyFields = ['LABEL', 'COLOUR', 'WIDTH', 'FILL'] 134 | setFields = set(polyFields) 135 | intersect = set(fieldNames).intersection(setFields) 136 | if polyFields.sort() == list(intersect).sort(): 137 | result = True 138 | 139 | return result 140 | 141 | def _getFeatures(self,fc): 142 | """Returns the features and attributes from the input feature class. 143 | """ 144 | pntFields = ['SHAPE@XY', 'LABEL'] 145 | lineFields = ['SHAPE@','LABEL', 'COLOUR', 'WIDTH'] 146 | polyFields = ['SHAPE@','LABEL', 'COLOUR', 'WIDTH', 'FILL'] 147 | 148 | shapeType = self._describe(fc).shapeType 149 | 150 | # check the fields 151 | if self._fieldCheck(fc): 152 | # extract the feature and attribute data 153 | 154 | if shapeType == 'Point': 155 | # read point information 156 | with arcpy.da.SearchCursor(fc,pntFields) as cursor: 157 | for row in cursor: 158 | x = str(row[0][0]) 159 | y = str(row[0][1]) 160 | label = str(row[1]) 161 | 162 | if label is None: 163 | label = "" 164 | # write the point element 165 | self._writeElement('point',x=x,y=y,label=label) 166 | 167 | elif shapeType == 'Polyline': 168 | with arcpy.da.SearchCursor(fc,lineFields) as cursor: 169 | for row in cursor: 170 | geom = eval(row[0].JSON) 171 | 172 | # return a string of point coordinates 173 | points = self._pointString(geom['paths'][0]) 174 | 175 | style = self._generateStyle(shapeType,colour=row[2],width=row[3]) 176 | label = row[1] 177 | if label is None: 178 | label = "" 179 | # create the element 180 | self._writeElement('polyline',points=points,label=label,style=style) 181 | 182 | elif shapeType == 'Polygon': 183 | with arcpy.da.SearchCursor(fc,polyFields) as cursor: 184 | for row in cursor: 185 | geom = eval(row[0].JSON) 186 | 187 | # return a string of point coordinates 188 | points = self._pointString(geom['rings'][0]) 189 | 190 | style = self._generateStyle(shapeType,colour=row[2],width=row[3],fill=row[4]) 191 | label = row[1] 192 | if label is None: 193 | label = "" 194 | 195 | # create the element 196 | self._writeElement('polygon',points=points,label=label,style=style) 197 | 198 | else: 199 | # need to raise an error and terminate the script 200 | raise arcpy.ExecuteError() 201 | return 202 | 203 | def _writeElement(self,element,**kwargs): 204 | """Sets keyword attributes for the supplied element. 205 | 206 | The element is the NVG element to write, for example point, polyline, 207 | polygon. 208 | 209 | **kwargs is a set of key value pairs for each of the attributes. For 210 | example a point will have the attributes x,y and label supplied. 211 | """ 212 | 213 | # creates a cild element of NVG with the element name and attributes 214 | # from the kwargs. Each keyword will become an attribute. 215 | 216 | # currently no checking of the kwargs to determine if they are valid 217 | # for the supplied nvg element. 218 | 219 | # need to ensure that the geometry tags are written first due to ComBAT 220 | # failing to read the items if this is not the case. 221 | 222 | return SubElement(self.nvg,element,kwargs) 223 | 224 | def write(self,inFC,outFile,prettyXML=True): 225 | """Writes the contents of the input feature class(es) to NVG format. 226 | 227 | inFC - can be a single or list of File GeoDatabase Feature Classes. These 228 | should be based on the ComBAT layer pack which contains features 229 | with the appropriate templates for editing in ArcGIS. 230 | outFile - location and filename for the output NVG document. This should 231 | .nvg file extension supplied. 232 | 233 | The method does not currently handle circle and ellipse polygons. This is 234 | due to ArcGIS representing these as curved lines with only 2 points. 235 | Future versions will add support by densifying the lines. 236 | """ 237 | 238 | fcs = list(inFC) 239 | 240 | for fc in fcs: 241 | self._getFeatures(fc) 242 | 243 | if prettyXML: 244 | with open(outFile,'wb') as nvgFile: 245 | nvgFile.write(prettify(self.nvg)) 246 | else: 247 | with open(outFile,'wb') as nvgFile: 248 | ElementTree(self.nvg).write(nvgFile,encoding="UTF-8", xml_declaration=True) 249 | 250 | return True 251 | -------------------------------------------------------------------------------- /toolbox/NVG.WriteNVG.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20150907143254001.0SS Custom MetadataTRUE201509141455561500000005000SSCustomMetadataWrite NVG<DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Loads Polylines and Polygons from the ComBAT layer pack into NVG format for use on ComBAT. Currently Points are not support due to limitations in the ComBAT software. Support for these will be added at a later date. </SPAN></P><P><SPAN STYLE="font-weight:bold;">WARNING: File Geodatabase Circle and Ellipses drawn using the editing tools will create invalid polygons in the output NVG file and will not draw on ComBAT. Support for these will be added later.</SPAN></P></DIV></DIV></DIV>NVGETLLoadArcToolbox Tool<DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Input Polyline and / or Polygon feature classes that will be loaded into an NVG document.</SPAN></P><P><SPAN /></P><P><SPAN STYLE="font-weight:bold;">WARNING: File Geodatabase Circle and Ellipses drawn using the editing tools will create invalid polygons in the output NVG file and will not draw on ComBAT. Support for these will be added later.</SPAN></P></DIV></DIV><DIV><P><SPAN /></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Loads Polylines and Polygons from the ComBAT layer pack into NVG format for use on ComBAT. Currently Points are not support due to limitations in the ComBAT software. Support for these will be added at a later date. </SPAN></P><P><SPAN STYLE="font-weight:bold;">WARNING: File Geodatabase Circle and Ellipses drawn using the editing tools will create invalid polygons in the output NVG file and will not draw on ComBAT. Support for these will be added later.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Load one of mre feature class(es) into NVG format.</SPAN></P><UL><LI><P><SPAN STYLE="font-weight:bold;">Input Feature Class(es)</SPAN><SPAN>- should be lines or polygons created using the ComBAT Layer Pack. Failure to use this will result in the tool failing to find the fields required.</SPAN></P></LI><LI><P><SPAN STYLE="font-weight:bold;">Output NVG File</SPAN><SPAN>- loaction and name of the output nvg file. All features will be loaded into this single file. </SPAN></P></LI><LI><P><SPAN>To load features into seperate NVG files only specify 1 </SPAN><SPAN STYLE="font-weight:bold;">Input Feature Class</SPAN><SPAN>.</SPAN></P></LI></UL></DIV></DIV></DIV> 3 | -------------------------------------------------------------------------------- /toolbox/NVG.loadNVG.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20150812160937001.0SS Custom MetadataTRUE201509141455561500000005000SSCustomMetadataLoad NVG<DIV STYLE="text-align:Left;"><DIV><P><SPAN>Loads features from 1 or more NATO Vector Graphics documents into Feature Classes</SPAN></P></DIV></DIV>NVGLoadArcToolbox Tool<DIV STYLE="text-align:Left;"><DIV><P><SPAN>The NVG file(s) to be loaded. The name of the file will be used to generate a unique output featrue class name with point, polyline, polygon and multipoint appended.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>The file geodatabase to save the features classes to.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>Loads features from 1 or more NATO Vector Graphics documents into Feature Classes</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><UL><LI><P><SPAN>Loads one or more valid NATO Vector Graphics file in File GDB Feature Classes.</SPAN></P></LI><LI><P><SPAN>The </SPAN><SPAN STYLE="font-weight:bold;">output Geodatabase</SPAN><SPAN> must be a file geodatabase</SPAN></P></LI><LI><P><SPAN>For each NVG file four output feature classes are generated.</SPAN></P></LI><LI><P><SPAN>A feature class is genereated for each type of POINT, POLYLINE, POLYGON or MULTIPOINT even if no features of that type are found.</SPAN></P></LI></UL></DIV></DIV> 3 | -------------------------------------------------------------------------------- /toolbox/NVG.pyt: -------------------------------------------------------------------------------- 1 | import arcpy, os 2 | import nvgReader 3 | import nvgWriter 4 | 5 | 6 | class Toolbox(object): 7 | def __init__(self): 8 | """Define the toolbox (the name of the toolbox is the name of the 9 | .pyt file).""" 10 | self.label = "NATO Vector Graphics" 11 | self.alias = "nvg" 12 | 13 | # List of tool classes associated with this toolbox 14 | self.tools = [LoadNVG,WriteNVG] 15 | 16 | 17 | class LoadNVG(object): 18 | def __init__(self): 19 | """Define the tool (tool name is the name of the class).""" 20 | self.label = "Load NVG" 21 | self.description = "Loads features from NVG version 1.4.0 files into feature classes." 22 | self.canRunInBackground = False 23 | 24 | def getParameterInfo(self): 25 | """Define parameter definitions""" 26 | param0 = arcpy.Parameter( 27 | displayName="Input NVG File", 28 | name="in_nvg", 29 | datatype="DEFile", 30 | parameterType="Required", 31 | direction="Input", 32 | multiValue=True) 33 | param1 = arcpy.Parameter( 34 | displayName="Output File Geodatabase", 35 | name="outGDB", 36 | datatype="DEWorkspace", 37 | parameterType="Required", 38 | direction="Input") 39 | 40 | param0.filter.list = ['nvg'] 41 | param1.filter.list = ["Local Database"] 42 | 43 | params = [param0,param1] 44 | return params 45 | 46 | def isLicensed(self): 47 | """Set whether tool is licensed to execute.""" 48 | return True 49 | 50 | def updateParameters(self, parameters): 51 | """Modify the values and properties of parameters before internal 52 | validation is performed. This method is called whenever a parameter 53 | has been changed.""" 54 | return 55 | 56 | def updateMessages(self, parameters): 57 | """Modify the messages created by internal validation for each tool 58 | parameter. This method is called after internal validation.""" 59 | return 60 | 61 | def execute(self, parameters, messages): 62 | """The source code of the tool.""" 63 | nvgs = (parameters[0].valueAsText).split(';') 64 | gdb = parameters[1].valueAsText 65 | sr = arcpy.SpatialReference(4326) 66 | 67 | # define the fields to be added to output feature classes. 68 | # SHAPE@ field used for the insert cursor only. 69 | fields = ["SHAPE@","uri","style","label","symbol","modifiers","course", 70 | "speed","width","min_alt","max_alt","parentNode"] 71 | 72 | for nvg in nvgs: 73 | # read the nvg file 74 | messages.addMessage("Reading features from: " + nvg) 75 | 76 | reader = nvgReader.Reader(nvg) 77 | points,polylines,polygons,multipoints = reader.read() 78 | 79 | # this should be an attribute of the Reader Class 80 | # probably in a statistics method. 81 | totalFeats = len(points) + len(polylines) + len(polygons) + len(multipoints) 82 | 83 | messages.addMessage("Read: " + str(totalFeats) + " NVG Features") 84 | 85 | 86 | featureTypes = ['point','polyline','polygon','multipoint'] 87 | 88 | fcs = [] 89 | # create the feature classes 90 | for fType in featureTypes: 91 | # create the output name 92 | name = os.path.basename(nvg) + "_" + fType 93 | outName = arcpy.ValidateTableName(name,gdb) 94 | outName = arcpy.CreateUniqueName(outName,gdb) 95 | 96 | outFC = arcpy.CreateFeatureclass_management(gdb,os.path.basename(outName),fType.upper(),spatial_reference=sr) 97 | # add the required fields 98 | for field in fields[1:]: 99 | arcpy.AddField_management(outFC,field,"TEXT",field_length=255) 100 | fcs.append(outFC) 101 | 102 | 103 | # load the features into each feature class 104 | for fc in fcs: 105 | fcName = arcpy.Describe(fc).baseName 106 | # this picks up multipoints as 107 | if '_point' in fcName: 108 | messages.addMessage("Loading: " + str(len(points)) + " Points into: " + fcName) 109 | cursor = arcpy.da.InsertCursor(fc,fields) 110 | for point in points: 111 | cursor.insertRow(point) 112 | 113 | elif '_polyline' in fcName: 114 | messages.addMessage("Loading: " + str(len(polylines)) + " Polylines into: " + fcName) 115 | cursor = arcpy.da.InsertCursor(fc,fields) 116 | for polyline in polylines: 117 | cursor.insertRow(polyline) 118 | 119 | elif '_polygon' in fcName: 120 | messages.addMessage("Loading: " + str(len(polygons)) + " Polygons into: " + fcName) 121 | cursor = arcpy.da.InsertCursor(fc,fields) 122 | for polygon in polygons: 123 | cursor.insertRow(polygon) 124 | 125 | elif '_multipoint' in fcName: 126 | messages.addMessage("Loading: " + str(len(multipoints)) + " Multipoints into: " + fcName) 127 | cursor = arcpy.da.InsertCursor(fc,fields) 128 | for multipoint in multipoints: 129 | cursor.insertRow(multipoint) 130 | 131 | del cursor 132 | 133 | return 134 | 135 | 136 | class WriteNVG(object): 137 | def __init__(self): 138 | """Define the tool (tool name is the name of the class).""" 139 | self.label = "Write NVG" 140 | self.description = """Writes features from 1 or more feature class into 141 | NVG version 1.4.0 file. The input features must be 142 | from the ComBAT Layer pack supplied with this tool 143 | to ensure that the features are loaded correctly.""" 144 | self.canRunInBackground = False 145 | 146 | def getParameterInfo(self): 147 | """Define parameter definitions""" 148 | param0 = arcpy.Parameter( 149 | displayName="Input Feature Class(es) File", 150 | name="in_fcs", 151 | datatype="GPFeatureLayer", 152 | parameterType="Required", 153 | direction="Input", 154 | multiValue=True) 155 | param1 = arcpy.Parameter( 156 | displayName="Output NVG File", 157 | name="outNVG", 158 | datatype="DEFile", 159 | parameterType="Required", 160 | direction="Output") 161 | 162 | param0.filter.list = ['Polygon','Polyline'] 163 | param1.filter.list = ['nvg'] 164 | 165 | params = [param0,param1] 166 | return params 167 | 168 | def isLicensed(self): 169 | """Set whether tool is licensed to execute.""" 170 | return True 171 | 172 | def updateParameters(self, parameters): 173 | """Modify the values and properties of parameters before internal 174 | validation is performed. This method is called whenever a parameter 175 | has been changed.""" 176 | return 177 | 178 | def updateMessages(self, parameters): 179 | """Modify the messages created by internal validation for each tool 180 | parameter. This method is called after internal validation.""" 181 | return 182 | 183 | def execute(self, parameters, messages): 184 | """The source code of the tool.""" 185 | fcs = (parameters[0].valueAsText).split(';') 186 | outFile = parameters[1].valueAsText 187 | 188 | writer = nvgWriter.Writer() 189 | writer.write(fcs,outFile,prettyXML=True) 190 | 191 | return 192 | -------------------------------------------------------------------------------- /toolbox/NVG.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20150812160936001.0SS Custom MetadataTRUE20150914145552c:\arcgis\desktop10.1\Help\gpNVGArcToolbox Toolbox 3 | -------------------------------------------------------------------------------- /toolbox/nvgReader.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: nvgReader.py 3 | # Purpose: Provide a way to import data in NVG format into ArcGIS File 4 | # Geodatabase format. 5 | # 6 | # Author: Dave Barrett 7 | # 8 | # Created: 16/09/2014 9 | # Copyright: (c) Dave 2014 10 | # Licence: 11 | #------------------------------------------------------------------------------- 12 | 13 | """ 14 | This module provides a number of functions and classes to read a NVG file into 15 | an ESRI ArcGIS file geodatabase. This module requires a licensed copy of ArcGIS 16 | in order to have access to the functions found in the arcpy site package. 17 | 18 | The implementation proivded here attempts to read all mandatory properties from 19 | the NVG specification for version 1.4, future versions of these tools will 20 | include support for future versions as required. 21 | """ 22 | import xml.dom.minidom 23 | import arcpy 24 | import math 25 | 26 | # namespace based on the version of the NVG document. 27 | namespaces = {'1.4.0': 'http://tide.act.nato.int/schemas/2008/10/nvg', 28 | '1.5.0': 'http://tide.act.nato.int/schemas/2009/10/nvg', 29 | '2.0.0': 'https://tide.act.nato.int/schemas/2012/10/nvg'} 30 | 31 | # , and features not yet implemented 32 | 33 | def geo2arithetic(inAngle): 34 | """converts a bearing to aritmetic angle. 35 | """ 36 | outAngle = -1.0 37 | # Force input angle into 0 to 360 range 38 | if inAngle > 360.0: 39 | inAngle = math.fmod(inAngle,360.0) 40 | 41 | # if anlge is 360 make it 0 42 | if inAngle == 360.0: 43 | inAngle = 0.0 44 | 45 | #0 to 90 46 | if (inAngle >= 0.0 and inAngle <= 90.0): 47 | outAngle = math.fabs(inAngle - 90.0) 48 | 49 | #90 to 360 50 | if (inAngle > 90.0 and inAngle < 360.0): 51 | outAngle = 360.0 - (inAngle - 90.0) 52 | 53 | return outAngle 54 | 55 | class Reader(object): 56 | """NATO Vector Graphic Reader instance. Reads and processes a NATO Vector 57 | Graphic to ESRI Geometry. 58 | """ 59 | def __init__(self,nvgFile): 60 | """Initiate the object and set the basic attributes 61 | """ 62 | self.nvgFile = nvgFile 63 | 64 | # parse the nvgFile 65 | self.dom = xml.dom.minidom.parse(self.nvgFile) 66 | # namespace based on the version of the NVG document. 67 | self.namespaces = {'1.4.0': 'http://tide.act.nato.int/schemas/2008/10/nvg', 68 | '1.5.0': 'http://tide.act.nato.int/schemas/2009/10/nvg', 69 | '2.0.0': 'https://tide.act.nato.int/schemas/2012/10/nvg'} 70 | 71 | # get the nvg version 72 | # consider moving this to a seperate method 73 | self.version = self.dom.documentElement.getAttribute("version") 74 | 75 | # update the namespace based on the version of the document 76 | if self.version == '1.4.0': 77 | self.namespace = self.namespaces['1.4.0'] 78 | elif self.version == '1.5.0': 79 | self.namespace = self.namespaces['1.5.0'] 80 | elif self.version == '2.0.0': 81 | self.namespace = self.namespaces['2.0.0'] 82 | 83 | # need to define the outputs based on the datatypes in the nvg 84 | self.esriPolygon = [] 85 | self.esriPolyline = [] 86 | self.esriPoint = [] 87 | self.esriMultiPoint = [] 88 | 89 | # define Spatial Reference Objects 90 | self.wgs84 = arcpy.SpatialReference(4326) 91 | self.world_merc = arcpy.SpatialReference(3395) 92 | 93 | return 94 | 95 | 96 | def _getElement(self,tag): 97 | """Return all elements with given tag with the correct namespace for the 98 | version of NVG. 99 | """ 100 | return self.dom.getElementsByTagNameNS(self.namespace,tag) 101 | 102 | def _cleanPoints(self,points): 103 | """Cleans a string of point coordinate pairs and returns a list of 104 | numerical coordinate pairs 105 | """ 106 | # get a list of coord pairs as strings from points 107 | # [['x1','y1'],['x2','y2'],[...]] 108 | listPoints = [p.split(",") for p in points.strip().split(" ")] 109 | 110 | # convert each coord to float 111 | 112 | fPoints = [[float(x) for x in row] for row in listPoints] 113 | 114 | return fPoints 115 | 116 | def _projectGeometry(self,geometry, spatial_refernce): 117 | """Projects the input geometry into the spatial_reference 118 | 119 | THis is used to ensure the maths works on the geometry functions 120 | """ 121 | projected = geometry.projectAs(spatial_refernce) 122 | 123 | return projected 124 | 125 | def _buildPoint(self,x,y): 126 | """build a point geometry from x,y coordinates. 127 | """ 128 | # construct the geometry 129 | pnt = arcpy.Point(x,y) 130 | pGeom = arcpy.PointGeometry(pnt,self.wgs84) 131 | 132 | return pGeom 133 | 134 | def _buildGeometry(self,points,geometry_type,spatial_reference): 135 | """Builds the relevant geometry from a string of points based on the 136 | geometry_type and spatial_reference. 137 | 138 | Valid Geometyr Types are: 139 | POLYGON 140 | POLYLINE 141 | MULTIPOINT 142 | 143 | Valid Spatial References are 144 | self.wgs84 - used for all default shapes 145 | self.world_merc - used for ellipses, arcs, and arcbands 146 | 147 | the returned geometry will always be projected as wgs84 148 | """ 149 | # clean the point string 150 | cPoints = self._cleanPoints(points) 151 | 152 | # array to hold point objects 153 | array = arcpy.Array() 154 | 155 | for point in cPoints: 156 | pnt = arcpy.Point() 157 | pnt.X = point[0] 158 | pnt.Y = point[1] 159 | array.add(pnt) 160 | 161 | if geometry_type == 'POLYGON': 162 | geom = arcpy.Polygon(array,spatial_reference) 163 | elif geometry_type == 'POLYLINE': 164 | geom = arcpy.Polyline(array,spatial_reference) 165 | elif geometry_type == 'MULTIPOINT': 166 | geom = arcpy.Multipoint(array,spatial_reference) 167 | 168 | # ensure final geom is returned in wgs84 169 | if geom.spatialReference.factoryCode != 4326: 170 | geom = self._projectGeometry(geom,self.wgs84) 171 | return geom 172 | 173 | def _pointString(self,points): 174 | """Returns a string in the format required by NVG for point coordinates. 175 | 176 | This method is used to parse the output of _buildElliptical and _buildCircular 177 | into a format used by the geometry construction methods. 178 | """ 179 | s = '' 180 | for pnt in points: 181 | pnt = str(pnt).strip('[]').replace(" ","") 182 | s = s + pnt + " " 183 | 184 | return s 185 | 186 | def _buildElliptical(self,cx,cy,rx,ry,rotation,startangle=0,endangle=360): 187 | """Generates a set of point cordinates that describe an ellipse or an arc. 188 | 189 | Coordinates need to be projected before using the tools. 190 | """ 191 | points = [] 192 | # projects the cx,cy to world mercator 193 | pGeom = arcpy.PointGeometry(arcpy.Point(cx,cy),self.wgs84) 194 | centrePnt = self._projectGeometry(pGeom,self.world_merc) 195 | cX = centrePnt.firstPoint.X 196 | cY = centrePnt.firstPoint.Y 197 | rotation = math.radians(float(rotation)) 198 | step = 1 199 | startangle = float(startangle) 200 | endangle = float(endangle) 201 | 202 | if startangle > endangle: 203 | endangle=endangle + 360 204 | 205 | # generate points and rotate 206 | for theata in range(int(startangle),int(endangle),step): 207 | #caclulate points on the ellipse 208 | theata = math.radians(float(theata)) 209 | X = float(rx) * math.cos(theata) 210 | Y = float(ry) * math.sin(theata) 211 | 212 | # rotate point around the centre 213 | rotX = cX + X * math.cos(rotation) + Y * math.sin(rotation) 214 | rotY = cY - X * math.sin(rotation) + Y * math.cos(rotation) 215 | 216 | points.append([rotX,rotY]) 217 | 218 | # build the geometry 219 | if startangle != 0 or endangle != 360: 220 | geom = self._buildGeometry(self._pointString(points),"POLYLINE",self.world_merc) 221 | else: 222 | geom = self._buildGeometry(self._pointString(points),"POLYGON",self.world_merc) 223 | 224 | return geom 225 | 226 | def _buildCircle(self,cx,cy,r): 227 | """Returns arcpy.Polygon circle from the cx, cy and radius. 228 | 229 | The radius needs to be in the same units as the cx,cy location. 230 | """ 231 | # may need to move the projection code to a seperate method as it will 232 | # need to be done multiple times 233 | 234 | # project the point to world mercator 235 | pGeom_wgs84 = arcpy.PointGeometry(arcpy.Point(cx,cy),self.wgs84) 236 | pGeom_WM = self._projectGeometry(pGeom_wgs84,self.world_merc) 237 | 238 | # buffer the point by the radius 239 | polygon_wm = pGeom_WM.buffer(float(r)) 240 | # return the polygon in wgs84 geographics 241 | polygon = self._projectGeometry(polygon_wm,self.wgs84) 242 | 243 | return polygon 244 | 245 | def _buildArcband(self,cx,cy,minr,maxr,start,end): 246 | """Builds a wedge describing an area between two concentric circles. 247 | """ 248 | # project the point to metres 249 | pGeom = arcpy.PointGeometry(arcpy.Point(cx,cy),self.wgs84) 250 | centrePnt = self._projectGeometry(pGeom,self.world_merc) 251 | cx = centrePnt.firstPoint.X 252 | cy = centrePnt.firstPoint.Y 253 | 254 | # convert values to float 255 | r1 = float(minr) 256 | r2 = float(maxr) 257 | start = float(start) 258 | end = float(end) 259 | 260 | # convert the bearings from north to maths for use in the coordinate calculations 261 | #start = math.radians(90 - start) 262 | if start > end: 263 | end = end + 360 264 | end = math.radians(90 - end) 265 | else: 266 | end = math.radians(90 - end) 267 | start = math.radians(90 - start) 268 | #Calculate the end x,y for the wedge 269 | x_end = cx + r2*math.cos(start) 270 | y_end = cy + r2*math.sin(start) 271 | 272 | #Set the step value for the x,y coordiantes 273 | i = math.radians(0.1) 274 | 275 | points = [] 276 | #Calculate the outer edge of the wedge 277 | a = start 278 | 279 | #If r1 == 0 then create a wedge from the centre point 280 | if r1 == 0: 281 | #Add the start point to the array 282 | points.append([cx,cy]) 283 | #Calculate the rest of the wedge 284 | while a >= end: 285 | X = cx + r2*math.cos(a) 286 | Y = cy + r2*math.sin(a) 287 | 288 | points.append([X,Y]) 289 | a -= i 290 | #Close the polygon 291 | X = cx 292 | Y = cy 293 | 294 | points.append([X,Y]) 295 | 296 | else: 297 | while a >= end: 298 | X = cx + r2*math.cos(a) 299 | Y = cy + r2*math.sin(a) 300 | a -= i 301 | points.append([X,Y]) 302 | 303 | 304 | #Caluclate the inner edge of the wedge 305 | a = end 306 | while a <= start: 307 | a += i ## should this be bofore the calc or after? 308 | X = cx + r1*math.cos(a) 309 | Y = cy + r1*math.sin(a) 310 | 311 | points.append([X,Y]) 312 | 313 | 314 | 315 | #Close the polygon by adding the end point 316 | points.append([x_end,y_end]) 317 | 318 | # build the geom 319 | geom = self._buildGeometry(self._pointString(points),"POLYGON",self.world_merc) 320 | 321 | return geom 322 | 323 | def _readAttributes(self,element): 324 | """reads attrbiutes from 325 | """ 326 | # get all the attributes for the element 327 | attributes = element.attributes 328 | data = [] 329 | # collect all the attributes that could be present for all features 330 | # any not present will be returned as None 331 | # uri 332 | if attributes.get('uri'): 333 | data.append(attributes.get('uri').value) 334 | else: 335 | data.append(None) 336 | # style 337 | if attributes.get('style'): 338 | data.append(attributes.get('style').value) 339 | else: 340 | data.append(None) 341 | # label 342 | # this wil need an addiitonal check to get the content tag for text elements 343 | # as this will be loaded into the label field 344 | if attributes.get('label'): 345 | data.append(attributes.get('label').value) 346 | # reads the contents of any content tags and appends to the text variable 347 | # this may not be the best way to extract data from content and further work 348 | # is needed. 349 | elif element.getElementsByTagName('content'): 350 | content = element.getElementsByTagName('content') 351 | text = '' 352 | for node in content: 353 | text += node.firstChild.data 354 | text += ' ' 355 | data.append(text) 356 | else: 357 | data.append(None) 358 | # symbol 359 | if attributes.get('symbol'): 360 | data.append(attributes.get('symbol').value) 361 | else: 362 | data.append(None) 363 | # modifier(s) not correctly specified in version 1.4 364 | if attributes.get('modifier'): 365 | data.append(attributes.get('modifier').value) 366 | elif attributes.get('modifiers'): 367 | data.append(attributes.get('modifiers').value) 368 | else: 369 | data.append(None) 370 | # course 371 | if attributes.get('course'): 372 | data.append(attributes.get('course').value) 373 | else: 374 | data.append(None) 375 | # speed 376 | if attributes.get('speed'): 377 | data.append(attributes.get('speed').value) 378 | else: 379 | data.append(None) 380 | # width 381 | if attributes.get('width'): 382 | data.append(attributes.get('width').value) 383 | else: 384 | data.append(None) 385 | # minaltitude 386 | if attributes.get('minaltitude'): 387 | data.append(attributes.get('minaltitude').value) 388 | else: 389 | data.append(None) 390 | # maxaltitude 391 | if attributes.get('maxaltitude'): 392 | data.append(attributes.get('maxaltitude').value) 393 | else: 394 | data.append(None) 395 | # parent node 396 | data.append(element.parentNode.nodeName) 397 | return data 398 | 399 | def read(self): 400 | """reads all elements in an NVG into the relevant esri feature types. 401 | 402 | Returns a tuple of 4 lists: points, polylines, polygons, multipoints. 403 | These contain the geometry and atributes for the extracted NVG features. 404 | Each list contains a list for each feature in the form: 405 | [geom,attr1,attr2,...] 406 | This is can be directly inserted into a feature class with the correct schema. 407 | """ 408 | # works through each element type and creates the geometry and extracts 409 | # attributes. The final ouput of this is list of geometries with associated 410 | # attributes. 411 | 412 | # lists for the results 413 | points = [] 414 | polylines = [] 415 | polygons = [] 416 | multipoints = [] 417 | 418 | # read point features 419 | pElems = self._getElement('point') 420 | 421 | # build geometries and get the aributes for each point element 422 | for pElem in pElems: 423 | pGeom = self._buildPoint(pElem.attributes.get('x').value, 424 | pElem.attributes.get('y').value) 425 | pAttrs = self._readAttributes(pElem) 426 | pAttrs.insert(0,pGeom) 427 | points.append(pAttrs) 428 | 429 | # text 430 | tElems = self._getElement('text') 431 | 432 | # build geometries and get the aributes for each text element 433 | for tElem in tElems: 434 | tGeom = self._buildPoint(tElem.attributes.get('x').value, 435 | tElem.attributes.get('y').value) 436 | tAttrs = self._readAttributes(tElem) 437 | tAttrs.insert(0,tGeom) 438 | points.append(tAttrs) 439 | 440 | # polyline 441 | lines = ['polyline','corridor','arc'] 442 | for line in lines: 443 | if line == 'arc': 444 | lnElems = self._getElement(line) 445 | for lnElem in lnElems: 446 | lnGeom = self._buildElliptical(lnElem.attributes.get('cx').value, 447 | lnElem.attributes.get('cy').value, 448 | lnElem.attributes.get('rx').value, 449 | lnElem.attributes.get('ry').value, 450 | lnElem.attributes.get('rotation').value, 451 | lnElem.attributes.get('startangle').value, 452 | lnElem.attributes.get('endangle').value) 453 | lnAttrs = self._readAttributes(lnElem) 454 | lnAttrs.insert(0,lnGeom) 455 | polylines.append(lnAttrs) 456 | 457 | else: 458 | # builds gemetries and reads attributes for polyines and corridor 459 | lnElems = self._getElement(line) 460 | 461 | # build geometries and get the aributes for each text element 462 | for lnElem in lnElems: 463 | lnGeom = self._buildGeometry(lnElem.attributes.get('points').value, 464 | 'POLYLINE',self.wgs84) 465 | lnAttrs = self._readAttributes(lnElem) 466 | lnAttrs.insert(0,lnGeom) 467 | polylines.append(lnAttrs) 468 | 469 | # get polygons, circles, ellipses and arcbands 470 | for polygon in ['polygon','circle','ellipse','arcband']: 471 | if polygon == 'polygon': 472 | polyElems = self._getElement('polygon') 473 | for polyElem in polyElems: 474 | polyGeom = self._buildGeometry(polyElem.attributes.get('points').value, 475 | 'POLYGON',self.wgs84) 476 | polyAttrs = self._readAttributes(polyElem) 477 | polyAttrs.insert(0,polyGeom) 478 | polygons.append(polyAttrs) 479 | elif polygon == 'circle': 480 | circleElems = self._getElement('circle') 481 | for circleElem in circleElems: 482 | circleGeom = self._buildCircle(circleElem.attributes.get('cx').value, 483 | circleElem.attributes.get('cy').value, 484 | circleElem.attributes.get('r').value,) 485 | circleAttrs = self._readAttributes(circleElem) 486 | circleAttrs.insert(0,circleGeom) 487 | polygons.append(circleAttrs) 488 | 489 | elif polygon == 'ellipse': 490 | ellipseElems = self._getElement('ellipse') 491 | for ellipseElem in ellipseElems: 492 | ellipseGeom = self._buildElliptical(ellipseElem.attributes.get('cx').value, 493 | ellipseElem.attributes.get('cy').value, 494 | ellipseElem.attributes.get('rx').value, 495 | ellipseElem.attributes.get('ry').value, 496 | ellipseElem.attributes.get('rotation').value) 497 | ellipseAttrs = self._readAttributes(ellipseElem) 498 | ellipseAttrs.insert(0,ellipseGeom) 499 | polygons.append(ellipseAttrs) 500 | 501 | elif polygon == 'arcband': 502 | arcElems = self._getElement('arcband') 503 | for arcElem in arcElems: 504 | arcGeom = self._buildArcband(arcElem.attributes.get('cx').value, 505 | arcElem.attributes.get('cy').value, 506 | arcElem.attributes.get('minr').value, 507 | arcElem.attributes.get('maxr').value, 508 | arcElem.attributes.get('startangle').value, 509 | arcElem.attributes.get('endangle').value) 510 | arcAttrs = self._readAttributes(arcElem) 511 | arcAttrs.insert(0,arcGeom) 512 | polygons.append(arcAttrs) 513 | # build geometries and get the aributes for each multipoint element 514 | mpElems = self._getElement('multipoint') 515 | for mpElem in mpElems: 516 | mpGeom = self._buildGeometry(mpElem.attributes.get('points').value, 517 | 'MULTIPOINT',self.wgs84) 518 | mpAttrs = self._readAttributes(mpElem) 519 | mpAttrs.insert(0,mpGeom) 520 | multipoints.append(mpAttrs) 521 | 522 | return points, polylines, polygons, multipoints 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | -------------------------------------------------------------------------------- /toolbox/nvgWriter.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: nvgWriter.py 3 | # Purpose: Provide a way to import data in NVG format into ArcGIS File 4 | # Geodatabase format. 5 | # 6 | # Author: Dave Barrett 7 | # 8 | # Created: 02/09/2014 9 | # Copyright: (c) Dave 2014 10 | # Licence: 11 | #------------------------------------------------------------------------------- 12 | 13 | """ 14 | This module provides a means to create NVG files from ArcGIS geodatabase feature 15 | classes. 16 | """ 17 | 18 | from xml.etree.ElementTree import ElementTree, Element, SubElement, Comment, tostring 19 | import sys 20 | from xml.dom import minidom 21 | import arcpy 22 | 23 | def prettify(elem): 24 | """Return a pretty-printed XML string for the element 25 | """ 26 | rough_string = tostring(elem, 'utf-8') 27 | reparsed = minidom.parseString(rough_string) 28 | return reparsed.toprettyxml(indent="\t") 29 | 30 | nvg = Element('nvg') 31 | nvg.set('version', '1.4.0') 32 | nvg.set('xmlns','http://tide.act.nato.int/schemas/2008/10/nvg') 33 | ## 34 | ##points = [Element('point',num=str(i)) for i in xrange(100)] 35 | ## 36 | ##nvg.extend(points) 37 | ## 38 | ##print prettify(nvg) 39 | 40 | class Writer(object): 41 | """NATO Vector Graphic Writer instance. Reads ESRI Feature Class into NVG. 42 | """ 43 | 44 | def __init__(self): 45 | """Create the main NVG document element. 46 | """ 47 | # setup the NVG document elments with basic attributes. All features 48 | # will be appended to this. 49 | self.nvg = Element('nvg') 50 | self.nvg.set('version', '1.4.0') 51 | self.nvg.set('xmlns', 'http://tide.act.nato.int/schemas/2008/10/nvg') 52 | #self.nvg.append(Comment('NVG generated by nvgWriter.py')) 53 | 54 | return 55 | 56 | def _generateStyle(self,geometryType,colour=None,width=None,fill=None): 57 | """generates a style string based on the colour, width and fill 58 | parameters. 59 | """ 60 | 61 | # ComBAT specific 62 | # these parameters are used to specify the style information for nvg features 63 | # on the ComBAT system. 64 | 65 | colours = {1: '#000000', 2: '#993333', 3: '#000066', 4: '#336600', 5: '#009900', 66 | 6: '#ccffff', 7: '#c9c9c9', 8: '#ccffcc', 9: '#ffffcc', 10: '#3333cc', 67 | 11: '#868686', 12: '#ff6600', 13: '#ffcccc', 14: '#ff0000', 68 | 15: '#e6e6e6', 16: '#cc9966', 17: '#ffffff', 18: '#ffff00'} 69 | 70 | fills = {1: "fill:none;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};", 71 | 2: "fill:{0};fill-opacity:0.333333;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};", 72 | 3: "fill:{0};fill-opacity:0.686275;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};", 73 | 4: "fill:{0};fill-opacity:0.333333;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};", 74 | 5: "fill:{0};fill-opacity:1.000000;stroke:{0};stroke-opacity:1.000000;stroke-width:{1};"} 75 | 76 | 77 | if geometryType == "Polyline": 78 | try: 79 | style = "stroke:{0};stroke-opacity:1.000000;stroke-width:{1};".format(colours[colour],str(width)) 80 | except: 81 | # default to 1 pt black stroke 82 | style = "stroke:#000000;stroke-opacity:1.000000;stroke-width:1;" 83 | elif geometryType == "Polygon": 84 | try: 85 | # build styles based on the fill pattern selected 86 | style = fills[fill].format(colours[colour],str(width)) 87 | except: 88 | # default to 1 pt black stroke with clear fill 89 | style = "fill:none;stroke:#000000;stroke-opacity:1.000000;stroke-width:1;" 90 | 91 | return style 92 | 93 | def _describe(self,fc): 94 | """Returns an arcpy Describe object. 95 | """ 96 | return arcpy.Describe(fc) 97 | 98 | def _pointString(self,points): 99 | """Returns a string in the format required by NVG for point coordinates. 100 | 101 | This method is used to parse the coordinates from each geometry into a 102 | string sutiable for writing into the NVG file. 103 | """ 104 | s = '' 105 | for pnt in points: 106 | pnt = str(pnt).strip('[]').replace(" ","") 107 | s = s + pnt + " " 108 | 109 | return s.rstrip() 110 | 111 | def _fieldCheck(self,fc): 112 | """Returns True if the required fields are present in the feature class. 113 | 114 | The Label, Colour, Width and Fill fields are required to generate an NVG 115 | file that can be read by ComBAT. 116 | """ 117 | result = False 118 | shapeType = self._describe(fc).shapeType 119 | fields = arcpy.ListFields(fc) 120 | fieldNames = [field.name for field in fields] 121 | 122 | # check required fileds are present 123 | if shapeType == 'Point': 124 | if 'LABEL' in fieldNames: 125 | result = True 126 | elif shapeType == 'Polyline': 127 | lineFields = ['LABEL', 'COLOUR', 'WIDTH'] 128 | setFields = set(lineFields) 129 | intersect = set(fieldNames).intersection(setFields) 130 | if lineFields.sort() == list(intersect).sort(): 131 | result = True 132 | elif shapeType == 'Polygon': 133 | polyFields = ['LABEL', 'COLOUR', 'WIDTH', 'FILL'] 134 | setFields = set(polyFields) 135 | intersect = set(fieldNames).intersection(setFields) 136 | if polyFields.sort() == list(intersect).sort(): 137 | result = True 138 | 139 | return result 140 | 141 | def _getFeatures(self,fc): 142 | """Returns the features and attributes from the input feature class. 143 | """ 144 | pntFields = ['SHAPE@XY', 'LABEL'] 145 | lineFields = ['SHAPE@','LABEL', 'COLOUR', 'WIDTH'] 146 | polyFields = ['SHAPE@','LABEL', 'COLOUR', 'WIDTH', 'FILL'] 147 | 148 | shapeType = self._describe(fc).shapeType 149 | 150 | # check the fields 151 | if self._fieldCheck(fc): 152 | # extract the feature and attribute data 153 | 154 | if shapeType == 'Point': 155 | # read point information 156 | with arcpy.da.SearchCursor(fc,pntFields) as cursor: 157 | for row in cursor: 158 | x = str(row[0][0]) 159 | y = str(row[0][1]) 160 | label = str(row[1]) 161 | 162 | if label is None: 163 | label = "" 164 | # write the point element 165 | self._writeElement('point',x=x,y=y,label=label) 166 | 167 | elif shapeType == 'Polyline': 168 | with arcpy.da.SearchCursor(fc,lineFields) as cursor: 169 | for row in cursor: 170 | geom = eval(row[0].JSON) 171 | 172 | # return a string of point coordinates 173 | points = self._pointString(geom['paths'][0]) 174 | 175 | style = self._generateStyle(shapeType,colour=row[2],width=row[3]) 176 | label = row[1] 177 | if label is None: 178 | label = "" 179 | # create the element 180 | self._writeElement('polyline',points=points,label=label,style=style) 181 | 182 | elif shapeType == 'Polygon': 183 | with arcpy.da.SearchCursor(fc,polyFields) as cursor: 184 | for row in cursor: 185 | geom = eval(row[0].JSON) 186 | 187 | # return a string of point coordinates 188 | points = self._pointString(geom['rings'][0]) 189 | 190 | style = self._generateStyle(shapeType,colour=row[2],width=row[3],fill=row[4]) 191 | label = row[1] 192 | if label is None: 193 | label = "" 194 | 195 | # create the element 196 | self._writeElement('polygon',points=points,label=label,style=style) 197 | 198 | else: 199 | # need to raise an error and terminate the script 200 | raise arcpy.ExecuteError() 201 | return 202 | 203 | def _writeElement(self,element,**kwargs): 204 | """Sets keyword attributes for the supplied element. 205 | 206 | The element is the NVG element to write, for example point, polyline, 207 | polygon. 208 | 209 | **kwargs is a set of key value pairs for each of the attributes. For 210 | example a point will have the attributes x,y and label supplied. 211 | """ 212 | 213 | # creates a cild element of NVG with the element name and attributes 214 | # from the kwargs. Each keyword will become an attribute. 215 | 216 | # currently no checking of the kwargs to determine if they are valid 217 | # for the supplied nvg element. 218 | 219 | # need to ensure that the geometry tags are written first due to ComBAT 220 | # failing to read the items if this is not the case. 221 | 222 | return SubElement(self.nvg,element,kwargs) 223 | 224 | def write(self,inFC,outFile,prettyXML=True): 225 | """Writes the contents of the input feature class(es) to NVG format. 226 | 227 | inFC - can be a single or list of File GeoDatabase Feature Classes. These 228 | should be based on the ComBAT layer pack which contains features 229 | with the appropriate templates for editing in ArcGIS. 230 | outFile - location and filename for the output NVG document. This should 231 | .nvg file extension supplied. 232 | 233 | The method does not currently handle circle and ellipse polygons. This is 234 | due to ArcGIS representing these as curved lines with only 2 points. 235 | Future versions will add support by densifying the lines. 236 | """ 237 | 238 | fcs = list(inFC) 239 | 240 | for fc in fcs: 241 | self._getFeatures(fc) 242 | 243 | if prettyXML: 244 | with open(outFile,'wb') as nvgFile: 245 | nvgFile.write(prettify(self.nvg)) 246 | else: 247 | with open(outFile,'wb') as nvgFile: 248 | ElementTree(self.nvg).write(nvgFile,encoding="UTF-8", xml_declaration=True) 249 | 250 | return True 251 | -------------------------------------------------------------------------------- /toolbox/readme.md: -------------------------------------------------------------------------------- 1 | ## Python Toolbox 2 | 3 | The NVG.pyt provides a smaple toolbox that demonstrates reading one or more NVG files into file geodatabase feature classes. 4 | 5 | In addition a sample Writer tool has been included that demonsrates the creation of NVG fies for use on ComBAT. This tool only takes polyline and polygon features due to 6 | an implmentation issue within ComBAT when handling points. ComBAT does not wite out the symbol tag (a mandatory tag) in NVG files that are created solely from the sketch toolbar. 7 | Features with an APP6A symbol cod will be written. At present the tool does not support writing symbol codes and this will be added later. 8 | 9 | The process of writing creating features for use on ComBAT requires a set of layer files. This are under development and wil be added to the archive in due course. 10 | 11 | The example is not the best implementation as it curently creates feature classes for each returned type regardless of whether there are any features returned. 12 | This functionality will be added at a later point. 13 | 14 | ### Note 15 | 16 | The accompanying xml files should not be editied directly. These are maintained by ArcGIS. 17 | --------------------------------------------------------------------------------