├── .gitignore ├── .idea └── esri-python-tools-master.iml ├── README.md ├── Utilities_ArcMap.pyt ├── Utilities_Pro.pyt ├── __init__.py ├── addins └── definitionquery │ ├── Images │ ├── Filter-256.png │ ├── Thumbs.db │ └── clean_brush-512.png │ ├── Install │ ├── __init__.py │ └── addins_addin.py │ ├── README.txt │ ├── addins.esriaddin │ └── makeaddin.py ├── img ├── Signature.PNG ├── defquery_addin.gif └── line_endpoint.PNG ├── lib ├── __init__.py ├── esri │ ├── Attachments.py │ ├── Extent.py │ ├── Geodatabase.py │ ├── Geometry.py │ ├── MapDocument.py │ ├── ProDataDrivenPages.py │ ├── Raster.py │ └── __init__.py └── util │ ├── File_Operations.py │ ├── String.py │ └── __init__.py ├── scripts ├── LabelRelatedRecords.py ├── Reproject.py ├── UpdateMXDMetadata.py ├── layer_selection.py └── update_related_ids.py └── tools ├── ClipTool.py ├── DataUpdater.py ├── ExtractAttachmentsTool.py ├── FTPMirrorTool.py ├── LineEndpointTool.py ├── PointElevationTool.py ├── PolygonCentroidTool.py ├── ReprojectTool.py ├── SitemapTool.py └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.mkv 3 | custom/ 4 | *.xml 5 | *.orig 6 | /.vs/Python/v15 7 | /Python.pyproj 8 | .vs/ 9 | Python.sln 10 | *.pyproj 11 | *.sln 12 | -------------------------------------------------------------------------------- /.idea/esri-python-tools-master.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArcGIS Desktop Python Tools 2 | 3 | A collection of custom Esri toolboxes written in python. 4 | 5 | ## Add-ins 6 | 7 | Python add-ins for ArcMap. 8 | 9 | ## Definition Query 10 | 11 | Quickly apply a definition query to selected layers using selected feature object id's 12 | 13 | ![screenshot](img/defquery_addin.gif) 14 | 15 | ### Installation 16 | 17 | 1. Install the .addin file 18 | 2. Open ArcMap and add the toolbar "Faribault" 19 | 20 | ### Usage 21 | 22 | 1. Select some features 23 | 2. Select the layer in the TOC 24 | 3. Apply the definition query using the filter button 25 | 26 | ## Utilities toolbox 27 | 28 | ArcGIS Utilities 29 | 30 | ### Generate Site Maps and CSV 31 | 32 | A custom data driven pages tool used for a variety of purposes. 33 | 34 | - Optionally buffers selected feature or features 35 | - Selects features that intersect the buffer 36 | - Exports map of all features 37 | - Optionally exports individual data driven pages for each feature 38 | - Exports a csv file with the selected features 39 | 40 | ### Clip geodatabase 41 | 42 | This tool clips each layer in a geodatabase and projects the data into an output database using a specified projection file. 43 | 44 | ### Reproject geodatabase 45 | 46 | Reprojects an entire geodatabase using a specified projection file. Layers not in a dataset will be placed in a `_top` dataset. 47 | 48 | ### Polygon Centroid Tool 49 | 50 | - Copies a polygon feature class's centroid geometry into a new feature class 51 | - Adds field `Rel_OID` type `Long` which represents the object id of the polygon 52 | 53 | ### Line Endpoint Tool 54 | 55 | ![screenshot](img/line_endpoint.PNG) 56 | 57 | - Copies the start and endpoint of lines into a new feature class 58 | - Adds field `related_oid` type `Long` which represents the object id of the line feature 59 | - Adds field `point_type` type `Text` which will either be value `START` or `END` 60 | - See 61 | 62 | ### Point Elevation Tool 63 | 64 | - Extracts elevation values at points and calculates them to a field 65 | - This may crash ArcMap with large data sets 66 | 67 | ### Extract attachments tool 68 | 69 | - Extracts attachments from a table and generates a unique file name for each file 70 | - Adds a new field to the table called `file_name` that is updated to the file's unique name 71 | - Requires fields `'DATA', 'ATT_NAME', 'ATTACHMENTID'` 72 | 73 | ## Scripts 74 | 75 | ### Update MXD Metadata 76 | 77 | `scripts/UpdateMXDMetadata.py` 78 | 79 | **Description:** Iterates through an mxd's layers and extracts data source metadata. Layer descriptions are updated to the latest abstract, purpose, and credits. 80 | 81 | Each metadata category is separated into a separate html paragraph using `

` tags. 82 | 83 | **Purpose:** Useful to update metadata on an arcgis server rest endpoint, because html paragraphs are formatted on the rest endpoint and in JSON responses. 84 | 85 | **Usage:** 86 | 87 | Run the script inside an arcmap python console that you want to update. 88 | 89 | ### Label Related Records 90 | 91 | `scripts/LabelRelatedRecords.py` 92 | 93 | **Description:** Looks up related records and generates a label with selected field in a list-like format. 94 | 95 | **Usage:** See documented script 96 | 97 | ### Calculate Dynamic Expression Related Records 98 | 99 | **Description:** Calculates the dynamic expression for a layer and can be used to dynamically display related records text for data driven pages or feature labels 100 | 101 | **Usage (see script for detail)** 102 | 103 | - Change the FindLabel parameter to the id field of the main table 104 | - This id should be related to each of the related tables 105 | - Also change the first line of FindLabel. Set parameter equal to the FindLabel parameter 106 | - Change the lookup tables and assign their appropriate formatter function 107 | -------------------------------------------------------------------------------- /Utilities_ArcMap.pyt: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | path = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.append(path) 5 | 6 | from tools.SitemapTool import SiteMapGenerator 7 | from tools.ClipTool import Clip 8 | from tools.ReprojectTool import Reproject 9 | from tools.PolygonCentroidTool import PolygonCentroidToPoint 10 | from tools.LineEndpointTool import LineEndPoints 11 | from tools.PointElevationTool import PointElevations 12 | from tools.ExtractAttachmentsTool import ExtractAttachments 13 | from tools.FTPMirrorTool import FTPMirror 14 | 15 | class Toolbox(object): 16 | def __init__(self): 17 | self.label = 'ArcMapUtilities' 18 | self.alias = 'ArcMapUtilities' 19 | self.tools = [ 20 | Clip, 21 | Reproject, 22 | SiteMapGenerator, 23 | PolygonCentroidToPoint, 24 | LineEndPoints, 25 | PointElevations, 26 | ExtractAttachments, 27 | FTPMirror, 28 | ] 29 | -------------------------------------------------------------------------------- /Utilities_Pro.pyt: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | path = os.path.dirname(os.path.abspath(__file__)) 4 | sys.path.append(path) 5 | from tools.DataUpdater import MultipleLayerUpdater 6 | from tools.ClipTool import Clip 7 | from tools.ReprojectTool import Reproject 8 | from tools.PolygonCentroidTool import PolygonCentroidToPoint 9 | from tools.LineEndpointTool import LineEndPoints 10 | from tools.PointElevationTool import PointElevations 11 | from tools.ExtractAttachmentsTool import ExtractAttachments 12 | from tools.FTPMirrorTool import FTPMirror 13 | 14 | class Toolbox(object): 15 | def __init__(self): 16 | self.label = 'Utilities' 17 | self.alias = 'Utilities' 18 | self.tools = [ 19 | Clip, 20 | Reproject, 21 | MultipleLayerUpdater, 22 | PolygonCentroidToPoint, 23 | LineEndPoints, 24 | PointElevations, 25 | ExtractAttachments, 26 | FTPMirror, 27 | ] 28 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/__init__.py -------------------------------------------------------------------------------- /addins/definitionquery/Images/Filter-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/addins/definitionquery/Images/Filter-256.png -------------------------------------------------------------------------------- /addins/definitionquery/Images/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/addins/definitionquery/Images/Thumbs.db -------------------------------------------------------------------------------- /addins/definitionquery/Images/clean_brush-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/addins/definitionquery/Images/clean_brush-512.png -------------------------------------------------------------------------------- /addins/definitionquery/Install/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from sys import path 3 | from os.path import dirname 4 | cwd = dirname(__file__) 5 | print(cwd) 6 | path.append(cwd) 7 | -------------------------------------------------------------------------------- /addins/definitionquery/Install/addins_addin.py: -------------------------------------------------------------------------------- 1 | import arcpy 2 | import pythonaddins 3 | 4 | 5 | def get_layer(layer): 6 | 7 | # we might be passed a layer object already 8 | if not isinstance(layer, basestring): 9 | return layer 10 | 11 | # always use current 12 | doc = arcpy.mapping.MapDocument('current') 13 | try: 14 | layer = [l for l in arcpy.mapping.ListLayers(doc) if l.name == layer][0] 15 | return layer 16 | except IndexError as e: 17 | raise Exception('Layer not found::{layer}'.format(layer=layer)) 18 | return 19 | 20 | def create(layer): 21 | # layer = get_layer(layer) 22 | 23 | # check selection set 24 | ids = [str(id) for id in layer.getSelectionSet()] 25 | if not len(ids): 26 | print('No selections on current layer') 27 | return 28 | 29 | id_field = arcpy.Describe(layer).OIDFieldName 30 | 31 | # set definition query 32 | layer.definitionQuery = '{field} IN ({ids})'.format(field=id_field, ids=','.join(ids)) 33 | arcpy.RefreshActiveView() 34 | 35 | def clear(layer): 36 | # layer = get_layer(layer) 37 | layer.definitionQuery = '' 38 | arcpy.RefreshActiveView() 39 | 40 | 41 | class applyDefButton(object): 42 | """Implementation for addins_addin.defButton (Button)""" 43 | def __init__(self): 44 | self.enabled = True 45 | self.checked = False 46 | def onClick(self): 47 | layer = pythonaddins.GetSelectedTOCLayerOrDataFrame() 48 | print(layer) 49 | create(layer) 50 | 51 | class clearDefinitions(object): 52 | """Implementation for addins_addin.clearDefs (Button)""" 53 | def __init__(self): 54 | self.enabled = True 55 | self.checked = False 56 | def onClick(self): 57 | layer = pythonaddins.GetSelectedTOCLayerOrDataFrame() 58 | print(layer) 59 | clear(layer) 60 | -------------------------------------------------------------------------------- /addins/definitionquery/README.txt: -------------------------------------------------------------------------------- 1 | This is a stub project created by the ArcGIS Desktop Python AddIn Wizard. 2 | 3 | MANIFEST 4 | ======== 5 | 6 | README.txt : This file 7 | 8 | makeaddin.py : A script that will create a .esriaddin file out of this 9 | project, suitable for sharing or deployment 10 | 11 | config.xml : The AddIn configuration file 12 | 13 | Images/* : all UI images for the project (icons, images for buttons, 14 | etc) 15 | 16 | Install/* : The Python project used for the implementation of the 17 | AddIn. The specific python script to be used as the root 18 | module is specified in config.xml. 19 | -------------------------------------------------------------------------------- /addins/definitionquery/addins.esriaddin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/addins/definitionquery/addins.esriaddin -------------------------------------------------------------------------------- /addins/definitionquery/makeaddin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import zipfile 4 | 5 | current_path = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | out_zip_name = os.path.join(current_path, 8 | os.path.basename(current_path) + ".esriaddin") 9 | 10 | BACKUP_FILE_PATTERN = re.compile(".*_addin_[0-9]+[.]py$", re.IGNORECASE) 11 | 12 | def looks_like_a_backup(filename): 13 | return bool(BACKUP_FILE_PATTERN.match(filename)) 14 | 15 | with zipfile.ZipFile(out_zip_name, 'w', zipfile.ZIP_DEFLATED) as zip_file: 16 | for filename in ('config.xml', 'README.txt', 'makeaddin.py'): 17 | zip_file.write(os.path.join(current_path, filename), filename) 18 | dirs_to_add = ['Images', 'Install'] 19 | for directory in dirs_to_add: 20 | for (path, dirs, files) in os.walk(os.path.join(current_path, 21 | directory)): 22 | archive_path = os.path.relpath(path, current_path) 23 | found_file = False 24 | for file in (f for f in files if not looks_like_a_backup(f)): 25 | archive_file = os.path.join(archive_path, file) 26 | print archive_file 27 | zip_file.write(os.path.join(path, file), archive_file) 28 | found_file = True 29 | if not found_file: 30 | zip_file.writestr(os.path.join(archive_path, 31 | 'placeholder.txt'), 32 | "(Empty directory)") 33 | -------------------------------------------------------------------------------- /img/Signature.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/img/Signature.PNG -------------------------------------------------------------------------------- /img/defquery_addin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/img/defquery_addin.gif -------------------------------------------------------------------------------- /img/line_endpoint.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/img/line_endpoint.PNG -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/lib/__init__.py -------------------------------------------------------------------------------- /lib/esri/Attachments.py: -------------------------------------------------------------------------------- 1 | from arcpy import AddField_management, ListFields, AddError, Describe, AddMessage 2 | from arcpy.da import UpdateCursor, SearchCursor 3 | from os.path import join 4 | from ..util.File_Operations import verify_path_exists 5 | 6 | def extract_attachments(att_table, out_folder, group_by_field=None): 7 | # [, ...] 8 | l_fields = ListFields(att_table) 9 | 10 | # [dbo.schema.fieldname, ...] 11 | field_names = [f.name for f in l_fields] 12 | 13 | # [DBO.SCHEMA.FIELDNAME, ...] 14 | uppercase = [f.upper() for f in field_names] 15 | 16 | 17 | data_field = None 18 | name_field = None 19 | id_field = None 20 | 21 | data_field = [f for f in uppercase if 'DATA' in f.split('.')][0] 22 | name_field = [f for f in uppercase if 'ATT_NAME' in f.split('.')][0] 23 | id_field = [f.name for f in l_fields if f.type == 'OID'][0] 24 | 25 | fields = [data_field, name_field, id_field] 26 | AddMessage(fields) 27 | 28 | if group_by_field: 29 | if not group_by_field in field_names: 30 | raise Exception('Field {} not found in fields. \n'.format(group_by_field, str(field_names))) 31 | fields.append(group_by_field) 32 | 33 | # verify path 34 | verify_path_exists(out_folder) 35 | 36 | with SearchCursor(att_table, fields) as cursor: 37 | for row in cursor: 38 | 39 | full_out_folder = out_folder 40 | if group_by_field: 41 | 42 | # get the field name 43 | group_folder = row[ fields.index(group_by_field) ] 44 | 45 | full_out_folder = join(out_folder, group_folder) 46 | 47 | # double check folder path 48 | verify_path_exists(full_out_folder) 49 | 50 | # get the attachment file and create a filename 51 | attachment = row[0] 52 | filename = 'ATT_{2}_{1}'.format(*row) 53 | 54 | # write the output file and update the row's value to the file name 55 | open(join(full_out_folder, filename), 'wb').write(attachment.tobytes()) 56 | 57 | # cleanup 58 | del row 59 | del filename 60 | del attachment 61 | 62 | 63 | -------------------------------------------------------------------------------- /lib/esri/Extent.py: -------------------------------------------------------------------------------- 1 | import arcpy 2 | 3 | __author__ = "groemhildt" 4 | 5 | def expand(extent, percent): 6 | '''expands an arcpy.Extent by a given percentage''' 7 | multiplier = percent / float(100) 8 | x_dif = (extent.XMax - extent.XMin) * multiplier 9 | y_dif = (extent.YMax - extent.YMin) * multiplier 10 | return arcpy.Extent(extent.XMin - x_dif, extent.YMin - y_dif, 11 | extent.XMax + x_dif, extent.YMax + y_dif) 12 | -------------------------------------------------------------------------------- /lib/esri/Geodatabase.py: -------------------------------------------------------------------------------- 1 | #Geodatabase tools 2 | 3 | def get_name(name): 4 | """ 5 | retrieves a file gdb friendly name with no '.' dots 6 | 7 | """ 8 | return name.split('.')[-1:][0] 9 | 10 | 11 | def delete_existing(path): 12 | from arcpy import Exists, Delete_management, AddMessage 13 | if Exists(path): 14 | AddMessage('File exists and will be removed: {}'.format(path)) 15 | Delete_management(path) 16 | 17 | def copy_tables(input_ws, output_ws, foreach_table = None): 18 | """ 19 | copies tables or sends each table to a function 20 | input_ws - the input database 21 | output_ws - the output database 22 | foreach_table - the optional function to process each table 23 | """ 24 | from arcpy import env, ListTables, AddMessage, AddWarning, \ 25 | TableToGeodatabase_conversion, GetCount_management, \ 26 | TableToTable_conversion 27 | from os.path import join 28 | 29 | env.workspace = input_ws 30 | for table in ListTables(): 31 | AddMessage('Processing table: {}'.format(table)) 32 | 33 | if env.skipAttach and '_attach' in table.lower(): 34 | AddWarning('Skipping attachments table {}'.format(table)) 35 | continue 36 | 37 | if env.skipEmpty: 38 | count = int(GetCount_management(table)[0]) 39 | if count == 0: 40 | AddWarning('Skipping because table is empty: {} (empty)'.format(table)) 41 | continue 42 | 43 | try: 44 | if foreach_table: 45 | foreach_table(input_ws, output_ws, table) 46 | else: 47 | output_path = join(output_ws, get_name(table)) 48 | delete_existing(output_path) 49 | TableToTable_conversion(table, output_ws, get_name(table)) 50 | except Exception as e: 51 | AddWarning('Error on table: {} - {}'.format(table, e)) 52 | pass 53 | 54 | def process_feature_classes(input_ws, output_ws, foreach_layer = None): 55 | """ 56 | processes each featureclass with an optional function 57 | input_ws - the database or dataset path to process feature classes 58 | output_ws - the output for the feature classes 59 | foreach_layer - the function to process the feature classes 60 | """ 61 | from arcpy import env, ListFeatureClasses, FeatureClassToGeodatabase_conversion, \ 62 | AddWarning, AddMessage, GetCount_management, FeatureClassToFeatureClass_conversion 63 | from os.path import join 64 | env.workspace = input_ws 65 | feature_classes = ListFeatureClasses() 66 | for feature_class in feature_classes: 67 | 68 | AddMessage('Processing {}...'.format(feature_class)) 69 | if env.skipEmpty: 70 | count = int(GetCount_management(feature_class)[0]) 71 | if count == 0: 72 | AddWarning('Skipping because table is empty: {}'.format(feature_class)) 73 | continue 74 | try: 75 | if foreach_layer: 76 | foreach_layer(input_ws, output_ws, feature_class) 77 | else: 78 | #copy each feature class over 79 | output_path = join(output_ws, get_name(feature_class)) 80 | delete_existing(output_path) 81 | FeatureClassToFeatureClass_conversion(feature_class, output_ws, get_name(feature_class)) 82 | except Exception as e: 83 | AddWarning('Error processing feature class {} - {}'.format(feature_class, e)) 84 | 85 | 86 | def process_datasets(from_db, to_db=None, 87 | foreach_layer=None, 88 | foreach_table=None, 89 | foreach_dataset=None): 90 | """ 91 | creates the projected datasets necessary and then calls the function 92 | to perform additional functions on each layer and table 93 | from_db - the input database to pull from 94 | to_db - the output database to place the processed data 95 | foreach_layer - the function to process each layer with 96 | foreach_table - the function to process each table with 97 | """ 98 | #get the datasets in the input workspace 99 | from arcpy import AddMessage, AddWarning, CreateFeatureDataset_management, ListDatasets, Exists, env, ExecuteError 100 | AddMessage('Workspace: {}'.format(env.workspace)) 101 | 102 | #handle feature classes at the top level. these are moved into _top dataset for 103 | #automatic projection handling 104 | AddMessage('Processing tables...') 105 | copy_tables(from_db, to_db, foreach_table) 106 | 107 | AddMessage('Processing feature classes...') 108 | process_feature_classes(from_db, to_db, foreach_layer) 109 | 110 | AddMessage('Processing datasets...') 111 | in_datsets = ListDatasets() 112 | if len(in_datsets): 113 | for dataset in in_datsets: 114 | to_dataset = get_name(dataset) 115 | from_dataset_path = '{}/{}'.format(from_db, dataset) 116 | to_dataset_path = '{}/{}'.format(to_db, to_dataset) 117 | AddMessage('Processing Dataset: {}'.format(from_dataset_path)) 118 | try: 119 | if foreach_dataset: 120 | foreach_dataset(from_db, to_db, dataset, skip_empty) 121 | else: 122 | CreateFeatureDataset_management(to_db, to_dataset, env.outputCoordinateSystem) 123 | except ExecuteError as e: 124 | AddWarning('Could not create dataset {}, {}'.format(to_dataset, e)) 125 | 126 | process_feature_classes(from_dataset_path, to_dataset_path, foreach_layer) 127 | 128 | 129 | def check_exists(output, name): 130 | from arcpy import Exists, CreateFileGDB_management, AddMessage 131 | from os.path import join 132 | if not Exists(join(output, name)): 133 | AddMessage('GDB does not exist, creating...') 134 | CreateFileGDB_management(output, name) 135 | -------------------------------------------------------------------------------- /lib/esri/Geometry.py: -------------------------------------------------------------------------------- 1 | from arcpy.da import SearchCursor, InsertCursor 2 | from arcpy import Describe, AddField_management, AddMessage 3 | 4 | def polygon_to_point(input_fc, output_fc): 5 | """ 6 | copies the centroid of polygon geometry to 7 | a point feature class 8 | """ 9 | 10 | oid_field = Describe(input_fc).OIDFieldName 11 | new_field = 'Rel_OID' 12 | 13 | AddField_management(output_fc, new_field, 'Long') 14 | 15 | search_cursor = SearchCursor(input_fc, ['SHAPE@', oid_field]) 16 | insert_cursor = InsertCursor(output_fc, ["SHAPE@", new_field]) 17 | spatial_reference = Describe(output_fc).spatialReference 18 | 19 | for row in search_cursor: 20 | point = row[0].projectAs(spatial_reference).centroid 21 | oid = row[1] 22 | insert_cursor.insertRow([point, oid]) 23 | 24 | del search_cursor, insert_cursor, spatial_reference 25 | 26 | def line_to_endpoints(input_fc, output_fc, id_field=None): 27 | """ 28 | copies the endpoints of a feature class into a point 29 | feature class 30 | """ 31 | 32 | # id field can either be provided or obtained automagically 33 | oid_field = id_field if id_field else Describe(input_fc).OIDFieldName 34 | 35 | # we add two custom fields to track the related id and the type of end point 36 | related_field = 'related_oid' 37 | type_field = 'point_type' 38 | AddField_management(output_fc, related_field, 'LONG') 39 | AddField_management(output_fc, type_field, 'TEXT', field_length=10) 40 | 41 | 42 | fields = [f.name for f in Describe(input_fc).fields if f.name not in ['SHAPE@', 'Shape', 'Shape_Length']] 43 | output_fields = fields + ['SHAPE@', related_field, type_field] 44 | 45 | #shape will be the last column selected 46 | search_cursor = SearchCursor(input_fc, fields + ['SHAPE@']) 47 | 48 | #our insert cursor 49 | insert_cursor = InsertCursor(output_fc, output_fields) 50 | 51 | #identify the spatial reference for projecting the geometry 52 | spatial_reference = Describe(output_fc).spatialReference 53 | 54 | for row in search_cursor: 55 | # project and store the geometry 56 | geom = row[len(row) - 1].projectAs(spatial_reference) 57 | 58 | # get the row id 59 | oid = row[fields.index(oid_field)] 60 | 61 | # insert two entries but with our new custom geometry 62 | insert_cursor.insertRow(row[:-1] + (geom.firstPoint, oid, 'START')) 63 | insert_cursor.insertRow(row[:-1] + (geom.lastPoint, oid, 'END')) 64 | 65 | del search_cursor, insert_cursor, spatial_reference 66 | -------------------------------------------------------------------------------- /lib/esri/MapDocument.py: -------------------------------------------------------------------------------- 1 | import arcpy 2 | import os 3 | 4 | def export_and_append(export_location, final_pdf, document = None ): 5 | """exports a temporary map document, then appends it to an existing pdf document""" 6 | if not document: 7 | document = arcpy.mapping.MapDocument("CURRENT") 8 | temp = os.path.join(export_location, 'temp.pdf') 9 | arcpy.AddMessage('Exporting pdf: {}'.format(document.title)) 10 | arcpy.mapping.ExportToPDF(document, temp) 11 | #append it 12 | final_pdf.appendPages(temp) 13 | #remove the temp 14 | arcpy.Delete_management(temp) 15 | 16 | def add_map_layer(file_name, symbol_layer, name='layer', data_frame = None): 17 | """creates and adds a symbolized layer to a data frame""" 18 | if not data_frame: 19 | data_frame = arcpy.mapping.ListDataFrames(arcpy.mapping.MapDocument("CURRENT"))[0] 20 | layer = arcpy.mapping.Layer(file_name) 21 | layer.name = name 22 | arcpy.ApplySymbologyFromLayer_management(layer, symbol_layer) 23 | arcpy.mapping.AddLayer(data_frame, layer) 24 | return layer 25 | -------------------------------------------------------------------------------- /lib/esri/ProDataDrivenPages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # A set of data driven pages like utilities for arcgis pro 3 | # 4 | # Usage: 5 | # import sys 6 | # sys.path.append('C:/path/to/Python_Tools') 7 | # from scripts.ProDataDrivenPages import export_layer_maps, FeatureExtentIter, MapLayoutHelper 8 | # help(export_layer_maps) 9 | # ... 10 | 11 | from lib.esri.Extent import expand 12 | from lib.util.String import get_safe_string 13 | 14 | def export_layer_maps(helper, title, layer_key='Map:', title_element='Title', subtitle_element='Subtitle', output='C:/Temp/', file_suffix=''): 15 | """ 16 | exports a set of maps by toggling a filtered set of layers 17 | By default, it searches for layers that start with 'Map:' and 18 | uses this list to generate a set of maps 19 | helper - A MapLayoutHelper 20 | title - The primary title of the map 21 | layer_key - (optional) the string to filter map layers on. The default is 'Map:' 22 | title_element - (optional) the string to search for a title text element. This item 23 | will be updated with the title text 24 | subtitle_element - (optional) the string to search for a subtitle text element. 25 | This item will be updated to the name of the layer, minus the layer_key text. 26 | If a layer's name is Map:Aerial, the subtitle will be set to "Aerial" 27 | output - (optional) The output folder path. 28 | file_suffix - (optional) The default filename will be the title_subtitle.pdf, 29 | adding this value will add an additional string to the end of the filepath. 30 | """ 31 | 32 | # get a list of layers to use for "maps" 33 | # set them all to not visible for starters 34 | layers = [l for l in helper.map.map.listLayers() if layer_key in l.name] 35 | for l in layers: 36 | l.visible = False 37 | 38 | # get the text elements 39 | subtitle_element = helper.layout.listElements('TEXT_ELEMENT', subtitle_element)[0] 40 | title_element = helper.layout.listElements('TEXT_ELEMENT', title_element)[0] 41 | title_element.text = title 42 | 43 | # Go through each layer, set it to visible, and export a pdf 44 | for l in layers: 45 | l.visible = True 46 | name = l.name.split(':')[1] 47 | subtitle_element.text = name 48 | filename = get_safe_string('{}_{}_{}'.format(title, name, file_suffix)) 49 | helper.layout.exportToPDF(output + filename) 50 | l.visible = False 51 | 52 | class FeatureExtentIter: 53 | """ 54 | A quick helper to iterate through a feature layer and set a map extent. 55 | This is similar to data driven pages in ArcMap. (Not tested with points!) 56 | Usage: 57 | helper = MapLayoutHelper('CURRENT') 58 | rows = FeatureExtentIter(helper, iter_layer='My Layer', expand_extent=50) 59 | for row in rows: 60 | helper.layout.exportToPDF('C:/Temp/' + row[3]) 61 | Constructor: 62 | helper - the MapLayoutHelper 63 | iter_layer - The layer to iterate through 64 | expand_extent - The percent to expand each feature's extent 65 | """ 66 | def __init__(self, helper, iter_layer='Industrial Development Sites', expand_extent=25): 67 | from arcpy.da import SearchCursor 68 | from arcpy import Describe 69 | layer = helper.map.map.listLayers(iter_layer)[0] 70 | fields = ['SHAPE@'] + [f.name for f in Describe(layer).fields] 71 | self.helper = helper 72 | self.cursor = SearchCursor(layer, fields) 73 | self.expand_extent = expand_extent 74 | 75 | def __iter__(self): 76 | return self 77 | 78 | def __next__(self): 79 | row = next(self.cursor) 80 | if not row: 81 | raise StopIteration 82 | self.helper.map.camera.setExtent(expand(row[0].extent, self.expand_extent)) 83 | return row 84 | 85 | class MapLayoutHelper(object): 86 | """ 87 | A helper object to initialize a project, layout, and a map for easy access. 88 | Usage: 89 | MapLayoutHelper('CURRENT') 90 | Constructor: 91 | project - (optional) The path to the ArcGIS Pro project or the text "CURRENT" 92 | if you are using an open proejct. The default is 'CURRENT'. 93 | layout - (optional) The name of the layout to initialize. The default 94 | is 'Template' 95 | map - (optional) The name of the map element inside the layout provided. 96 | The default is 'Map' 97 | """ 98 | def __init__(self, project='CURRENT', layout='Template', map='Map'): 99 | from arcpy.mp import ArcGISProject 100 | self.project = ArcGISProject(project) 101 | self.layout = self.project.listLayouts(layout)[0] 102 | self.map = self.layout.listElements('MAPFRAME_ELEMENT', map)[0] 103 | -------------------------------------------------------------------------------- /lib/esri/Raster.py: -------------------------------------------------------------------------------- 1 | # 2 | # Thanks to FelixIP 3 | # http://gis.stackexchange.com/questions/187322/extracting-values-to-points-without-arcgis-spatial-analyst 4 | 5 | from arcpy.da import UpdateCursor, SearchCursor 6 | from arcpy import Describe, RasterToNumPyArray, PointGeometry, Point, SetProgressor, SetProgressorPosition, ResetProgressor, MakeTableView_management, GetCount_management, AddMessage, CalculateField_management 7 | 8 | def getElevationAtPoint(raster, point): 9 | """ 10 | retrieves the elevation at a point 11 | """ 12 | 13 | # project the point 14 | sr = Describe(raster).spatialReference 15 | projected_point = point.projectAs(sr).getPart(0) 16 | 17 | # Get the first cell that starts at the point 18 | result = RasterToNumPyArray(raster, projected_point, 1, 1, -999) 19 | return result[0][0] 20 | 21 | def calculatePointElevationField(points, raster, field_name): 22 | 23 | #monitor progress by counting features 24 | view = MakeTableView_management(points, 'points') 25 | count = int(GetCount_management('points').getOutput(0)) 26 | SetProgressor('step', 'Extracting point elevations', 0, count) 27 | AddMessage('{} features to process'.format(count)) 28 | 29 | # Get the object id field 30 | oid = Describe(points).OIDFieldName 31 | 32 | # make an update cursor and update each row's elevation field 33 | cursor = UpdateCursor(points, [field_name, 'SHAPE@', oid]) 34 | 35 | # make a temporary dict to store our elevation values we extract 36 | elevations = {} 37 | 38 | for row in cursor: 39 | row[0] = getElevationAtPoint(raster, row[1]) 40 | cursor.updateRow(row) 41 | AddMessage('row updated to {}; oid: {}'.format(row[0], row[2])) 42 | SetProgressorPosition() 43 | 44 | # release the data 45 | del cursor 46 | 47 | #reset this progressor 48 | ResetProgressor() 49 | -------------------------------------------------------------------------------- /lib/esri/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/lib/esri/__init__.py -------------------------------------------------------------------------------- /lib/util/File_Operations.py: -------------------------------------------------------------------------------- 1 | #directory operation functions 2 | from os.path import split, join, isfile, exists 3 | from os import makedirs 4 | import errno 5 | from shutil import copyfile 6 | from arcpy.da import SearchCursor 7 | 8 | def verify_path_exists(path): 9 | """ 10 | verifies that the path exists and if not 11 | creates it 12 | """ 13 | try: 14 | makedirs(path) 15 | except OSError as exception: 16 | if exception.errno != errno.EEXIST: 17 | raise 18 | 19 | try: 20 | from PyPDF2 import PdfFileWriter, PdfFileReader 21 | except ImportError as e: 22 | print('WARNING: PyPDF2 is not available, and is used to extract pdf pages: {}'.format(e)) 23 | 24 | def clean_folder(folder): 25 | """ 26 | tries to remove all files in a folder recursively 27 | """ 28 | import os, shutil 29 | 30 | for the_file in os.listdir(folder): 31 | 32 | # skip log files 33 | if '.log' in the_file: 34 | continue 35 | file_path = os.path.join(folder, the_file) 36 | try: 37 | if os.path.isfile(file_path): 38 | os.unlink(file_path) 39 | elif os.path.isdir(file_path): shutil.rmtree(file_path) 40 | except Exception as e: 41 | print(e) 42 | 43 | def get_page_filename(filename, page): 44 | """ 45 | formats a filename 46 | """ 47 | return "Page_{}.pdf".format(page + 1) 48 | 49 | def copy_file(input_folder, destination_folder, filename): 50 | """ 51 | copy a file from a path into a new directory, creating new directories where necessary 52 | duplicating the directory structure 53 | input_folder - The input file folder 54 | destination_folder - The destination file folder 55 | filename - the name of the file to copy 56 | """ 57 | 58 | filepath = join(input_folder, filename) 59 | #make sure the file exists 60 | if not isfile(filepath): 61 | print('{} is not a file'.format(filepath)) 62 | return False 63 | 64 | # make sure the output folder exists 65 | verify_path_exists(destination_folder) 66 | 67 | #copy the file 68 | new_filepath = join(destination_folder, filename) 69 | if not isfile(new_filepath): 70 | copyfile(filepath, new_filepath) 71 | print('Created file: {}'.format(new_filepath)) 72 | return True 73 | 74 | def extract_page(input_file, number, output_path, filename_formatter=get_page_filename): 75 | """ 76 | extracts a page from a pdf and saves it to a file 77 | input_file - the input file pdf path 78 | number - the page number to extract 79 | format_string - the string formatter that recieves the original filename and page number 80 | """ 81 | 82 | if not isfile(input_file): 83 | print ('{} is not a file'.format(input_file)) 84 | return False 85 | 86 | #get a file name 87 | file_parts = split(input_file) 88 | file_name = file_parts[1].split('.pdf')[0] 89 | 90 | #check if file exists and skip if it does 91 | if not output_path: 92 | output_path = file_parts[0] 93 | output_file = join(output_path, filename_formatter(file_name, number).strip('/')) 94 | verify_path_exists(split(output_file)[0]) 95 | if exists(output_file): 96 | return True 97 | 98 | #initialize the pdf reader/writer 99 | input_pdf = PdfFileReader(open(input_file, 'rb')) 100 | output_pdf = PdfFileWriter() 101 | 102 | #extract the page 103 | page = None 104 | try: 105 | page = input_pdf.getPage(number) 106 | except: 107 | print('error extracting page from {}'.format(input_file)) 108 | return False 109 | 110 | #add the page to the output writer 111 | output_pdf.addPage(page) 112 | 113 | print('writing output - {}'.format(output_file)) 114 | with open(output_file, 'wb') as output: 115 | try: 116 | output_pdf.write(output) 117 | except: 118 | print('error while writing output to {}'.format(output_file)) 119 | return False 120 | return True 121 | 122 | def copy_layer_filepath(layer, output_base, fields, log_file=None): 123 | """ 124 | copies files in a path to a new path and splits the required pages into 125 | separate files for quicker loading 126 | layer: the path to the layer or layer name in workspace 127 | output_base: the base path to the output file 128 | fields: A list of field names to use. Order should be like this: 129 | [path, name, doc_id, page_num] 130 | log_file: an open file or class with a `write` method 131 | """ 132 | cursor = SearchCursor(layer, fields) 133 | print('Scanning layer - {}'.format(layer)) 134 | if log_file: 135 | print ('Writing logfile - {}'.format(log_file)) 136 | log_file.write('Problem,Layer,Plan,Page\n') 137 | 138 | # a dict to keep track of which files/pages to copy 139 | # each key references a file, and the value is a list of pages to copy 140 | file_list = {} 141 | for row in cursor: 142 | #unpack row 143 | folder, name, doc_id, page_num = row 144 | 145 | plan = join(folder, name) 146 | 147 | #make sure the plan isn't already in the dict 148 | if not doc_id in file_list: 149 | file_list[doc_id] = { 150 | 'pages': [], 151 | 'folder': folder, 152 | 'filename': name 153 | } 154 | 155 | #add the page index to the list 156 | #page numbers are one more than the page index so we 157 | #subtract 1 because the index is one less than the page number 158 | try: 159 | page_num = int(page_num) - 1 160 | if page_num < 0: 161 | page_num = 0 162 | except: 163 | #handle invalid values (null) 164 | page_num = 0 165 | if not page_num in file_list[doc_id]['pages']: 166 | file_list[doc_id]['pages'].append(page_num) 167 | 168 | for plan, props in file_list.iteritems(): 169 | input_folder = props['folder'] 170 | pages = props['pages'] 171 | filename = props['filename'] 172 | input_file = join(input_folder, filename) 173 | output_folder = join(output_base, filename) 174 | 175 | #copy the entire file 176 | #log errors 177 | if not copy_file(input_folder, output_folder, filename) and log_file: 178 | log_file.write('Copy file failed,{},{},{} \n'.format(layer, plan, -99)) 179 | if filename.split('.')[-1].upper() == 'PDF': 180 | for page in pages: 181 | # and split the pages 182 | #log errors for future purposes 183 | #functions return false if error occurs 184 | if not extract_page(input_file, page, output_folder) and log_file: 185 | log_file.write('Extract page failed,{},{},{} \n'.format(layer, plan, page)) 186 | print('----Done-----!\n\n') 187 | -------------------------------------------------------------------------------- /lib/util/String.py: -------------------------------------------------------------------------------- 1 | #a collection of string functions 2 | 3 | def get_safe_string(text): 4 | """returns a safe string that can be used for file names""" 5 | return "".join([c for c in text if c.isalnum()]).rstrip() 6 | -------------------------------------------------------------------------------- /lib/util/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "groemhildt" 2 | __date__ = "$Aug 5, 2015 2:04:32 PM$" -------------------------------------------------------------------------------- /scripts/LabelRelatedRecords.py: -------------------------------------------------------------------------------- 1 | # Calculates the dynamic expression or label for a layer and can be used 2 | # to dynamically display related records text for data driven pages 3 | # or feature labels 4 | 5 | # Change the FindLabel parameter to the id field of the main table 6 | # This id should be related to each of the related tables 7 | # Also change the first line of FindLabel. Set parameter equal to the FindLabel parameter 8 | def FindLabel ( [PARCELID] ): 9 | return get_lookup_text([PARCELID]) 10 | 11 | # create formatter functions to return what you want displayed 12 | # for each row. These should return plain text for each row. 13 | 14 | # returns a string with the parcel zone 15 | def get_parcel(row): 16 | return row["ZONE"] 17 | 18 | # change the lookup tables and assign their appropriate formatter function 19 | # Properties of each lookup item: 20 | # table: the name of the table in the mxd 21 | # row_formatter: the function that will format each row's properties and return 22 | # the text string 23 | # id_field: the name of the field in the table that is related to the 24 | # id passed in to FindLabel 25 | # fields: list of fields by name (optiononal) default '*' 26 | lookup_tables = [{ 27 | "table": "Related_Zones", 28 | "row_formatter": get_parcel, 29 | "id_field": "PARCELID", 30 | "fields": ['PARCELID', 'ZONE'], 31 | "header": 'Parcel Zones:', 32 | }, 33 | #other lookup tables... 34 | ] 35 | 36 | 37 | def get_lookup_text(id): 38 | # init the return value 39 | text = '' 40 | if id is not None: 41 | for table in lookup_tables: 42 | if table.has_key('header'): 43 | text += "{} \n".format(table['header']) 44 | if type(id) is str or type(id) is unicode: 45 | query = "{} = '{}'".format(table["id_field"], id) 46 | else: 47 | query = "{} = {}".format(table["id_field"], id) 48 | fields = table['fields'] if table.has_key('fields') else '*' 49 | with arcpy.da.SearchCursor(table["table"], fields, where_clause=query) as rows: 50 | for row in rows: 51 | dictionary = dict() 52 | for idx, val in enumerate(rows.fields): 53 | dictionary[val] = row[idx] 54 | text += "{} \n".format(table["row_formatter"](dictionary)) 55 | return text 56 | -------------------------------------------------------------------------------- /scripts/Reproject.py: -------------------------------------------------------------------------------- 1 | import lib.Geodatabase 2 | import lib.Reproject_Tool import Reproject 3 | 4 | def run_tool(from_db, to_db, projection): 5 | Geodatabase.clean(to_db) 6 | r = Reproject() 7 | r.reproject(from_db, to_db, projection) 8 | 9 | #defaults if run at command line 10 | if __name__ == '__main__': 11 | #defaul values 12 | from_db = 'N:/BaseData/data.gdb' 13 | to_db = 'C:/Data/projected.gdb' 14 | projection = 'N:/BaseData/proj.prj' 15 | 16 | #run the tool 17 | run_tool(from_db, to_db, projection) 18 | -------------------------------------------------------------------------------- /scripts/UpdateMXDMetadata.py: -------------------------------------------------------------------------------- 1 | from xml.dom.minidom import parse 2 | from arcpy import GetInstallInfo, AddError 3 | import sys 4 | try: 5 | from arcpy.mapping import MapDocument, ListDataFrames, ListLayers 6 | from arcpy import ExportMetadata_conversion 7 | except ImportError as e: 8 | AddError('This tool requires ArcMap: {}'.format(e)) 9 | print('This tool requires ArcMap: {}'.format(e)) 10 | 11 | def update_metadata(document='CURRENT'): 12 | """ 13 | updates metadata in an arcmap document 14 | document - (optional) the path to an arcmap document or the keyword 'CURRENT'. 15 | The default is 'CURRENT' 16 | """ 17 | #set local variables 18 | dir = GetInstallInfo("desktop")["InstallDir"] 19 | translator = dir + "Metadata/Translator/ESRI_ISO2ISO19139.xml" 20 | mxd = MapDocument(document) 21 | df = ListDataFrames(mxd) 22 | temp_path = "C:/temp" 23 | for layer in ListLayers(mxd, "*", df[0]): 24 | if not layer.isGroupLayer: 25 | description_text = "" 26 | path = temp_path + '/' + layer.datasetName + '.xml' 27 | print(path) 28 | ExportMetadata_conversion(layer.dataSource, translator, path) 29 | dom = parse(path) 30 | fields = ('abstract', 'purpose', 'credit') 31 | for field in fields: 32 | tags = dom.getElementsByTagName(field) 33 | print(str( len(tags) ) + ' | ' + str( tags )) 34 | if len(tags): 35 | tag_string = tags[0].getElementsByTagName('gco:CharacterString')[0].childNodes[0].nodeValue 36 | description_text = "{}

{}: {}

".format(description_text, field.capitalize(), tag_string) 37 | if field == 'credit': 38 | layer.credit = tag_string 39 | print(description_text) 40 | layer.description = description_text 41 | 42 | if __name__ == '__main__': 43 | update_metadata(sys.argv[1]) 44 | -------------------------------------------------------------------------------- /scripts/layer_selection.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # A better way to create selection set layers 4 | # This method uses object id's to create a definition expression 5 | # rather than a weird layer that we can't modify later like 6 | # arcmap's builtin method 7 | # 8 | # 9 | 10 | import arcpy 11 | 12 | # always use current 13 | doc = arcpy.mapping.MapDocument('current') 14 | 15 | def get_layer(layer): 16 | try: 17 | layer = [l for l in arcpy.mapping.ListLayers(doc) if l.name == layer][0] 18 | return layer 19 | except IndexError as e: 20 | raise Exception('Layer not found::{layer}'.format(layer=layer)) 21 | return 22 | 23 | def create(layer): 24 | layer = get_layer(layer) 25 | 26 | # check selection set 27 | ids = [str(id) for id in layer.getSelectionSet()] 28 | if not len(ids): 29 | print('No selections on current layer') 30 | return 31 | 32 | id_field = arcpy.Describe(layer).OIDFieldName 33 | 34 | # set definition query 35 | layer.definitionQuery = '{field} IN ({ids})'.format(field=id_field, ids=','.join(ids)) 36 | 37 | def clear(layer): 38 | layer = get_layer(layer) 39 | layer.definitionQuery = '' 40 | -------------------------------------------------------------------------------- /scripts/update_related_ids.py: -------------------------------------------------------------------------------- 1 | from ..esri.Geometry import line_to_endpoints 2 | import arcpy 3 | 4 | arcpy.AddToolbox('../Utilities.pyt') 5 | 6 | related_layers = [{ 7 | 'layer': 'storm_pipe', 8 | 'related': 'storm_struc', 9 | 'from_foreign_key': 'from_struc_id', 10 | 'to_foreign_key': 'to_struc_id', 11 | 'reference_key': 'cid' 12 | }] 13 | 14 | for layer in related_layers: 15 | print('processing layer {}...'.format(layer['layer'])) 16 | 17 | # create the points layer 18 | foreign_key = 'related_oid' 19 | points = 'in_memory/end_point' 20 | points = arcpy.Utilities.LineEndPoints(layer['layer'], 'false', points).getOutput(0) 21 | print('points layer created: {}'.format(points)) 22 | 23 | # spatially join to the related point layer 24 | joined_points = 'in_memory/joined_points' 25 | joined_points = arcpy.SpatialJoin_analysis(points, layer['related'], joined_points).getOutput(0) 26 | print('spatial join completed: {}'.format(joined_points)) 27 | 28 | # add a join on the line layer 29 | line_layer = arcpy.MakeFeatureLayer_management(layer['layer'], 'line_layer').getOutput(0) 30 | oid_field = arcpy.Describe(line_layer).OIDFieldName 31 | arcpy.AddJoin_management(joined_points, foreign_key, line_layer, oid_field) 32 | print('join added: {}'.format(joined_points)) 33 | 34 | # create two feature layers one for each related field 35 | start_points = arcpy.MakeFeatureLayer_management(joined_points, 'start_points', where_clause='point_type="START"').getOutput(0) 36 | end_points = arcpy.MakeFeatureLayer_management(joined_points, 'end_points', where_clause='point_type="END"').getOutput(0) 37 | print('layers created: {}, {}'.format(start_points, end_points)) 38 | 39 | # calculate the fields new values 40 | arcpy.CalculateField_management(start_points, layer['from_foreign_key'], layer['reference_key']) 41 | arcpy.CalculateField_management(end_points, layer['to_foreign_key'], layer['reference_key']) 42 | -------------------------------------------------------------------------------- /tools/ClipTool.py: -------------------------------------------------------------------------------- 1 | from lib.esri import Geodatabase 2 | from arcpy import Parameter, AddMessage, Exists, Clip_analysis, Delete_management, FeatureClassToFeatureClass_conversion, env 3 | 4 | #arcpy toolbox 5 | #parameter indexes 6 | clip_from_db = 0 7 | clip_to_db = 1 8 | clip_projection = 2 9 | clip_clip_layer = 3 10 | 11 | class Clip(object): 12 | """arcpy toolbox for clipping an entire database""" 13 | def __init__(self): 14 | self.label = 'Clip Geodatabase' 15 | self.description = """ 16 | Clip an entire database preserving datasets and 17 | feature class names 18 | """ 19 | def getParameterInfo(self): 20 | return [Parameter( 21 | displayName = 'From Database', 22 | name = 'from_db', 23 | direction = 'Input', 24 | datatype = 'Workspace', 25 | parameterType = 'Required', 26 | ), Parameter( 27 | displayName = 'To Database (Existing features in here will be deleted!)', 28 | name = 'to_db', 29 | direction = 'Input', 30 | datatype = 'Workspace', 31 | parameterType = 'Required', 32 | ), Parameter( 33 | displayName = 'Projection File', 34 | name = 'projection', 35 | direction = 'Input', 36 | datatype = 'DEPrjFile', 37 | parameterType = 'Required', 38 | ), Parameter( 39 | displayName = 'Clipping Layer', 40 | name = 'clip_layer', 41 | direction = 'Input', 42 | datatype = 'DEFeatureClass', 43 | parameterType = 'Required', 44 | )] 45 | def execute(self, parameters, messages): 46 | AddMessage('{}; {}; {};'.format( 47 | parameters[clip_from_db].valueAsText, 48 | parameters[clip_to_db].valueAsText, 49 | parameters[clip_projection].valueAsText)) 50 | from_db = parameters[clip_from_db].valueAsText 51 | to_db = parameters[clip_to_db].valueAsText 52 | projection = parameters[clip_projection].valueAsText 53 | clip_layer = parameters[clip_clip_layer].valueAsText 54 | 55 | self.clip(from_db, to_db, projection, clip_layer) 56 | 57 | def clip(self, from_db, to_db, projection, clip_layer): 58 | 59 | if not Exists(projection): 60 | AddMessage('Projection file {} does not exist'.format(projection)) 61 | return 62 | def foreach_layer(from_dataset_path, to_dataset_path, feature_class): 63 | from_feature_path = '{}/{}'.format(from_dataset_path, feature_class) 64 | to_feature_path = '{}/{}'.format(to_dataset_path, feature_class.split('.')[-1:][0]) 65 | 66 | Clip_analysis('{}/{}'.format(from_dataset_path, feature_class), clip_layer, to_feature_path) 67 | 68 | Geodatabase.process_datasets(from_db, 69 | to_db, 70 | projection, 71 | foreach_layer=foreach_layer) 72 | -------------------------------------------------------------------------------- /tools/DataUpdater.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: Data_Updater 3 | # Purpose: See tool metadata 4 | # 5 | # Author: groemhildt 6 | # 7 | # Created: 17/03/2016 8 | # Copyright: (c) groemhildt 2016 9 | #------------------------------------------------------------------------------- 10 | from arcpy import Parameter, AddError 11 | from arcpy.da import UpdateCursor 12 | try: 13 | from arcpy.mp import ArcGISProject 14 | except ImportError as e: 15 | AddError('This script requires ArcGIS Pro: {}'.format(e)) 16 | 17 | 18 | def update_layer(layer, field, value, new_value): 19 | if(field and value): 20 | where_clause = '{} = \'{}\''.format(field, value) 21 | else: 22 | where_clause = None 23 | try: 24 | cursor = UpdateCursor(layer, field, where_clause) 25 | except(TypeError): 26 | return "Error loading table {}".format(layer) 27 | 28 | try: 29 | for row in cursor: 30 | row[0] = new_value 31 | cursor.updateRow(row); 32 | except(RuntimeError): 33 | del cursor 34 | return "Error modifying table {}".format(layer) 35 | 36 | return 'Layer Updated: {}'.format(layer) 37 | 38 | def get_layer_names(map='Map'): 39 | doc = ArcGISProject('current') 40 | m = doc.listMaps(map)[0] 41 | 42 | return [layer.name for layer in map.listLayers() if layer.visible and layer.isFeatureLayer] 43 | 44 | def main(): 45 | layers = get_layer_names() 46 | for layer in layers: 47 | print(update_layer(layer, 'Plan_ID', '1415', '1415TownSquareLn')) 48 | 49 | if __name__ == '__main__': 50 | main() 51 | 52 | class MultipleLayerUpdater(object): 53 | def __init__(self): 54 | self.label = 'Multiple Layer Updater' 55 | self.description = """ 56 | Updates a field in multiple layers in the current map document with a value. 57 | If the field is not visible or is not a feature layer it will be skipped. 58 | """ 59 | self.canRunInBackground = False 60 | 61 | def getParameterInfo(self): 62 | return [Parameter( 63 | displayName='Map', 64 | name='map', 65 | datatype='GPString', 66 | parameterType='Required', 67 | direction='Input' 68 | ),Parameter( 69 | displayName='Field', 70 | name='field', 71 | datatype='GPString', 72 | parameterType='Required', 73 | direction='Input' 74 | ), Parameter( 75 | displayName='Old Value', 76 | name='old_value', 77 | datatype='GPString', 78 | parameterType='Required', 79 | direction='Input' 80 | ), Parameter( 81 | displayName='New Value', 82 | name='new_value', 83 | datatype='GPString', 84 | parameterType='Required', 85 | direction='Input' 86 | )] 87 | 88 | def execute(self, parameters, messages): 89 | layers= get_layer_names(parameters[0].valueAsText) 90 | for layer in layers: 91 | messages.addMessage( 92 | update_layer(layer, parameters[1].valueAsText, parameters[2].valueAsText,parameters[3].valueAsText)) 93 | -------------------------------------------------------------------------------- /tools/ExtractAttachmentsTool.py: -------------------------------------------------------------------------------- 1 | from arcpy import Parameter, AddField_management, AddMessage, ListFields 2 | from lib.esri.Attachments import extract_attachments 3 | 4 | #parameter indexes 5 | _attach_table = 0 6 | _out_folder = 1 7 | _group_by_field = 2 8 | 9 | class ExtractAttachments(object): 10 | def __init__(self): 11 | self.label = 'Extract table attachments' 12 | self.description = 'Extract table attachments and add a file name field' 13 | self.canRunInBackground = True 14 | 15 | def initializeParameters(self): 16 | self.parameters[_group_by_field].parameterDependencies = [_attach_table] 17 | 18 | def updateParameters(self, parameters): 19 | if parameters[_attach_table]: 20 | layer = parameters[_attach_table].valueAsText 21 | parameters[_group_by_field].filter.list = [f.name for f in ListFields(layer)] 22 | 23 | def getParameterInfo(self): 24 | """ 25 | defines the parameters 26 | """ 27 | params = [Parameter( 28 | displayName='Input attachment table', 29 | name='attach_table', 30 | datatype=['Table', 'GPTableView'], 31 | parameterType='Required', 32 | direction='Input' 33 | ), Parameter( 34 | displayName='Output attachments folder', 35 | name='out_folder', 36 | datatype=['DEFolder'], 37 | parameterType='Required', 38 | direction='Input' 39 | ), Parameter( 40 | displayName='Group By Field (place images in subfolders)', 41 | name='group_by_field', 42 | datatype='GPString', 43 | parameterType='Optional', 44 | direction='Input' 45 | )] 46 | return params 47 | 48 | def execute(self, params, messages): 49 | """ 50 | gets the parameters and passes them to process 51 | """ 52 | 53 | attach_table = params[_attach_table].valueAsText 54 | out_folder = params[_out_folder].valueAsText 55 | group_by_field = params[_group_by_field].valueAsText 56 | 57 | # run the task 58 | extract_attachments(attach_table, out_folder, group_by_field) 59 | 60 | -------------------------------------------------------------------------------- /tools/FTPMirrorTool.py: -------------------------------------------------------------------------------- 1 | # 2 | # Download an ftp directory to a local folder as a scriptable 3 | # and model builder friendly arcpy tool 4 | # http://stackoverflow.com/questions/5230966/python-ftp-download-all-files-in-directory 5 | # 6 | 7 | from arcpy import Parameter, AddMessage, AddWarning 8 | from os.path import join 9 | from lib.util.File_Operations import verify_path_exists 10 | from ftplib import FTP 11 | 12 | #paramter indexes 13 | _output_folder = 0 14 | _ftp_folder = 1 15 | _ftp_server = 2 16 | _ftp_username = 3 17 | _ftp_password = 4 18 | 19 | def is_file(ftp, filename): 20 | try: 21 | ftp.cwd(filename) 22 | except Exception as e: 23 | return True 24 | return False 25 | 26 | def retrieve_directory_recursive(ftp, output_folder, directory, pattern=None): 27 | """ 28 | recursively retrieves files from an ftp directory 29 | """ 30 | 31 | verify_path_exists(output_folder) 32 | AddMessage('Navigating to {}'.format(directory)) 33 | 34 | if is_file(ftp, directory): 35 | AddMessage('Processing file: {}'.format(directory)) 36 | local_filename = join(output_folder, directory) 37 | 38 | if pattern is not None and pattern not in directory: 39 | return 40 | try: 41 | f = open(local_filename, 'wb') 42 | ftp.retrbinary('RETR '+ directory, f.write) 43 | f.close() 44 | except Exception as e: 45 | AddWarning('Could not access file {}'.format(directory)) 46 | else: 47 | output_folder = join(output_folder, directory) 48 | filenames = ftp.nlst() # get filenames within the directory 49 | 50 | #navigate sub directories 51 | for d in filenames: 52 | retrieve_directory_recursive(ftp, output_folder, d) 53 | 54 | 55 | class FTPMirror(object): 56 | def __init__(self): 57 | """ 58 | Define the tool (tool name is the name of the class). 59 | """ 60 | self.label = "Mirror FTP Directory" 61 | self.description = 'Downloads an ftp directory to a local drive' 62 | self.canRunInBackground = True 63 | 64 | def getParameterInfo(self): 65 | """ 66 | Define parameter definitions 67 | """ 68 | params = [Parameter( 69 | displayName='Output Folder', 70 | name='output_folder', 71 | datatype='DEFolder', 72 | parameterType='Required', 73 | direction='Output' 74 | ), Parameter( 75 | displayName='FTP Folder', 76 | name='ftp_folder', 77 | parameterType='Optional', 78 | direction='Input' 79 | ), Parameter( 80 | displayName='FTP Server', 81 | name='ftp_server', 82 | parameterType='Required', 83 | direction='Input' 84 | ), Parameter( 85 | displayName='FTP Username', 86 | name='ftp_username', 87 | parameterType='Optional', 88 | direction='Input' 89 | ), Parameter( 90 | displayName='FTP Password', 91 | name='ftp_password', 92 | parameterType='Optional', 93 | direction='Input' 94 | )] 95 | return params 96 | 97 | def execute(self, params, messages): 98 | """ 99 | The source code of the tool 100 | """ 101 | output_folder = params[_output_folder].valueAsText 102 | ftp_folder = params[_ftp_folder].valueAsText 103 | ftp_server = params[_ftp_server].valueAsText 104 | ftp_username = params[_ftp_username].valueAsText 105 | ftp_password = params[_ftp_password].valueAsText 106 | self.mirror(output_folder, ftp_folder, ftp_server, ftp_username, ftp_password) 107 | 108 | def mirror(self, output_folder, ftp_folder, ftp_server, ftp_username=None, ftp_password=None, pattern=None): 109 | """ 110 | connects to an ftp server and downloads a directory 111 | output_folder - The local path to the download directory 112 | ftp_folder - The path on the server to download or mirror 113 | ftp_server - The server url to the ftp server 114 | ftp_username - The username to use to connect to the ftp server. The default is None 115 | ftp_password - The usernames password to connect to the ftp server. The default is None 116 | AddMessage - A logging function. The default is print 117 | """ 118 | 119 | ftp = FTP(ftp_server) 120 | 121 | AddMessage('Logging in.') 122 | ftp.login(ftp_username, ftp_password) 123 | retrieve_directory_recursive(ftp, output_folder, ftp_folder, pattern=pattern) 124 | 125 | ftp.quit() # This is the polite way to close a connection 126 | -------------------------------------------------------------------------------- /tools/LineEndpointTool.py: -------------------------------------------------------------------------------- 1 | from arcpy import Parameter, CreateFeatureclass_management, Describe, AddMessage 2 | from os.path import split as Split_Path 3 | from lib.esri.Geometry import line_to_endpoints 4 | 5 | #paramter indexes 6 | _input_fc = 0 7 | _use_template = 1 8 | _output_fc = 2 9 | 10 | class LineEndPoints(object): 11 | def __init__(self): 12 | """ 13 | Define the tool (tool name is the name of the class). 14 | """ 15 | self.label = "Extract Polyline Endpoints" 16 | self.description = 'Converts a polyline geometry into start and endpoints' 17 | self.canRunInBackground = True 18 | 19 | def getParameterInfo(self): 20 | """ 21 | Define parameter definitions 22 | """ 23 | params = [Parameter( 24 | displayName='Input Polyline Layer', 25 | name='input_fc', 26 | datatype='DEFeatureClass', 27 | parameterType='Required', 28 | direction='Input' 29 | ), 30 | Parameter( 31 | displayName='Use input as template', 32 | name='use_template', 33 | datatype='GPBoolean', 34 | direction='Input' 35 | ), 36 | Parameter( 37 | displayName='Output Points Layer', 38 | name='output_fc', 39 | datatype='DEFeatureClass', 40 | parameterType='Required', 41 | direction='Output' 42 | )] 43 | 44 | params[_use_template].value = False 45 | return params 46 | 47 | def execute(self, params, messages): 48 | """ 49 | The source code of the tool 50 | """ 51 | input_fc = params[_input_fc].valueAsText 52 | output_fc = params[_output_fc].valueAsText 53 | use_template = params[_use_template].valueAsText 54 | self.process(input_fc, output_fc, use_template) 55 | 56 | def process(self, input_fc, output_fc, use_template='false'): 57 | 58 | template = None 59 | if use_template == 'true': 60 | template = input_fc 61 | 62 | #get the directory, filename, and spatial reference 63 | sp_ref = Describe(input_fc).spatialReference 64 | directory, filename = Split_Path(output_fc) 65 | 66 | #create a new feature class 67 | AddMessage('Creating feature class {}'.format(output_fc)) 68 | CreateFeatureclass_management(directory, filename, 'POINT', template, 'DISABLED', 'DISABLED', sp_ref) 69 | 70 | #copy the geometry centroid 71 | AddMessage('Extracting endpoints...') 72 | line_to_endpoints(input_fc, output_fc) 73 | -------------------------------------------------------------------------------- /tools/PointElevationTool.py: -------------------------------------------------------------------------------- 1 | from arcpy import Parameter 2 | from arcpy import AddField_management 3 | from lib.esri.Raster import calculatePointElevationField 4 | 5 | #parameter indexes 6 | _input_raster = 0 7 | _input_points = 1 8 | _field_name = 2 9 | 10 | class PointElevations(object): 11 | def __init__(self): 12 | self.label = 'Calculate Point Elevations' 13 | self.description = 'Adds an elevation field value from a raster dem' 14 | self.canRunInBackground = True 15 | 16 | def getParameterInfo(self): 17 | """ 18 | defines the parameters 19 | """ 20 | params = [Parameter( 21 | displayName='Input Raster DEM', 22 | name='input_raster', 23 | datatype=['DERasterDataset', 'DERasterCatalog'], 24 | parameterType='Required', 25 | direction='Input' 26 | ), Parameter( 27 | displayName='Input Points Layer', 28 | name='input_points', 29 | datatype=['DEFeatureClass', 'GPFeatureLayer'], 30 | parameterType='Required', 31 | direction='Input' 32 | ), Parameter( 33 | displayName='New Elevation Field Name', 34 | name='title', 35 | direction='Input', 36 | datatype='GPString', 37 | parameterType='Required', 38 | )] 39 | params[_field_name].value = 'point_elevation' 40 | return params 41 | 42 | def execute(self, params, messages): 43 | """ 44 | gets the parameters and passes them to process 45 | """ 46 | 47 | input_raster = params[_input_raster].valueAsText 48 | input_points = params[_input_points].valueAsText 49 | field_name = params[_field_name].valueAsText 50 | self.process(input_raster, input_points, field_name) 51 | 52 | def process(self, input_raster, input_points, field_name): 53 | """ 54 | creates a new field called point_elevation on the feature class 55 | and populates it with values from the raster at the intersection 56 | """ 57 | AddField_management(input_points, field_name, "FLOAT") 58 | calculatePointElevationField(input_points, input_raster, field_name) 59 | -------------------------------------------------------------------------------- /tools/PolygonCentroidTool.py: -------------------------------------------------------------------------------- 1 | from arcpy import Parameter 2 | from arcpy import CreateFeatureclass_management, Describe 3 | from os.path import split as Split_Path 4 | from lib.esri.Geometry import polygon_to_point 5 | 6 | #paramter indexes 7 | _input_fc = 0 8 | _use_template = 1 9 | _output_fc = 2 10 | 11 | class PolygonCentroidToPoint(object): 12 | def __init__(self): 13 | """ 14 | Define the tool (tool name is the name of the class). 15 | """ 16 | self.label = "Polygon Centroid To Point" 17 | self.description = 'Converts a polygon into point feature class using the polygon centroid' 18 | self.canRunInBackground = True 19 | 20 | def getParameterInfo(self): 21 | """ 22 | Define parameter definitions 23 | """ 24 | params = [Parameter( 25 | displayName='Input Polygon Layer', 26 | name='input_fc', 27 | datatype='DEFeatureClass', 28 | parameterType='Required', 29 | direction='Input' 30 | ), 31 | Parameter( 32 | displayName='Use input as template', 33 | name='use_template', 34 | datatype='GPBoolean', 35 | direction='Input' 36 | ), 37 | Parameter( 38 | displayName='Output Points Layer', 39 | name='output_fc', 40 | datatype='DEFeatureClass', 41 | parameterType='Required', 42 | direction='Output' 43 | )] 44 | 45 | params[_use_template].value = False 46 | return params 47 | 48 | def execute(self, params, messages): 49 | """ 50 | The source code of the tool 51 | """ 52 | input_fc = params[_input_fc].valueAsText 53 | output_fc = params[_output_fc].valueAsText 54 | use_template = params[_use_template].valueAsText 55 | 56 | template = None 57 | if use_template == 'true': 58 | template = input_fc 59 | 60 | #get the directory, filename, and spatial reference 61 | sp_ref = Describe(input_fc).spatialReference 62 | directory, filename = Split_Path(output_fc) 63 | 64 | #create a new feature class 65 | messages.addMessage('Creating feature class {}'.format(output_fc)) 66 | CreateFeatureclass_management(directory, filename, 'POINT', template, 'DISABLED', 'DISABLED', sp_ref) 67 | 68 | #copy the geometry centroid 69 | messages.addMessage('Copying polygon centroids...') 70 | polygon_to_point(input_fc, output_fc) 71 | -------------------------------------------------------------------------------- /tools/ReprojectTool.py: -------------------------------------------------------------------------------- 1 | from arcpy import Parameter, AddMessage 2 | from lib.esri import Geodatabase 3 | 4 | #arcpy toolbox 5 | #parameter indexes 6 | reproject_from_db = 0 7 | reproject_to_db = 1 8 | reproject_projection = 2 9 | reproject_skip_empty = 3 10 | 11 | class Reproject(object): 12 | """arcpy toolbox for reprojecting an entire database""" 13 | def __init__(self): 14 | self.label = 'Reproject Geodatabase' 15 | self.description = """ 16 | Reproject an entire geodatabase preserving datasets 17 | and layer names. Useful for reprojecting a geodatabase 18 | or converting from a .mdb or sde to a .gdb""" 19 | def getParameterInfo(self): 20 | return [Parameter( 21 | displayName = 'From Database', 22 | name = 'from_db', 23 | direction = 'Input', 24 | datatype = 'Workspace', 25 | parameterType = 'Required', 26 | ), Parameter( 27 | displayName = 'To Database (Existing features in here will be deleted!)', 28 | name = 'to_db', 29 | direction = 'Input', 30 | datatype = 'Workspace', 31 | parameterType = 'Required', 32 | ), Parameter( 33 | displayName = 'Projection File', 34 | name = 'projection', 35 | direction = 'Input', 36 | datatype = 'DEPrjFile', 37 | parameterType = 'Required', 38 | ), Parameter( 39 | displayName = 'Skip Empty Rows', 40 | name = 'skip_empty', 41 | direction = 'Input', 42 | datatype = 'GPBoolean', 43 | parameterType = 'Optional' 44 | )] 45 | def execute(self, parameters, messages): 46 | 47 | from_db = parameters[reproject_from_db].valueAsText 48 | to_db = parameters[reproject_to_db].valueAsText 49 | projection = parameters[reproject_projection].valueAsText 50 | skip_empty = parameters[reproject_skip_empty].valueAsText 51 | 52 | AddMessage('Tool received parameters: {}'.format(', '.join([p.valueAsText for p in parameters]))) 53 | 54 | from arcpy import env, Exists 55 | 56 | if skip_empty == 'true': 57 | env.skipEmpty = True 58 | else: 59 | env.skipEmpty = False 60 | 61 | #run the functions 62 | if not Exists(projection): 63 | AddMessage('Projection file {} does not exist'.format(projection)) 64 | return 65 | 66 | # just set the output coordinate system and outputs 67 | # will be projected :) 68 | env.skipAttach = True 69 | env.outputCoordinateSystem = projection 70 | 71 | #call the create datasets function passing the foreach layer function to it 72 | Geodatabase.process_datasets(from_db, to_db, None, None, None) 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /tools/SitemapTool.py: -------------------------------------------------------------------------------- 1 | import arcpy 2 | import csv 3 | import os 4 | import time 5 | from subprocess import Popen 6 | from datetime import date 7 | from lib.esri import Extent, MapDocument 8 | from lib.util.String import get_safe_string 9 | from lib.util.File_Operations import verify_path_exists 10 | 11 | ### 12 | # Esri Toolbox 13 | ### 14 | 15 | # parameter indexes. Change here once and be done 16 | # when updating parameter order 17 | p_layer = 0 18 | p_autocreate_folder = 2 19 | p_export_location = 1 20 | p_buffer_dist = 3 21 | p_title = 4 22 | p_title_field = 6 23 | p_individual = 5 24 | 25 | # get year from date object 26 | date_object = date.today() 27 | year = date_object.strftime("%Y") 28 | 29 | # default tool field values 30 | d_layer = 'Mailing Parcels' 31 | d_export_location = 'N:/PlanningZoning/SiteMaps/{}'.format(year) 32 | d_buffer_dist = '350 Feet' 33 | 34 | # symbol layers 35 | symbols = { 36 | 'red': 'N:/ArcGIS10/Layerfiles/Generic/Red_Boundary.lyr', 37 | 'gray': 'N:/ArcGIS10/Layerfiles/Generic/Gray_Boundary.lyr', 38 | 'orange': 'N:/ArcGIS10/Layerfiles/Generic/Orange_Boundary.lyr', 39 | } 40 | 41 | 42 | class SiteMapGenerator(object): 43 | """ 44 | Buffers, selects, and exports a map or set of individual maps 45 | for chosen features, and a csv with the rows. 46 | """ 47 | def __init__(self): 48 | self.label = 'Generate Site Maps and CSV' 49 | self.description = """ 50 | Generates a site map buffering where necessary 51 | """ 52 | self.canRunInBackground = False 53 | 54 | def getParameterInfo(self): 55 | params = [arcpy.Parameter( 56 | displayName='Layer', 57 | name='layer', 58 | direction='Input', 59 | datatype='GPLayer', 60 | parameterType='Required', 61 | ), arcpy.Parameter( 62 | displayName='Export Folder', 63 | name='export_location', 64 | direction='Input', 65 | datatype='DEFolder', 66 | parameterType='Optional', 67 | ), arcpy.Parameter( 68 | displayName='Create output folder if it does not exist?', 69 | name='autocreate_folder', 70 | direction='Input', 71 | datatype='GPBoolean', 72 | parameterType='Optional', 73 | ),arcpy.Parameter( 74 | displayName='Buffer Distance (include units, example: 350 Feet)', 75 | name='buffer_dist', 76 | direction='Input', 77 | datatype='GPString', 78 | parameterType='Optional', 79 | ), arcpy.Parameter( 80 | displayName='Document Title and Export Folder (Insert Document Title into layout)', 81 | name='title', 82 | direction='Input', 83 | datatype='GPString', 84 | parameterType='Optional', 85 | ), arcpy.Parameter( 86 | displayName='Include individual maps of selected features?', 87 | name='individual', 88 | direction='Input', 89 | datatype='GPBoolean', 90 | parameterType='Required', 91 | ), arcpy.Parameter( 92 | displayName='Title Field (field to use as individual map document title)', 93 | name='title_field', 94 | direction='Input', 95 | datatype='GPString', 96 | parameterType='Optional', 97 | )] 98 | params[p_layer].value = d_layer 99 | params[p_export_location].value = d_export_location 100 | params[p_buffer_dist].value = d_buffer_dist 101 | params[p_title_field].filter.type = 'valueList' 102 | params[p_title_field].filter.list = [] 103 | params[p_title_field].enabled = False 104 | params[p_individual].value = False 105 | params[p_autocreate_folder].value = False 106 | 107 | return params 108 | 109 | def updateMessages(self, parameters): 110 | 111 | # auto create output folder 112 | if parameters[p_autocreate_folder].valueAsText == 'true': 113 | path = parameters[p_export_location].valueAsText 114 | if not arcpy.Exists(path): 115 | try: 116 | verify_path_exists(path) 117 | parameters[p_export_location].clearMessage() 118 | except Exception as e: 119 | parameters[p_export_location].setErrorMessage('The path you entered is not valid: {}'.format(e)) 120 | 121 | # filter the field list 122 | if not parameters[p_layer].hasError() and parameters[p_layer].valueAsText: 123 | parameters[p_title_field].filter.list = [ 124 | f.baseName for f in arcpy.Describe(parameters[p_layer].valueAsText).fields] 125 | 126 | # set title field to enabled if we are creating individual maps 127 | parameters[p_title_field].enabled = ( 128 | parameters[p_individual].valueAsText == 'true') 129 | 130 | def execute(self, parameters, messages): 131 | """generates the site map pdf and csv files""" 132 | 133 | # set up local vars for easy access 134 | layer = parameters[p_layer].valueAsText 135 | export_location = parameters[p_export_location].valueAsText 136 | buffer_dist = parameters[p_buffer_dist].valueAsText 137 | title_field = parameters[p_title_field].valueAsText 138 | individual = parameters[p_individual].valueAsText 139 | document_title = parameters[p_title].valueAsText 140 | current_document = arcpy.mapping.MapDocument("CURRENT") 141 | data_frame = arcpy.mapping.ListDataFrames(current_document)[0] 142 | 143 | arcpy.AddMessage("""layer={}; export_location={}; buffer_dist={}; 144 | title_field={}; individual={}; document_title={};""".format( 145 | layer, export_location, buffer_dist, title_field, individual, 146 | document_title)) 147 | 148 | # generate the output workspace 149 | if document_title and document_title != '': 150 | directory = document_title 151 | file_name = get_safe_string(document_title) 152 | else: 153 | directory = time.strftime('%m-%d-%y-(%H.%M)') 154 | file_name = time.strftime('%m-%d-%y-(%H.%M)') 155 | 156 | export_location = os.path.join(export_location, directory) 157 | verify_path_exists(export_location) 158 | arcpy.AddMessage( 159 | 'Creating temp workspace: {}/{}_data.gdb'.format(export_location, file_name)) 160 | arcpy.CreateFileGDB_management( 161 | export_location, '{}_data.gdb'.format(file_name)) 162 | file_gdb = '{}/{}_data.gdb'.format(export_location, file_name) 163 | 164 | # activate export view 165 | current_document.activeView = 'PAGE_LAYOUT' 166 | 167 | # export the selected features 168 | arcpy.AddMessage( 169 | 'Exporting: layer={}, {}/selected'.format(layer, file_gdb)) 170 | arcpy.FeatureClassToFeatureClass_conversion( 171 | layer, file_gdb, 'selected') 172 | 173 | # perform buffer if necessary 174 | if buffer_dist: 175 | # buffer and select features 176 | arcpy.AddMessage( 177 | 'Buffering and Selecting: layer={}, {}/buffer'.format(layer, file_gdb)) 178 | arcpy.Buffer_analysis(layer, '{}/buffer'.format(file_gdb), 179 | buffer_dist, 'FULL', 'ROUND', 'ALL') 180 | arcpy.SelectLayerByLocation_management(layer, 'INTERSECT', 181 | '{}/buffer'.format(file_gdb), 0, 'NEW_SELECTION') 182 | buffer = MapDocument.add_map_layer('{}/buffer'.format(file_gdb), 183 | symbols['orange'], '{} Buffer'.format(buffer_dist)) 184 | 185 | # export the selected features 186 | arcpy.AddMessage( 187 | 'Exporting: layer={}, {}/buffer_selected'.format(layer, file_gdb)) 188 | arcpy.FeatureClassToFeatureClass_conversion( 189 | layer, file_gdb, 'buffer_selected') 190 | buffer_selected = MapDocument.add_map_layer('{}/buffer_selected'.format(file_gdb), 191 | symbols['gray'], 'Buffered Features') 192 | 193 | # add original selection 194 | selected = MapDocument.add_map_layer('{}/selected'.format(file_gdb), 195 | symbols['red'], 'Selected Features') 196 | 197 | # prep output pdf 198 | final_pdf = arcpy.mapping.PDFDocumentCreate(os.path.join( 199 | export_location, '{}_Final.pdf'.format(file_name))) 200 | arcpy.AddMessage('exporting pdf file {}_Final.pdf to {}'.format( 201 | file_name, export_location)) 202 | 203 | # activate export view 204 | current_document.activeView = 'PAGE_LAYOUT' 205 | 206 | #zoom to the extent of the buffer 207 | data_frame.extent = Extent.expand(buffer_selected.getExtent(), 10) 208 | 209 | #set the title 210 | current_document.title = document_title 211 | 212 | # clear selection in layers with selected rows 213 | for l in arcpy.mapping.ListLayers(current_document): 214 | try: 215 | if arcpy.Describe(l).fidSet != '': 216 | # throws errors on mosaic layers 217 | arcpy.SelectLayerByAttribute_management(l, "CLEAR_SELECTION") 218 | except: 219 | pass 220 | 221 | # export 222 | MapDocument.export_and_append(export_location, final_pdf) 223 | 224 | rows = [] 225 | cursor = arcpy.da.SearchCursor( 226 | '{}/buffer_selected'.format(file_gdb, file_name), ['SHAPE@', '*']) 227 | for row in cursor: 228 | rows.append(row) 229 | if individual == 'false': 230 | continue 231 | # generate the pdf 232 | if title_field in cursor.fields: 233 | current_document.title = row[cursor.fields.index(title_field)] 234 | data_frame.extent = Extent.expand(row[0].extent, 10) 235 | MapDocument.export_and_append(export_location, final_pdf) 236 | 237 | # write out csv rows 238 | arcpy.AddMessage('Exporting csv: {}_output.csv to {}'.format( 239 | file_name, export_location)) 240 | # open csv file as binary so it avoids windows empty lines 241 | csv_output = open(os.path.join( 242 | export_location, '{}_output.csv'.format(file_name)), 'wb') 243 | csv_writer = csv.writer(csv_output, delimiter=',', 244 | quoting=csv.QUOTE_ALL, dialect='excel') 245 | csv_writer.writerow(cursor.fields) 246 | csv_writer.writerows(rows) 247 | 248 | # save the outputs 249 | csv_output.close() 250 | final_pdf.saveAndClose() 251 | 252 | # open explorer 253 | Popen('explorer "{}"'.format(export_location.replace('/', '\\'))) 254 | 255 | # clean up 256 | del cursor 257 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/esri-python-tools/2f5c428b76b2f96c83803351681624bcc8bbc348/tools/__init__.py --------------------------------------------------------------------------------