Dreaming of Code

NGS Survey Control REST API

March 26, 2015

I've been working for a Land Surveying company as both a field chief and a project manager for almost a decade now. After deciding to pursue a career in web development, I've been trying to think of ways to use my newly found knowledge to benefit the Land Surveying Community. I've noticed that the NGS website is outdated and not easily accessible on mobile devices so I decided to put together a quick web page to make the search for survey control that much easier.

Demo

A demo for the application can be found at ngs-survey-control.herokuapp.com. All you need to know is the particular coordinates of a site you are working on and you can search for results within ten miles of the site.

Behind the Scenes

The application is built on the Ruby programming language but could be easily applied to other languages. The following ruby class is used to make the request to the API and it only depends on the Faraday gem. There's quite a bit of math related to figuring out the latitudes and longitudes of the envelope of the query. Essentialy the envelope_distance method creates a 10 by 10 mile square (or close to it) with the center being the requested coordinates. The distance_to method will calculate the distance between a particular result and the initialized location. The results_within_mile method narrows the results down to a radial search based on the entered distance.

class NgsQuery
  attr_accessor :latitude, :longitude, :response

  def initialize(latitude, longitude)
    @latitude = latitude
    @longitude = longitude
    send_request
  end

  def results_within_mile(distance)
    JSON.parse(response.body)['features'].select { |feature| distance_to(feature['geometry']) < distance.to_f }
  end

  def distance_to(location={})
    # Haversine Formula
    φ1 = latitude.to_f * Math::PI / 180
    φ2 = location['y'].to_f * Math::PI / 180
    long1 = longitude.to_f * Math::PI / 180
    long2 = location['x'].to_f * Math::PI / 180

    earth_radius = (6371000 * 0.000621371).to_f # miles
    Δφ = (φ2-φ1)
    Δλ = (long2-long1)

    a = (Math.sin(Δφ/2) * Math.sin(Δφ/2)) + (Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2))
    c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))

    (earth_radius * c.to_f).round(2)
  end

  def send_request
    @response = conn.get "/ArcGIS/rest/services/NGS_Survey_Control_Points/MapServer/1/query", request_params
  end

  private

  def conn
    Faraday.new(:url => query_url) do |faraday|
      faraday.request  :url_encoded             # form-encode POST params
      faraday.adapter  Faraday.default_adapter  # make requests with Net::HTTP
    end
  end

  def request_params
    {
      f: "json",
      outSR: "4326",
      geometryType: 'esriGeometryEnvelope',
      geometry: envelope_distance,
      inSR: "4326",
      outFields: "DATA_SRCE,NAME,LAST_COND,ELEV_ORDER,STABILITY"
    }
  end

  def query_url
    "http://maps1.arcgisonline.com"
  end

  def envelope_distance
    lat = latitude.to_f * Math::PI / 180

    m1 = 111132.92
    m2 = -559.82
    m3 = 1.175
    m4 = -0.0023
    p1 = 111412.84
    p2 = -93.5
    p3 = 0.118

    latlen = m1 + (m2 * Math.cos(2 * lat)) + (m3 * Math.cos(4 * lat)) + (m4 * Math.cos(6 * lat))
    longlen = (p1 * Math.cos(lat)) + (p2 * Math.cos(3 * lat)) + (p3 * Math.cos(5 * lat))

    latfeet = latlen * 3.280833333
    latsm = latfeet / 5280
    latenv = 10 / latsm

    longfeet = longlen * 3.280833333
    longsm = longfeet / 5280
    longenv = 10 / longsm
    "{xmin: #{longitude.to_f - longenv}, ymin: #{latitude.to_f - latenv}, xmax: #{longitude.to_f + longenv}, ymax: #{latitude.to_f + latenv}}"
  end
end

Retrieving Results

You can initialize and output results like so:

@query = NgsQuery.new('40','-105')
@results = query.results_within_mile(2)
# =>
[
  {
    "attributes" => {
      "DATA_SRCE"   => "http://www.ngs.noaa.gov/cgi-bin/ds_mark.prl?PidBox=AB3292",
      "NAME"        => "BASELINE",
      "LAST_COND"   => "GOOD",
      "ELEV_ORDER"  => " ",
      "STABILITY"   => "A"
    },
    "geometry" => { "x" => -104.97833635353035, "y" => 39.99991861318452 }
  },
  {
    "attributes" => {
      "DATA_SRCE"   => "http://www.ngs.noaa.gov/cgi-bin/ds_mark.prl?PidBox=AI3578",
      "NAME"        => "LUCY",
      "LAST_COND"   => "GOOD",
      "ELEV_ORDER"  => " ",
      "STABILITY"   => "C"
    },
    "geometry" => { "x" => -105.01147022230495, "y" => 40.00009957099936 }
  },
  {
    "attributes" => {
      "DATA_SRCE"   => "http://www.ngs.noaa.gov/cgi-bin/ds_mark.prl?PidBox=KK2064",
      "NAME"        => "SLATER",
      "LAST_COND"   => "GOOD",
      "ELEV_ORDER"  => " ",
      "STABILITY"   => "C"
    },
    "geometry" => { "x" => -104.9939545780444, "y" => 39.99348464681532 }
  }
]

Creating the Views

Here's a simple html table built using erb syntax where @results is an array of results similar to what's shown above and @query is the initialized NgsQuery class. The application that I'm using is built on the Ruby on Rails framework which uses the link_to method to generate the link for the datasheet.

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Elevation Order</th>
      <th>Stability</th>
      <th>Condition</th>
      <th>Link</th>
      <th>Distance from coordinates</th>
    </tr>
  </thead>
  <tbody>
    <% @results.each do |result| %>
      <tr>
        <td><%= result['attributes']['NAME'] %></td>
        <td><%= result['attributes']['ELEV_ORDER'] %></td>
        <td><%= result['attributes']['STABILITY'] %></td>
        <td><%= result['attributes']['LAST_COND'] %></td>
        <td><%= link_to result['attributes']['DATA_SRCE'], result['attributes']['DATA_SRCE'], target: "_blank" %></td>
        <td><%= @query.distance_to(result['geometry']) %></td>
      </tr>
    <% end %>
  </tbody>
</table>

Sample of Results that can be ouput to a table

Name Elevation Order Stability Condition Link Distance from Coordinates
AS P 313 C MONUMENTED http://www.ngs.noaa.gov/cgi-bin/ds_mark.prl?PidBox=KK1950 1.77
BALD MTN GOOD http://www.ngs.noaa.gov/cgi-bin/ds_mark.prl?PidBox=KK1949 1.77
C 175 2 C MARK NOT FOUND http://www.ngs.noaa.gov/cgi-bin/ds_mark.prl?PidBox=JK0449 2.85

The most useful part of displaying the results to a table is that it returns the condition of the monument. You can see that point C 175 says "MARK NOT FOUND". Normally a surveyor won't notice this until they are out in the field with the datasheet and it's too late at that point. You can easily skip over that point without even looking at the datasheet.


I hope you enjoyed this post and you can see the full source of the project at https://github.com/rlafranchi/ngs-survey-control

Resources