Building the Find Zoning District By Parcel App

screen shot of a municipal zoning map

Our IT department is developing an online plan submission website for the County’s Planning office.  In talking with the developers, the topic of finding the zoning district for a tax parcel (or piece of real estate) came up.  Our GIS department publishes services for both tax parcels and zoning districts.  I’ve also been wanting to dig deeper into the querying features of the Esri Leaflet library.  And so, an idea was born for a simple web app: a user types in a parcel number, and gets back the zoning district(s) for that parcel, along with a map of the area.

I haven’t completed the app just yet, but the major logic is complete.  I also heard our developers talk about getting your app to work before you focus on the design/styling.  So I also decided to approach the app with that mindset.  Although I’m still working on the design for the app, I wanted to share the work-in-progress.

I’m putting this project on Github.  You can view the in-progress demo.  I have no idea if this app will get green lighted, but I do plan on sharing with our Planning department once it’s finished.

Here’s some sample values to enter in the app:

  • a parcel with a single zoning district: 40-31-2187-062
  • a parcel with multiple zoning districts: 22-12-0350-052
  • a parcel number that does not exist: 52-01-123-004
screen shot of the get zoning by parcel web map app
Screen shot of the Find Zoning By Parcel concept web map app.

Functions, Functions, Functions

Another thing I tried with this project was to break the logic into several smaller helper functions.  There are two main queries occurring: 1) take the user entered parcel ID and query that against the tax parcel service; and 2) pass the returned tax parcel object into a spatial query (intersect) against the zoning service.  I had to call the zoning service query within the tax parcel query.  It might be possible to have them completely separate, but I was not able to figure that out.

// function to select parcel based upon the pin (parcel ID)
function selectParcelByPin(pin, taxParcelLayer) {
   // "pin" is entered in the search form
   // "taxParcelLayer" is an empty L.geoJson (Leaflet GeoJSON) object
   // the returned tax parcel record with be GeoJSON and we will add this data to the empty "taxParcelLayer"

   // call function to get zoning district for user entered parcel ID
   // this function will pass the tax parcel object into a spatial query against the zoning service ("zoningUrl")
   getParcelZoningDistrict(taxParcelLayer, zoningUrl);
}

Although having a single layer for every municipality’s zoning districts would be ideal, our business needs require us to have a separate layer for each municipality.  So we’ll need a way to get the correct zoning layer (sub-layer within parent service) to run the query against.  Fortunately, the first two characters of a valid parcel ID represent a municipal code (technically a “tax ward”).  We’ll need to create another helper function to get the correct zoning service.

// function to get correct zoning service for parcel ID
function selectZoningService(pin) {
   // "pin" is the parcel ID entered by the user in the search form
   // base zoning URL. Each municipality is a sub-layer
   var baseUrl = '[domain]/rest/services/Zoning/MapServer';
   // first two digits of parcel ID are the municipal code
   // we'll run the string split method to get the municipal code
   var muniCode = pin.split("-")[0];
   // zoning URL for municipality
   // we'll set this using a switch statement and return the variable
   var zoningUrl;

   // set URL based upon muni code
   switch(muniCode) {
      // Municipality A
      case '01':
         zoningUrl = baseUrl + '/5';
         break;
      // Municipality B
      case '02':
         zoningUrl = baseUrl + '/6';
         break;
      // cover all municipalities
      // default case would "break" query - no worries - we can handle query errors and no results later
      default:
         zoningUrl = '';
         break;
   }
   // return the correct zoning url
   return zoningUrl;
}

// We can pass this helper function as an argument to our get zoning district function
getParcelZoningDistrict(taxParcel,selectZoningService(pin));

Let’s keep tracking on the function to get the zoning district(s) for the selected tax parcel.  Esri Leaflet’s query class (L.esri.Query) allows us to perform attribute and various spatial queries.  Within “[q]ueries features from the service within (fully contained by) the passed geometry object.”  Contains “[q]ueries features from the service that fully contain the passed geometry object.”  Intersects “[q]ueries features from the service that intersect (touch anywhere) the passed geometry object.”

For better or for worse, there are many instances where more than one zoning district are within a parcel.  And I would like this app to let users know about all zoning districts within their parcel.  So after some testing, I realized the Intersects query was the most appropriate.  I’ll also want to loop through all returned zoning district features.  For each feature, I’ll want to grab information about the zoning district name, code, and generalized category.  I’ll put this data into an array.  Finally, I’ll loop through that array and put the data into some nice markup to display for the user.

section of the zoning map for Monroe Township
Here’s a parcel with two zoning districts

I also write a lot of code in Python, usually in the context of running scripts as scheduled tasks. This has given me an appreciation for handling situations where things don’t work as planned.  So within my queries, I run conditional logic to handle errors with the requests, and situations where no objects are returned.  And I should also state that I am not finished with this part of the project.  I will be displaying error and warning messages to the user. I have notes in my code for future action steps.

// function to get zoning district for parcel
// use intersects method (query) to catch cases where multiple zones are within a parcel
function getParcelZoningDistrict(parcel,zoningURL) {
   // performing spatial query
   // We are asking: "What zoning district features within provided service intersect the parcel object we are providing
   L.esri.query({url: zoningURL}).intersects(parcel).run(function(error,response) {
      if (error) {
         console.log('An error with the request has occurred');
         // place message in results panel - action step
      } else if (response.features < 1) {
            console.log('No features returned or an issue occured');
            // place message in results panel - action step
      }  else {
            // array to hold all zoning information
            // we will loop through this to create output for user
            var zoningInfo = [];

            // loop through all zoning districts intersecting parcels
            for (var i = 0; i < response.features.length; i++) {
            // fields from service
            var zoningDistrictName = response.features[i].properties.ZoneName;
            var zoningDistrictCode = response.features[i].properties.ZoneCode;
            var zoningDistrictCategory = response.features[i].properties.ZoneType;

            // array to hold results for each zoning district that intersects
            var resultsArray = [];
            resultsArray[0] = zoningDistrictName;
            resultsArray[1] = zoningDistrictCode;
            resultsArray[2] = zoningDistrictCategory;

            // add array for each zoning district to master array
            zoningInfo.push(resultsArray);
          }

         // call populate results function
         // I will review this function below
         populateResults(zoningInfo);
      }
    });
}

The array zoningInfo will contain zoning district information for each district that intersects the tax parcel matching the user’s entered PIN value.  Let me review the populateResults function.  This will convert the contents of the array to HTML mark-up, so we can inform the user of the results of the analysis.  I was having troubles with the code highlighting, so I provided an image of this function.

code snippet of populateResults function
populateResults function displays analysis results to user

 

 

Cumberland County comprehensive plan public meeting
Comprehensive Plan meeting for Cumberland County

Most of the heavy lifting is occurring with the spatial query to get the zoning districts.  The query to get the parcel object is much simpler.  I’ll share that next (again, issues, so I’m using an image).

screenshot of function to query Parcels
Function to query Parcel’s service with parcel ID

 

The  final part of this app is a few clean-up actions that occur on the function that is called when the form element containing the parcel ID is clicked.  I set the opacity of the results element to zero to hide it.  I also clear the previous selected tax parcel from the GeoJSON layer.

/*** DOM Objects ***/
// panel containing zoning district information
var resultsPanel = document.getElementById('panelResults');
// element within panel containing results of analysis
var resultsEl = document.getElementById('results');
// search form element
var userForm = document.getElementById('search');

// code related to creating the Leaflet map and adding layers,controls, etc.

// Container for selected parel
var taxParcel =  L.geoJson().addTo(map);

// add event listener to form
userForm.addEventListener('click', function(e) {
   // default default behavior
   e.preventDefault();

   // change opacity on results element back to 0
   resultsPanel.style.opacity = 0;

   // get pin value entered in form
   var pin = document.getElementById('pin').value;   

   // Remove previous tax parcel GeoJSON feature
   if (taxParcel.getLayers().length > 0) {
       taxParcel.clearLayers();
   }

   // call parcel query function
   selectParcelByPin(pin, taxParcel, resultsEl, resultsPanel);
});
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s