GIS Programming with ESRI

Geographic Information System (GIS) Programming is the software development for using and making maps. The technologies that enable GIS software are the Geospatial Database (GDB) and the map server. GDBs are SQL relational DBs with the ability to make geometric/spatial queries, for example selecting all the GDB entries within a specific area. GDBs can make these spatial queries efficiently, for example log-linear time.

Although open source GIS technologies exist, the field is new and expanding, and it is dominated by commercial software companies. Esri is one of the leading GIS software companies, particularly among government and research scientists. Esri offers an array of technology including ArcGIS JavaScript (JS) which is the platform for developing web based GIS applications. Currently, Esri is transitioning from ArcGIS JS version 3 to version 4. ArcGIS JS version 3 offers all the capabilities for working with 2 dimensional maps. ArcGIS JS version 4 extends version 3 to working with 3 dimensional maps, but does not offer all the widgets available in version 3. This tutorial will use ArcGIS JS version 3 because the API is more complete for 2 dimensional maps and there are more resources on the web. The primary resource for ArcGIS JS v3 is Esri development website:

https://developers.arcgis.com/javascript/3/

This lecture is composed of multiple tutorials. The tutorials will demonstrate how to programmatically load a map into webpage, interact with the map by drawing features, give the features attributes which are stored in the GDB, and search a GDB. In the process you will learn how to make spatial queries, display the queries on the map and manipulate the webpage Document Object Model (DOM).

Maps and Layers Tutorial

In ArcGIS JS v3 the Map is the primary object for containing, viewing and interacting with maps. A Map is composed of one or more Layer. The layers are stacked on top of each other. As the code loads the layers into the map, their attributes can be specified and modified. Esri offers several maps that can be used as base maps. This tutorial will demonstrate how to create a map, change the base maps and add historical map layers on top of the base maps so that current towns of Calumet and Laurium can be visually compare with Calumet and Laurium of 1949.

In this tutorial, you will learn:

  • Creating a Map using Esri Basemaps
  • Adding historical map layers

A good tutorial at ArcGIS JS v3 to study is

https://developers.arcgis.com/javascript/3/jshelp/intro_firstmap_amd.html

Dojo

ArcGIS JS uses the Dojo JavaScript framework.

https://dojotoolkit.org/

Dojo is much like JQuery and offers all the tools for manipulating the DOM and making interactive web pages. There are many good reasons for using a JS framework. They make standard task easy to code, and more important, JS frameworks insure that the JS code works across browsers.

In addition Dojo uses an Asynchronous Module Definition (AMD) for managing JS packages.

https://dojotoolkit.org/documentation/tutorials/1.10/hello_dojo/index.html

You will not be making packages, but you will be using many Dojo and Esri packages. Using Dojo AMD package manager, only requires the uses of the “require” statement with two arguments, which I call the “require-array” for loading the packages and the “require-callback” function which gives the package variable names to use in your script.

<script>
require([ "package-name-1", "package-name-2"], function (name1, name2) {
 // your script goes here using name1 and name2
});
</script>

You can give the packages any variable name you wish in the callback function argument list. The order of the variable names in the require-callback determines which package it is associated with. While developing the code, I format the require-array and require-callback arguments vertically, and when I choose to add a new package, I add the package name and variable name to the bottom of both lists. This minimizes confusion and coding errors. In addition, I give the variable name a very similar name to the package name.

Some packages do not require arguments in the require-callback, for example they can add additional features to other packages. A special package, “dojo/domReady!”, calls the require-callback after the webpage has been loaded on the webpage. I put the “dojo/domReady!” package name last in the require-array. It does not need a require-callback argument.

We are ready to look at the code.

The Code

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
    <title>Map and Layers</title>
    <link rel="stylesheet" href="https://js.arcgis.com/3.18/dijit/themes/claro/claro.css">
    <link rel="stylesheet" href="https://js.arcgis.com/3.18/esri/css/esri.css">
    <style>
        html, body, #map {
            height: 100%;
            margin: 0;
            padding: 0;
        }
    </style>
    <script src="https://js.arcgis.com/3.18/"></script>
    <script>
        var map;

        require( ["esri/map",
                "esri/dijit/BasemapGallery",
                "dojo/parser", // for parsing the data-dojo-***
                "esri/layers/ArcGISDynamicMapServiceLayer",

                "dijit/layout/BorderContainer",
                "dijit/layout/ContentPane",
                "dijit/TitlePane",
                "dojo/domReady!"
        ],

        function(Map,
                 BasemapGallery,
                 parser,
                 ArcGISDynamicMapServiceLayer
                 )
        {
            parser.parse() // this is for data-dojo-type
            map = new Map("mapDivId", {
                basemap: "satellite", //"topo",  //For full list of pre-defined basemaps, navigate to http://arcg.is/1JVo6Wd
                center: [-88.448, 47.242], // longitude, latitude
                zoom: 16,
                logo: false
            });

            map.on("click", function (evt) {
                console.log("map on click evt", evt);
                console.log("mapevt.mapPoint.x="+evt.mapPoint.x+", evt.mapPoint.y="+evt.mapPoint.y);
            });

            var basemapGallery = new BasemapGallery({
                showArcGISBasemaps: true,
                map: map
            }, "basemapGallery");
            basemapGallery.startup();

            basemapGallery.on("load", function(){
                for(var i = 0, len = basemapGallery.basemaps.length; i < len; i++){
                    console.log("basemaps")
                    console.log("basemap ",i, basemapGallery.basemaps[i])
                };
                // Can only remove after the Basemap Gallery is looaded
                basemapGallery.remove("basemap_11");
                basemapGallery.remove("basemap_9");
                basemapGallery.remove("basemap_8");
//                basemapGallery.remove("basemap_7");
                basemapGallery.remove("basemap_6");
                basemapGallery.remove("basemap_5");
                basemapGallery.remove("basemap_4");
                basemapGallery.remove("basemap_3");
                basemapGallery.remove("basemap_1");
                basemapGallery.remove("basemap_0");
            });

            basemapGallery.on("error", function(msg) {
                console.log("basemap gallery error:  ", msg);
            });

            map.on("layer-add", function(evt){
                console.log("layer-add evt", evt);
            });

            var CalumetTiledServiceURL = "http://gis-core.sabu.mtu.edu:6080/arcgis/rest/services/KeweenawHSDI/Cal49FIPS_core/MapServer";
            var historicCalumetMapLayer = new ArcGISDynamicMapServiceLayer(CalumetTiledServiceURL);
            historicCalumetMapLayer.on("error",function(error){
                alert ("There is a problem on loading:" + CalumetTiledServiceURL);
            });
            historicCalumetMapLayer.setOpacity(0.7);
            historicCalumetMapLayer.on("error",function(error){
                alert ("There is a problem on loading:" + LauriumServiceURL);
            });
            map.addLayer(historicCalumetMapLayer);


            var LauriumServiceURL = "http://gis-core.sabu.mtu.edu:6080/arcgis/rest/services/KeweenawHSDI/Laurium49FIPS/MapServer";
            var historicLauriumMapLayer = new ArcGISDynamicMapServiceLayer(LauriumServiceURL);
            historicLauriumMapLayer.on("error",function(error){
                alert ("There is a problem on loading:" + LauriumServiceURL);
            });
            historicLauriumMapLayer.setOpacity(0.7);
            map.addLayer(historicLauriumMapLayer);

        });
    </script>
</head>

<body class="claro">
<div data-dojo-type="dijit/layout/BorderContainer"
     data-dojo-props="design:'headline', gutters:false"
     style="width:100%;height:100%;margin:0;">

    <div id="mapDivId"
         data-dojo-type="dijit/layout/ContentPane"
         data-dojo-props="region:'center'"
         style="padding:0;">

        <div style="position:absolute; right:20px; top:10px; z-Index:999;">
            <div data-dojo-type="dijit/TitlePane"
                 data-dojo-props="title:'Switch Basemap', closable:false, open:false">
                <div data-dojo-type="dijit/layout/ContentPane" style="width:380px; height:130px; overflow:auto;">
                    <div id="basemapGallery"></div>
                </div>
            </div>
        </div>

    </div>
</div>
</body>

</html>

You can view the webpage here or from resources/gis-programs/maps_layers.html. Copy the source from “view page source” and save it your local machine. You can loadit into your browser using localhost. In addition, you can make an IntelliJ Static Web project and use the IDEA to edit the html and script. IntelliJ makes it very easy to check the code on multiple browsers. In the editing a window, an array of browser icons appear as you hover the cursor near the upper right corner. Click on a browser icon in the array to load the webpage into the browser of your choosing.

Creating Maps

A Esri JS code sample for demonstrating creating a map also explains the dojo AMD require syntax.

https://developers.arcgis.com/javascript/3/jssamples/map_simple.html

A map is created with Map constructor.

map = new Map("mapDiv", options)

The first argument of the Map constructor is a string identifying the id of the tag to load the map into. The options is an object with key-value pairs. See the Map API for all the available options:

https://developers.arcgis.com/javascript/3/jsapi/map-amd.html#map1

This code makes use of the basemap, center, zoom and logo options. The basemap specifies a base map to load first. The center specifies the longitude and latitude coordinates to center the map on, and the zoom specifies the zoom level. These levels are specified by the base map. Finally the “logo: false” specifies to load the map without the “Esri” logo. It is true by default.

Events and JS

JS is asynchronous and event driven. Generally, the JS code runs in a single thread. Some operations requires time, for example requesting an image to load into the web page or waiting for the user to interact with the webpage, so Java splits this task off of by making an event and adding the event listener. When the task completes the event handler is placed in a queue. The JS checks the event queue when the script end and calls the event handler.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

There are two primary syntaxes for handling events. One way is to give the task a callback function in the arguments of the function requesting the task. The callback function is called after the task completes. Using this technique the event firing and listening is handled in the background by the JS engines. Another popular technique is to create events and specify functions to listen to the event firing. The code fires the events and the event managing package runs the functions listening to event. In Dojo, the events are given names, and Dojo uses the “on” function to set the event listener. The callback function in the “on” function is typically an anomalous function. For example

map.on("click", function (evt) {
 console.log("map on click evt", evt);
 console.log("mapevt.mapPoint.x="+evt.mapPoint.x+", evt.mapPoint.y="+evt.mapPoint.y);
});

the “on” function specifies the event name and an anomalous function to log the event object, evt, to the JS console after the map has been click.

Load the webpage into a browser and click on the map. After loading the page, open the browser’s development tools. In Chrome, this is done by right clicking on the webpage and selecting “inspect.” After the development tools loads into a panel, make sure that the console is showing by clicking on the “console” tab. You can inspect the object in the console window by clicking on the object. It will expand and list all the properties.

You can learn more about the Map fire event at

https://developers.arcgis.com/javascript/3/jsapi/map-amd.html#events

Base Map Gallery

The Basemap Gallery is a dijit, meaning it is widget using Dojo.

http://dojotoolkit.org/reference-guide/1.10/dijit/

The Basemap Gallery constructor arguments are reverse from the map constructor, meaning first an object representing the key-value parameters and then the id of the html tag of where to render the widget.

http://dojotoolkit.org/reference-guide/1.10/dijit/

The map variable must be specified in the parameter object.

Inspect the tags in the body surrounding the basemapGallery div. They use standard css styling and data-dojo-*** attributes to style, layout add interactivity to the base map gallery.

https://dojotoolkit.org/documentation/tutorials/1.10/declarative/
http://dojotoolkit.org/reference-guide/1.10/dijit/layout.html

The dojo/parser module object parses the DOM, looking for data-dojo-*** tags and decorating them.

https://dojotoolkit.org/reference-guide/1.10/dojo/parser.html

This is the reason for calling “parser.parse()” at the top of the script.

Many of the base maps provided by the basemap gallery, I do not care for, so the code removes some of them after the basemap gallery has been loaded.

Historical Maps

Professor Don LaFreniere has digitized many Keweenaw historical maps. You can see the list at

http://gis-core.sabu.mtu.edu:6080/arcgis/rest/services/KeweenawHSDI

The names of the maps are specified by an abbreviated town name, year and type of maps. For example the Calumet 1917 map is named “Cal17FIPS”.

The “FIPS” maps were made from scans of historical paper maps. Clicking on a FIPS map link will take you to specification of the map. After clicking on a map link, the url appearing in the browser’s navigation field is the url the code will use to request the map. To see the map, click on the “View In: ArcGIS JavaStript” link in the specification page for the map.

To load a historical map on to map, the code must create a map service layer and then call map.addlayer(…) on the layer.

https://developers.arcgis.com/javascript/3/jsapi/layer-amd.html

There are many Esri map service layers, but the two we can use for the historical maps are ArcGISDynamicMapServiceLayer and ArcGISTiledMapServiceLayer.

https://developers.arcgis.com/javascript/3/jsapi/arcgisdynamicmapservicelayer-amd.html
https://developers.arcgis.com/javascript/3/jsapi/arcgistiledmapservicelayer-amd.html

The titled map server is generally preferred because it caches the maps and therefore loads into the browser faster. I could not get it work with the base maps because of a miss match in the zoom levels between the base maps and the historical maps. Because the dynamic map service does not cache the map, it can modify the map match the zoom level of the base map.

Before adding the historical map layer, the code set opacity of the map

https://developers.arcgis.com/javascript/3/jsapi/layer-amd.html#setopacity

The default opacity is 1. An opacity 1 would completely hide the base map and the viewer would not be able to see current map. An opacity of 0 would make the historical map completely transparent and the viewer would not be able to the historical map. I wanted the base map to be predominate so I chose a value close to 1.

After a layer is added to the map, the map fires the “layer-add” event. A listener for the event is specified in the callback for the map.on(…) method.

map.on("layer-add", function(evt){
 console.log("layer-add evt", evt);
});

In this case, the script just prints the event to the console.

Play with the map and try modifying the script. Be sure to look at the JS console output. For extra credit, try to get the historical maps to be a titled map service layer and working with the base maps. The base maps are layers that have more scales then the historical maps. You could try setting the minimum and maximum scale of the base maps after they have been loaded.

Features Tutorial

This tutorial show how users can draw markers on a layer added to the map and view the information of markers added to the layer.

In this tutorial, you will learn about:

  • FeatureLayers
  • Graphic
  • Drawing
  • Controlling the Tooltip

Good references for this tutorial are the Esri sample code.

https://developers.arcgis.com/javascript/3/samples/ed_simpletoolbar/
https://developers.arcgis.com/javascript/3/samples/ed_feature_creation/

Feature Layer

The feature layer is the primary means for a map to interact with the GDB.

https://developers.arcgis.com/javascript/3/jsapi/featurelayer-amd.html

It is associated with a table in the GDB via the URL of a Feature Server. Because cartographer make maps by drawing graphics, a feature layer inherit from graphic layer.

https://developers.arcgis.com/javascript/3/jsapi/graphicslayer-amd.html

So a feature layer is also a graphic layer. Points, polylines and polygons are Graphic objects added to graphic layer.

https://developers.arcgis.com/javascript/3/jsapi/graphic-amd.html

A graphic can have geometry, symbol, attributes and infoTemplate. The geometry specifies shape and location of the graphic. The symbol specifies how to render the graphic in the graphic layer. For graphics in a feature layer, the attributes represent an entry in the GDB table of the feature layer. The attribute is a key-value object associated the table field names with values for the fields.

A diagram of the relationships

FeatureLayer -------------> Table in the GDB
has Graphic ---------------> has entry
   Geometry --------------------> shape and location 
   Attribute -------------------> field values
is GraphicLayer-------------> Layer in the Map
   Symbol -----------------------> how to render the geometry
   InfoTemplate -----------------> how to display attributes

The FeatureLayer, its Graphics and the Graphics’ Geometries and Attributes are the key components for working with a GIS and the GDB. I believe the terminology would have been a little less confusing if FeatureLayers had Features with Attributes and Geometry instead of Graphics. I continuously remind myself that “Graphics are Features”. The Esri API does not have a Feature, so I also remind myself that “Features are Graphics”. The ArcGIS JS v4 has not rectified this confusing terminology.

The Geometry of a graphic can be one of five different types

  • Point – has x and y map coordinates
  • Multipoint – is a collection of points
  • Polyline – has points and the segments between the points
  • Polygon – has Polylines that enclose an area
  • Extent – is a rectangular area

The point, polyline and polygon are fundamental geometric concepts. The Multipoint is necessary because a single feature could require multiple points to be defined but is not a curve or area. The
Extent is special and is included because rectangular area is frequently used to approximate Multipoint, Polyline and Polygon in geospatial queries.

A feature layer can have only one of these geometric types, meaning that all the graphics/features in the layer must be of the same geometry type. So if you are constructing a map with points, curves and areas, you will need three feature layers, one for Points, one for Polylines and one for Polygons.

When the attributes for a feature/graphic represent an entry in to the GDB table, all the features/graphics have the same set of attributes albeit the attributes can have null values. For example suppose all the features are points but represent different types of point such as the location of fire hydrants and signs along the road. These features can be contained in a single feature layer even though the signs might have an attribute such as text and hydrant might have the attribute capacity. The entire attribute set for the feature layer would contain both text and capacity. You may not want to do this, but you can.

A mental model that use for a feature layer is that it is a table of attributes for all the features/graphics of one geometric type. The table also contains the geometric representations for the features which is commonly displayed has graphical map layer.

This tutorial uses a ArcGIS feature server made by Professor Don Lafreniere.

http://services2.arcgis.com/RPhrOu9XQzI31xTa/arcgis/rest/services/Robert_Point_Test/FeatureServer

Point your browser at the URL above and you will see specification for the server. This server is severing only a single layer so clicking on either “All Layers and Tables” or “Roberttest(0)” will lead to pages with the same information. The interesting information:

  • “Geomtery Type: esriGemoteryPoint” saying that the layer is for points
  • “Drawing Info:” which is default symbol for the graphic
  • “Has Attachments: true” which says that the features can have attachments, more on this in the next tutorial
  • “Object ID Field: OBJECTID” which is the auto incremented unique id field for the table
  • “Fields” which is a list of the fields/attributes for the table. The first word is the name of the field.

The field names are

  • OBJECTID
  • shortint
  • longint
  • text50
  • GlobalID

They are rather arbitrary and suggest their types.

We are ready to look at the code.

The Code

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
    <title>Draw Features</title>
    <link rel="stylesheet" href="https://js.arcgis.com/3.18/dijit/themes/claro/claro.css">
    <link rel="stylesheet" href="https://js.arcgis.com/3.18/esri/css/esri.css">
    <style>
        html, body, #map {
            height: 100%;
            margin: 0;
            padding: 0;
        }
    </style>
    <script src="https://js.arcgis.com/3.18/"></script>
    <script>

//        var featureLayer;

        require(["esri/map",
                "esri/layers/FeatureLayer",
                "esri/toolbars/draw",
                "esri/symbols/SimpleMarkerSymbol",
                "esri/Color",
                "esri/graphic",
                "esri/renderers/SimpleRenderer",
                "esri/InfoTemplate",
                "dojo/query",

                "dojo/domReady!"
        ],
        function (Map,
                  FeatureLayer,
                  Draw,
                  SimpleMarkerSymbol,
                  Color,
                  Graphic,
                  SimpleRenderer,
                  InfoTemplate,
                  dojoQuery
        ) {
            var map;

            // Create the map with a basemap
            map = new Map("map", {
                basemap: "osm", //"topo",  //For full list of pre-defined basemaps, navigate to http://arcg.is/1JVo6Wd
                center: [-88.448, 47.242], // longitude, latitude
                zoom: 16,
                logo: false
            });

            // create Feature Layer and add
            var featureURL = "http://services2.arcgis.com/RPhrOu9XQzI31xTa/arcgis/rest/services/Robert_Point_Test/FeatureServer/0";
            var featureLayer = new FeatureLayer(featureURL, {
                mode: FeatureLayer.MODE_ONDEMAND,
                outFields: ["*"]
            });

            var marker = new SimpleMarkerSymbol(); // default is a circle
//            marker.setPath("M16,3.5c-4.142,0-7.5,3.358-7.5,7.5c0,4.143,7.5,18.121,7.5,18.121S23.5,15.143,23.5,11C23.5,6.858,20.143,3.5,16,3.5z M16,14.584c-1.979,0-3.584-1.604-3.584-3.584S14.021,7.416,16,7.416S19.584,9.021,19.584,11S17.979,14.584,16,14.584z");
//            marker.setStyle(SimpleMarkerSymbol.STYLE_PATH);
            var renderer = new SimpleRenderer(marker);
            featureLayer.setRenderer(renderer);

            var template = new InfoTemplate();
            template.setTitle("Feature Details");
            featureLayer.setInfoTemplate(template);

            var drawToolbar = new Draw(map);
            var simpleMarkerSymbol = new SimpleMarkerSymbol();
            simpleMarkerSymbol.setStyle(SimpleMarkerSymbol.STYLE_CROSS);
            drawToolbar.setMarkerSymbol(simpleMarkerSymbol);


            featureLayer.on("mouse-over", function (evt) {
                console.log("mouse-over", evt);
                var nodes = dojoQuery(".tooltip");
                console.log("tooltip nodes", nodes)
                nodes[0].innerHTML = "click for info";
//                dojoQuery(".tooltip")[0].innerHTML = "click for info";
            });

            featureLayer.on("mouse-out", function (evt) {
                console.log("mouse-out", evt);
                dojoQuery(".tooltip")[0].innerHTML = "click to add a point";
            });


            var drawGraphic = true;

            map.on("click", function (evt) {
                console.log("map click", evt);
                if(evt.graphic){ // A graphic exist only if clicked on.
                    drawGraphic = false; // We set a flag to stop drawing the graphic and feature layer
                }
            });

            map.addLayers([featureLayer]);
            map.on("layers-add-result", initEditing);

            function initEditing(evt) {
                console.log("initEditing: ", evt);
                drawToolbar.activate(Draw.POINT);
                drawToolbar.on("draw-complete", function (evt) {
                    console.log("draw-complete", evt);
                    if (drawGraphic) {
                        var attributes = featureLayer.templates[0].prototype.attributes;
                        var graphic = new Graphic(evt.geometry, simpleMarkerSymbol, attributes);
                        featureLayer.applyEdits([graphic], null, null); // Does the CRUD on the FeatureLayer and then draws on the map
//                        map.graphics.add(graphic); // draws the symbol on the map, don't need to use if using // FeatureLayer.applyEdits()
                    }
                    else {
                        drawGraphic = true;
                    }
                });
            }; // end initEditing
        });
    </script>
</head>
<body>
<div>Drawing Features </div>

<div id="map"></div>

</body>
</html>

You can view the webpage here. Load the webpage into your browser at localhost and editor or IDE.

The map is created as in the previous tutorial. We dispense with using a basemap gallery and use only the Open Street Map (osm) basemap.

Feature Layer

The constructor for FeatureLayer needs a URL.

https://developers.arcgis.com/javascript/3/jsapi/featurelayer-amd.html#featurelayer1

The URL should be to a single feature layer and not to the feature server. That is reason for the “0” in the URL.

The important options are the mode and outfield. Mode describes how the feature layer should be accessed. The common modes are when needed (MODE_ONDEMAND) or all at once (MODE_SNAPSHOT). Outfield is an array of the field values that should be downloaded for the graphics’ attributes. The “[*]” means all the fields in the table.

We want to specific how the features are rendered. Because the features are points, we only need to render them using SimpleMarkerSymbol.

https://developers.arcgis.com/javascript/3/jsapi/simplemarkersymbol-amd.html

Click on the “ArcGIS Symbol Playground” in the API specification for SimpleMarkerSymbol

https://developers.arcgis.com/javascript/3/samples/playground/index.html

Play with web app to see the different symbols that ArcGIS has. Be sure to look at SimpleMarkerSymbol and PictureMarkerSymbol. The default simple marker is a circle with diameter 16 pixels.

After creating the marker symbol we use it to make the renderer and set the feature layer’s renderer.

Next we make the infoWindow and set the feature layer’s to the new info window.

https://developers.arcgis.com/javascript/3/jsapi/infotemplate-amd.html

Drawing Tool

To draw new features on the feature layer we need a drawing tool. This is called Draw in ArcJIS.

https://developers.arcgis.com/javascript/3/jsapi/draw-amd.html

Funny that it is part of the toolbar, even though the webpage will not have a toolbar. Because the new features will be points, the drawing tool only needs a SimpleMarkerSymbol. The script use a different marker simple so that we differentiate the new symbol for the existing symbols during development. Finally the script set drawing tool marker symbol.

Tooltip

The tooltip is the message that appears adjacent to the cursor. It hints at what the user can do at the location. By default the drawToolbar’s tooltip will show “click to add point”. We would like the message at the tooltip to change to “click for info” when the cursor is over a feature. To do this we manipulate the Document Object Model (DOM).

Load the webpage into a browser and open the “inspect” development panel. Make sure that the “Elements” panel is visible. The html tags in the “Element” panel is the DOM. Look for the div with class=”tooltip”

<div class="tooltip" …>click to add point</div>

We want to change the text inside this div when the cursor is over a graphic/feature. FeatureLayer fire the “mouse-over” event when the mouse cursor enters a graphic.

https://developers.arcgis.com/javascript/3/jsapi/featurelayer-amd.html#event-mouse-over

There is also a “mouse-out” event that is fired by the feature layer when the cursor exits the graphics. To change the tooltip message, we use some dojo query magic.

https://dojotoolkit.org/reference-guide/1.10/dojo/query.html

Dojo query searches the DOM and returns an array of nodes matching the filter. The filter or better called selector is the argument of the query. The selector specification is very similar to the CSS selector.

http://dojotoolkit.org/reference-guide/1.10/dojo/query.html#examples
http://dojotoolkit.org/reference-guide/1.10/dojo/query.html#standard-css2-selectors

The script specifies the selector “.tooltip”.

var nodes = dojoQuery(".tooltip");

Which means select all the nodes in the DOM that have class equal to “tooltip”. The array of nodes returned from dojo query is a NodeList, which comes with manipulation package.

https://dojotoolkit.org/reference-guide/1.10/dojo/NodeList-manipulate.html#dojo-nodelist-manipulate

We want to change the text in the div, so we use innerHTML method to change the message.

https://dojotoolkit.org/reference-guide/1.10/dojo/NodeList-manipulate.html#innerhtml

Note that dojo query and node list use chaining so that the operation could be more concisely written.

dojoQuery(".tooltip")[0].innerHTML = "click for info"

The feature layer on mouse-out event changes the message back to “click to add a point”.

Drawing logic

We do not want a mark drawn when the cursor is over a graphic, rather we want drawing to be enabled where there are not any graphics, so we use a flag to indicate when drawing is possible.

var drawGraphic = true;

We need to change the flag when the cursor is over a graphic, and we need to change it soon as the mouse the mouse has been clicked. The map click event is called before the feature layer click event, so we should use this event handler or callback to change the drawGraphic flag. But we need to know if a graphic has been clicked on. We use a trick. The map click event will have a graphic property if it is on a graphic of any feature layer. We set drawGraphic false if the map click has a graphic.

Drawing

Draw has a “draw-complete” event that fires when the drawing is complete.

https://developers.arcgis.com/javascript/3/jsapi/draw-amd.html#events

Drawing a point is complete after a single click, so we could use the map click callback to make the mark on the map and add the point to the feature layer, but not all graphics are a single click, so the tradition is to use the draw-complete event. We should stick to the tradition.

Note that we activate the drawToolba and defined the draw-complete callback only when the layer is ready for drawing, meaning after the layer has been added to the map, meaning in the layer-add callback, in this case the initEdit() function.

If we can draw, meaning drawGraphic is true, then we make a new graphics which requires attributes. We just use the prototype template for the feature layer to make the attributes for this tutorial. The next tutorial will show how to make specific attributes. To add the attributes to the feature layer we use FeatureLayer.ApplyEdits

https://developers.arcgis.com/javascript/3/jsapi/featurelayer-amd.html#applyedits

FeatureLayer.applyEdits can add, update or delete an array of graphics in the feature layer. We want to “add” a point, so we put our array of graphics in the first argument

featureLayer.applyEdits([graphic], null, null);

We do not really need the two nulls in the argument list, but tradition is to have them so that it is easy to identify which operation is being performed.

FeatureLayer.applyEdits also has callbacks, but we don’t use them in this tutorial. As a side effect the applyEdits method also draws the marker on the map.

If we only wanted to add the marker to the screen and not store it in the feature layer GDB table we could use

map.graphics.add(graphic);

Attachments Tutorial

This tutorial demonstrates how users can store photos associated with a graphic in a feature layer and how they can view photos associated with a graphic.

In this tutorial you will learn about:

  • Editing Features
  • Adding Attachments
  • Modifying the Info Window

In the ArcGIS sample code, there are related examples, but none of them same show how to upload photos. These sample code were referenced during the development of this tutorial:

An Esri feature layer can store documents associated with graphics/features. The documents stored with graphics/features are called attachments. Esri handles them differently than graphic attributes. This is because more than one document can be associated with a graphic. Recall that feature layer has just a single table in the GDB, so attachments cannot be handled like attributes.

Esri has a widget, AttachmentEditor, to work with attachments.

https://developers.arcgis.com/javascript/3/jsapi/attachmenteditor-amd.html

But we will also want to edit the attributes. Esri has another widget, AttributeInspector, can show and edit the attributes associated with the graphics.

https://developers.arcgis.com/javascript/3/jsapi/attributeinspector-amd.html

AttributeInspector also has an AttachmentEditor. The constructor for the AttributeInspector has options for controlling how the attributes are edited and displayed in the info window, see layerInfos and fieldInfos options specifications.

https://developers.arcgis.com/javascript/3/jsapi/attributeinspector-amd.html#attributeinspector1

We are ready to look at the code.

The Code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
    <title>Attach Editing</title>
    <link rel="stylesheet" href="https://js.arcgis.com/3.18/dijit/themes/claro/claro.css">
    <link rel="stylesheet" href="https://js.arcgis.com/3.18/esri/css/esri.css">
    <style>
        html, body, #map {
            height: 100%;
            margin: 0;
            padding: 0;
        }
    </style>
    <script src="https://js.arcgis.com/3.18/"></script>
    <script>
        require(["esri/map",
                    "esri/layers/FeatureLayer",
                    "esri/symbols/SimpleMarkerSymbol",
                    "esri/Color",
                    "esri/graphic",
                    "esri/renderers/SimpleRenderer",
                    "esri/InfoTemplate",
                    "dojo/query",
                    "dojo/_base/array",
                    "esri/toolbars/draw",
                    "esri/dijit/AttributeInspector",
                    "esri/tasks/query",
                    "dojo/dom-construct",
                    "dojo/_base/array",
                    "esri/geometry/Extent",
                    "esri/geometry/screenUtils",

                    "dojo/NodeList-manipulate",
                    "dojo/NodeList-traverse",
                    "dojo/domReady!"
                ],
                function (Map,
                          FeatureLayer,
                          SimpleMarkerSymbol,
                          Color,
                          Graphic,
                          SimpleRenderer,
                          InfoTemplate,
                          dojoQuery,
                          arrayUtils,
                          Draw,
                          AttributeInspector,
                          EsriQuery,
                          domConstruct,
                          dojoArray,
                          Extent,
                          screenUtils
                ) {
                    var drawGraphic = true;
                    var map;
                    // Create the map with a basemap
                    map = new Map("map", {
                        basemap: "osm", //"topo",  //For full list of pre-defined basemaps, navigate to http://arcg.is/1JVo6Wd
                        center: [-88.448, 47.242], // longitude, latitude
                        zoom: 16,
                        logo: false
                    });

                    // create Feature Layer
                    var featureURL = "http://services2.arcgis.com/RPhrOu9XQzI31xTa/arcgis/rest/services/Robert_Point_Test/FeatureServer/0";
                    var featureLayer = new FeatureLayer(featureURL, {
                        mode: FeatureLayer.MODE_ONDEMAND,
                        outFields: ["*"]
                    });
                    var marker = new SimpleMarkerSymbol();
                    var renderer = new SimpleRenderer(marker);
                    featureLayer.setRenderer(renderer);

                    featureLayer.on("mouse-over", function (evt) {
                        var node = dojoQuery(".tooltip")[0];
                        node.innerHTML = "click for info";
                    });
                    featureLayer.on("mouse-out", function (evt) {
                        console.log("mouse-out", evt);
                        var node = dojoQuery(".tooltip")[0];
                        node.innerHTML = "click to add a point";
                    });

                    // create drawing toolbar for drawing a point
                    // need to activate the drawingToolbar before the first click
                    var drawToolbar = new Draw(map);
//                    drawToolbar.activate(Draw.POINT);

                    map.on("click", function (evt) {
                        console.log("map click event", evt);
                        if(evt.graphic){ // A graphic(eg. feature) exists in the event only if clicked on.
                            drawGraphic = false;
                            viewFeature(evt);
                            // Can not change drawGraphic to true here because of race conditions,
                            // on draw-complete callback will he called before the info window is made,
                            // need to change drawGraphic in the select features callback.
                        }
                    });

                    map.on("layers-add-result", initEdit);
                    map.addLayers([featureLayer]);

                    function initEdit(evt) {
                        console.log("initEdit evt: ", evt);
                        drawToolbar.activate(Draw.POINT);
                        drawToolbar.on("draw-complete", function (evt) {
                            console.log("draw-complete", evt);
                            if (drawGraphic) {
                                // Create the graphic
                                var prototypeAttributes = featureLayer.templates[0].prototype.attributes;
                                var newGraphic = new Graphic(evt.geometry, marker, prototypeAttributes);

                                // add the attributeInspector
                                var layerInfos = makeLayerInfos(true);
                                var attributeInspector = new AttributeInspector(
                                        {
                                            layerInfos: layerInfos
                                        },
                                        domConstruct.create("div")
                                );
                                attributeInspector.startup(); // Not really needed
                                console.log("attributeInspector", attributeInspector);
                                console.log("attributeInspector.domNode", attributeInspector.domNode);

                                // Add the graphics to the feature layer
                                featureLayer.applyEdits([newGraphic], null, null, function () {
                                    // This callback makes the info window
                                    map.infoWindow.setTitle("Make Feature");
                                    map.infoWindow.setContent(attributeInspector.domNode);
                                    map.infoWindow.resize(310, 600);

                                    var screenPoint = map.toScreen(newGraphic.geometry);
                                    map.infoWindow.show(screenPoint, map.getInfoWindowAnchor(screenPoint));
                                }); // Does the CRUD on the FeatureLayer and then draws on the map

                                // Attribucte Inspector Event Handlers
                                attributeInspector.on("attribute-change", function (evt) {
                                    console.log("attribute-change evt", evt);
                                    var feature = evt.feature;
                                    feature.attributes[evt.fieldName] = evt.fieldValue;
                                    feature.getLayer().applyEdits(null, [feature], null);
                                });

                                attributeInspector.on("delete", function (evt) {
                                    console.log("delete", evt);
                                    evt.feature.getLayer().applyEdits(null, null, [evt.feature]);
                                    map.infoWindow.hide();
                                })
                            } // end if drawGraphic
                        }); // end on draw-complete
                    } // end function initEditing(evt)

                    function viewFeature(evt) {
                        console.log("viewFeature evt:", evt);

                        // create attribute inspector that cannot edit
                        // NOTE: the Attribute Inspector needs to exist before the features are selected
                        // Other wise it does not know about the selection.
                        var layerInfos = makeLayerInfos(false);
                        var attributeInspector = new AttributeInspector(
                                {
                                    layerInfos: layerInfos
                                },
                                domConstruct.create("div")
                        );
                        attributeInspector.startup(); // Not really needed
                        console.log("attributeInspector.domNode", attributeInspector.domNode);

                        // Select features (ie Graphics) in the feature layer
                        // Geometry to query against
                        var markerSize = 20; // should be the approximate screen size of marker symbol size
                        var extent = new Extent(0, 0, markerSize, markerSize, null);
                        var screenExtent = extent.centerAt(evt.screenPoint);
                        var mapExtent = screenUtils.toMapGeometry(map.extent, map.width, map.height, screenExtent);

                        // construct the query
                        var query = new EsriQuery();
                        query.geometry = mapExtent;
                        query.spatialRelationship = EsriQuery.SPATIAL_REL_CONTAINS;

                        // make the spatial selection
                        featureLayer.selectFeatures(query, FeatureLayer.SELECTION_NEW, function (features) {
                            // info window cannot be completed until after the selection
                            map.infoWindow.setTitle("View Feature");
                            map.infoWindow.setContent(attributeInspector.domNode);
                            map.infoWindow.resize(310, 600);
                            map.infoWindow.show(evt.screenPoint, map.getInfoWindowAnchor(evt.screenPoint));

                            // Fix the attachment editor by removing the attachment list and upload
                            var uploadFormNode = dojoQuery("form[dojoattachpoint=_uploadForm]");
                            uploadFormNode.remove("*");

                            dojoQuery("span[dojoattachpoint=_attachmentList]").remove("*");
                            dojoQuery("div[dojoattachpoint=_attachmentError]").siblings("br")
                                    .at(0)
                                    .after('<span dojoattachpoint="attachmentList" style="word-wrap: break-word;"></span>');
                            // Note: I removed the "_", just in case.

                            attributeInspector.first();

                            drawGraphic = true;
                        }); // end featureLayer.selectFeatures

                        attributeInspector.on("next", function (evt) {
                            console.log("next feature", evt);
                            // Remove the attachment list and then but the empty node back end
                            dojoQuery("span[dojoattachpoint=attachmentList]").remove("*");
                            dojoQuery("div[dojoattachpoint=_attachmentError]").siblings("br")
                                    .at(0)
                                    .after('<span dojoattachpoint="attachmentList" style="word-wrap: break-word;"></span>');

                            addAttachmentLinks(evt.feature.attributes.OBJECTID);
                        });

                        function addAttachmentLinks(objectId) {
                            featureLayer.queryAttachmentInfos(objectId, function (infos) {
//                                console.log("infos",infos);
                                // Need to put back in the <span><a href=" ?? " traget="_blank"> ??</a> <br> </span>
                                var attachmentListString ="";
                                dojoArray.forEach(infos,function(info){
                                    attachmentListString += '<span><a href="'+info.url+'" target="_blank">'+info.name+'</a><br></span>';
                                }); // end dojoArray.forEach
                                var attachmentNode = dojoQuery("span[dojoattachpoint=attachmentList]")[0];
                                domConstruct.place(attachmentListString, attachmentNode);
                            }); // end featureLayer.queryAttachmentInfos
                        }
                    } // end  function viewFeature(evt)

                    function makeLayerInfos(isEditable) {
//                        console.log("makeLayerInfos isEditable: ", isEditable);
                        var layerInfos = [
                            {
                                'featureLayer': featureLayer,
                                'showAttachments': true,
                                'isEditable': isEditable,
                                'showDeleteButton': isEditable,
                                'fieldInfos': [
                                    {'fieldName': 'shortint', 'isEditable': isEditable, 'label': 'Short Int:'},
                                    {'fieldName': 'longint', 'isEditable': isEditable,  'label': 'Long Int:'},
                                    {'fieldName': 'text50', 'isEditable': isEditable, 'label': 'Text 50:'},
                                    {'fieldName': 'OBJECTID', 'isEditable': false, 'label': 'Object Id:'}
                                ]
                            }
                        ];
                        return layerInfos;
                    } // end function makeLayerInfos(isEditable)
                }); // end require-function
    </script>
</head>
<body>
<div> View or Make Features </div>
<div id="map"></div>

</body>
</html>

You can view the webpage here. Load the webpage into your browser at localhost and editor or IDE.

We create the map and add the feature layer just as the previous tutorial. We also control the tooltip message and editing with drawGraphic flag as we did in the previous tutorial.

Editing Attributes

Drawing and editing the attributes occurs in the callback for map “layers-add-result” event, initEdit function. The “layers-add-result” event is slight different than the “layer-add” event. The Map.addLayers(…) simultaneously adds an array of feature layers to the map and the layers-add-result does not fire until all the layers have been added. This is the standard design pattern to use when editing multiple feature layers simultaneously. This tutorial has a single feature layer, so we could have used the layer-add event callback, but we should stick with the standard design pattern.

After checking for drawGraphic in the initEdit function, the code creates the graphics as in the previous tutorial. A function used to make the layersInfos option for the attribute inspector constructor, makeLayerInfos. You can find makeLayerInfos function at the bottom of the script.

The attribute inspector needs a “div” to be added to the DOM. The dojo/dom-construct package is used to create the div for the attribute inspector.

https://dojotoolkit.org/reference-guide/1.10/dojo/dom-construct.html#create

We are supposed to start up the attribute inspector, but I found it is not necessary. We apply the edits to the feature layer as in the previous tutorial, but we use the callback for applyEdits to make the infoWindow. The attribute inspector node is attached to the infoWindow by:

map.infoWindow.setContent(attributeInspector.domNode);

The API description does specify the domNode property, but the code samples illustrate it use.

We will locate the infoWindow near the click point. The newGraphic has the geometry property which gives the location of newGraphics in map coordinates. We use the map method, toScreen, to get the location in screen coordinates, pixels.

The API specification does not specify much for getInfoWindowAnchor

https://developers.arcgis.com/javascript/3/jsapi/map-amd.html#getinfowindowanchor

But it is always used when locating and showing the infoWindow.

Attribute editing is handled by the callback for attribute inspector “attribute-change” event. The event is fired whenever a field has changed and the field uses the focus, for example when the user clicks on new field after entering text in the previous field. We need to set the feature/graphic attribute value before calling applyEdits

feature.attributes[evt.fieldName] = evt.fieldValue;
feature.getLayer().applyEdits(null, [feature], null);

Note that the feature attribute is updated by using the second argument of applyEdits.

Viewing Attributes

The function viewFeatures is called when clicking on a map graphic. The function makes another attribute inspector but with isEditable set to false by the makeLayerInfos function.

When zoomed out the graphic markers can be stacked, but the click event only returns a single graphic not all the stacked graphics. To collect all the stacked graphic features that were clicked, we have to make a geospatial query. We first make a screen extent that is approximate size of the marker symbol centered at the screen click point.

var markerSize = 20; // should be the approximate screen size of marker symbol size
var extent = new Extent(0, 0, markerSize, markerSize, null);
var screenExtent = extent.centerAt(evt.screenPoint);
var mapExtent = screenUtils.toMapGeometry(map.extent, map.width, map.height, screenExtent);

See the API description for Extent.

https://developers.arcgis.com/javascript/3/jsapi/extent-amd.html

Then we transform the screen extent to a map extent using screenUtilits.

https://developers.arcgis.com/javascript/3/jsapi/esri.geometry.screenutils-amd.html

The mapExtent now describes a geospatial area and can be used in a geospatial query. We first setup the query.

var query = new EsriQuery();
query.geometry = mapExtent;
query.spatialRelationship = EsriQuery.SPATIAL_REL_CONTAINS;

The geometry query property can be any geometry, in this case it is extent of the marker in map coordinates. The spatialRealtionship query property sets the type of query

https://developers.arcgis.com/javascript/3/jsapi/query-amd.html#constants
https://developers.arcgis.com/javascript/3/jsapi/query-amd.html#properties

The relationship between the query geometry and the geometries of the entries in the GDB table can have different relationship. For example the query geometry can cross (SPATIAL_REL_CROSSES) or intersects (SPATIAL_REL_INTERSECTS) the entry geometries. Esri documentation about spatial relationships is concise.

http://desktop.arcgis.com/en/arcmap/10.3/manage-data/using-sql-with-gdbs/relational-functions-for-st-geometry.htm

Note that ST_*** are the functions used by the query when called using SPATIAL_R_*** spatial relationship constant, for example if the spatialRelationship = EsriQuery.SPATIAL_REL_CONTAINS then the ST_Contains function will be used in the query. This is a very large topic. The topic is best learned in a series of tutorial. A good tutorial is at Boundless.

http://workshops.boundlessgeo.com/postgis-intro/

Note that if you install Boundless OpenGeo Suite (as in the third tutorial) it uses port 8080 of your localhost and does not release the port. If you run a Grails app on your localhost after installing Boundless OpenGeo Suite you will get an error.

ERROR org.apache.coyote.http11.Http11NioProtocol - Failed to start end point associated with ProtocolHandler ["http-nio-8080"]
java.net.BindException: Address already in use: bind

I could only resolve the error by uninstalling OpenGeo Suite. Recall that the browser cashes webpages so that just check localhost:8080 is not sufficient after uninstalling OpenGeo Suite. You must load localhost:8080 with a new webpage.

We want to select all the graphics/features within mapExtent, so the spatial relation to use SPATIAL_REL_CONTIANTS. After setting up the spatial query, we use FeatureLayer.selectFeature method to make the query and select all the features/graphics within mapExtent.

https://developers.arcgis.com/javascript/3/jsapi/featurelayer-amd.html#selectfeatures

We use the callback function of selectFeatures to make the info window for the attribute inspector.

Although we specified that the layer should not be editable in this attribute inspector it does not affect the attachment editor, meaning the “upload file” and delete attachment red X appear in the info window. You can check this, by commenting out in the selectFeatures callback the following lines.

var uploadFormNode = dojoQuery("form[dojoattachpoint=_uploadForm]");
uploadFormNode.remove("*");
dojoQuery("span[dojoattachpoint=_attachmentList]").remove("*");
dojoQuery("div[dojoattachpoint=_attachmentError]").siblings("br")
 .at(0)
 .after('<span dojoattachpoint="attachmentList" style="word-wrap: break-word;"></span>');
// Note: I removed the "_", just in case.
attributeInspector.first();

Load the webpage into the browser and click on the graphic, circle, in Agassiz Park in Calumet. This graphic has an attached photo. In the attachment panel there is a link to the photo and a red X. Do not click on the red X; this will delete the photo. Below is the “Choose File” button to upload another file. You can an upload file if you wish. We do not want these operations when viewing a graphics.

The commented out code is dojo query magic to remove the upload form and the list of attachments. See the dojo query, node list manipulation and transverse packages API for details.

https://dojotoolkit.org/reference-guide/1.10/dojo/query.html
https://dojotoolkit.org/reference-guide/1.10/dojo/NodeList-manipulate.html#dojo-nodelist-manipulate
https://dojotoolkit.org/reference-guide/1.10/dojo/NodeList-traverse.html#dojo-nodelist-traverse

With the above code commented out, you can view the original attribute inspector html code using the developer tools to inspect the code printed in the console by the log:

console.log("attributeInspector.domNode", attributeInspector.domNode);

You will have expand many tags to find the div tag containing the attachments. If you do not comment out the dojo magic lines then by the time you look at the html code in the console it will already have been modified by dojo query magic.

For the same reason, we have to remove the entire attachmentList from the DOM. Originally, I tried to extract the anchor tags from the list to reuse, but the dojo query for the anchors always returned an empty list. This is because JavaScript is asynchronous and the attachment list had not been populated. We replace the span tag with class dojoattachpoint=_attachmentList with a slightly different class, so that the attachment editor code cannot attach the list of anchor.

You can uncomment the dojo query magic lines now.

We call attribute inspector first() method so that we know what feature is shown in the infoWindow.

Before we leave the call back for selectFeatures note that we set the drawGraphic flag to true. This is the latest time that we can set drawGraphic in the code. The callback for map click finishes before the viewFeature method finishes, so we cannot set drawGraphic in the map click callback.

The attribute inspector “next” event fires whenever the user clicks the arrow to look at the next feature to view in the stack.

https://developers.arcgis.com/javascript/3/jsapi/attributeinspector-amd.html#event-next

We have to remove the attachment list again and add the span back into the attachment editor DOM.

The method addAttachmentLinks uses the object id to query the attachment info.

https://developers.arcgis.com/javascript/3/jsapi/featurelayer-amd.html#queryattachmentinfos

In the callback, the code constructs the list of attachment links and then locates it into the DOM.

var attachmentListString ="";
dojoArray.forEach(infos,function(info){
     attachmentListString += '<span><a href="'+info.url+'" target="_blank">'+info.name+'</a><br></span>';
}); // end dojoArray.forEach
var attachmentNode = dojoQuery("span[dojoattachpoint=attachmentList]")[0];
domConstruct.place(attachmentListString, attachmentNode);

See the API description for the array utility and DOM construct

http://dojotoolkit.org/reference-guide/1.10/dojo/_base/array.html
https://dojotoolkit.org/reference-guide/1.10/dojo/dom-construct.html#place

Evaluate the Program

Play with the code. It works, but I not happy with it. I think that it would have been better not to use the attribute inspector. The code is slow to load the attachment list. This is because it calls the database twice to get the attachment list. I think making our own content for the info window would be better. See the code sample:

https://developers.arcgis.com/javascript/3/jssamples/widget_extendInfowindow.html
https://developers.arcgis.com/javascript/3/jssamples/widget_formatInfoWindow.html

Also the edit is not intuitive. There is not a submit or save button. This code sample shows how to add a save button.

https://developers.arcgis.com/javascript/3/jssamples/ed_attribute_inspector.html

You can even put the infoWindow in a side panel

https://developers.arcgis.com/javascript/3/jssamples/popup_sidepanel.html

For extra credit you can change the delete button text to “Cancel” or make your own content for the info window.

Feature Class and Creating a Feature Service

You will need a feature server for your app. Although Don or his student will make the feature service for you, you need to specify the feature service that your app will need. First, we should review terminology.

Feature Terminology

ESRI defines a “feature” as a representation of an object on the a map.

http://support.esri.com/sitecore/content/support/Home/other-resources/gis-dictionary/term/feature

A “feature class” is defined as a set of features all the same geometry type and attributes.

http://support.esri.com/sitecore/content/support/Home/other-resources/gis-dictionary/term/feature%20class

We have already discussed geometries. There are basically 4 types: extent, point, multi-point, point, poloylines, and polygons.

https://developers.arcgis.com/javascript/3/jsapi/geometry-amd.html

There is also a “feature dataset”, which is a collection of feature classes. They do not have to have the same geometric type, but they are in the same geographic area. Generally they have an association such as “potholes” feature class (with geometric type point) and “sewer lines” feature class (with geometric type lines) making up a “drainage” feature dataset. I doubt you’ll need a feature dataset, so I’ll not write more.

http://support.esri.com/sitecore/content/support/Home/other-resources/gis-dictionary/term/feature%20dataset

A “feature service” is basic a feature class that is available on the web.

http://support.esri.com/sitecore/content/support/Home/other-resources/gis-dictionary/term/feature%20service

The “feature server” is server serving the feature over the web.

http://support.esri.com/sitecore/content/support/Home/other-resources/gis-dictionary/term/feature%20server

There is also the “feature layer” which you have already seen is the data from the feature class rendered on a map.

http://support.esri.com/sitecore/content/support/Home/other-resources/gis-dictionary/term/feature%20layer

That is a lot of features. This diagram can help.

feature < feature class < feature dataset < feature service < feature server --> feature layer

In other words feature class is a collection of features. Feature dataset is a collection of feature class. Both feature class and dataset can be made into services which the feature server can provide to clients to display on their maps as feature layers.

Creating the feature service is long process, involving making the feature class using ArcMap or ArcDesktop, defining the feature service and publishing the feature service. The key step is making the feature class.

Defining the Feature Class

Feature Class Type

http://webhelp.esri.com/arcgisdesktop/9.2/index.cfm?TopicName=Feature_class_basics

The above reference is rather involved. The point is the first step in making the feature class is deciding on the feature class. There are 7 feature class types, but generally the feature class type is either Points, Multipoints, Lines, or Polygons. The feature class types of Annotation, Dimensions, and Multipatches are advance types for rendering maps or working in 3D.

The historical photo app in this tutorial would use a Point feature class type.

Feature Attributes

The next step is to specify feature attributes. These are the fields in the geodatabase table. The data type needs to specified. The data types are:

  • BLOB 
  • DATE 
  • DOUBLE 
  • FLOAT 
  • GUID – Global Universital ID. You always get this field.
  • LONG INTEGER
  • OBJECT ID – this is for the primaray key that is auto incremented. You always get this field.
  • SHORT INTEGER
  • TEXT – number of characters specified. Default is 50 characters.

http://webhelp.esri.com/arcgisdesktop/9.2/index.cfm?TopicName=Geodatabase%20field%20data%20types

Domains

Any data type can have restrictions. The are specified by the domain.

http://webhelp.esri.com/arcgisdesktop/9.2/index.cfm?TopicName=An_overview_of_attribute_domains

For example, integers can have a range domain that restricts allowable values from 1 to 10. A text data type can have a coded domain that restricts it to specific values, for example “cat”, “dog” and “mouse”.  Associating a domain to a field data type restricts the values that can be put into that field. If you have a categorical variable in your app, you will probably want to data type of the field in the geodatabase to be associated with a coded domain.

Specifying the Feature Class

Because ArcMap interacts well with Excel, to specify your feature class requirements to Don, you should create an excel file. The excel should have a single row with the field names. Then you specify data type by right click on the column of the field and selecting “Format Cell”. In the window that opens, select the “Number” tab. In the Number tab, select the data type that you want:

  • Number
  • Date
  • Time
  • Scientific
  • Text

If you desire that a field should have a domain, then specify the domain in the body of the email.

Search Widget Tutuorial

ESRI’s Search widget is a powerful user interface for searching locator services, maps and feature service layers. A description for the Search Widget is at:

https://developers.arcgis.com/javascript/3/jshelp/intro_search_widget.html

The API Reference for Search is at:

https://developers.arcgis.com/javascript/3/jsapi/search-amd.html

Search is loaded with default values that searches ESRI’s Locator, //geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer, and moves the map to the selected address. A good tutorial demonstrating the basic Search with its default value is at:

https://developers.arcgis.com/javascript/3/jssamples/search_basic.html

Try the tutorial and search for “Houghton, MI, USA”. Note that by just adding a div for the Search Widget in the DOM and creating a Search object specifying the Map and the div id, the app has fully functional search.

The Code

We can overwrite the default values to turn Search into a simple widget for generating events for passing strings.

<!DOCTYPE html>
<html dir="ltr">

<head>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
 <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
 <title>ArcGIS API for JavaScript | Basic Search</title>
 <link rel="stylesheet" href="https://js.arcgis.com/3.27/esri/themes/calcite/dijit/calcite.css">
 <link rel="stylesheet" href="https://js.arcgis.com/3.27/esri/themes/calcite/esri/esri.css">
 
<style>
   html,
   body,
   #map {
      height: 100%;
      width: 100%;
      margin: 0;
      padding: 0;
   }
   #search {
     display: block;
     position: absolute;
     z-index: 2;
     top: 20px;
     left: 74px;
    }
 </style>

 <script src="https://js.arcgis.com/3.27/"></script>
 <script>
   require([

     "esri/map",
     "esri/dijit/Search",
     "dojo/domReady!"

   ], function (Map, Search) {
   var map = new Map("map", {
     basemap: "gray",
     center: [-120.435, 46.159], // lon, lat
     zoom: 7
   });
   // Draws the search widget at <div id="search"></div>
   var search = new Search({
      // map: map // Comment out map defeats moving the map.
      // Overwrite the default sources and sets the place holder in the search textbox.
     sources: [{ placeholder: "Search story" }]
   }, "search");
   // search.startup(); // Comment out startup does not change behavior.
   // Event search-results fires whenever the search button is clicked.
   search.on('search-results', function(e){
      console.log("search-results received");
      // Retrieve the search textbox value either in the event or search object.
      console.log("e.value = "+e.value);
      console.log("search.value = "+ search.value);
   });
  });
 </script>
</head>

<body class="calcite">
   <div id="search"></div>
   <div id="map"></div>
</body>

</html>

You can view the webpage here or from resources/gis-programs/search-basic.html. To view the output of the program you will need to open the JavaScript console for the browser. There are several things to notice about the basic search. First that the Search does not really need “search.startup()” to run. Also that the map does not move to the search address after removing the Map specification from the Search constructor. In addition by overwriting the “sources” in the Search constructor with just the “placeholder” then Search does not access a locator.

Finally that Search will always send an event when the search button is clicked. The event can be got with:

   search.on('search-results', function(e){
      console.log("search-results received");
      // Retrieve the search textbox value either in the event or search object.
      console.log("e.value = "+e.value);
      console.log("search.value = "+ search.value);
   });

And the string in the search box can be recovered.

Copy the code and paste it into your favorite editor. Play with the code by uncommenting and commenting lines in the constructor.

XmlHttpRequest (XHR) Tutorial

Dojo has an AJAX request API accessed via the xhr object.  The documentation is at:

https://dojotoolkit.org/reference-guide/1.10/dojo/request/xhr.html

The API is for dojo/request/xhr is very similar to JQuery’s AJAX API.

The Code

We can use xhr object to make a GET to a PHP script.

<!DOCTYPE html>
<html>
<head>
   <title>get-stories</title>
   <script src="https://js.arcgis.com/3.27/"></script>

   <script>
   console.log('start!')
   require([

      "dojo/request/xhr",
      "dojo/domReady!"

    ], function (xhr) {

       var protocol = "http://";
       var searchPhpDomain = 'geospatialresearch.mtu.edu';
       var SearchStoryURL = protocol + searchPhpDomain + "/search_by_story.php";//?q=place&p=story

       sValue = "peterson";

       xhr(SearchStoryURL, {
           handleAs: "json",//"text",
           headers: { 'X-Requested-With': null },
           query: { q: sValue } //,p:'story'
      }).then(function (data) {
           console.log("In xhr callback.")
           var results = data.results;
           var jsonStr = JSON.stringify(results, null, 2);
           document.body.innerHTML = jsonStr; // Does not look pretty in the web page,
           // probably have to add HTML.
           console.log((jsonStr)); // Looks better in the console.
     }); // end xhr(SearchStoryURL, ...) callback.
   }); // end require callback
 </script>
</head>

<body>
    JSON
</body>

</html>

You can view the webpage here or from resources/gis-programs/get-stories.html. Basically, the webpage makes a request to

http://geospatialresearch.mtu.edu/search_by_story.php?q=peterson

which is a PHP script accessing the databases at geospatialresearch.mtu.edu for entries with “peterson”. The script returns a JSON. The JSON is just dumped into the body of the webpage. You can view a better formatting of the JSON in the JavaScript console for the browser.

The xhr object is called with the URL and options:

 sValue = "peterson";

 xhr(SearchStoryURL, {
       handleAs: "json",//"text",
       headers: { 'X-Requested-With': null },
       query: { q: sValue } //,p:'story'
 }).then(function (data) {

The handleAs option specifies how the content should be handled. In this case as a JSON. The headers option for adding custom headers to the request. The query option adds the query string that is added to the URL. Open the network tab for developer’s tools of the browser to view the complete URL with query string.

The callback for a successful response has only one argument, data. The program stringify the JSON then dumps it to the webpage and writes it to the console.

We can combine the the code from this tutorial with the code from the previous tutorial about Search Widget to use the Search Widget as input story searches.

<!DOCTYPE html>
<html dir="ltr">

<head>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
   <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
   <title>search-stories</title>
   <link rel="stylesheet" href="https://js.arcgis.com/3.27/esri/themes/calcite/dijit/calcite.css">
   <link rel="stylesheet" href="https://js.arcgis.com/3.27/esri/themes/calcite/esri/esri.css">
   <style>
     html,
     body {
        height: 100%;
        width: 100%;
        margin: 0;
        padding: 0;
      }
     #search {
       display: block;
       position: absolute;
       z-index: 2;
       top: 20px;
       left: 74px;
     }
 </style> 

 <script src="https://js.arcgis.com/3.27/"></script>

 <script>
   require([

      "esri/dijit/Search",
      "dojo/request/xhr",
      "dojo/dom",
      "dojo/domReady!"

    ], function ( Search, xhr, dojoDom) {
       var protocol = "http://";
       var searchPhpDomain = 'geospatialresearch.mtu.edu';
       var SearchStoryURL = protocol + searchPhpDomain + "/search_by_story.php";//?q=place&p=story

       var search = new Search({
            sources: [{ placeholder: "Search story" }]
       }, "search");
       search.on('search-results',function(e){
           console.log("search.value = "+ search.value);
           xhr(SearchStoryURL, {
               handleAs: "json",//"text",
               headers: { 'X-Requested-With': null },
               query: { q: search.value } //,p:'story'
           }).then(function (data) {
              console.log("In xhr callback.")
               var results = data.results;
               var jsonStr = JSON.stringify(results, null, 2);
               dojoDom.byId("json").innerHTML = jsonStr; 
               console.log((jsonStr)); 
           }); // end xhr(SearchStoryURL, ...) callback.
      });
   });
 </script>
</head>

<body class="calcite">
    <div id="search"></div>
    <div id="json"></<div>
</body>

</html>

You can view the webpage here or from resources/gis-programs/search-stories.html.  I’ll let you study the code on your own.

Note that the program uses Dojo’s dom object to search the DOM to find where to dump the JSON on the webpage. The documentation on dojo/dom is at

https://dojotoolkit.org/reference-guide/1.10/dojo/dom.html

Keweenaw Time Traveler (KeTT) Examples

The code examples in this section are simplifications of the KeTT Explorer app website. You can visit the Explorer app website at:

http://geospatialresearch.mtu.edu/kettexplorerapp/

The website has many features. The main feature of the website, making text “stories” including photos, was developed by students in this HCI class. Feel free to try the website. You can click on the purple discs to view the stories. Please do not make false stories on the map. In addition, users can search the databases and feature layers for addresses, people, places and stories.

The original website has multiply files and the index.html file which includes the bulk of the JavaScript code is 3000 lines long. I have made two simplifications. The KeTT Stories website  allows users to make and search stories. The index.html file is 2000 lines. The KeTT Maps website does not have the story functionality. Users can only change the base maps. The index.html file is 700 lines long.

KeTT Maps

This example goes through the interaction and program flow for loading and changing maps.

You can download the KeTT Maps code:

KETT-Maps.zip

Unzip the file and run the website locally by double clicking the index.html file in the KETT-Map/ directory. The website loads with default Calumet 1900 historic map. You can change the historic map by selecting the “Location” and then the “Year” in the top banner. There is an empty panel on the left that be closed by clicking the “<<” button. The slider along the left adjusts the transparency of historic map. Moving the the slider down allows base map to show through the historic map. You can select the base map to either “Historic Topo” or “Modern Satellite” from the drop down menu adjacent to the transparency slider.

Website Structure

The directory structure of the website:

  • KETT-Maps/
    • images/
      • top_buttons
        • help.png
        • home.png
      • nav-hide.png
      • nav-show.png
      • pointerMarker.png
    • js/
      • exploredatafile.js
    • index.html

The bulk of the code is in the index.html file. Besides the layout information in the body section of index.html, the head section contains styling rules and JavaScript code. This file is not the best example how to structure a website. Better would be to separate the styling and JavaScript code into separate files.

The image directory contains images for the user interaction widgets. In the top_buttons/ directory are the home and help buttons in the banner. In the top level image/ directory is the nav-hide and nav-show which are the “<<” the “>>” buttons for hiding and showing the left panel. The pointerMarker.png image is not used in the website. It is there for your use.

In the js/ directory is exploredatafile.js. This file does not contain any logic. It is a list of URL for the feature layers and historic maps. The JavaScript code in index.html uses these variables to designate the URL. In particular the “kht” variable, which is an array of objects specifying the historic maps with their id, name, year and map service.

The JavaScript code in index.html is complex because it is production ready covering many edge cases. I’ll not cover all the functionality of the JavaScript code and only outline the code for the interaction for changing historic maps.

Change Historic Map Interaction

The user selects the historic map from the “Location” and “Year” drop down menu in the banner. The interaction is complex because not all locations (i.e. towns) do not have the same years. If the user selects a new location that have the current year selection then the map can be switched, but if the current year does not exist for the new location then the user must also select the year before the map can be switch.

Because the new maps can be added, the options for selecting location and year can not be hardwired into the html code.

<div data-dojo-type="dijit/layout/ContentPane" id="header" data-dojo-props="region:'top'">
 <table border="0" cellpadding="0" cellspacing="0" class="bannerTable" >
 <tr >
...
     <td class="bannerTd">
        <label for="locationSelect" style="color:white;"><strong>Location:</strong></label>
        <select name="locationSelect" id="locationSelect" > </select>
     </td>

    <td class="bannerTd" style="text-align:left;">
       <label for="yearSelect" style="color:white;"><strong>Year:</strong></label>
       <select name="yearSelect" id="yearSelect" >
           <option>select a year</option>
       </select>
    </td>
...
 </tr>
 </table>
</div>

The JavaScript code must construct and add the options to the “locationSelect” and “yearSelect” select taps. When the DOM is ready the initial or default location and year, “pLocation” and “pYear” are set.

 /****************************************************************
 * registers function when DOM been resolved
 ***************************************************************/
 dojo.ready(function(){
    var pLocation = 'Calumet';//getUrlParameter('location');
    var pYear = '1900';//getUrlParameter('year');

    for (var i = 0; i < kht.Services.length; i++) {
       if (kht.Services[i].name == pLocation && kht.Services[i].year ==pYear && kht.Services[i].mapservice){
          validServiceConfig = kht.Services[i];
          break;
       }
    }
    if (validServiceConfig) {
       populateLocations();
    }
    else {alert('no valid param');}

    domStyle.set(dojo.byId("yearTopDiv"),'visibility','visible');
    domStyle.set(dojo.byId("yearUnderDiv"),'visibility','visible');
    domStyle.set(dojo.byId("tranSliderABC"),'visibility','visible');
    domStyle.set(dojo.byId("verticalSliderDiv"),'visibility','visible');
 });

After building “validServiceConfig”, it calls populateLocations() to populate the locationSelect options.

function populateLocations(){
    var locationOptionObj = document.getElementById('locationSelect');
    var opt = document.createElement('option');
    opt.value = "";
    opt.innerHTML = "select a location";
    locationOptionObj.appendChild(opt);

    for (var i = 0; i < kht.Services.length; i++) {
       if (locationItemExists(kht.Services[i].value)) continue;
       var opt = document.createElement('option');
       opt.value = kht.Services[i].value ;
       opt.innerHTML = kht.Services[i].name;
       locationOptionObj.appendChild(opt);
    }

    var selectIndex = 0;
    for (var i = 0; i < locationOptionObj.length; i++) {
       if (locationOptionObj.options[i].value == validServiceConfig.value){
          selectIndex = i;
          break;
       }
    }
    // to handle the passed location, year
    locationOptionObj.selectedIndex = selectIndex;
    if (validServiceConfig && selectIndex>0){
       resetYearSelectionList(validServiceConfig.value);
    }
} // end function populateLocations

After populating the locationSelect options and selecting the current year, the populateLocation() function calls resetYearSelectionList() function to populate the yearSelect options and set the year for the historic map.

function resetYearSelectionList(locationValue) {
    var yearOptionObj = document.getElementById('yearSelect');
    for(var i = yearOptionObj.options.length - 1 ; i >= 0 ; i--){
        yearOptionObj.remove(i);
    }

    var opt = document.createElement('option');
    opt.value = "";
    opt.innerHTML = "select a year";
    yearOptionObj.appendChild(opt);

    //town name with space
    var locationName = locationValue;
    for (var i = 0; i < kht.Services.length; i++) {
       if (Equals(kht.Services[i].value,locationValue)){
          locationName = kht.Services[i].name;
          break;
       }
    }

    var queryYearTask = new QueryTask(placeNameURL);
    var queryYear = new Query();
    queryYear.returnGeometry = false;
    queryYear.outFields = ['*'];//["objectid", "id", "region_nam", "years_csv", "years_color"];
    queryYear.where = "upper(region_nam) ='" + locationName.toUpperCase() + "'"; //Houghton'";
    queryYearTask.execute(queryYear, function (results) {
       var resultCount = results.features.length;
       var yearField = "years_cache";
       //this should be only one record for each town
       var index = 0;
       var featureAttributes = results.features[0].attributes;
       var yearItems = featureAttributes[yearField].split(",");
       var opt = document.createElement('option');
       for (var i = 0; i < yearItems.length; i++) {
          var opt = document.createElement('option');
          opt.value = yearItems[i];
          opt.innerHTML = yearItems[i];
          yearOptionObj.appendChild(opt);

         if (validServiceConfig){
            if (validServiceConfig.year == yearItems[i] ){
                index = i+1;
            }
         }
       }
      // to handle the passed location, year
      yearOptionObj.options[index].selected=true;
      if (validServiceConfig && index>0){
        var town_year = locationValue + "_" + yearOptionObj.options[index].value;
        for (var i = 0; i < kht.Services.length; i++) {
            if (town_year == kht.Services[i].id){
                if (kht.Services[i].name && kht.Services[i].year && kht.Services[i].mapservice){
                      validServiceConfig = kht.Services[i];
                      SwitchMainTiledMapService(validServiceConfig.mapservice);
                      break;
                }
            }
         }
      } 
    },function(err){
         alert(err);
   });
 } // end function resetYearSelectionList(locationValue)

After creating the option tag, “opt”, the function resetYearSelectionList(locationValue) builds the query, “queryYear”, to the place name map. The results of the query is a list of years for the locationValue, or the current location in locationSelect. The callback for

queryYearTask.execute(queryYear, function(results){
    ...
});

builds adds the year options to “opt”. It also checks if the current year is in the list of years for the town. If it is then it calls SwitchMainTiledMapService().

 /****************************************************************
 * switch the main tiled map service
 ***************************************************************/
 function SwitchMainTiledMapService(mapServiceUrl){
    // remove current layers
   var maplayer = map.getLayer("maplayer");
   if (maplayer) map.removeLayer(maplayer);

   //request made to tiled map service. If the requested year is not in the validserviceconfig.year list, fallback to dynamic below.
   if (kettAllInOneByYears.indexOf(validServiceConfig.year)>-1){
       mapServiceUrl = kettAllInOneByYearPattern.replace("YYYY",validServiceConfig.year);
       tiledMapServiceLayer = new ArcGISTiledMapServiceLayer(mapServiceUrl,{id:"maplayer",opacity:1});
   }
   else {
     // this must be ArcGISDynamicMapServiceLayer, otherwise the spyglass does not work
     // this is the fallback if there is not a tile cache built for the requested year
     tiledMapServiceLayer = new ArcGISDynamicMapServiceLayer(mapServiceUrl,{id:"maplayer",opacity:1});
   }

   map.addLayer(tiledMapServiceLayer);
   //map.setLevel(17);

  tiledMapServiceLayer.on("load",function(error){
  //tiledMapServiceLayer.on("update-end",function(error){
     if (app.MapCenterAt) {
       if (!app.townManualChanged && app.yearManualChanged){
         //the map should not re-center
       }
       else {
          map.centerAt(app.MapCenterAt);
       }
     }
     if (validServiceConfig){
        dojoDom.byId("yearTopDiv").innerHTML = validServiceConfig.name + " " + validServiceConfig.year;
     }

    domStyle.set(dojo.byId("swipeDiv"),'visibility','hidden'); // RLP need to keep "Click to share... text"

    if (!app.townManualChanged && app.yearManualChanged){
        //the map should not re-center
    }
    else {
       app.CenterAtTownCentroid(validServiceConfig.name);
    }

    app.townManualChanged = false;
    app.yearManualChanged = false;
   }); // end tiledMapServiceLayer.on("load",function(error)

   tiledMapServiceLayer.on("error",function(error){
     alert ("There is a problem with loading:" + mapservice);
   });

 } // end function SwitchMainTiledMapService(mapServiceUrl)

The function SwitchMainTiledMapService() removes the current map layer then build the URL for the new map. It then adds the new map layer on to the map. After the map is loaded it centers the maps if it should.

All the above happens when the website is initiated. For the user to change the historic map, the JavaScript code needs to set “onchange” listener for locationSelect.

 dojo.query('#locationSelect').onchange( function(evt) {
    if (evt.target.value){
       var locationValue = evt.target.value;
       app.MapCenterAt = null;
       app.townManualChanged = true;
       app.tiledMapChangeRecenterMap = true;
       resetYearSelectionList(locationValue);
    } else {
       //alert("invalid value!");
    }
    dojo.stopEvent(evt);
 });

The “onchange” listener calls resetYearSelectionList() function. The JavaScript also needs to register “onchange” listener for the yearSelect drop down menu.

 /****************************************************************
 * Reset the tiled map when the user chooses a year
 ***************************************************************/
 dojo.query('#yearSelect').onchange( function(evt) {
    if (evt.target.value){
       var locationOpt = document.getElementById('locationSelect') ;
       var year = evt.target;
       if (locationOpt.value.length>0 && year.value.length>0){
          var town_year = locationOpt.value + "_" + year.value;
          for (var i = 0; i < kht.Services.length; i++) {
             if (town_year == kht.Services[i].id){
                if (kht.Services[i].name && kht.Services[i].year && kht.Services[i].mapservice){
                   validServiceConfig = kht.Services[i];
                   app.yearManualChanged = true;
                   //alert(app.townManualChanged);
                   SwitchMainTiledMapService(kht.Services[i].mapservice);
                   break;
               }
            }
         }
      }
    } else {
       //alert("invalid value!");
    }
    dojo.stopEvent(evt);
 });

The yearSelect “onchange” listener calls SwitchMainTiledMapService() function.

KeTT Story Search

This example follows the interaction and program flow for searching stories.

You can download the KeTT Story Search code:

KETT-StorySearch.zip

Unzip the file and run the website locally by double clicking the index.html file in the KETT-Map/ directory. The website loads with default Calumet 1900 historic map. You can change the historic map by selecting the “Location” and then the “Year” in the top banner. There is a panel on the left which contains the Search widget textbox. You can perform a search for stories by entering a search terms in the textbox clicking the search button or entering return. The results will appear below categorized by People, Places, and Stories. Try entering “perterson” and two stories appear in the Stories categories. Clicking one of the stories will move the map to the story location and the story details appear below the tabs. You can also click on the People tab and click a button to move the map and display the details of the person.

In KETT-StorySearch code, I have commented out or removed creating stories and searches for buildings, people and places.

The layout of left pane is located near the bottom of index.html

<!-- leftPane: Searching textbox and results -->
    <div id="leftPane" data-dojo-type="dijit/layout/ContentPane" data-dojo-props="region:'left', splitter:false">
    <!-- Search widget -->
    <div id="storySearch" class="hidden"> </div>
    <br>
    <div id="messageDiv" style="height:50px;"> Search Address, People, Places, Stories or Images </div>

   <!-- Search Results -->
   <div data-dojo-type="dijit/layout/TabContainer" id="tabPaneContainer" doLayout="false">
       <div data-dojo-type="dijit/layout/ContentPane" title="People" id="PeoplePane">
            <p id="searchResultPeople" style="max-height: 100px;"> </p>
       </div>
       <div data-dojo-type="dijit/layout/ContentPane" title="Place" id="PlacePane">
           <p id="searchResultPlace" style="max-height: 100px;"> </p>
       </div>
       <div data-dojo-type="dijit/layout/ContentPane" title="Stories/images" id="StoryPane" selected="true">
          <p id="searchResultStory" style="max-height: 100px;"> </p>
       </div>
    </div>
    <br/>
    <div id="photoStoryHolderDiv">
        <p id="photoPlaceHolder"></p>
        <p id="searchResultDetails"> </p>
    </div>
 </div> <!-- End of leftPane -->

You can find the html code by searching for “leftPane” in index.html. The layout for the left pane uses “dijit/layout” for making the pane and tabs. The documentation for dijit/layout at

https://dojotoolkit.org/reference-guide/1.10/dijit/layout.html

The Search widget is located at

<div id="storySearch" class="hidden"> </div>

To find the JavaScript code making the Search widget, search on “storySearch”.

 var searchStory = new Search({
    sources: [{ placeholder: "Search story and imgaes" }]
 }, "storySearch");
 searchStory.on('search-results', function (e) {
    SearchStoryFunc(searchStory.value);
    searchStory.clear();
 });

The design pattern of the code is should be familiar.  The search object, searchStory, is constructed by over writing the “sources” option with a place holder. Then the code setups an event listener for “search-results”. The event listener is primarily defined in the “SearchStoryFunc” function:

 function SearchStoryFunc(sValue) {
    PrepareCleanForSearch();
    dojoDom.byId("messageDiv").innerHTML = "Searching....";
    xhr(SearchStoryURL, {
       handleAs: "json",//"text",
       headers: { 'X-Requested-With': null },
       query: { q: sValue } //,p:'story'
    }).then(function (data) {
       //alert(JSON.stringify(data.results[0].stories.length));
       var results = data.results;
       app.peopleResultJson = results.people;
       app.placeResultJson = results.places;
      // app.buildingResultJson = results.buildings;
      app.storyResultJson = results.stories;
      GetShortDescription();

      dijit.byId("tabPaneContainer").selectChild(registry.byId("StoryPane"));
      if ((app.peopleResultJson.length + app.placeResultJson.length + /* app.buildingResultJson.length + */ app.storyResultJson.length) > 0) {
           dojoDom.byId("messageDiv").innerHTML = "Select a result below";
       } else {
           dojoDom.byId("messageDiv").innerHTML = "No results found";
       }

   }, function (error) {
       dojoDom.byId("searchResultStory").innerHTML = error.message;
   });
 }

Also this code should be familiar. The function SearchStoryFunc makes an XHR (AJAX call) to SearchStoryURL with the string from searchStory. The response of the query to SearchStoryURL is a JSON. The structure of the JSON is

{
    people: [
       { 
          person: {
             objectid: 1
             ...
          }
       },
       ...       
       { 
          person: {
             objectid: 2
             ...
          }
       }
    ]
    places: [
       {
           place: {
              objectid: 11
              ...
           }
       }
    ]
    buildings: []
    stories: [
       {
            story: {
                objectid: 21
                ...
            }
       },
       {
            story: {
                objectid: 22
                ...
            }
       }
    ]
}

JSON response is composed of “people”, “places”, “buildings” and “stories” arrays. The building array is always empty because the SearchStoryURL  PHP script does not search the building table. The people array is a list of “person” objects, the places arrays is a list of “place” object, and the stories array is a list of “story” objects. Each object has an “objectid” besides other attributes specific to the object type. You can find the attributes by studying the output of the “search-stories.html”.

The callback for the xhr call splits up the response JSON into people, places and stories arrays and stores them into app.peopleResultJson, app.placesResultJson and the app.storiesResultJson. The “app” prefix is a hint that these variables are global. The function GetShortDescription function call fills in leftPane’s dijit/layout/TabContainer.

function GetShortDescription() {
    var resultHTML = "";
    for (var i = 0; i < app.peopleResultJson.length; i++) {
         var shortDesc = app.peopleResultJson[i].person.short_descr;
         var button4 = new Button({ label: shortDesc, value: i, class: "dynNavButton" });
         button4.startup();
         button4.placeAt(dojoDom.byId("searchResultPeople"));
         button4.on("click", function (event) { app.RetrievePersonDetails(this.value); });
     }

     resultHTML = "";
     for (var i = 0; i < app.placeResultJson.length; i++) {
         resultHTML += '<button class ="buttonToDetail" '
         resultHTML += ' onclick="app.RetrievePlaceDetails(' + i + ');">';
         resultHTML += app.placeResultJson[i].place.short_descr + "</button>";
      }
      dojoDom.byId("searchResultPlace").innerHTML = resultHTML;

     resultHTML = "";
     for (var i = 0; i < app.storyResultJson.length; i++) {
         if (app.storyResultJson[i].story.short_descr) {
              resultHTML += '<button class ="buttonToDetail" '
              resultHTML += ' onclick="app.RetrieveStoryDetails(' + i + ');">';
              resultHTML += app.storyResultJson[i].story.short_descr + "</button>";
         }
      }
     //alert(JSON.stringify(app.storyResultJson ));
     dojoDom.byId("searchResultStory").innerHTML = resultHTML;

    registry.byId("PeoplePane").set('title', "People(" + app.peopleResultJson.length + ")");
    registry.byId("PlacePane").set('title', "Places(" + app.placeResultJson.length + ")");
    // registry.byId("BldgPane").set('title', "Bldgs(" + app.buildingResultJson.length + ")"); // RLP Comments out
    registry.byId("StoryPane").set('title', "Stories(" + app.storyResultJson.length + ")");
 }

The HTML is created in a string. Note that each object is created as a button with the “onclick” attribute. For example the story object buttons:

 resultHTML += ' onclick="app.RetrieveStoryDetails(' + i + ');">';

 

app.RetrieveStoryDetails = function (index) {
   var story = app.storyResultJson[index].story;
   var aGeometry = new Point(story.x, story.y, new SpatialReference({ wkid: 102100 }));
   app.CenterAtPoint(story.x, story.y); // moves the map to the point

   var newPointMarker = new Graphic(aGeometry, storyPointSymbol);
   map.graphics.add(newPointMarker);

   // // query placename
   // RLP: All this does is write the place name into messageDiv
   var queryTaskPlaceName = new QueryTask(placeNameURL);
   var queryPlaceName = new Query();
   queryPlaceName.outFields = ["*"];
   queryPlaceName.geometry = aGeometry;
   queryPlaceName.returnGeometry = true;
   queryTaskPlaceName.execute(queryPlaceName, function (results) {
   var resultFLCount = results.features.length;
   if (resultFLCount > 0) {
       var resultFeature = results.features[0];
       var disTown = resultFeature.attributes['region_nam'];
       var disYear = story.mapyear;
       dojoDom.byId("messageDiv").innerHTML = disTown + ',' + disYear;
       ResetTileMapFromSearchResult(disTown, disYear, 'story');
   }
   }, function (error) {
       writeLog('RetrieveStoryDetails-Intersect_PlaceNameFS:' + error.message);
   });

   // show story image and detail
   StoryImageSidePane(story.objectid);

 } // end RetrieveStoryDetails

The function app.RetrieveStoryDetails moves the map to the story location and draws a marker at the location. A query is made to the placeNameURL feature service to see if there is “place name” associated with location and writes the place name into the messageDiv. The details of the story are created by StoryImageSidePane.

 function StoryImageSidePane(objectid) {
    // There should only ever be one result
    var queryStoryPt = new Query();
    queryStoryPt.returnGeometry = false;
    queryStoryPt.where = "objectid=" + objectid;
    queryStoryPt.outFields = ["*"];
    var queryTaskPlaceName = new QueryTask(StoryPointURL);
    queryTaskPlaceName.execute(queryStoryPt, function (results) {
       //alert(JSON.stringify(results));
       // Pull off attributes we're interested in
       var feature = results.features[0];
       var title = feature.attributes["title"];
       var author = feature.attributes["name"];
       var descr = feature.attributes["description"];
       var date = feature.attributes["userdate"] == null ? null : feature.attributes["userdate"];
       var gid = feature.attributes["globalid"];

       $("#searchResultDetails").html($("#existingPointPopup").html());

       // Set the title of the sidebar to be the point's title
       $("#cur-point-name").text(title);
 
       if (date != null) {
            $("#cur-point-date").text(date);
       } else {
           $("#cur-point-date").hide();
       }

      // Set the author of the point in the sidebar
      if (author.length > 0) {
           $("#cur-point-author").text("By " + author);
      } else { // If the point is anonymous, hide the author section
           $("#cur-point-author").hide();
      }

      // Set the description of the point in the sidebar
      if (descr.length > 0) {
          // Convert links in the description to actual links
          $("#cur-point-desc").text(descr).linkify();
      } else {
           // If no description, don't show the section
           $("#cur-point-desc-section").hide();
      }

      // Show the image attachments for the point
      let imageList = $("#cur-point-attachments");
      imageList.empty();

      app.storyPointLayer.queryAttachmentInfos(objectid, function (attachments) {
           synchronizeAttachmentPreviews(attachments);
           $("#point-attachments-section").show();
      }, function (error) {
           writeLog(error.message);
      }); 

  }, function (error) {
     writeLog(error.message);
  });
}

StoryImageSidePane uses the objectid to query the StoryPointURL feature service. It extracts the attributes from the query result and then constructs the HTML for the story details using JQuery, $. Finally, the attachments (images) are loaded in to the HTML.  The HTML for the story details is

<!-- Contents of the popup for an existing point -->
<div id="existingPointPopup" hidden>
   <div class="modal-header">
       <!--
       <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
       -->

      <h4 class="modal-title" id="cur-point-name">Point Name</h4>
      <h5 id="cur-point-date">1/1/1970</h5>
      <h5 id="cur-point-author">By John Doe</h5>
   </div>

   <section id="cur-point-desc-section">
      <p id="cur-point-desc">
         This is a point that has a description that is cool
      </p>
   </section>

   <section id="point-attachments-section" hidden>
      <ul id="cur-point-attachments">
      </ul>
   </section>
 </div> <!-- end existingPointPopup -->

Building your App

You should use KETT-SearchStory code as a basis for your coding so that you can avoid the complexity of setup and interacting with the historic maps, and searching for stories. You will replace the tabPaneContainer in the leftPane with the interaction you design for categorizing and interacting with the search results.  I hope that your code can be more modular then the code demonstrated in this example.

To make your app public you can add a sub-directory within your team’s www/ directory and link to the index.html in your team website.

KeTT Stories

This example includes the code for making stories. You can download the KETT Stories code:

KETT-Stories.zip

Unzip the file and run the website locally by double clicking the index.html file in the KETT-Stories/ directory. This web app adds the functionality of adding, viewing and searching stories on the map. The existing stories are on the purple discs on the map. You can view the story by clicking on the purple disc. Note that you must click near the center of the purple disc in order to select the story. The story content will appear in the left pane. Also you can search existing stories by entering a search text in the “Search Story” text box.

I’ll not describe the code in this website. Note that the JavaScript code does not use attachment editor to enter or display the feature attributes, rather it builds it own form for entering the attribute values and the left pane to display the value.

This example is provide for you to reference while developing your web app.

Complete KeTT Website

This is the complete KeTT website as of 1/18/2019.

kettexplorerappdev.zip