Skip to content

Commit

Permalink
Updates for 4.0 release
Browse files Browse the repository at this point in the history
  • Loading branch information
mnelsonGIS committed Jun 18, 2023
1 parent 0abfab4 commit 0605dae
Show file tree
Hide file tree
Showing 16 changed files with 405 additions and 0 deletions.
Binary file added v4.0/Configuration/Basemap Configuration.docx
Binary file not shown.
Binary file added v4.0/Configuration/ERM API Configuration.docx
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
32 changes: 32 additions & 0 deletions v4.0/Operational-Documentation/Delete Plan - Job Aid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
**Deleting a Plan** from ERM - [Job Aid](https://en.wiktionary.org/wiki/job_aid)
This job aid uses the Portal UI and ArcGIS Pro to find and delete plans. These tasks could be automated through the [Python API](https://developers.arcgis.com/python/) as well
1. Open the ERM_Registry table in ArcGIS Pro.
2. Search for a plan by the "Dispatch Location", "First Event" (Creation time), "Last Event" (Last updated time), or any of the other fields. Optionaly, set a definition query on any field.
![Image for Pro Query](https://user-images.githubusercontent.com/3834298/90927990-a97aac80-e3bb-11ea-9e40-0c90c28fe903.jpg)
3. Note the "Item Id" which corresponds to Feature Layer ID. The "Webmap Id" which corresponds to the Web Map, and (if applicable), the "Dashboard Id" which corresponds to a plan dashboard if one was created.

*Note: If you are using the **Workforce Extension for ERM**, save the "Item ID" somewhere you can paste it later if you also need to clear the Workforce project.*

4. In Portal, search for and delete the following items in the following order
* If applicable: the Dashboard
* Webmap item
![Image for delete web map](https://user-images.githubusercontent.com/3834298/90928011-b4cdd800-e3bb-11ea-9c39-65be90843180.jpg)
* Feature layer
* Row for the plan in ERM_Registry

The plan is now completely deleted and will no longer appear in any items inventory including RPE.

If you are using the **Workforce Extension for ERM**, follow Steps 5-7 below to delete records published into Workforce from ERM

*Note: This process is not needed for cleaning out older (prior days) features since those features will be purged on a schedule. This process is intended to truncate an active plan so that it can be replaced by a new plan.*

5. Retrive the ItemID you noted in Step 3 above.
6. Generate a token using the steps below
* a. Go to https://\<server name\>/portal/sharing/rest/generateToken. \<server name\> represents the fully qualified domain name of the server that portal is deployed on.
* b. Enter Username and Password. Use Portal credentials for an administrator level account.
* c. For Webapp URL, enter the Portal URL. This should match the config.portalUrl value in the Middleware API config file.
![Image for generate token](https://user-images.githubusercontent.com/3834298/94065006-211f6b00-fdb0-11ea-9b81-90c254e36db9.png)

7. Paste https://\<middleware server name\>/ermapi/workforce/deletePlan?planItemId=XXX&token=YYY into your browser. \<middleware server name\> represents the fully qualified domain name of the server that Middleware API is deployed on.
* Replace XXX with the plan ID previously identified.
* Replace YYY with the token that was generated.
Binary file not shown.
45 changes: 45 additions & 0 deletions v4.0/Operational-Documentation/ERM_CopySolveParameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

"""
--------------------------------
Name: ERM_Copy Solve Parameters.py
Purpose: Copy default solve parameter records for each location
Author: Mike Nelson
Created 5/26/2020
Copyright: (c) Esri
ArcGIS Version: 2.4 (Pro)
PYTHON Version: 3.6 (API 1.8)
Requirements: update variables for feature class/table paths
Expects DepotTemplate to be fully populated before running
--------------------------------
"""

import arcpy


def get_value_list(in_layer, field_name):
with arcpy.da.SearchCursor(in_layer, [field_name]) as cursor:
return sorted({row[0] for row in cursor})

################### UPDATE VARIABLES ##############################
depot_template = r"C:\ERM\services\fgdbs\ERM_Plan_Defaults.gdb\DepotTemplate"
depot_name_field = "depotname"
solve_param_defaults_table = r"C:\ERM\services\fgdbs\ERM_Solve_Parameters.gdb\Solve_Parameters_Restrictions_DefaultValues"
solve_param_table = r"C:\ERM\services\fgdbs\ERM_Solve_Parameters.gdb\Solve_Parameters_Restrictions"
solve_para_depot_field = "displocname"

# make view for tables for selection/calculate tools to use
solve_param_view = "solve_param_view"
arcpy.MakeTableView_management(solve_param_table, solve_param_view)

# get list of all depot locations
depot_list = get_value_list(depot_template, depot_name_field)

for depot in depot_list:
arcpy.AddMessage(f"Processing {depot}...")

arcpy.AddMessage("Append default records...")
arcpy.Append_management(solve_param_defaults_table, solve_param_table, "NO_TEST")
arcpy.AddMessage("Select blank records...")
arcpy.management.SelectLayerByAttribute(solve_param_view, "NEW_SELECTION", solve_para_depot_field + " = ''", None)
arcpy.AddMessage("Calculate depot name...")
arcpy.management.CalculateField(solve_param_view, solve_para_depot_field, f"'{depot}'", "PYTHON3", '', "TEXT")
37 changes: 37 additions & 0 deletions v4.0/Operational-Documentation/ERM_CreateGroups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
--------------------------------
Name: ERM_CreateGroups.py
Purpose: Create Portal groups used by Enterprise Route Management system
Author: Mike Nelson
Created 5/26/2020
Copyright: (c) Esri
ArcGIS Version: 2.4 (Pro)
PYTHON Version: 3.6 (API 1.8)
Requirements: update group_list variable.
Create Environment variables exist for credentials, or just change variables to store directly
--------------------------------
"""

import arcpy, os, sys
from arcgis.gis import GIS
from os import environ

group_list = ["COV", "OCC", "GOL", "INL"]

# portal credentials
erm_portal = environ["ERM_PORTAL"]
erm_user = environ["ERM_USER"]
erm_pswd = environ["ERM_PWD"]

# Sign in/connect to portal
gis = GIS(erm_portal, erm_user, erm_pswd, verify_cert=False)

for group_name in group_list:
tag_list = f"dispatch-location-{group_name}, ERM"
arcpy.AddMessage(f"Creating group {group_name}...")
geocaching_group = gis.groups.create(title=group_name,
tags=tag_list,
description=f"ERM group for location {group_name}",
access='org',
is_invitation_only='False')

140 changes: 140 additions & 0 deletions v4.0/Operational-Documentation/ZoneChecker.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Slivers, Excessive Vertices, Interior Voids, Excessive Parts, OID, Shape Specifics\n",
"False,False,True,False,DispLoc=ABC RouteName=ABCN parts=2 length=1094062 area=9966623390 point count=1159 ring count=58\n",
"False,False,True,False,DispLoc=ABC RouteName=ABCW parts=1 length=989373 area=10664017204 point count=950 ring count=35\n",
"False,False,True,False,DispLoc=ABC RouteName=ABCE parts=2 length=734292 area=6943725857 point count=748 ring count=28\n",
"False,False,True,False,DispLoc=ABC RouteName=ABC_LG parts=2 length=636477 area=5623342121 point count=759 ring count=29\n",
"False,False,True,False,DispLoc=ABC RouteName=ABC_City parts=1 length=316778 area=1699683040 point count=462 ring count=23\n",
"False,False,True,False,DispLoc=ABC RouteName=ABC_Gated parts=1 length=890466 area=8401019328 point count=1117 ring count=39\n",
"False,False,True,False,DispLoc=ABC RouteName=ABC_Resi parts=2 length=325441 area=1532937368 point count=457 ring count=39\n"
]
}
],
"source": [
"import arcpy\n",
"from arcgis.gis import *\n",
"#these next two support random string generation\n",
"from random import choice\n",
"from string import ascii_uppercase\n",
"\n",
"\n",
"arcpy.env.overwriteOutput = True\n",
"\n",
"#Get the layer from the TOC\n",
"featurelayer = \"ZoneTemplate\"\n",
"zoneNameField = \"displocname\"\n",
"fields = [\"displocname\",\"routename\",\"objectid\",\"shape@\"]\n",
"\n",
"#Set an expression to optionally use as a whereclause\n",
"expression = \"{} = 'DEN'\".format(arcpy.AddFieldDelimiters(featurelayer, zoneNameField))\n",
"\n",
"#create a table in scratch space to use as output \n",
"explicit_scratch = \"C:/temp\"\n",
"GDB_GUID = ''.join(choice(ascii_uppercase) for i in range(12))\n",
"explicit_scratch_ws = arcpy.CreateFileGDB_management(explicit_scratch, \"scratch_\" + GDB_GUID + \".gdb\")\n",
"arcpy.env.workspace = os.path.join(explicit_scratch, \"scratch_\" + GDB_GUID + \".gdb\")\n",
"\n",
"failedZoneCheck_tbl = arcpy.management.CreateTable(arcpy.env.workspace, \"failedZoneCheck\")\n",
"arcpy.AddField_management(failedZoneCheck_tbl, \"slivers\", \"TEXT\", field_alias=\"Sliver Polygons\", field_length = 5)\n",
"arcpy.AddField_management(failedZoneCheck_tbl, \"toodense\", \"TEXT\", field_alias=\"Excessive Vertices\", field_length = 5)\n",
"arcpy.AddField_management(failedZoneCheck_tbl, \"donuts\", \"TEXT\", field_alias=\"Interior Voids\", field_length = 5)\n",
"arcpy.AddField_management(failedZoneCheck_tbl, \"manyparts\", \"TEXT\", field_alias=\"Exessive Parts\", field_length = 5)\n",
"arcpy.AddField_management(failedZoneCheck_tbl, \"sourceOID\", \"LONG\", field_alias=\"Source OBJECTID\")\n",
"arcpy.AddField_management(failedZoneCheck_tbl, \"zoneinfo\", \"TEXT\", field_alias=\"Zone Metadata\", field_length = 200)\n",
"\n",
"#Set the fields we will use for the insert cursor\n",
"insertFields = ['slivers','toodense','donuts','manyparts','sourceOID','zoneinfo']\n",
"updateCur = arcpy.da.InsertCursor(failedZoneCheck_tbl,insertFields)\n",
"\n",
"#We are sorting the zone records by service area name and zone name, which will order them so that duplicate geometry zones are \n",
"#grouped together. We will use a \"lastArea\" shape.area tracker to recognize when we have encountered a new shape to evaluate - no need to report \n",
"#issues with each identical shape record. \n",
"lastArea = int(0)\n",
"print (\"Slivers, Excessive Vertices, Interior Voids, Excessive Parts, OID, Shape Specifics\") \n",
"\n",
"#uncomment the version with a whereclause if you want to evaluate a subset of zones - or just set a definition query on the layer\n",
"for row in sorted(arcpy.da.SearchCursor(featurelayer, fields)):\n",
"#for row in sorted(arcpy.da.SearchCursor(featurelayer, fields, where_clause = expression)):\n",
" shape = row[3]\n",
" thisArea = int(shape.area)\n",
" if (thisArea != lastArea):\n",
" if(shape.length > shape.area):\n",
" sliverShape = True\n",
" else:\n",
" sliverShape = False\n",
" if(shape.length/shape.pointCount <200):\n",
" excessiveVertices = True\n",
" else:\n",
" excessiveVertices = False\n",
" if(shape.boundary().partCount - 1 > shape.partCount):\n",
" interiorRings = True\n",
" else:\n",
" interiorRings = False\n",
" if(shape.partCount > 4):\n",
" manyParts = True\n",
" else:\n",
" manyParts = False\n",
" if(sliverShape or excessiveVertices or interiorRings or manyParts): \n",
" rowMetadata = \"DispLoc={} RouteName={} parts={:d} length={:d} area={:d} point count={:d} ring count={:d}\".format(\n",
" row[0],\n",
" row[1],\n",
" shape.partCount, \n",
" int(shape.length), \n",
" int(shape.area), shape.pointCount, \n",
" shape.boundary().partCount)\n",
" \n",
" updateCur.insertRow((str(sliverShape),str(excessiveVertices),str(interiorRings),str(manyParts),row[2],rowMetadata))\n",
" print(str(sliverShape) + \",\" + str(excessiveVertices) + \",\" + str(interiorRings) + \",\" + str(manyParts) + \",\" + rowMetadata)\n",
" \n",
" \n",
" lastArea = thisArea\n",
"del updateCur "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "ArcGISPro",
"language": "Python",
"name": "python3"
},
"language_info": {
"file_extension": ".py",
"name": "python",
"version": "3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
31 changes: 31 additions & 0 deletions v4.0/Operational-Documentation/Zones Sanity Check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
**Sanity Quality Check for Route Zones**- [Job Aid](https://en.wiktionary.org/wiki/job_aid)
This job aid uses an ArcGIS Pro notebook to run geometric evaluations of a ZoneTemplate layer and creates a report of zone polygon shapes that would likely cause a VRP solve request to fail.
1. Create a new project in ArcGIS Pro
2. Add a your ZoneTemplate layer to the map
![Image for Add Zones](https://github.com/rConger/doc_images/blob/main/ZonesLayer.jpg?raw=true)

3. Fron the Insert menu, choose to insert an existing notebook and browse to the location where you downloaded [ZoneChecker.ipynb](https://github.com/EsriPS/enterprise-route-management/blob/master/Operational-Documentation/ZoneChecker.ipynb) to add it to the project

![Image for Add Notebook](https://github.com/rConger/doc_images/blob/main/addNotebook1.jpg?raw=true)

4. From the Notebook folder in the project catalog view, find the notebook and open it
![Image for Open Notebook](https://github.com/rConger/doc_images/blob/main/OpenNotebook.jpg?raw=true)

5. Make any necessary changes to temporary files path or zones layer name, then run the notebook
![Image for Adjust and Run Notebook](https://github.com/rConger/doc_images/blob/main/AdjustNotebookAndRun.jpg?raw=true)

6. Depending on the number of zones, the notebook may take up to 2 minutes to run. When it's finished, you'll notice run report data in the notebook output an a new table added to the TOC.
![Image for Run Complete](https://github.com/rConger/doc_images/blob/main/RunReport.jpg?raw=true)

7. Open the new table added and have a look at the attributes. first four columns indicate the quality check that was failed. Records with at least one failed metric will be added to the table. Any given record could fail more than one check.
![Image for Report Table](https://github.com/rConger/doc_images/blob/main/ReportTable.jpg?raw=true)

The tests corresponding to the columns are:
* **Sliver Polygons** - any feature where perimiter length >= area
* **Excessive Vertices** - any feature whose aveage vertex spacing is closer than 200 meters
* **Interior Voids** - any feature where the number if rings is more than one greater than the number of feature parts
* **Excessive Parts** - any feture with more than 4 discontigous parts (e.g. islands)

8. You can join the report table to the original Zones polygon layer on failedZoneCheck.Source OBJECTID -> ZoneTemplate.OBJECTID to more easily examine any failed feature. For example:
![Image for Failed Check](https://github.com/rConger/doc_images/blob/main/FailedCheck1.jpg?raw=true)
![Image for Failed Check](https://github.com/rConger/doc_images/blob/main/FailedCheck2.jpg?raw=true)
14 changes: 14 additions & 0 deletions v4.0/Schema/BSI Collections Sync Schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
| Field Name | Data Type | Description | Optional/Required |
|----------------------|------------|-------------------------------------------------------------------------------------------|-------------------|
| CollectionName | String 128 | Name of this collection | Required |
| Destination | String 50 | Freeform contextual description | Optional |
| DestinationETA | Date | Time this collection is expected to be arrived/available for loading onto P&D routes. UTC | Optional |
| DispatchInstructions | String 100 | Freeform contextual description | Optional |
| EarliestCommit | Date | Time of the earliest service commitment in this collection. UTC | Optional |
| FinalDestination | String 50 | Freeform contextual description | Optional |
| LocalOrders | Int | Descriptive Double precision | Optional |
| LocationDescription | String 50 | Freeform contextual description | Optional |
| Origin | String 50 | Freeform contextual description | Optional |
| ServiceLevel | String 50 | Freeform contextual description | Optional |
| ToBeRemoved | String 5 | true or false - Should this order be deleted from the plan | Optional |
| TotalOrders | Int | Freeform contextual description | Optional |
6 changes: 6 additions & 0 deletions v4.0/Schema/BSI Order Pairs Sync Schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
| Field Name | Data Type | Description | Optional/Required |
|-----------------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
| FirstOrderName | String 128 | Instructions to optimize an order relative to another. PU order prior to DEL the same order. FK to GeoOrder.orderid | Required |
| SecondOrderName | String 128 | Instructions to optimize an order relative to another. DEL order after it's PU. FK to GeoOrder.orderid | Required |
| MaxTransitTime | Float | max time this order can spend on a route from the time it's picked up to the time it's delivered. Minutes | Optional |
| ToBeRemoved | String 5 | true or false - Should this order pair be removed from the plan? | Optional |
Loading

0 comments on commit 0605dae

Please sign in to comment.