Tag Archives: JavaScript

Bypass Cognos Forms

29 Jul

The City of Albuquerque has a Cognos report that allows you to see employee earnings. You can view the report on their transparency website or you can download a copy on the open data site. I do not want to go through a Cognos front page every time I need the data and I do not want to check when the last time they exported the xls or xml version to the open data site. I want to grab a fresh copy – I really want a service. Since they do not have one, we are stuck using Cognos every time to get the live data. Luckily, we can script it.

Cognos has several JavaScript functions cognosLaunch(), cognosLaunchWithTarget() and cognosLaunchInWindow(). These functions take different parameters and then call cognosLaunchArgArray(). Where do you get the JavaScipt library from? The City of Albuquerque – or anyone who has Cognos installed. The file is located at:

http://cognospublic.cabq.gov/cabqcognos/cognoslaunch.js

You can link to this file in your HTML

http://cognospublic.cabq.gov/cabqcognos/cognoslaunch.js

Now, you just need to know how to format the inputs properly. You can find all the information you need by running the report on the transparency site first. When the report finishes, view the source. You will see all the variables highlighted in the image below:

Cognos Report Source reveals all the parameters.

Cognos Report Source reveals all the parameters.

Now, format the function, passing the correct data. For cognosLaunch(), you will have the function below:

cognosLaunch(“ui.gateway”,”http://cognospublic.cabq.gov/cabqcognos/cgi-bin/cognos.cgi”,”ui.tool”,”CognosViewer”,”ui.object”,
“/content/folder[@name=’Transparency’]/report[@name=’Transparency Report – Graded employees’]”,”ui.action”,”run”,”run.prompt”,
“false”,”ui”,”h1h2h3h4″,”run.outputFormat”,”CSV”);

Put this in an HTML file in the <script> section and you will launch a window and download the CSV automatically. I have put a file on GitHub. There is another example which includes HTML and a JS file. The CABQ.js file formats the function for you. In this file, you could pass optional search parameters. I will leave that part up to you – I like grabbing all the data.

You can pas different outputFormats as well – CSV, HTML, PDF, singleXLS, XHTML, XLWA, and XML. Lastly, the City does not allow ajax calls from cross domain, so you may need to have the CORS extension installed in chrome. You can get it from chrome.google.com.

How would I use this in production? I think I would run a simple Python(cherrypy) or Go server that hosts the HTML and JS. Then I would write my application to call the site. I know where the download will go, so I could parse it when done. Then I could either return it to the server or do something with it on my machine.

CSV in JavaScript Using Albuquerque Open Data

28 Jan

In my last two posts, I showed you how to read KML (XML) over the web using AJAX. In this example, we will do the same with a tab delimited file from Albuquerque Open Data. The application will look like the image below.

The finished app. Choose a committee and see their financial information.

The finished app. Choose a committee and see their financial information.

 

Get the Data

On the Albuquerque Open Data site, we will use Campaign Finalized. You will see two options: XML or CSV. We will use the CSV – which is really tab delimited. Grab the data using AJAX.

var url = “http://data.cabq.gov/government/campaignfinalized/CampaignReportFinalizedVersionCABQ-en-us.csv&#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) {
var results=http.responseText;

Parse the Data

The data is tab delimited in rows that have a newline character at the end. This file also has new line characters in fields – but they are wrapped in quotes. Handling this will be difficult on our own, so let’s use a library for it. Download PapaParse 4. This will simplify the process significantly. We can grab the data with:

p=Papa.parse(results,{
delimiter:”\t”,
newline:”\n” });

Now I have an array with each line as p.data[x] and each item as p.data[x][0-8]. For this example, we will use the committee name(p.data[x][0]) and the amount(p.data[x][8]).

Let’s populate the dropdown box. First, we need to create it in the HTML and add a default value.

<select id=’D1′></select>
var select = document.getElementById(“D1”);
var d= document.createElement(“option”);
d.value = -1;
d.id=-1;
d.textContent = “—Choose—“;
select.appendChild(d);

We can now iterate through our data and create an array of individual groups.

for(r=1;r<p.data.length;r++){
if(groups.indexOf(p.data[r][0].trim())>-1){
//do nothing
}
else{groups.push(p.data[r][0].trim());}
}

And then populate the combo box

for(g=0;g<groups.length-1;g++){
var opt = document.createElement(“option”);
opt.value = g;
opt.id=g;
opt.textContent = groups[g];
select.appendChild(opt);
}

Displaying the Data

We need to add an Event Listener on the combo box and execute a function.

document.getElementById(‘D1’).addEventListener(‘change’, money, false);

The function will grab the value of the item selected then get its text content (name). With this information we can pull all the data from the CSV for the financials of the committee selected. Lastly, update the DOM to display the results.

function money(m){
m=document.getElementById(this.value).textContent;
console.log(m);
for(y=0;y<p.data.length;y++){
if(p.data[y][0].trim()==m){
if(parseFloat(p.data[y][8])>0){
raised+=parseFloat(p.data[y][8]);
}
else{spent+=parseFloat(p.data[y][8]);}
}
else{}
}
document.getElementById(“output”).innerHTML='<h3>Raised:</h3>’+raised.toLocaleString()+'<h3>Spent:</h3>’+spent.toLocaleString()+'<h3>Difference</h3>’+(raised+spent).toLocaleString();

}

Without Papa Parse 4, this would have been a much more complicated task. There is no CSV standard, so dealing with individuals CSV files requires a lot of modification. In this example, having new line characters in a field wreaked havoc on my first attempts to parse it out manually. Also, I originally set the PapaParse option header=true, but the application complained. Instead of figuring out why it didn’t like it, I just started my loops at an index of 1 – dropping the headers at index 0.

Handling Dates in Open Data with JavaScript and Python

13 Jan

Have you ever opened public data and been greeted by a date field with a value like 1419490800000? If it is not obvious to you that the number represents December 25, 2014 then you are not alone. The date has been returned to you in milliseconds since January 1, 1970 at 00:00:00. It might be difficult for you and me to do the math and get a human readable value but for the computer it is a trivial operation. In this article, I will show you how to manipulate a date in milliseconds using data from cabq.gov.   Albuquerque has a growing collection of open data and one newer data sets is Crime Incidents. For this example. let’s grab the REST endpoint. Scrolling to the bottom of the page and clicking the supported operation link QUERY, we can look at the data before coding against it. In the form, set the where field to 1=1, the out fields to * and click the Query (GET) button. It will take a few minutes, but I received 24,629 records (this data set returns incidents for the last 180 days).

Sample of my query results

Sample of my query results

As you can see, the date field is in milliseconds. We will move on to writing a webpage to grab the data and return the date in a variety of other formats – which will be more useful for building our applications.

JavaScript Date

There are many date libraries available in JavaScript but for this example we will only use the built-in date object. The first thing we need to do is grab some data. I will start by using a query to grab a subset of incidents from 1/9/2015 until today. While the data is returned in milliseconds, we can query it using a human readable date format. Below, I have created a variable params by concatenating several strings. The strings use the escape() function because browsers tend to garble URLs with special characters and the function allows us to pre-encode them so we know what the conversion of the characters will be.

q1=’where=’; q2=escape(‘”date”>date’); q3=escape(” ‘2015-01-09 00:00:00′”) ; q4=”&outfields=*&f=json”; var params=q1.concat(q2,q3,q4);

Now that we have the parameter to pass, the rest is a standard AJAX request to the URL of the service.

var url = “http://coagisweb.cabq.gov/…/query “; http=new XMLHttpRequest(); http.open(“POST”, url, true); http.setRequestHeader(“Content-type”, “application/x-www-form-urlencoded”); http.onreadystatechange = function() { if(http.readyState == 4 && http.status == 200) { //Code goes here }} http.send(params);

Now we have queried the data and have an if statement that will run when the server hands us our results. The first thing to do is grab the response and convert it to JSON. Then we will grab the date from the first result and display it in the console.

theGeom= JSON.parse(http.responseText); adate=new Date(theGeom.features[0].attributes.date); console.log(“RESPONSE FROM ESRI: ” + theGeom.features[0].attributes.date);

If we were successful, you should see a millisecond date in the console. Using the date object, you can convert milliseconds to any of the below formats. Note that months are indexed starting at 0. That means January is month 0 and December is month 11. You need to add 1 to a month for it to be correct.

console.log(“TIME STRING: ” + adate.toTimeString()); //0=january 11 = december console.log(“MONTH: ” + adate.getMonth()+1); console.log(“DAY: ” + adate.getDay()); console.log(“YEAR: ” + adate.getFullYear()); console.log(“HOURS: ” + adate.getHours()); console.log(“MINUTES: ” + adate.getMinutes()); console.log(“SECONDS: ” + adate.getSeconds()); console.log(“ISO 8601: ” + adate.toISOString()); console.log(“UTC: ” + adate.toUTCString()); console.log(“STRING: ” + adate.toString()); console.log(“LOCALE: ” + adate.toLocaleDateString());

Results in the console

Results in the console

Using Python

Using Python, we can do exactly the same thing as the JavaScript example, but this time we will run the code of our desktop. We will need to import the urllibs and datetime. I have chosen to use a third party library simplejson.

import urllib, urllib2, datetime, simplejson

To query a REST endpoint in Python, you will:

  1. create a parameters object
  2. concatenate it to a url
  3. then open the url and read the reply.

param = {‘where’:’1=1′,’outFields’:’*’,’f’:’json’} url = ‘http://coagisweb.cabq.gov/…/MapServer/0/query? ‘ + urllib.urlencode(param) rawreply = urllib2.urlopen(url).read()

If you print rawreply, you will see a large string. You will need to convert the string to JSON. This is where I used simplejson.

reply = simplejson.loads(rawreply) print reply[“features”][0][“attributes”][“date”] print reply[“features”][0][“attributes”][“CVINC_TYPE”]

Your program should have printed out the millisecond date for the first result and the incident type. Now, we can assign the date to a variable and convert it to a readable format.

d=reply[“features”][0][“attributes”][“date”] readable = datetime.datetime.fromtimestamp(d/1000) print readable

The above code should print out something like: 2014-07-14 18:00:00. Now that you have the readable date, you can extract and convert it.

print “YEAR: ” + str(readable.year) print “MONTH: “+ str(readable.month) print “DAY: “+ str(readable.day) print “HOUR: ” + str(readable.hour) print “MINUTE: ” + str(readable.minute) print “SECOND: ” + str(readable.second) print “ISO: ” + str(readable.isoformat())

When all the code is executed, you will see the image below.

Console output of python code

Console output of python code

There you have it, converting milliseconds to a human readable date using two different languages.

Open Data and Projections

13 Jan

I am not a geographer, nor am I a math wiz. So for me, projections have been a constant source of pain and suffering. I have opened my mouth and said that WGS84 should be the default for all GIS data. It didn’t go over well – mostly because I think the old school GIS folks have just always done things a certain way and will not stop. Apparently, I am not the only person that hates projections. Calvin Metcalf hates projections too. In the real world, we are the minority and are stuck dealing with them. So in this article I will show you how the ESRI REST API makes it much simpler.

The Open Data

I will start by connection to some Albuquerque Open Data. In this example, I will query the Parks REST service. First, open the link for the parks service and notice that the spatial reference is 102100  (3857). This is WGS 1984 Web Mercator. If you query where 1=1, you can see coordinates like [-1.1855471312199999E7, 4173947.5449]. I want to use WGS84 (4326) so I need coordinates like [35.10418, -106.62987]. We could use a library like Proj4js to convert, but the API already has the functionality we need built right in. On the query form, add 4326 to the output spatial reference field and now you (-1.xx , 417.xxx) becomes [-106.xxx, 35.xxx]. This is what we need.

When you query, you may also want to pass in coordinates from a different spatial reference. You can do this by changing the input spatial reference to 4326. The server will handle the conversion and return the correct feature for you.

An Example

I will start by creating a simple Leaflet.js map that returns coordinates in 4326 on a map click.

<html>
<head><title>Projections</title>
<link rel=”stylesheet” href=”http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css&#8221; />
<style>
html, body, #map {
padding: 0;
margin: 0;
height: 100%;}

</style>
</head>
<body>
<script src=”http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js”></script&gt;
<div id=”map”></div>
<script>
var map = L.map(‘map’,
{center: [35.10418, -106.62987],
zoom:15 });
L.tileLayer(‘http://{s}.tile.osm.org/{z}/{x}/{y}.png’).addTo(map);
map.on(“click”,function(e){
alert(e.latlng);
});
</script>
</body>
</html>

To make a query, we need to replace the alert() function. I will use an AJAX query to the Parks REST endpoint.

First, we need to capture the (lat,lng) in a format we can use. You could split the e.latlng string but that is too much trouble. I will create a marker when the user clicks. I will not add the marker to the map, but rather, I will convert it to GeoJSON. Then I will create a parameter string from several other strings.

var marker=L.marker(e.latlng);
coords=marker.toGeoJSON();
q1=”inSR=4326&outSR=4326&f=json&geometryType=esriGeometryPoint&geometry=”;
q2=String(coords.geometry.coordinates[0]);
q3=”,”;
q4=String(coords.geometry.coordinates[1]);
var params=q1.concat(q2,q3,q4);

In the above code, I grabbed the click e.latlng and passed it to create a marker. The coords variable holds a GeoJSON representation of the marker.

The variable q1 holds my parameters:

  1. inSR = 4326 – this is the projection I want to use
  2. outSR = 4326 – I want the server to give me back the same, not 3857
  3. f=json – I want json, not the HTML
  4. geometryType = esriGeometryPoint – I am taking a point and want the polygon (park) it is in

The last parameter is the geometry.  I specified a point so the server expects two coordinates. I use coords.geometry.coordinates[0] and coords.geometry.coordinates[1] – These are the values for longitude and latitude from the GeoJSON representation of the marker.

Now, I will make the AJAX call and close the map.on function.

var params=q1.concat(q2,q3,q4);
var url = “http://coagisweb.cabq.gov/arcgis/rest/services/public/recreation/MapServer/0/query?&#8221;;
http=new XMLHttpRequest();
http.open(“POST”, url, true);
http.setRequestHeader(“Content-type”, “application/x-www-form-urlencoded”);
http.onreadystatechange = function() {
if(http.readyState == 4 && http.status == 200) {
theGeom= JSON.parse(http.responseText);
console.log(theGeom.features[0].attributes.PARKNAME);
console.log(JSON.stringify(theGeom.features[0].geometry.rings));
}
}
http.send(params);
}); // close map.on

Now you should have a map that waits for a click, then queries the REST endpoint passing a point in 4326 and gets back a park name with coordinates in 4326.

I zoomed in to Robinson Park.

I zoomed in to Robinson Park.

Clicking Robinson Park gave me the screenshot below.

Robinson Park with Rings

Robinson Park with Rings

You do not have to be an expert on projections, just use the tools provided and pass what you want to input and what you want sent back to you as parameters in your queries.

Geoprocessing on the Client Side with Turf.js

7 Jan

Geoprocessing has been primarily a desktop activity. Using ArcServer, you can publish geoprocessing services. This takes geoprocessing off the desktop but requires communication between a client and server. I don’t mind you downloading my data and processing it on your desktop, but I really don’t like the idea of you using my CPU and memory running some harebrained geoprocessing task off my server. Given the advances in web technology, especially JavaScript, can’t we come up with something better? Can’t we let the client handle the work?

We can with Turf.js.

Using Turf.js you can perform a large number of commonly used geoprocessing functions client side.  In this post, I will show you how to buffer, point in polygon and sum a field for points in a polygon.

Buffer a Point

1. Using Leaflet.js, create a map and add a tile layer:

  • var map = L.map(‘map’, { center: [35.10418, -106.62987],zoom: 9});
  • L.tileLayer(‘http://{s}.tile.osm.org/{z}/{x}/{y}.png’).addTo(map);

2. Create two points using turf.point and Long,Lat.

  • pointOne = turf.point(-106.32568,35.11542);
  • pointTwo = turf.point(-106.33,35.22)

3. The points are now in GeoJSON. to add them to Leaflet.js us L.geoJson.

  • L.geoJson(pointOne).addTo(map);
  • L.geoJson(pointTwo).addTo(map);

4. Buffer a point and assign the result to a variable. Then add the buffer to the map. the buffer function takes a feature (point, line, polygon, feature collection), a distance, and the units (miles, kilometers or degrees).

  • var b = turf.buffer(pointOne,2,”miles”);
  • L.geoJson(b).addTo(map);

Now you should have a map that looks like the one below.

Two points with one buffered.

Two points with one buffered.

Point in Polygon

Now that we have two points and a buffer, let’s perform a point in polygon.

1. Create a polygon from the buffer.

  • var polygon = turf.polygon(b.features[0].geometry.coordinates, {
    “fill”: “#6BC65F”,
    “stroke”: “#6BC65F”,
    “stroke-width”: 5,
    “title”:”Polygon”,
    “description”:”A sample polygon”
    });

2. To PIP, use turf.inside() passing the point and polygon as parameters. the result will be true or false.

  • alert(“pointTwo is inside? “+turf.inside(pointTwo, polygon));

Now you will be alerted that the point is not inside the polygon.

Point not in Polygon

Point not in Polygon

In the previous example, the features did not have any attributes. In the next geoprocessing example, we will calculate a value from points in a polygon.

Using Statistics: Sum

1. This example starts with a Leaflet.js map.

  • var map = L.map(‘map’, {center: [35.10418, -106.62987],zoom: 9});
  • L.tileLayer(‘http://{s}.tile.osm.org/{z}/{x}/{y}.png’).addTo(map);

2. Add a function for iterating through features so we can add a popup.

  • function onEachFeature(feature, layer) {
    layer.bindPopup(“<h3>Add this number: “+feature.properties.title+”</h3>”+feature.properties.description);}

3. Now add your points, but this time we will add properties to the points.

  • var p1 = turf.point(-106,35, {“marker-color”: “#6BC65F”,”title”: 100, “description”: “Not in Polygon”, “someOtherProperty”:”I am another property” });
  • var p2 = turf.point(-106.62987,35.10418, {“marker-color”: “#6BC65F”,”title”: 4, “description”: “In Polygon”, “someOtherProperty”:”I am another property” });
  • var p3 = turf.point(-106.64429,35.14125, {“marker-color”: “#6BC65F”,”title”: 1, “description”: “Also in Polygon”, “someOtherProperty”:”I am another property” });

4. To sum a filed, you will need at least one polygon – you can use multiple as well.

  • var polygon = turf.polygon([ [
    [-106.73355,35.21197],[-106.73355,35.04911],[ -106.51932,35.04911],[-106.49872,35.19177]
    ]], {
    “fill”: “#6BC65F”,
    “stroke”: “#6BC65F”,
    “stroke-width”: 5,
    “title”:”Polygon”,
    “description”:”A sample polygon”
    });

5. Create feature collections for the polygon(s) and points. Add them to the map using an option to call your onEachFeature function.

  • var p = turf.featurecollection([polygon]);
    var t = turf.featurecollection([p1,p2,p3]);
  • L.geoJson(p).addTo(map);
  • L.geoJson(t, {
    onEachFeature: onEachFeature
    }).addTo(map);

6. Now pass the sum function the polygon, points, the field to sum and the name of the output field.

  • var sum = turf.sum(p,t,”title”,”output”);

7. when you click the map you will get the result. Notice the marker with the value of 100 is ignored since it is outside the polygon.

  • map.on(“click”,function(){alert(sum.features[0].properties.output);});

Lastly, when you can click a marker and the popup information is displayed.

sum

sum2

 

Running geoprocessing tasks without having to pass data back and forth from client to server is the way to go. It also means your browser can now work as a simple desktop GIS application.

Parcel Values in Bernalillo County

6 Jan

In the last post, you learned how to use the ESRI REST API to access data. I am going to show one more example. this example will grab 2500 parcel records and return the total property value for all the parcels.

The example is almost identical to the previous post. You will create an array to hold the values of each parcel. Then we will use a reduce function to sum them. The full code is below.

Results of REST query

Results of REST query

<html>
<head>
<title>PUBLIC ART</title>
</head>
<body>
<div id=”title”>Loading…</div>
<div id=”total”></div>
<div id=”tax”></div>
<script>
var query;
var holdtotal=[0];

var params = “where=1=1&outfields=TOTVALUE&f=json”;
var url = “http://coagisweb.cabq.gov/arcgis/rest/services/public/BernCoParcels/MapServer/0/query&#8221;;
http=new XMLHttpRequest();
http.open(“POST”, url, true);
http.setRequestHeader(“Content-type”, “application/x-www-form-urlencoded”);
http.onreadystatechange = function() {
if(http.readyState == 4 && http.status == 200) {
theGeom= JSON.parse(http.responseText);
var key, count = 0;
for(key in theGeom.features) {
if(theGeom.features.hasOwnProperty(key)) {
count++;
}}
document.getElementById(“title”).innerHTML=”<h3>I grabbed “+count+” parcels in Bernalillo County”;

for(i=0;i<count;i++){

holdtotal.push(theGeom.features[i].attributes.TOTVALUE);
}

var grandtotal=holdtotal.reduce(function(t,n){return t+n},0);

document.getElementById(“total”).innerHTML=”The total value of the parcels is $”+grandtotal.toLocaleString();

}}

http.send(params);
</script>
</body>
</html>

 

The above code should be straight forward with the exception of the variable grandtotal. The reduce function allows you to iterate through an array and add the previous value to the next returning a single value. the variable t is the total starting at 0 and n is the next value. when it is added, the value of t increases and adds itself to the next item. Then you just write the value to HTML.

With this technique you can create forms that would allow users to query data that you already publish in GIS. You can change your where clause to only grab residential properties, properties owned by a specific person or by lot size. Try some variations like where=acreage>600.

Database Queries Using GIS: The ESRI REST API Without Geography

6 Jan

When people think of GIS they think of maps. But GIS is about data and if we think about it as data it is not special. In this post, I will show you how to use your GIS data as a database and nothing more. I am going to build a website that displays a table of all the public art in Albuquerque with the artist, title and a link to an image of the piece.

First, go to the Albuquerque Public Data website and select Public Art. You will see a directory listing. Choose the last option: PublicArtREST. You will then see a bunch of the ArcServer information for the service. This URL could be used to load the data in to a map for display, but we are only interested in the tabular data at this time. On the bottom of the page you will see the supported operations. We are going to use the query operation. Click the query link and you will see a form. In the where box, type 1=1 and then click the button query (GET). a list of features will show up under the form. See the geographic data available as (x.y) coordinates? In the outfields box, type a *. Then set the return geometry option to False. Now press Query (GET) again. Now you have all the data from the database (there are x,y fields only because they are an actual column in the table. This is not the geometry from ESRI. You can tell because the projection changed – it is now in wkid:4326 and the ESRI version is in wkid:3857).

Now you know where the data lives and how to get it without the geographic portion. The next step is to grab it using AJAX. The key line in the AJAX query will be the parameters:

var params = “where=1=1&outfields=*&f=json”;

You used the first two parameters in the form. The 1=1 returns all the records(ArcServer limits the number of results so a large dataset may only return between 1000-2500 unless it is set for more). The outfield=* returns all the fields for each feature. The last parameter “f=JSON” is returning the JSON data and not the HTML underneath the form. This makes it easy to parse in JavaScript.

The complete code will create two DIV elements: one for the count of public art pieces; and one for the table of results. It will make an AJAX post request, count the records and write out the number, then create a table of the results by sticking each line in to an array then dumping it at the end. The result is below with the complete code after the image.

Table of Public Art with Link to Image

Table of Public Art with Link to Image

<html>
<head>
<title>PUBLIC ART</title>
</head>
<body>
<div id=”count”></div>
<div id=”data”></div>
<script>
var lines=[“<table border=’1′><tr><td>TITLE</td><td>ARTIST</td><td>IMAGE URL</td></tr>”];
var params = “where=1=1&outfields=*&f=json”;
var url = “http://coagisweb.cabq.gov/arcgis/rest/services/public/PublicArt/MapServer/0/query&#8221;;
http=new XMLHttpRequest();
http.open(“POST”, url, true);
http.setRequestHeader(“Content-type”, “application/x-www-form-urlencoded”);
http.onreadystatechange = function() {
if(http.readyState == 4 && http.status == 200) {
theGeom= JSON.parse(http.responseText);
var key, count = 0;
for(key in theGeom.features) {
if(theGeom.features.hasOwnProperty(key)) {
count++;
}}
document.getElementById(“count”).innerHTML=”<h3>There are “+count+” pieces of Public Art in Albuquerque</h3>”;

for(i=0;i<count;i++){

lines.push(“<tr><td>”+theGeom.features[i].attributes.TITLE+”</td><td>”+theGeom.features[i].attributes.ARTIST+”</td><td><a href='”+theGeom.features[i].attributes.IMAGE_URL+”‘>Image</a></td></tr>”);}

lines.push(“</table>”);

document.getElementById(“data”).innerHTML=lines.join(” “);

}}

http.send(params);
</script>
</body>
</html>

Now you know how to use a GIS service to retrieve data without a map. You can refine your query to only pieces by a certain artist, or create a table that lists artists and the number of pieces they have in the collection.

Happy Hacking.