Showing posts with label Feature Layers. Show all posts
Showing posts with label Feature Layers. Show all posts

Sunday, 24 July 2016

Flashing a graphic on an ArcGIS API for JavaScript map

Hi people! It's been a while since I last posted, but I am definitely not falling off - just been a bit busy. Still not an excuse for not posting. Now that the apology is out of the way, let's get back to these posts, shall we?

Recently, a client requested us to make an application where he would be able to query points and zoom to them individually. OK, no problem. However, he had an additional requirement, he wanted the queried graphic to 'flash' after being zoomed to because it would be in the midst of other points and would need to be distinguishable from the rest. Though we asked the client to consider highlighting the point, he would have none of it and insisted on the flashing requirement. As my manager always says - the customer is always right, even if he is wrong. We therefore got to work on it and the result, however unflattering it may be, is what we will look at today.

Before we start, just in case someone is wondering what I mean by flashing , the GIF below should be of some assistance. The GIF is an example of a flashing graphic, derived from an implementation of the same from the browser based version of Operations Dashboard for ArcGIS.

Yes, that's what I mean by flashing

SO how do we go about flashing a graphic? A while back, this method would have helped us get there quick but sadly, the times they-are-a-changing. That said, the concept remains the same and this is what we shall focus on understanding. 

The method that we ended up using relies heavily on dojo.Animation - the foundation class for all Dojo animations. It provides several simple methods good for controlling your animation, such as play, pause, and stop. Several types of animation are made possible through this class but our main interest was in two of these methods - namely the fadeIn and the fadeOut methods. To use these functions, we included the dojo.fx module:


The fadeIn and fadeOut methods do exactly what you think they do - they make the graphic appear to be fading in and fading out. These are exactly the type of effects that will be used to make our graphic appear to be flashing. In the example in the jsfiddle below, the map loads a FeatureLayer on to it and at the click of the button on its top right hand corner, a random graphic flashes.




The following is the procedure that was followed:

After loading the map, a point FeatureLayer is added to it - no fancy properties need to be placed in the constructor. The bulk of the work, if not all of it, actually happens when one clicks the 'Flash a point' button.

Clicking the button begins the flashing process. In this application, only one point is flashed at a time therefore the graphic must be identified. Since we intend to flash random points in the layer, we first determine how many points there are in the layer using the following line:


Once we know how many points the layer contains, we can now apply a formula to make sure each time the flash button is clicked, a random feature within the layer is picked and flashed.


Once our random graphic has been identified, we can now get to the bit of flashing it. (Please note that in a more realistic case, the graphic to be flashed will be determined through a query that fits certain parameters. This is just an example). The next thing we do is add this graphic to the default graphics layer(map.graphics) or a graphics layer that you have designated.

The animateGraphic method/function takes in the graphic and applies the dojo methods that we talked about earlier. Both fadeIn and fadeOut methods have a standard set of properties for specifying animation settings, some of these include:

  • node - The domNode reference or string id of a node to apply the animation effects to. In our case, we use the .getDojoShape().getNode() graphics method to get the node by which our graphic is referenced by.
  • duration - how long the animation will run in milliseconds. In this case we chose to make both fadeIn and fadeOut animations last 0.7 seconds.
  • easing - A timing function applied to the animation effect. In our example, the fading is applied linearly.
As seen below, these properties are applied to both functions.



All that code only accounts for one fadeIn and fadeOut cycle. Hoowever, we'd like to have three of those in quick succession. What to do? Probably what Aretha Franklin talked about in the hook of this 1968 hit. Enter the dojo.fx module and the chain convenience function which merges the animations into a single animation, playing back each of the animations one right after the other.
In our example, we create an array of animations based on repeating animations A and B three times, chain that together and then play the animation - resulting in a graphic that flashes 3 times then disappears.


(Though our example ends there, it may be prudent to clear graphics once you are done flashing them).

Well then, I hope you have enjoyed getting to know how we implemented flashing of graphics in our application. Feel free to point out any ways our code can be made cleaner or faster.

Happy coding!!

Thursday, 16 June 2016

Using ArcGIS API for JavaScript Edit functionality without a map AND without the Attribute Inspector Widget

Remember the last post I made? Well, it turns out the client didn't want the use of the Attribute Inspector - she wanted a plain form for editing a feature layer's attributes!! What to do?
Turns out it's not that much different from what we did the last time. This post will describe how we went about it.


JS Bin on jsbin.com

Once again, the objective was to edit attributes of features from a Feature Layer without the confines of a map container.

A basic HTML form was created on a blank page where they could make changes to features without having to see them on a map - choosing instead to reference them on the basis of IDs which are part of their attributes. So what did we do different? Very few things actually.

For starters since we did not have the advantage of having the attribute inspector load up all our required fields, we created the html form for data input as shown below:



  
Choose Work ID:

As you can see, actual field inputs needed to be placed in. I did not place them in an organized manner in this example but I hope you get the general idea of what I was doing. One may notice that the first two inputs are actually dojo comboBoxes. These are the fields that had coded values as domains and thus required only predetermined values to be entered. We therefore had to populate the comboBoxes with values from the feature service. We did this by querying the feature service for coded domain values and loading them into a datastore that feeds the comboBox as shown below:

 //Now query to get distinct values for Project Type
 // query distinct use code values from parcels
 var queryTask = new QueryTask(url);


 var query = new Query();
 query.where = "PROJTYPE is not null";  // query for non-null values
 query.orderByFields = ["PROJTYPE"];    // sort it so we won't have to later
 query.returnGeometry = false;          // turn geometry off, required to be false when using returnDistinctValues
 query.returnDistinctValues = true;
 query.outFields = ["PROJTYPE"];


 queryTask.execute(query, function(results){
  var resultItems = [];
  var resultCount = results.features.length;
  
  var store2 = new Memory({data:[]}); 
    
  dijit.byId("uniqueValuesSelect").set('store',store2);  
  var data = array.map(results.features,lang.hitch(this,function(info,index){  
    var value = info.attributes.PROJTYPE; 
    console.log(info);       
   
    var dataItem = {  
   id:index,  
   name:value  
    };  
    return dataItem;  
  }));  
  store2 = new Memory({data:data});  
  dijit.byId("uniqueValuesSelect").set('store',store2);
 });
 
 //Now query to get distinct values for Work Status
 // query distinct use code values from parcels
 var queryTask2 = new QueryTask(url);


 var query2 = new Query();
 query2.where = "WORKSTATUS is not null";  // query for non-null values
 query2.orderByFields = ["WORKSTATUS"];    // sort it so we won't have to later
 query2.returnGeometry = false;          // turn geometry off, required to be false when using returnDistinctValues
 query2.returnDistinctValues = true;
 query2.outFields = ["WORKSTATUS"];

 //Now use query results to find out the coded domain values for the layer field

 queryTask2.execute(query2, function(results){
  var resultItems = [];
  var resultCount = results.features.length;
  
  var store3 = new Memory({data:[]}); 
    
  dijit.byId("uniqueValuesSelect2").set('store',store3);  
  var data = array.map(results.fields[0].domain.codedValues,lang.hitch(this,function(info,index){  
    var value = info.name; 
    console.log(info);       
   
    var dataItem = {  
   id:index,  
   name:value  
    };  
    return dataItem;  
  }));  
  store3 = new Memory({data:data});  
  dijit.byId("uniqueValuesSelect2").set('store',store3);
 });
   


Once this was done, everything else was pretty easy. On the select-change event of the dropdown menu, the feature service would be queried for the relevant work ID and the query results pointed to the relevant HTML inputs for display as shown below:

//Capture the select - change event of the HTML select element
 on(dom.byId("workID"),"change",function (evt){
 
  //Initialize query based on the selected value
  var selectQuery = new Query();

  selectQuery.where = "WORKID = " + evt.target.value;
  teamsFL.selectFeatures(selectQuery, FeatureLayer.SELECTION_NEW, function(features) {
    if (features.length > 0) {
   //store the current feature
   updateFeature = features[0];
   document.getElementById("uniqueValuesSelect").value = updateFeature.attributes.PROJTYPE;
   document.getElementById("uniqueValuesSelect2").value = updateFeature.attributes.WORKSTATUS;
   document.getElementById("CHARGECODE").value = updateFeature.attributes.CHARGECODE;
   document.getElementById("LOCATION").value = updateFeature.attributes.LOCATION;
   
   console.log(updateFeature.attributes.CHARGECODE);
    }
    else {
   
    }
  }); 
  
  
  
   if(i == 0){
   i++;
   
   //Add the Save button
   var saveButton = new Button({ label: "Save", "class": "saveButton"},domConstruct.create("div"));
   domConstruct.place(saveButton.domNode, "buttonious");
     
   //Add the functionality of the save button
     saveButton.on("click", function() {
     
    updateFeature.attributes.PROJTYPE = document.getElementById("uniqueValuesSelect").value;
    updateFeature.attributes.WORKSTATUS = document.getElementById("uniqueValuesSelect2").value;
    updateFeature.attributes.CHARGECODE = document.getElementById("CHARGECODE").value;
    updateFeature.attributes.LOCATION = document.getElementById("LOCATION").value;
    
    
     
    updateFeature.getLayer().applyEdits(null, [updateFeature], null, function (adds, updates, deletes) {
     alert("Updated feature successfully, OBJECTID: " + updates[0].objectId);
     document.getElementById("CHARGECODE").value = "";
     document.getElementById("LOCATION").value = "";
    }, function (err) {
     //when an error occurs
     alert("Apply Edits Failed: " + err.message);
    })
     });
   
   }else{
   console.log("already added attribute inspector");
   }
    
 });


Saving the changes on the feature service layer followed the same pattern as before, the only difference being that this time, specifications were made as to which input value belonged to which feature attribute as shown below:


updateFeature.attributes.PROJTYPE = document.getElementById("uniqueValuesSelect").value;
updateFeature.attributes.WORKSTATUS = document.getElementById("uniqueValuesSelect2").value;
updateFeature.attributes.CHARGECODE = document.getElementById("CHARGECODE").value;
updateFeature.attributes.LOCATION = document.getElementById("LOCATION").value;
    

That was pretty much it. Of course, the sample given here needs to be made more appealing (don't present such work to a client - dress it up with good formatting and CSS) but that was basically the guts of what our form solution entailed. I hope it was informative and helps someone some day.

Happy Coding!!!

Sunday, 12 June 2016

Using the ArcGIS API for JavaScript AttributeInspector widget without a map

Though I am not currently working as a GIS developer, I am frequently called upon to make custom applications using the ArcGIS API for JavaScript. Recently a colleague of mine and I were faced with the task of creating a form from which to make updates to a Feature Layer without actually loading a map on the interface. Usually, people use the Edit Widget to edit attributes and geometry of features from a Feature Layer but as the linked sample above shows, it is mostly done within the confines of a map container.
The clients we were working for did not want a map container but instead needed a form on a blank page where they could make changes to features without having to see them on a map - choosing instead to reference them on the basis of IDs which are part of their attributes. The following is what we implemented.

JS Bin on jsbin.com

Though you can have an idea of how we tackled the situation through the JSBin above, let's go through a bit of the JavaScript code:

  • After referencing the FeatureLayer as shown below, we initialized a variable to simulate the first loading of the attribute inspector:
       
//Feature Layer representing Capital Projects around Nairobi
var teamsFL = new FeatureLayer("http://services.arcgis.com/CmINIEzurW7Tagtl/arcgis/rest/services/Infrastructural_Alerts/FeatureServer/4", {
      outFields: ["*"]
    });

//Initialize i to show that this is the first time the script is being run
i = 0;
 
 
  • We then set up the process that would allow for querying of a feature from the layer based on the Work ID attribute provided from the dropdown menu. This was bound to the select-change event of the menu:
       
//Initialize query based on the selected value
var selectQuery = new Query();
selectQuery.where = "WORKID = " + evt.target.value;

teamsFL.selectFeatures(selectQuery, FeatureLayer.SELECTION_NEW, function(features) {
if (features.length > 0) {
   //store the current feature
   updateFeature = features[0];
}
else {}
}); 
 
 
  • A definition of the feature attributes that would be available for changing was then specified, along with the type of data field that they were:
       
       //Define the details that will be dislayed in the Attribute Inspector element
      var layerInfos = [
        {
          'featureLayer': teamsFL,
          'showAttachments': false,
          'showDeleteButton': false,
          'isEditable': true,
          'fieldInfos': [
            {'fieldName': 'PROJTYPE', 'isEditable': true},
            {'fieldName': 'WORKSTATUS', 'isEditable': true,"stringFieldOption": AttributeInspector.STRING_FIELD_OPTION_TEXTAREA},
            {'fieldName': 'CHARGECODE', 'isEditable': true,"stringFieldOption": AttributeInspector.STRING_FIELD_OPTION_TEXTAREA},
            {'fieldName': 'ACTSTART', 'isEditable': true},
            {'fieldName': 'LOCATION', 'isEditable': true,"stringFieldOption": AttributeInspector.STRING_FIELD_OPTION_TEXTAREA}
          ]
        }
      ];

 
  • Finally, the Attribute Inspector widget was initialized and its functionality applied i.e. the possibility to post edits to a Feature Service using the applyEdits method. The main posting happens within the on click function of the Save button as shown in the code below:
       
if(i == 0){
   i++;
   //Initialize Attribute Inspector
   attInspector = new AttributeInspector({
    layerInfos: layerInfos
   }, "mapDiv");
   
   //Add the Save button
   var saveButton = new Button({ label: "Save", "class": "saveButton"},domConstruct.create("div"));
     domConstruct.place(saveButton.domNode, attInspector.deleteBtn.domNode, "after");
    
   //Add the functionality of the save button
     saveButton.on("click", function() {
    updateFeature.getLayer().applyEdits(null, [updateFeature], null, function (adds, updates, deletes) {
     
     alert("Updated feature successfully, OBJECTID: " + updates[0].objectId);
    }, function (err) {
     //when an error occurs
     alert("Apply Edits Failed: " + err.message);
    })
     });
     
     
     attInspector.on("attribute-change", function(evt) {
    //store the updates to apply when the save button is clicked
    updateFeature.attributes[evt.fieldName] = evt.fieldValue;
     });
   
   }else{
   console.log("already added attribute inspector");
   }

 

PS: Please note that this sample uses dropdown menus to get the parameter to be used for querying. It is not a generic sample either.  Therefore, you may need to learn the concept well (at least what happens on the save button click event) and apply it to your situation.

For more information on how to utilize the editing capabilities without using a map, check out this and this Stack Overflow Post as well as the ArcGIS API for JavaScript API reference.

Happy Coding!!