#Geo - Basic Spatial Intersections
I have had success implementing basic spatial intersections in forms by storing spatial data in GeoJSON format within the form's page level JavaScript.
Example:
A data collector takes a point using the location widget, and it is necessary to insert into the form what statistical area, county, zone, etc the point is within. This could be done in post-processing, but suppose this information is needed for conditional logic within the form – for example, the form may change to collect different data depending on where the point was taken.
One approach to solve this I considered was to get the polygon the point was within by sending the lat and lon of the point to my agency’s map server – but this would not work if the form were offline.
A better solution was to export the polygons I was interested in as GeoJSON data, including only the fields for the feature shapes and the data I was interested in obtaining. For polygons with complex edges like rivers or shorelines, I simplified the geometry to reduce the vertex count, since there is a 65k character limit on page-level JS.
The GeoJSON data was stored as variable in PageLevel JS, along with a function from GitHub here to determine if an point is within a polygon. The point is input as an x,y array and the polygon is represented as an array of x,y arrays.
Then a function was added to take an input lat/lon and iterate through each feature in the GeoJSON, along with each part of the feature for multi-feature parts, and test if the point is within the feature. If the point is within the feature, the desired attribute/attributes of the feature are appended to an array, allowing the function to handle overlapping polygons in the input GeoJSON. The argument returns each of the polygon attributes the input point intersects, sorted alphabetically and omitting duplicates.
A full example of this is on GitHub – this idea seems to work well for simple geometries like municipal/state boundaries, keeping in mind more complex geometries will contain more vertices.
Another idea would be to use the Haversine distance equation to determine the closest point/line/polygon to a collected point, but I have not had a need to implement this yet. Ideally, it would be great if this information could be passed as arrays from other forms rather than stored within the form as a GeoJSON. Example: based on an input location, find the nearest port/town/sample site etc from a Smart Table Search. Basically a Smart Spatial Search.
-
I have a need for something similar to this, but am not well versed with coding or PageLevel JS. Would it be possible to get a sample of the code that you input into the PageLevel JS of your form to make this work?
I basically want to populate a value for County name, based on coordinates from the location widget. For Washington State Only. I accomplished this in the past with the Google Maps API, but that solution is finicky and doesn't work without internet connection. Here's a GeoJSON I was thinking I could use for this as well: https://raw.githubusercontent.com/deldersveld/topojson/master/countries/us-states/WA-53-washington-counties.json
Thanks for any help you can provide!
-
Hi Brant - Are you with WDFW? Did not realize the link I had provided above was dead - I edited it to the code on my Gist page: https://gist.github.com/bdevoe/bc2637105b59ef0599b291f0941f215b
The example uses some zone lines encoded as a GeoJSON. The GeoJSON was built by exporting the zone polygons from ArcGIS as a GeoJSON, after first removing unneeded attributes and then simplifying areas of the polygons that contained numerous vertices (ie, shorelines were simplified to have fewer vertices). This is necessary because you can store a maximum of 2 ^ 16 characters in Page Level JavaScript, so the resulting GeoJSON needs to be short enough to fit. Also notice in the example provided, the JSON is inserted as one line with no carriage returns (as opposed to a "pretty" JSON), as this would increase the character count. The projection was also set to WGS84 (EPSG: 4326) to match the lat/lon coordinates put out by the Location widget.
The example you have provided is not actually a GeoJSON, but is a TopoJSON - an extension of GeoJSON that supports topology. This is a much more efficient way of representing something with colinear edges like counties, but would not work with the code I had provided. It is also not in the same projection as used by iFormBuilder (EPSG:4326). Using TopoJSONs for this is an intriguing idea though, as you could fit many more vertices within the form - maybe I will look into this at some point.
So to get this to work in your case with the code I provided you would get a ShapeFile of Washington counties (the source for the TopoJSON you provided is here), load it into your GIS of choice, and either simplify the vertice count (which I would think would be essential giving the complexity of the WA coast), or more simply using a county boundary layer that extends past the shoreline to the limits of state waters.
Once you have this to a small enough size to fit in the form, you add the PointInPolygon function from RPI and then edit the WhatZone function to iterate over each polygon and determine if the lat/lon provided is within the polygon - and if it is, add the attribute value of choice to the output list. The way I have it configured in the provided example will work with overlapping polygons, but this would not be required for counties.
Note this is not a true spatial intersection as you would get with GIS software. You are intersecting lat/lon coordinates that are not projected, basically treating them as Euclidean x/y coordinates even though they are angular (same idea as how a Mercator projection distorts distance/area), but it seems to work well enough for the accuracy required. Also as noted this does not work with "donut hole" features or features with indentations wherein a ray cannot be cast without intersecting the polygon, but it will work with multi-part features. Given that municipal boundaries typically do not have these geometry types I would not expect this to be a problem.
-
Bill,
Thanks for the prompt, (and detailed!) response. Also appreciate the updated link and explanations. I actually work with WSDA, but I know WDFW has been a heavy iForm user for a few years now as well.
I was able to simplify, reproject, and export out a GeoJSON of WA counties that is around 43K characters. I went in and gave the JS a whirl as well, but I've not been successful with getting it to work in my form.
Not sure if my coding is off or if maybe I'm just not calling on the function correctly. Any chance you could glance over my script?: https://gist.github.com/bcarman/22baadbaa3c7a821dd626d98ae4e6b16
To extract a value from this in my form I assumed that I needed to set {WhatCounty(point)} as a dynamic value in the element to be populated with a county name. 'point' being the DCN of an element that I am populating with "longitude,latitude" from my location widget. Is this right?
I'm somewhat of a novice when it comes to JS and iForm's PageLevel JS, so sorry for all the questions. I really appreciate any help on this, and everything you've provided so far.
Thanks again.
-
You got it 99.5% right - remove the equal sign on line 35:
for (x in county=.features)
to
for (x in county.features)
Then, call the function by passing your point as an x, y array - for example:
WhatCounty([-120.929806, 46.457982]);
I tried this on jsfiddle.net and it worked. In iFormBuilder, for your dynamic value you would call the function from Page Level Javascript in the same manner, either passing the DCN of the lon and lat fields, or by accessing the Location widget directly - say you have a Location Widget with a DCN of 'my_location' - the Dynamic Value would be:
WhatCounty([my_location.longitude, my_location.latitude]);
If it does not work remove the semicolon, I can't seem to figure out when iFormBuilder wants or does not want semicolons in Dynamic Values and Conditional Statements.
-
Thanks Jon!
Actually while I've got your attention you might be able to help me something given what I've seen of your JS posts -
Do you know if there is anyway to access the entire JSON of a lookup table from a SmartTable Search field? The entire table is obviously stored locally on the device, but I can only access the JSON of the record that is selected in the SmartTable Search field, not the entire table.
Reason I am asking - I have a use case for a field sampler to input their current location, then show the nearest sampling station, as well as validate that they are within a certain distance of the station. I could do this with the workflow outlined above, storing the sampling stations as a JSON in the page level JS and iterating over them with the Haversine equation to find the closest, but this is a pain if something changes (a station is added for example.) It would be much more robust if there was a way to pass the JSON data for a lookup table into a user-defined function.
-
After those tweaks, it is working perfectly in my form! This will be a huge improvement over the Google Maps API solution that I was using before, since many of our users go in and out of cell coverage frequently.
The detail level in your responses is beyond awesome. Thanks again for the fast responses and answering my questions so clearly.
I will also be very interested to hear of any progress you make with using TopoJSONs in a similar fashion. Please keep us posted here if you ever give that a try!
-
Your welcome, glad to hear you got it working. The existence of a TopoJSON specification was news to me, so I'm not sure if/how this would be worked - most likely would be a function to convert the TopoJSON to a regular GeoJSON or arrays of polygon outlines to plug into the current code.
Please sign in to leave a comment.
Comments
8 comments