Archive | February, 2015

Use GIS to Fill a Form

27 Feb

We use maps to answer questions and the information taken from a map may need to find its way somewhere else. In this post, I will show you how to take a map and populate form data with the properties in a GIS feature class just by clicking a location on the map.

The Application

1

The application will consist of a form with a button to launch a map. When the map opens, the user will click on a parcel. The map will make a call to an Albuquerque Open Data GIS service and retrieve the information for the parcel. The map will send the data to the form.

The Code

The form is a simple Bootstrap form.

<form class=”form-horizontal” role=”form”>
<div class=”form-group”>
<label class=”control-label col-sm-2″ for=”email”>Street Number:</label>
<div class=”col-sm-10″>
<input type=”text” class=”form-control” id=”snumber” placeholder=”Enter Street Number”>
</div>
</div>
<div class=”form-group”>
<label class=”control-label col-sm-2″ for=”pwd”>Street Name:</label>
<div class=”col-sm-10″>
<input type=”text” class=”form-control” id=”sname” placeholder=”Enter Street Name”>
</div>
</div>
<div class=”form-group”>
<label class=”control-label col-sm-2″ for=”email”>Street Designation:</label>
<div class=”col-sm-10″>
<input type=”text” class=”form-control” id=”sdes” placeholder=”Enter Street Designation”>
</div>
</div>
<div class=”form-group”>
<label class=”control-label col-sm-2″ for=”pwd”>Street Quadrant:</label>
<div class=”col-sm-10″>
<input type=”text” class=”form-control” id=”squad” placeholder=”Enter Street Quadrant”>
</div>
</div>
<div class=”form-group”>
<div class=”col-sm-offset-2 col-sm-10″>
<button type=”submit” onclick=”openMap()” class=”btn btn-default”>Open Map</button>
</div>
</div>
</form>

The button calls a JavaScript function which opens the map in a new window.

function openMap(){window.open(“fillformmap.html”,”Map”,”width=600,height=600″);}

The goal is to fill the form with data from the map. The map that opens is a standard Leaflet.js map. I use an on click function to create a buffer – just to make sure we hit a parcel – and send the buffer to the service query and ask for the parcel information back (for more information on the buffer with Turf.js see my post on Albuquerque Incidents).

map.on(“click”,function(e){

a=L.marker(e.latlng);
b=a.toGeoJSON();
buffered = turf.buffer(b,.005,”miles”);
L.geoJson(buffered).addTo(map);
var result = turf.featurecollection(buffered.features.concat(b));

g='{“rings”:’+JSON.stringify(buffered.features[0].geometry.coordinates)+’}’;

var params=”f=json&outSR=4326&outFields=*&geometryType=esriGeometryPolygon&spatialRel=esriSpatialRelIntersects&inSR=4326&geometry=”+g;

var url = “http://coagisweb.cabq.gov/arcgis/rest/services/public/fullviewer/MapServer/9/query&#8221;;
console.log(params);
http=new XMLHttpRequest();
http.open(“POST”, url, true);
http.setRequestHeader(“Content-type”, “application/x-www-form-urlencoded”);
http.onreadystatechange = function() {//Call a function when the state changes.
if(http.readyState == 4 && http.status == 200) {
console.log(http.responseText);
var result= JSON.parse(http.responseText);
……..CODE GOES HERE….
}}

http.send(params);
});

Nothing special about the code above, it is been used in several previous posts. To send the data to the form, we need to use the code below.

opener.document.getElementById(“sname”).value=result.features[0].attributes.STREETNAME;
opener.document.getElementById(“snumber”).value=result.features[0].attributes.STREETNUMBER;
opener.document.getElementById(“squad”).value=result.features[0].attributes.STREETQUADRANT;
opener.document.getElementById(“sdes”).value=result.features[0].attributes.STREETDESIGNATION;
window.close();

The above code grabs the text boxes by ID for the document that opened the map. It sets the value of those text boxes to the value returned from the map query. Now we have a completed form.

2 Sometimes clicking a map is better than filling out information by hand. I only queried a single GIS feature, but do not think that is the limit. We could easily query a number of layers to populate a form. Just add another AJAX call and pass the results.

Advertisements

Am I Near Public Art? How about Crime?

26 Feb

When you are new to a city, it is hard to walk around and carry a map of all the sites. Wouldn’t it be nice if your phone could alert you when you were near something of interest? In this post, I will show you how to create an alert system.

The Application

The application will build off of our earlier post using Albuquerque Crime Data. In that application, the user clicked the map and was presented with all crimes in the last 180 days within a half mile of the click.

This application will use the same logic but replace the click with the users location.

Building the Application

We will start with a Leaflet.js Mobile map. Place the following HTML in the head of your document.

<meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no” />

<style>
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
}
</style>

This will make the map look nice on mobile – and desktop as well. Then create the map.

var map = L.map(‘map’);

L.tileLayer(‘http://{s}.tile.osm.org/{z}/{x}/{y}.png’).addTo(map);

map.locate({setView: true, maxZoom: 16,enableHighAccuracy:true,watch:true});

With mobile maps, we do not pass the setView to the map, we add that parameter to the locate() function. Now we need two functions: one for found and one for not found.

map.on(‘locationfound’, gotcha);

function gotcha(e){ALL OUR CODE GOES HERE}

map.on(‘locationerror’, onLocationError);

function onLocationError(e) {
alert(e.message);}

When our location is found, we want to setup the buffer and prep for calling the Albuquerque Data for Public Art or Incidents.

a=L.marker(e.latlng);
b=a.toGeoJSON();
L.circle(e.latlng, 20).addTo(map);
buffered = turf.buffer(b,.05,”miles”);
var result = turf.featurecollection(buffered.features.concat(b));
g='{“rings”:’+JSON.stringify(buffered.features[0].geometry.coordinates)+’}’;

We create a marker, convert it to GeoJSON to use with Turf.js, then buffer and get the rings to use in the query.

Now we can make our standard AJAX call to an ESRI REST Endpoint. I have commented out the URL for Public Art and the marker. You can uncomment them and comment out the incident url and marker to switch the application.

var params=”f=json&outSR=4326&outFields=*&geometryType=esriGeometryPolygon&spatialRel=esriSpatialRelIntersects&inSR=4326&geometry=”+g;

//var url = “http://coagisweb.cabq.gov/arcgis/rest/services/public/PublicArt/MapServer/0/query&#8221;;
var url = “http://coagisweb.cabq.gov/arcgis/rest/services/public/APD_Incidents/MapServer/0/query&#8221;;
http=new XMLHttpRequest();
http.open(“POST”, url, true);
http.setRequestHeader(“Content-type”, “application/x-www-form-urlencoded”);
http.onreadystatechange = function() {//Call a function when the state changes.
if(http.readyState == 4 && http.status == 200) {
var result= JSON.parse(http.responseText);
…….CODE GOES HERE……..

}}
http.send(params);

Now we get back all the incidents, or art, within 300 feet (.05 miles) of our location. We need to take the results and add them to the map and notify the user they are near something of interest.

for(x=0;x<Object.keys(result.features).length;x++){
xy=result.features[x].geometry;
pattern.push(100);
pattern.push(100);
L.marker([result.features[x].geometry.y,result.features[x].geometry.x]).bindPopup(“<h3>”+result.features[x].attributes.CVINC_TYPE+”</h3>”).addTo(map);}
//L.marker([result.features[x].geometry.y,result.features[x].geometry.x]).bindPopup(“<h3>”+result.features[x].attributes.TITLE+”</h3>”).addTo(map);}

navigator.vibrate(pattern);
pattern=[];

As we move, the service is queried again, constantly watching us and letting us know when we are near something of interest. The vibration pattern is 100ms for each feature we are near and pause for 100ms between each feature. If we are near a lot of features -say crime incidents – our phone will vibrate many times. Possibly signaling a dangerous part of town. If we are using public art, we know we are near a particularly artistic part of the city and maybe we should explore more.

To use the same map for multiple features, you could switch up the vibration patterns, say short fast vibrations for art and longer for crime. Maybe vibrate once for crime but the duration is a function of the number of incidents – 10 incidents results in a 10 second vibration.

Just an idea on how to use open data to alert new comers to the features of the city that they may not be aware of.

Reverse Engineer City of Albuquerque Web Server Logs API

25 Feb

I recently stumbled across a Code For America experiment that shows live information from the City of Albuquerque Web Server. This is cool, but if you read this blog, you know I want the service! Give me that REST endpoint. I set out to reverse engineering their application and will show you two real-time endpoints with some different parameters.

The Application

The Code for America application is clearly hitting an endpoint. It’s just the way it has to work. So where is the endpoint? Well, a cursory glance at the code reveals two: realtime and historic. So hit the endpoint

https://shrouded-fjord-5635.herokuapp.com/realtime?

and you get the following error:

{“error”:{“errors”:[{“domain”:”global”,”reason”:”required”,”message”:”Required parameter: ids”,”locationType”:”parameter”,”location”:”ids”}],”code”:400,”message”:”Required parameter: ids”}}

Interesting. we need a parameter ids. Hit the endpoint with the parameter.

{“error”:{“errors”:[{“domain”:”global”,”reason”:”invalidParameter”,”message”:”Invalid value ”. Values must match the following regular expression: ‘ga:[0-9]+'”,”locationType”:”parameter”,”location”:”ids”}],”code”:400,”message”:”Invalid value ”. Values must match the following regular expression: ‘ga:[0-9]+'”}}

Now I know the value of

ids=ga: SOME NUMBER

The number is in the code, so I grabbed it. Reading the code, you can see the structure of the endpoint.

endpoint: function(){
return “/realtime?ids=ga:”+matrix.settings.profileId+”&metrics=rt:activeUsers&max-results=10”
}

So let’s give it a try.

{
“kind”: “analytics#realtimeData”,
“id”: “https://www.googleapis.com/analytics/v3/data/realtime?ids=ga:48754&metrics=rt:activeUsers&#8221;,
“query”: {
“ids”: “ga:48754”,
“metrics”: [
“rt:activeUsers”
],
“max-results”: 1000
},
“totalResults”: 1,
“selfLink”: “https://www.googleapis.com/analytics/v3/data/realtime?ids=ga:48754&metrics=rt:activeUsers&#8221;,
“profileInfo”: {
“profileId”: “48754”,
“accountId”: “86004”,
“webPropertyId”: “UA-86004-1”,
“internalWebPropertyId”: “97739”,
“profileName”: “2. http://www.cabq.gov All Traffic”,
“tableId”: “realtime:48754”
},
“columnHeaders”: [
{
“name”: “rt:activeUsers”,
“columnType”: “METRIC”,
“dataType”: “INTEGER”
}
],
“totalsForAllResults”: {
“rt:activeUsers”: “172”
},
“rows”: [
[
“172”
]
]
}

There you have it: 172 active users. We can pass another parameter

metrics=rt:pageViews

Then we see:

{“kind”:”analytics#realtimeData”,”id”:”https://www.googleapis.com/analytics/v3/data/realtime?ids=ga:48754&metrics=rt:pageviews&#8221;,”query”:{“ids”:”ga:48754″,”metrics”:[“rt:pageviews”],”max-results”:1000},”totalResults”:1,”selfLink”:”https://www.googleapis.com/analytics/v3/data/realtime?ids=ga:48754&metrics=rt:pageviews&#8221;,”profileInfo”:{“profileId”:”48754″,”accountId”:”86004″,”webPropertyId”:”UA-86004-1″,”internalWebPropertyId”:”97739″,”profileName”:”2. http://www.cabq.gov All Traffic”,”tableId”:”realtime:48754″},”columnHeaders”:[{“name”:”rt:pageviews”,”columnType”:”METRIC”,”dataType”:”INTEGER”}],”totalsForAllResults”:{“rt:pageviews”:”2172″},“rows”:[[“2172”]]}

I formatted the first result nicely and will just post JSON blobs so as not to take up to much space. Now you see 2172 views. My guess is that this is within either today or some time time frame of today.

What are people looking at? Lets change our metrics to pageviews and get the pagetitle. We also pass the largest possible result value.

metrics=rt:pageviews&dimensions=rt:pageTitle&max-results=10000

Right now, this is what we get.

{“kind”:”analytics#realtimeData”,”id”:”https://www.googleapis.com/analytics/v3/data/realtime?ids=ga:48754&dimensions=rt:pageTitle&metrics=rt:pageviews&max-results=10000&#8243;,”query”:{“ids”:”ga:48754″,”dimensions”:”rt:pageTitle”,”metrics”:[“rt:pageviews”],”max-results”:10000},”totalResults”:454,”selfLink”:”https://www.googleapis.com/analytics/v3/data/realtime?ids=ga:48754&dimensions=rt:pageTitle&metrics=rt:pageviews&max-results=10000&#8243;,”profileInfo”:{“profileId”:”48754″,”accountId”:”86004″,”webPropertyId”:”UA-86004-1″,”internalWebPropertyId”:”97739″,”profileName”:”2. http://www.cabq.gov All Traffic”,”tableId”:”realtime:48754″},”columnHeaders”:[{“name”:”rt:pageTitle”,”columnType”:”DIMENSION”,”dataType”:”STRING”},{“name”:”rt:pageviews”,”columnType”:”METRIC”,”dataType”:”INTEGER”}],”totalsForAllResults”:{“rt:pageviews”:”2127″},”rows”:[[“2015 Spring Green Waste Pick-up — City of Albuquerque”,”1″],[“4th Magic Treehouse: Dinosaurs after Dark — City of Albuquerque”,”1″],[“A Night in the 40’s: Big Band Swing — City of Albuquerque”,”1″],[“ABQ BioPark — City of Albuquerque”,”12″],[“ABQ RIDE Providing Free Transportation to Veterans with V.A. Hospital Cards — City of Albuquerque”,”5″],[“ABQ Recycles — City of Albuquerque”,”2″],[“ABQ View — City of Albuquerque”,”24″],[“ABQ Volunteers — City of Albuquerque”,”6″],[“ACH Services Information — City of Albuquerque”,”1″],[“About 311 — City of Albuquerque”,”1″],[“About Us — City of Albuquerque”,”2″],[“About the BioPark — City of Albuquerque”,”1″],[“About the Councilor — City of Albuquerque”,”1″],[“Activities & Education Programs — City of Albuquerque”,”1″],[“Activities Catalog — City of Albuquerque”,”1″],[“Address Atlas Pages — City of Albuquerque”,”2″],[“Address Query – Advanced Map Viewer — City of Albuquerque”,”5″],[“Address Report — City of Albuquerque”,”19″],[“Administration, Two AFSCME Unions Reach Agreement — City of Albuquerque”,”1″],[“Admission & Hours — City of Albuquerque”,”5″],[“Advanced Map Viewer User Guide — City of Albuquerque”,”1″],[“Advanced Map Viewer — City of Albuquerque”,”14″],[“Agendas from Greater Albuquerque Bicycling Advisory Committee Meetings — City of Albuquerque”,”1″],

Notice there were only 454 total results. Each record looks like

[“ABQ RIDE Providing Free Transportation to Veterans with V.A. Hospital Cards — City of Albuquerque”,”5″]

This is pageTitle comma PageViews as stated in the columnHeaders property below.

columnHeaders”:[{“name”:”rt:pageTitle”,”columnType”:”DIMENSION”,”dataType”:”STRING”},{“name”:”rt:pageviews”,”columnType”:”METRIC”,”dataType”:”INTEGER”}],

Where is the page with the title ABQ View – City of Albuquerque? Using pagePath we can find out.

{“kind”:”analytics#realtimeData”,”id”:”https://www.googleapis.com/analytics/v3/data/realtime?ids=ga:48754&dimensions=rt:pagePath,rt:pageTitle&metrics=rt:pageviews&max-results=10000&#8243;,”query”:{“ids”:”ga:48754″,”dimensions”:”rt:pagePath,rt:pageTitle”,”metrics”:[“rt:pageviews”],”max-results”:10000},”totalResults”:704,”selfLink”:”https://www.googleapis.com/analytics/v3/data/realtime?ids=ga:48754&dimensions=rt:pagePath,rt:pageTitle&metrics=rt:pageviews&max-results=10000&#8243;,”profileInfo”:{“profileId”:”48754″,”accountId”:”86004″,”webPropertyId”:”UA-86004-1″,”internalWebPropertyId”:”97739″,”profileName”:”2. http://www.cabq.gov All Traffic”,”tableId”:”realtime:48754″},”columnHeaders”:[{“name”:”rt:pagePath”,”columnType”:”DIMENSION”,”dataType”:”STRING”},{“name”:”rt:pageTitle”,”columnType”:”DIMENSION”,”dataType”:”STRING”},{“name”:”rt:pageviews”,”columnType”:”METRIC”,”dataType”:”INTEGER”}],”totalsForAllResults”:{“rt:pageviews”:”2173″},”rows”:[[“/311/311-Information/about-311″,”About 311 — City of Albuquerque”,”1″],[“/311/311-Information/contacting-311″,”How to Contact 311 — City of Albuquerque”,”1″],[“/311/311-Information/contacting-311/dialing-311/index.html”,”Dialing 311 — City of Albuquerque”,”1″],[“/311/resident-services”,”Online Resident Services — City of Albuquerque”,”1″],[“/311/resident-services/online-resident-services/folder_summary_view?b_start:int=15&-C=”,”Online Resident Services — City of Albuquerque”,”1″],[“/311/resident-services/online-resident-services/folder_summary_view?b_start:int=30&-C=”,”Online Resident Services — City of Albuquerque”,”1″],[“/a-z”,”Albuquerque A-Z — City of Albuquerque”,”27″],[“/a-z/a-z”,”Albuquerque A-Z — City of Albuquerque”,”7″],[“/about/offsite.html”,”Leaving http://www.cabq.gov External Link Disclaimer — City of Albuquerque”,”1″],[“/abq-apps”,”ABQ Apps — City of Albuquerque”,”1″],[“/abq-data”,”ABQ Data — City of Albuquerque”,”1″],[“/abq-view/index.html”,”ABQ View — City of Albuquerque”,”27″],[“/abq-volunteers”,”ABQ Volunteers — City of Albuquerque”,”9″],[“/abq-volunteers/index.html”,”ABQ Volunteers — City of Albuquerque”,”1″],[“/acl_users/credentials_cookie_auth/require_login?came_from=https://www.cabq.gov/abq-volunteers/volunteering-announcement-form”,”City of Albuquerque”,”1″],[“/airport”,”Airport — City of Albuquerque”,”7″],[“/airport/airlines-flight-services”,”Airlines — City of Albuquerque”,”7″]

Here is a single record:

[“/311/311-Information/about-311″,”About 311 — City of Albuquerque”,”1″]

And appending the URL to cabq.gov gives us:

Capture

 

Now you can see what people are looking at on the City of Albuquerque website. Try gathering statistics and compile the most popular page for a day as Code for America has done.

I said I was only going to show the real-time but I had to grab historic.

Historic Data

You can grab historic data by changing the endpoint to /historic. The metric values change to:

start-date

end-date

They are formatted as YYYY-MM-DD. You can also pass dimensions of:

nthMinute

nthHour

And here is an example of a session from 2015-02-21 to 2015-02-23 using Hours:

https://shrouded-fjord-5635.herokuapp.com//historic?ids=ga:48754&dimensions=ga:nthHour&metrics=ga:sessions&start-date=2015-02-21&end-date=2015-02-23

{“kind”:”analytics#gaData”,”id”:”https://www.googleapis.com/analytics/v3/data/ga?ids=ga:48754&dimensions=ga:nthHour&metrics=ga:sessions&start-date=2015-02-21&end-date=2015-02-23&#8243;,”query”:{“start-date”:”2015-02-21″,”end-date”:”2015-02-23″,”ids”:”ga:48754″,”dimensions”:”ga:nthHour”,”metrics”:[“ga:sessions”],”start-index”:1,”max-results”:1000},”itemsPerPage”:1000,”totalResults”:72,”selfLink”:”https://www.googleapis.com/analytics/v3/data/ga?ids=ga:48754&dimensions=ga:nthHour&metrics=ga:sessions&start-date=2015-02-21&end-date=2015-02-23&#8243;,”profileInfo”:{“profileId”:”48754″,”accountId”:”86004″,”webPropertyId”:”UA-86004-1″,”internalWebPropertyId”:”97739″,”profileName”:”2. http://www.cabq.gov All Traffic”,”tableId”:”ga:48754″},”containsSampledData”:false,”columnHeaders”:[{“name”:”ga:nthHour”,”columnType”:”DIMENSION”,”dataType”:”STRING”},{“name”:”ga:sessions”,”columnType”:”METRIC”,”dataType”:”INTEGER”}],”totalsForAllResults”:{“ga:sessions”:”48097″},“rows”:[[“000000″,”242”],[“000001″,”110”],[“000002″,”91”],[“000003″,”91”],[“000004″,”106”],[“000005″,”147”],[“000006″,”308”],[“000007″,”671”],[“000008″,”912”],[“000009″,”1098”],[“000010″,”1119”],[“000011″,”1019”],[“000012″,”1016”],[“000013″,”959”],[“000014″,”901”],[“000015″,”880”],[“000016″,”800”],[“000017″,”665”],[“000018″,”693”],[“000019″,”646”],[“000020″,”610”],[“000021″,”543”],[“000022″,”443”],[“000023″,”289”],[“000024″,”219”],[“000025″,”133”],[“000026″,”100”],[“000027″,”84”],[“000028″,”81”],[“000029″,”149”],[“000030″,”231”],[“000031″,”524”],[“000032″,”744”],[“000033″,”859”],[“000034″,”894”],[“000035″,”848”],[“000036″,”861”],[“000037″,”892”],[“000038″,”860”],[“000039″,”832”],[“000040″,”842”],[“000041″,”809”],[“000042″,”744”],[“000043″,”650”],[“000044″,”711”],[“000045″,”583”],[“000046″,”483”],[“000047″,”305”],[“000048″,”220”],[“000049″,”115”],[“000050″,”98”],[“000051″,”56”],[“000052″,”127”],[“000053″,”269”],[“000054″,”481”],[“000055″,”787”],[“000056″,”1269”],[“000057″,”1467”],[“000058″,”1595”],[“000059″,”1613”],[“000060″,”1462”],[“000061″,”1446”],[“000062″,”1528”],[“000063″,”1545”],[“000064″,”1343”],[“000065″,”995”],[“000066″,”792”],[“000067″,”742”],[“000068″,”812”],[“000069″,”728”],[“000070″,”508”],[“000071″,”302”]]}

A single records contains the hour and the number of sessions:

[“000064″,”1343”]

What to the People of Albuquerque want from their Government?

By analyzing what the people of Albuquerque search for on the City website, we can come to some conclusions as to what they deem important or what concerns they have. To do so, we need the pages they looked at over a period of time. We can use the historic endpoint and pass pageTitle to find out.

https://shrouded-fjord-5635.herokuapp.com//historic?ids=ga:48754&dimensions=ga:nthHour&metrics=ga:pageviews&dimensions=ga:pageTitle&sessions&start-date=2015-02-21&end-date=2015-02-23

{“kind”:”analytics#gaData”,”id”:”https://www.googleapis.com/analytics/v3/data/ga?ids=ga:48754&dimensions=ga:pageTitle&metrics=ga:pageviews&start-date=2015-02-21&end-date=2015-02-23&#8243;,”query”:{“start-date”:”2015-02-21″,”end-date”:”2015-02-23″,”ids”:”ga:48754″,”dimensions”:”ga:pageTitle”,”metrics”:[“ga:pageviews”],”start-index”:1,”max-results”:1000},”itemsPerPage”:1000,”totalResults”:3090,”selfLink”:”https://www.googleapis.com/analytics/v3/data/ga?ids=ga:48754&dimensions=ga:pageTitle&metrics=ga:pageviews&start-date=2015-02-21&end-date=2015-02-23&#8243;,”nextLink”:”https://www.googleapis.com/analytics/v3/data/ga?ids=ga:48754&dimensions=ga:pageTitle&metrics=ga:pageviews&start-date=2015-02-21&end-date=2015-02-23&start-index=1001&max-results=1000&#8243;,”profileInfo”:{“profileId”:”48754″,”accountId”:”86004″,”webPropertyId”:”UA-86004-1″,”internalWebPropertyId”:”97739″,”profileName”:”2. http://www.cabq.gov All Traffic”,”tableId”:”ga:48754″},”containsSampledData”:false,”columnHeaders”:[{“name”:”ga:pageTitle”,”columnType”:”DIMENSION”,”dataType”:”STRING”},{“name”:”ga:pageviews”,”columnType”:”METRIC”,”dataType”:”INTEGER”}],”totalsForAllResults”:{“ga:pageviews”:”129345″},”rows”:[[“\”A Senior I Know\” Essay Contest — City of Albuquerque”,”30″],[“\”On the Commons\” Magazine Features Albuquerque Open Space — City of Albuquerque”,”3″],[“\”We Love Our BioPark\” Video Contest Launches — City of Albuquerque”,”3″],[“\”Website Transparency Team\”: Week of April 18, 2011 — City of Albuquerque”,”1″],[“‘Pacific Coral Reef’ Exhibit Now Open — City of Albuquerque”,”1″],[“(Memorial) Tribute Tree Program — City of Albuquerque”,”3″],[“(not set)”,”13″],[“01-28-2015 – 15-11-104F – Follow-up – On-Call Contractors – Department of Municipal Development — City of Albuquerque”,”1″],[“01-28-2015 – 15-12-107F – Follow-up – Health and Social Service Centers – Department of Family and Community Services — City of Albuquerque”,”1″],[“02-17-12 CASE# 12-202 Whistleblower Complaint against the Solid Waste Management Department — City of Albuquerque”,”1″],[“02-28-07 — 06-04-105F Follow-Up – Environmental Health Department Expenditures — City of Albuquerque”,”2″],[“04-16-09 — 07-204 – Final Investigative Report – Stolen Vehicles, SWMD — City of Albuquerque”,”1″],[“06-25-12 Case# 12-213 Collision Involving a Motor Coach Operator — City of Albuquerque”,”1″],[“06-25-2014 Case # 204 Water Authority — City of Albuquerque”,”1″],[“1 – Educated, literate residents — City of Albuquerque”,”1″],[“1-11-10 RYAB Facilitated Meeting Notes — City of Albuquerque”,”1″],[“1.1 Adult Educational Achievement Rates — City of Albuquerque”,”2″],[“10 – Basic needs provided for — City of Albuquerque”,”1″],[“10-Year Spending Trends — City of Albuquerque”,”8″],[“10.1 Residents Living in Poverty — City of Albuquerque”,”1″],[“10.2 Unemployment Rate — City of Albuquerque”,”1″],[“11 – The public is safe — City of Albuquerque”,”1″],[“11-19-07 — 07-202 – Investigation – Alleged Excessive Overtime Claim, ABQ Ride Department — City of Albuquerque”,”1″],

Conclusion

I have given you the endpoints. It is up to you to parse out the data. The code for that is on almost every post on this blog. Just grab any AJAX request up to JSON.parse() and grab results.rows[x].[0 through 1]. Something like that should be close.

 

Building Games with Leaflet, Turf and Albuquerque Open Data

24 Feb

I remember playing Risk as a kid. It was my first time playing a game that involved a map. When I got older, I loved playing games of world domination and strategy. As I build web maps, I often think, why couldn’t I make a game using Leaflet.js? In this post, I will describe how a game could be built with some sample code.

The Simple Game

There is no point to this game. It is an attempt to

  1. create pieces
  2. load data
  3. move the pieces with rules and
  4. accomplish a goal.

Winning the game is shown in the image below. You can test the game on JsBin.

I won the game by moving my piece in to a park.

I won the game by moving my piece in to a park.

The Game Components

The game board is made of a Leaflet.js map and Albuquerque Parks Data. The park data is loaded as a turf.polygon and pushed in to an array.

var url = “http://coagisweb.cabq.gov/arcgis/rest/services/public/fullviewer/MapServer/33/query?f=json&outSR=4326&outFields=*&where=1=1&#8221;;

http=new XMLHttpRequest();
http.open(“GET”, url, true);
http.setRequestHeader(“Content-type”, “application/x-www-form-urlencoded”);
http.onreadystatechange = function() {//Call a function when the state changes.
if(http.readyState == 4 && http.status == 200) {
console.log(“loading”);
var result= JSON.parse(http.responseText);
for(x=0;x<Object.keys(result.features).length;x++){
var pts=result.features[x].geometry.rings;
polygons.push(turf.polygon(pts));
}
L.geoJson(polygons).addTo(map);

}}
http.send();

Now that I have added the base layer, I want to create game pieces. It would be nice to also use Open Data for the players, but for now I will allow the user to click the map to drop a marker.

map.on(“click”,function(e){

var point=L.marker(e.latlng,{draggable:true}).addTo(map);

CODE BELOW GOES HERE
});

When the user clicks, a marker is added to the map and is set draggable. The game will allow the user to move the marker by dragging and attempt to land on a park. I want rules. The first rule I will create will only allow the user to drag the marker 30 pixels. This creates a problem. 30 pixels is a different distance based on the view level of the map. To fix this, I prevent the map from zooming so I know the 30 pixel fixed distance.

var map = L.map(‘map’, {center: [35.10418, -106.62987], zoom:15, touchZoom:false,scrollWheelZoom:false,doubleClickZoom:false,boxZoom:false
});

Now the user can pan but not zoom. If the user drags the marker too far, I need to set the marker to its original position. When the user starts dragging, we will get the lat,lng of it.

point.on(“dragstart”,function(z){
ll=this._latlng;
});

When the user finishes dragging the marker, we will check the distance. If it is greater than 30, we will alert them to the error and reset the marker position.

point.on(“dragend”,function(x){
if(x.distance>30){
alert(“You cannot move that far on a single turn”);
point.setLatLng(ll);}

Otherwise, we will allow the marker to move and test to see if the marker is inside of a park polygon. If it is, YAY, you win! else, keep going.

else{

for(k=0;k<polygons.length;k++){if(turf.inside(point.toGeoJSON(),polygons[k])){alert(“you made it to a park”);}}

}//else

});

This is the basic foundations of a game. To make it better, you would need to have turns and allow a computer controlled player to intervene. Maybe a zombie chase? Or you could network the game using websockets and allow users to use their real world position to play against each other(PDF) in a capture the flag or race to points type game.

Crime Density in Albuquerque

23 Feb

I posted earlier on Air Pollution Density in Albuquerque. I used pollution sources because the crime data was missing x,y values. In this post, I will add a line to the code to allow us to use Crime Data for the last 180 days.

The Application

The application maps the density of crime over the last 180 days. The result is in the image below.

crime

To change the data from pollution to crime, just switch out the url using the link to the Crime Data.

url=”http://coagisweb.cabq.gov/arcgis/rest/services/public/APD_Incidents/MapServer/0/query&#8221;

Because the data is missing (x,y) values, our application will crash. We can add an if statement to check if the coordinates are null and if so, don’t map it.

if(result.features[x].geometry===undefined){}
else{var t = L.marker([result.features[x].geometry.y,result.features[x].geometry.x]);//.addTo(map);
p.push(t.toGeoJSON());}

Lastly, you will need to change the color scale to account for the change in values for each polygon.

function getColor(d) {
return d > 140 ? ‘#800026’ :
d > 120 ? ‘#BD0026’ :
d > 80 ? ‘#E31A1C’ :
d > 60 ? ‘#FC4E2A’ :
d > 40 ? ‘#FD8D3C’ :
d > 20 ? ‘#FEB24C’ :
d > 5 ? ‘#FED976’ :
‘#FFFFFF’;
}

Data does not come perfectly clean. It is our job to try to deal with problems in the data in our applications – or at least make it fail gracefully.

Try changing the grid size or the color values:
JS Bin

 

QGIS and MongoDB

20 Feb

There was an excellent plugin for Qgis that used MongoDB. I have found a copy on my old computer and put it on GitHub.

Elevation Along a Path

20 Feb

In my last post, I showed you how to get the elevation at any point in Albuquerque. In this post, we are going to improve the application and get an elevation profile of the City. I have put the code on JsBin for you to play with and test out.

The Application

The application will allow the user to draw a line and will then draw a chart showing the elevation at the endpoints of each line segment. The results will look like the image below. ElevationLine In the image above, you will see the green line I have drawn and a chart above the map. The chart shows the elevation profile. Notice how the elevation dips at the river and rises significantly as we approach the Sandia Mountains.

Required Plugins

To make the application work, you will need to use the Leaflet.Draw plugin. This is what lets the user draw the line. We only want the user to draw a line, so I will modify the sample config code on github to:

var drawnItems = new L.FeatureGroup(); map.addLayer(drawnItems); var drawControl = new L.Control.Draw({ draw: { marker: false, position: ‘topleft’, polygon: false, polyline: { metric: false, shapeOptions:{color:’green’,opacity:1} }, circle:false, rectangle: false }, edit: { featureGroup: drawnItems, edit: false } }); map.addControl(drawControl); map.on(‘draw:created’, function (e) { var type = e.layerType, layer = e.layer; if (type === ‘polyline’) { CODE GOES HERE } drawnItems.addLayer(layer);

This will only give the option for drawing a line. We will add our code in the if statement that equals polyline. You will also need to reference Charts.js. I really like Charts.js for static charts and Smoothie charts for streaming data.

The Completed Logic

We are going to allow the user to draw a line. When they are done, we need the points for each segment. Once we have the points, we will buffer them and get the elevation just as we did in the last post.

drawLine=layer.getLatLngs(); for(var x=0;x<drawLine.length;x++){ a=L.marker([drawLine[x].lat,drawLine[x].lng]); var b=a.toGeoJSON(); var buffered = turf.buffer(b,0.01,”miles”); var result = turf.featurecollection(buffered.features.concat(b)); var g='{“rings”:’+JSON.stringify(buffered.features[0].geometry.coordinates)+’}’; howhigh(g); }

We calla function howhigh(g) and pass the geometry. This function is the AJAX call.

var d=JSON.parse(http.responseText); points.push(d.features[0].attributes.ELEV); l.push(label); label=label+1; drawChart();

The AJAX call pushes the elevation to an array and increments the label variable. It then calls drawChart(). We will update the chart as each result comes in from the AJAX calls.

function drawChart(){ data={ labels: l, datasets: [ { label: “$”, fillColor: “rgba(151,187,205,0.5)”, strokeColor: “rgba(151,187,205,0.8)”, highlightFill: “rgba(151,187,205,0.75)”, highlightStroke: “rgba(151,187,205,1)”, data: points }]}; var ctx = document.getElementById(“data”).getContext(“2d”); myBarChart = new Chart(ctx).Bar(data, {scaleShowGridLines : true,responsive:false,showTooltips:false,scaleBeginAtZero : false,barValueSpacing : .5}); }

The function configures the data and labels and adds the chart to the canvas.

To Do

The application only allows you to draw a single line. In the future, the application needs a reset function to clear the data and chart and allow the user to draw a new line.