├── PolylineAnimation.js ├── README.md ├── Widget.html ├── Widget.js ├── config.json ├── css └── style.css ├── images ├── i_draw_line.png ├── icon.png ├── w_addstart.png └── widget.svg ├── manifest.json ├── nls └── strings.js └── setting ├── Setting.html ├── Setting.js └── css └── style.css /PolylineAnimation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 1. Create an instance for each graphic to animate; pass in graphic, graphicsLayer, and duration (ms) to the constructor. 3 | * 2. Call animatePolyline() 4 | * Example: 5 | * var pla = new PolylineAnimation({ 6 | * graphic : gpcConnector, // The polyline graphic you want to animate from 0 - 100% 7 | * graphicsLayer : glCityConnectors, // The graphics layer to display the graphic on (you haven't added the graphic yet) 8 | * duration : 2500 // How long the animation should last, in milliseconds 9 | * }); 10 | * pla.animatePolyline(); 11 | */ 12 | define( 13 | [ 14 | "dojo/_base/declare", 15 | "dojo/_base/fx", 16 | "dojo/_base/lang", 17 | "esri/graphic", 18 | "esri/geometry/Point", 19 | "esri/geometry/Polyline" 20 | ], 21 | function( 22 | declare, 23 | fx, 24 | lang, 25 | Graphic, 26 | Point, 27 | Polyline 28 | ) { 29 | return declare(null, { 30 | graphic : null, 31 | graphicsLayer : null, 32 | originalGeometry: null, 33 | duration : null, 34 | pathInfos : null, 35 | 36 | constructor: function(parameters) { 37 | /** 38 | * graphic : the polyline graphic to animate 39 | * graphicsLayer : the graphics layer to add the polyline to before and while animating 40 | * duration : the duration of the animation, in milliseconds 41 | */ 42 | this.graphic = parameters.graphic; 43 | this.originalGeometry = parameters.graphic.geometry; 44 | this.graphicsLayer = parameters.graphicsLayer; 45 | this.duration = parameters.duration; 46 | this.pathInfos = []; 47 | }, 48 | 49 | /* Calculate the geometry for a given (initialized graphic) and a given percentage */ 50 | percentOfPolyline: function( percent ) { 51 | var nFraction = percent / 100.0; 52 | var plGeom = new Polyline( this.originalGeometry.spatialReference ); 53 | for ( var iPath = 0; iPath < this.pathInfos.length; iPath++ ) { 54 | var oPath = this.pathInfos[ iPath ]; 55 | var nRequestedPathLen = oPath.pathLength * ( nFraction ); 56 | var nCurrentPathLen = 0; // Length of path-being-constructed 57 | var aryPathPts = []; 58 | 59 | for ( var iSegment = 0; iSegment < oPath.segmentInfos.length; iSegment++ ) { 60 | var oSegment = oPath.segmentInfos[ iSegment ]; 61 | 62 | var ptSegmentStart = oSegment.startPoint; 63 | var ptSegmentEnd = oSegment.endPoint; 64 | var nCurrentSegmentLen = oSegment.segmentLength; 65 | 66 | if ( iSegment == 0 ) aryPathPts.push( oSegment.startPoint ); 67 | 68 | // If this segment won't complete the requested percentage of the total path, 69 | // add the whole segment 70 | if ( nCurrentPathLen + nCurrentSegmentLen < nRequestedPathLen ) { 71 | aryPathPts.push( ptSegmentEnd ); 72 | nCurrentPathLen += nCurrentSegmentLen; 73 | } 74 | // If this segment will complete or surpass the requested percentage of the total line, 75 | // it's the last segment; calculate the proper percentage to add 76 | else { 77 | var nPathLenStillNeeded = nRequestedPathLen - nCurrentPathLen; 78 | var nPctOfThisPathNeeded = nPathLenStillNeeded / nCurrentSegmentLen; 79 | var nDeltaX = (ptSegmentEnd.x - ptSegmentStart.x) * nPctOfThisPathNeeded; 80 | var nDeltaY = (ptSegmentEnd.y - ptSegmentStart.y) * nPctOfThisPathNeeded; 81 | var ptNewEnd = new Point(ptSegmentStart.x + nDeltaX, ptSegmentStart.y + nDeltaY, this.originalGeometry.spatialReference); 82 | aryPathPts.push( ptNewEnd ); 83 | 84 | // And exit the loop 85 | break; 86 | } 87 | } 88 | plGeom.addPath( aryPathPts ); 89 | } 90 | return plGeom; 91 | }, 92 | 93 | // Do necessary, initial calculations about the polyline's paths and segments 94 | initPolyline: function() { 95 | this.graphic.visible = false; 96 | this.graphicsLayer.add(this.graphic); 97 | // Make a record of the original, full geometry 98 | var pln = this.originalGeometry; 99 | // Get info about the various paths and their lengths 100 | for ( var iPath = 0; iPath < pln.paths.length; iPath++ ) { 101 | var aryPath = pln.paths[ iPath ]; 102 | var arySegmentsForPath = []; 103 | var nPathLen = 0; 104 | 105 | // Store each pair of points in the path, plus the distance between them 106 | for ( var iPathPt = 1; iPathPt < aryPath.length; iPathPt++ ) { 107 | var ptStart = new Point( aryPath[ iPathPt - 1 ][ 0 ], aryPath[ iPathPt - 1 ][ 1 ], pln.spatialReference ); 108 | var ptEnd = new Point( aryPath[ iPathPt ][ 0 ], aryPath[ iPathPt ] [ 1 ], pln.spatialReference ); 109 | 110 | // Figure out distance between start/end point using pythagorean theorem (this needs improvement) 111 | var nSegmentLen = Math.sqrt( Math.pow( ptEnd.x - ptStart.x, 2 ) + Math.pow( ptEnd.y - ptStart.y, 2 ) ); 112 | 113 | arySegmentsForPath.push( { 114 | "startPoint" : ptStart, 115 | "endPoint" : ptEnd, 116 | "segmentLength" : nSegmentLen 117 | } ); 118 | nPathLen += nSegmentLen; 119 | } 120 | 121 | this.pathInfos.push( { 122 | "segmentInfos" : arySegmentsForPath, 123 | "pathLength" : nPathLen 124 | } ); 125 | } 126 | }, 127 | 128 | updatePolyline: function(percent){ 129 | var geometry = this.percentOfPolyline(percent); 130 | this.graphic.setGeometry(geometry); 131 | }, 132 | 133 | animatePolyline: function(){ 134 | fx.animateProperty({ 135 | node: dojo.create('div'), 136 | properties:{ 137 | along:{ 138 | start:0, 139 | end:100 140 | } 141 | }, 142 | duration: this.duration, 143 | beforeBegin: this.initPolyline(), 144 | onAnimate: lang.hitch(this, function(values){ 145 | // values assumes to be in pixels so we need to remove px from the end... 146 | var percent = parseFloat(values.along.replace(/px/,'')); 147 | this.updatePolyline(percent); 148 | if (!this.graphic.visible) this.graphic.visible = true; 149 | }), 150 | onEnd: lang.hitch(this, function() { 151 | this.graphic.setGeometry(this.originalGeometry); 152 | }) 153 | }).play(); 154 | } 155 | 156 | }); 157 | } 158 | ); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # closest-facility-wab-js 2 | Closest Facility widget for Esri JavaScript Web Application Builder 3 | 4 | Description: 5 | This widget uses the Network Analyst Closest Facility service to find routes from given events to facilities or locations of interest. The user supplies event locations, an optional barrier, the number of closest facilities to find, and a maximum travel time. 6 | The resulting route symbology is fully configurable, and is drawn by a custom animation. 7 | 8 | This is a deployable widget for Esri's Web AppBuilder. It demonstrates how to use the Network Analyst Closest Facility solver. 9 | 10 | To use it, download the .zip archive to your local disk and extract the inner "closest-facility-wab-js-master" folder. Next, rename the closest-facility-wab-js-master folder to "ClosestFacility". Then follow these directions. Once installed as a widget under your stemapp, you'll need to use the Web AppBuilder to create a new web app. When you configure the app's widgets, you should see the Closest Facility Widget appear in the list. It includes a configuration page in case you want to use your own facility dataset, change the result animation duration, etc. 11 | 12 | Configuration: 13 | This can be configured inside the Web Application Builder. Here are the configurable parameters: 14 | * Closest Facility service - the closest facility network analyst solver service 15 | * Facilities feature service - a feature service with any route facilities you want to use with the solver 16 | * Route result order-by attribute - the name of the route attribute that contains the result rank assigned by the solver 17 | * Route renderer attribute - the attribute the unique-value renderer should use when drawing the results 18 | * Route animation duration - how long (in thousands of a second) the route-line-drawing animation should last 19 | 20 | More parameters are configurable by editing config.json directly. 21 | 22 | By the way, if you want to animate polyline features in your own apps or widgets, feel free to grab and use the included PolylineAnimation.js file. I made it modular so it could be used more easily. 23 | -------------------------------------------------------------------------------- /Widget.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | 30 | 32 | 33 | 34 | 35 | 36 | 45 | 46 | 47 | 48 | 52 | 55 | 56 | 57 |
Navigation/Drawing Tools: 6 | 8 | 10 |
# of facilities to find: 16 | 22 |
27 | 28 | 29 |
Result Rank Symbols: 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
49 | 50 |
51 |
53 | 54 |
58 | 59 |
-------------------------------------------------------------------------------- /Widget.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dojo/_base/lang', 4 | 'dojo/on', 5 | 'dojo/dom', 6 | 'dijit/registry', 7 | 'dojo/dom-construct', 8 | 'dojo/query', 9 | 'dojox/widget/Standby', 10 | 'jimu/BaseWidget', 11 | 'esri/config', 12 | 'esri/Color', 13 | 'esri/layers/GraphicsLayer', 14 | 'esri/layers/FeatureLayer', 15 | 'esri/renderers/UniqueValueRenderer', 16 | 'esri/graphicsUtils', 17 | 'esri/toolbars/draw', 18 | 'esri/graphic', 19 | 'esri/symbols/SimpleMarkerSymbol', 20 | 'esri/symbols/SimpleLineSymbol', 21 | 'esri/tasks/FeatureSet', 22 | 'esri/tasks/ClosestFacilityTask', 23 | 'esri/tasks/ClosestFacilityParameters', 24 | 'esri/tasks/ClosestFacilitySolveResult', 25 | 'esri/tasks/NATypes', 26 | './widgets/ClosestFacility/PolylineAnimation.js' 27 | ], 28 | function(declare, lang, on, dom, registry, domConstruct, query, 29 | Standby, 30 | BaseWidget, 31 | esriConfig, 32 | Color, 33 | GraphicsLayer, FeatureLayer, 34 | UniqueValueRenderer, 35 | graphicsUtils, Draw, 36 | Graphic, 37 | SimpleMarkerSymbol, SimpleLineSymbol, 38 | FeatureSet, 39 | ClosestFacilityTask, ClosestFacilityParameters, ClosestFacilitySolveResult, NATypes, 40 | PolylineAnimation) { 41 | 42 | /* require(["/WAB_CF/widgets/ClosestFacility/PolylineAnimation.js"], function() { 43 | console.log("Load animation class"); 44 | }); */ 45 | var m_lyrResultRoutes, m_lyrAllFacilities, m_lyrEvents, m_lyrBarriers; 46 | var m_drawToolbar; 47 | 48 | var m_aryResultSymbolInfos; // Symbols for ranked results 49 | 50 | // Event handlers needing later removal 51 | var m_zoomToFacilities, m_clickDrawEvent, m_clickDrawBarrier, 52 | m_clickSolve, m_clickClear, m_changeFacilitiesCount, 53 | m_chkLimitTravelTime, m_numMaxTravelTime; 54 | // Closest Facility solver objects 55 | var m_closestFacilityTask; 56 | // Busy indicator handle 57 | var m_busyIndicator; 58 | 59 | //To create a widget, you need to derive from BaseWidget. 60 | return declare([BaseWidget], { 61 | // Custom widget code goes here 62 | 63 | baseClass: 'jimu-widget-customwidget' 64 | 65 | //this property is set by the framework when widget is loaded. 66 | ,name: 'ClosestFacilityWidget' 67 | 68 | //methods to facilitate communication with app container: 69 | 70 | ,postCreate: function() { 71 | this.inherited(arguments); 72 | console.log('postCreate'); 73 | 74 | m_lyrAllFacilities = new FeatureLayer( 75 | this.config.facilities.url, {"mode":FeatureLayer.MODE_SNAPSHOT, "outFields":["*"]}); 76 | m_zoomToFacilities = m_lyrAllFacilities.on('update-end', this.zoomToFacilities); 77 | 78 | m_lyrResultRoutes = new GraphicsLayer(); 79 | m_lyrEvents = new GraphicsLayer(); 80 | m_lyrBarriers = new GraphicsLayer(); 81 | 82 | var slsDefault = new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([32, 32, 32]), 2); 83 | var resultRenderer = new UniqueValueRenderer(slsDefault, this.config.symbology.routeRenderer.field1); 84 | for (var i = 0; i < this.config.symbology.routeRenderer.uniqueValueInfos.length; i++) { 85 | var info = this.config.symbology.routeRenderer.uniqueValueInfos[i]; 86 | var sls = new SimpleLineSymbol(info.style, info.sym.color, info.sym.width); 87 | resultRenderer.addValue(info.value, sls); 88 | } 89 | m_lyrResultRoutes.setRenderer(resultRenderer); 90 | 91 | m_drawToolbar = new Draw(this.map); 92 | var sms = new SimpleMarkerSymbol(this.config.symbology.eventSymbol); 93 | m_drawToolbar.setMarkerSymbol(sms); 94 | var sls = new SimpleLineSymbol(this.config.symbology.barrierSymbol); 95 | m_drawToolbar.setLineSymbol(sls); 96 | m_drawToolbar.on("draw-complete", lang.hitch(this, this.onDrawEvent)); 97 | 98 | m_closestFacilityTask = new ClosestFacilityTask(this.config.closestFacilitySvc.url); 99 | } 100 | 101 | ,startup: function() { 102 | this.inherited(arguments); 103 | console.log('startup'); 104 | 105 | // Add ranks and colors from config 106 | var rankNumbers = dom.byId("trRankNumbers"); 107 | var rankColors = dom.byId("trRankColors"); 108 | for (var i = 0; i < this.config.symbology.routeRenderer.uniqueValueInfos.length; i++) { 109 | var info = this.config.symbology.routeRenderer.uniqueValueInfos[i]; 110 | var className = this.getRankSymbolDomClassName(info.value); 111 | var aryColor = info.sym.color; 112 | 113 | var tdRankNumber = '' + info.value + ''; 115 | domConstruct.place(tdRankNumber, rankNumbers); 116 | 117 | var tdRankColor = ' ' 121 | domConstruct.place(tdRankColor, rankColors); 122 | } 123 | 124 | // Create busy indicator 125 | m_busyIndicator = new Standby({target: "busyIndicator"}); 126 | document.body.appendChild(m_busyIndicator.domNode); 127 | m_busyIndicator.startup(); 128 | } 129 | 130 | ,onOpen: function(){ 131 | console.log('onOpen'); 132 | 133 | this.map.addLayer(m_lyrAllFacilities); 134 | this.map.addLayer(m_lyrResultRoutes); 135 | this.map.addLayer(m_lyrBarriers); 136 | this.map.addLayer(m_lyrEvents); 137 | 138 | m_clickDrawEvent = on(dom.byId("btnDrawEvent"), "click", this.onClickDrawEvent); 139 | m_clickDrawBarrier = on(dom.byId("btnDrawBarrier"), "click", this.onClickDrawBarrier); 140 | m_clickSolve = on(dom.byId("btnSolve"), "click", lang.hitch(this, this.onClickSolve)); 141 | m_clickClear = on(dom.byId("btnClear"), "click", lang.hitch(this, this.onClickClear)); 142 | m_chkLimitTravelTime = on(dom.byId("chkLimitTravelTime"), "change", lang.hitch(this, this.onCheckLimitTravelTime)); 143 | m_numMaxTravelTime = on(dom.byId("numMaxTravelTime"), "change", lang.hitch(this, this.onChangeMaxTravelTime)); 144 | 145 | var numFacilities = dom.byId("numFacilities"); 146 | m_changeFacilitiesCount = on(numFacilities, "change", lang.hitch(this, this.onChangeFacilitiesCount)); 147 | on.emit(numFacilities, "change", { bubbles: true, cancelable: true }); 148 | } 149 | 150 | ,onClose: function(){ 151 | console.log('onClose'); 152 | 153 | this.map.removeLayer(m_lyrBarriers); 154 | this.map.removeLayer(m_lyrEvents); 155 | this.map.removeLayer(m_lyrAllFacilities); 156 | this.map.removeLayer(m_lyrResultRoutes); 157 | 158 | m_clickDrawEvent.remove(); 159 | m_clickDrawBarrier.remove(); 160 | m_clickSolve.remove(); 161 | m_clickClear.remove(); 162 | m_changeFacilitiesCount.remove(); 163 | m_chkLimitTravelTime.remove(); 164 | m_numMaxTravelTime.remove(); 165 | } 166 | 167 | // onMinimize: function(){ 168 | // console.log('onMinimize'); 169 | // }, 170 | 171 | // onMaximize: function(){ 172 | // console.log('onMaximize'); 173 | // }, 174 | 175 | // onSignIn: function(credential){ 176 | // /* jshint unused:false*/ 177 | // console.log('onSignIn'); 178 | // }, 179 | 180 | // onSignOut: function(){ 181 | // console.log('onSignOut'); 182 | // } 183 | 184 | // onPositionChange: function(){ 185 | // console.log('onPositionChange'); 186 | // }, 187 | 188 | // resize: function(){ 189 | // console.log('resize'); 190 | // } 191 | 192 | //methods to communication between widgets: 193 | 194 | // Other methods 195 | ,zoomToFacilities: function(event) { 196 | var extent = graphicsUtils.graphicsExtent(event.target.graphics); 197 | event.target.getMap().setExtent(extent); 198 | m_zoomToFacilities.remove(); 199 | } 200 | /* ,zoomToResults: function(lyrResults) { 201 | var extent = graphicsUtils.graphicsExtent(lyrResults.graphics); 202 | this.map.setExtent(extent); 203 | } */ 204 | 205 | ,onClickDrawEvent: function() { 206 | console.log("Draw Event Click"); 207 | m_drawToolbar.activate(Draw.POINT); 208 | } 209 | ,onClickDrawBarrier: function() { 210 | console.log("Draw Barrier"); 211 | m_drawToolbar.activate(Draw.POLYLINE); 212 | } 213 | 214 | ,onDrawEvent: function(event) { 215 | console.log("Draw Event Complete"); 216 | m_drawToolbar.deactivate(); 217 | 218 | var geom = event.geometry; 219 | if (event.geometry.type === "point") { 220 | var symbol = event.target.markerSymbol; 221 | var graphic = new Graphic(geom, symbol); 222 | m_lyrEvents.add(graphic); 223 | this.checkSolveEnabledState(); 224 | } else if (event.geometry.type === "polyline") { 225 | var symbol = event.target.lineSymbol; 226 | var graphic = new Graphic(geom, symbol); 227 | m_lyrBarriers.add(graphic); 228 | } 229 | 230 | } 231 | 232 | ,onClickSolve: function() { 233 | console.log("Solve"); 234 | var params = new ClosestFacilityParameters(); 235 | 236 | var facilities = this.fs4gl(m_lyrAllFacilities); 237 | params.facilities = facilities; 238 | 239 | var events = this.fs4gl(m_lyrEvents); 240 | params.incidents = events; 241 | 242 | var barriers = this.fs4gl(m_lyrBarriers); 243 | params.polylineBarriers = barriers; 244 | 245 | params.defaultCutoff = (dom.byId("chkLimitTravelTime").checked 246 | ? dom.byId("numMaxTravelTime").value 247 | : Number.MAX_VALUE ); 248 | params.defaultTargetFacilityCount = numFacilities.value; 249 | params.outSpatialReference = this.map.spatialReference; 250 | params.outputLines = NATypes.OutputLine.TRUE_SHAPE; 251 | params.returnFacilities = false; 252 | 253 | m_busyIndicator.show(); 254 | dom.byId("btnSolve").disabled = "disabled"; 255 | 256 | m_closestFacilityTask.solve(params, 257 | lang.hitch(this, this.onSolveSucceed), 258 | function(err) { 259 | console.log("Solve Error"); 260 | m_busyIndicator.hide(); 261 | dom.byId("btnSolve").disabled = ""; 262 | alert(err.message + ": " + err.details[0]); 263 | }); 264 | } 265 | 266 | ,onSolveSucceed: function(result) { 267 | console.log("Solve Callback"); 268 | m_busyIndicator.hide(); 269 | dom.byId("btnSolve").disabled = ""; 270 | 271 | var routes = result.routes; 272 | routes.sort(lang.hitch(this, function(g1, g2) { 273 | var rank1 = g1.attributes[this.config.symbology.routeZOrderAttrName]; 274 | var rank2 = g2.attributes[this.config.symbology.routeZOrderAttrName]; 275 | // Reverse sort 276 | return rank2 - rank1; 277 | })); 278 | m_lyrResultRoutes.clear(); 279 | for (var i = 0; i < routes.length; i++) { 280 | var g = routes[i]; 281 | // Set animation here? 282 | var pla = new PolylineAnimation({ 283 | graphic : g, 284 | graphicsLayer : m_lyrResultRoutes, 285 | duration : this.config.symbology.animateRoutesDuration 286 | }); 287 | pla.animatePolyline(); 288 | } 289 | // Zoom to results? 290 | // graphicsUtil.graphicsExtent() not working properly 291 | /* if (dom.byId("chkZoomToResults").checked) 292 | this.zoomToResults(m_lyrResultRoutes); */ 293 | } 294 | 295 | ,onClickClear: function() { 296 | console.log("Clear"); 297 | m_lyrEvents.clear(); 298 | m_lyrBarriers.clear(); 299 | m_lyrResultRoutes.clear(); 300 | this.checkSolveEnabledState(); 301 | } 302 | 303 | ,onChangeFacilitiesCount: function(event) { 304 | console.log("Change Facilities Count: " + event.currentTarget.value); 305 | var count = event.currentTarget.value; 306 | for (var i = 0; i < this.config.symbology.routeRenderer.uniqueValueInfos.length; i++) { 307 | var className = this.getRankSymbolDomClassName(i+1); 308 | if ( i+1 <= count) 309 | query("." + className).style("visibility", "visible"); 310 | else 311 | query("." + className).style("visibility", "hidden"); 312 | } 313 | } 314 | 315 | ,getRankSymbolDomClassName: function(rank) { 316 | return "rank" + rank; 317 | } 318 | 319 | ,checkSolveEnabledState: function() { 320 | dom.byId("btnSolve").disabled = (m_lyrEvents.graphics.length > 0 ? "" : "disabled"); 321 | } 322 | ,onCheckLimitTravelTime: function(event) { 323 | dom.byId("numMaxTravelTime").disabled = (event.target.checked ? "" : "disabled"); 324 | } 325 | ,onChangeMaxTravelTime: function(event) { 326 | console.log("Change Max Travel Time"); 327 | // Check for valid number 328 | if (!this.isPositiveInt(event.target.value)) 329 | event.target.value = event.target.oldValue; 330 | // Update old value 331 | else 332 | event.target.oldValue = event.target.value; 333 | } 334 | ,isPositiveInt: function(str) { 335 | // Taken from StackOverflow post, http://stackoverflow.com/questions/10834796/validate-that-a-string-is-a-positive-integer 336 | var n = ~~Number(str); 337 | return String(n) === str && n >= 0; 338 | } 339 | 340 | ,fs4gl: function( lyrGraphics ) { 341 | // Attributes are unused and seem to confuse the solver. Remove attributes here before solving. 342 | // http://desktop.arcgis.com/en/desktop/latest/guide-books/extensions/network-analyst/closest-facility.htm#ESRI_SECTION2_DA68E1A676A44CB58F38FA5DE0829F2B 343 | // http://desktop.arcgis.com/en/desktop/latest/guide-books/extensions/network-analyst/closest-facility.htm#ESRI_SECTION2_644000BCF05944929685C3FE6EFD549F 344 | var fs = new FeatureSet(); 345 | var ga = []; 346 | for (var i = 0; i < lyrGraphics.graphics.length; i++) { 347 | var g = new Graphic(lyrGraphics.graphics[i].geometry); 348 | // Add ObjectID if the original layer has an ID field 349 | if (lyrGraphics.objectIdField) { 350 | g.setAttributes({"ObjectID":lyrGraphics.graphics[i].attributes[lyrGraphics.objectIdField]}); 351 | } 352 | ga.push(g); 353 | } 354 | fs.features = ga; 355 | // fs.features = lyrGraphics.graphics; 356 | return fs; 357 | } 358 | }); 359 | }); -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "facilities": { 3 | "url" : "http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Louisville/LOJIC_PublicSafety_Louisville/MapServer/3" 4 | }, 5 | "closestFacilitySvc": { 6 | "url" : "http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Network/USA/NAServer/Closest%20Facility" 7 | }, 8 | "symbology" : { 9 | "routeZOrderAttrName" : "FacilityRank", 10 | "routeRenderer" : { 11 | "type" : "uniqueValue", 12 | "field1": "FacilityRank", 13 | "defaultSymbol" : { 14 | "color": [0,0,0,255], 15 | "style": "esriSLSSolid", 16 | "width": 2 17 | }, 18 | "uniqueValueInfos": [ 19 | { 20 | "value" : 1, "type" : "esriSLS", "style" : "esriSLSSolid", 21 | "sym" : {"color" : [26,150,65,255], "width" : 2} 22 | }, 23 | { 24 | "value" : 2, "type" : "esriSLS", "style" : "esriSLSSolid", 25 | "sym" : {"color" : [166,217,106,255], "width" : 5} 26 | }, 27 | { 28 | "value" : 3, "type" : "esriSLS", "style" : "esriSLSSolid", 29 | "sym" : {"color" : [253,174,97,255], "width" : 7} 30 | }, 31 | { 32 | "value" : 4, "type" : "esriSLS", "style" : "esriSLSSolid", 33 | "sym" : {"color" : [215,25,28,255], "width" : 9} 34 | } 35 | ] 36 | }, 37 | "barrierSymbol" : {"type" : "esriSLS", "style" : "esriSLSDashDot", "color" : [170,0,0,255], "width" : 3}, 38 | "eventSymbol" : {"size" : 12, "type" : "esriSMS", "style" : "esriSMSCircle", "color" : [0,204,0,255], 39 | "outline" : {"color" : [0,0,0,255], "width" : 2} 40 | }, 41 | "animateRoutesDuration" : 4000 42 | } 43 | } -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | .col1 { 2 | margin-right: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /images/i_draw_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdeaton/closest-facility-wab-js/9b5f1351de0b2b692f791da40fe03e878b698d9b/images/i_draw_line.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdeaton/closest-facility-wab-js/9b5f1351de0b2b692f791da40fe03e878b698d9b/images/icon.png -------------------------------------------------------------------------------- /images/w_addstart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdeaton/closest-facility-wab-js/9b5f1351de0b2b692f791da40fe03e878b698d9b/images/w_addstart.png -------------------------------------------------------------------------------- /images/widget.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 27 | Off-Page Connector 29 | Exit to or entry from a page. (IBM) 31 | 35 | 36 | 38 | Connector 40 | Exit to or entry from another part of chart. 42 | 48 | 49 | 50 | 69 | 71 | 72 | 74 | image/svg+xml 75 | 77 | 78 | 79 | 80 | 81 | 86 | 95 | 104 | 113 | 122 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ClosestFacility", 3 | "2D": true, 4 | "3D": false, 5 | "platform": "HTML", 6 | "version": "1.3", 7 | "wabVersion": "1.3", 8 | "author": "Esri Applications Prototype Lab", 9 | "description": "Custom Facilities Network Analysis", 10 | "copyright": "", 11 | "license": "http://www.apache.org/licenses/LICENSE-2.0", 12 | "properties": { 13 | "inPanel":true, 14 | "hasLocale": false, 15 | "hasStyle":false, 16 | "hasConfig":true, 17 | "hasUIFile":true, 18 | "hasSettingPage":true, 19 | "hasSettingUIFile":true, 20 | "hasSettingLocale":false, 21 | "hasSettingStyle":true, 22 | "IsController":false 23 | } 24 | } -------------------------------------------------------------------------------- /nls/strings.js: -------------------------------------------------------------------------------- 1 | define({ 2 | root:({ 3 | 4 | }) 5 | }); 6 | -------------------------------------------------------------------------------- /setting/Setting.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
-------------------------------------------------------------------------------- /setting/Setting.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////// 2 | // Copyright © 2014 Esri. All Rights Reserved. 3 | // 4 | // Licensed under the Apache License Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | /////////////////////////////////////////////////////////////////////////// 16 | 17 | define([ 18 | 'dojo/_base/declare', 19 | 'dojo/_base/lang', 20 | 'dojo/on', 21 | 'dojo/dom', 22 | 'jimu/BaseWidgetSetting' 23 | ], 24 | function(declare, lang, on, dom, BaseWidgetSetting) { 25 | var m_animationTimeMS; 26 | 27 | return declare([BaseWidgetSetting], { 28 | baseClass: 'jimu-widget-demo-setting', 29 | 30 | postCreate: function(){ 31 | console.log('postCreate'); 32 | //the config object is passed in 33 | this.setConfig(this.config); 34 | } 35 | 36 | ,startup: function(){ 37 | console.log('startup'); 38 | m_animationTimeMS = on(dom.byId("animTimeMS"), "change", lang.hitch(this, this.onChangeAnimTime)); 39 | } 40 | 41 | ,onClose: function(){ 42 | console.log('onClose'); 43 | m_animationTimeMS.remove(); 44 | } 45 | 46 | ,setConfig: function(config){ 47 | this.svcCF.value = config.closestFacilitySvc.url; 48 | this.svcFacilities.value = config.facilities.url; 49 | this.attrRank.value = config.symbology.routeZOrderAttrName; 50 | this.attrUVRender.value = config.symbology.routeRenderer.field1; 51 | this.durationRouteAnim.value = config.symbology.animateRoutesDuration; 52 | this.durationRouteAnim.oldValue = config.symbology.animateRoutesDuration; 53 | } 54 | 55 | ,getConfig: function(){ 56 | //WAB will get config object through this method 57 | this.config.closestFacilitySvc.url = this.svcCF.value; 58 | this.config.facilities.url = this.svcFacilities.value; 59 | this.config.symbology.routeZOrderAttrName = this.attrRank.value; 60 | this.config.symbology.routeRenderer.field1 = this.attrUVRender.value; 61 | this.config.symbology.animateRoutesDuration = this.durationRouteAnim.value; 62 | 63 | return this.config; 64 | } 65 | 66 | ,onChangeAnimTime: function(event) { 67 | console.log("Change Animation Time"); 68 | // Check for valid number 69 | if (!this.isPositiveInt(event.target.value)) 70 | event.target.value = event.target.oldValue; 71 | // Update old value 72 | else 73 | event.target.oldValue = event.target.value; 74 | } 75 | ,isPositiveInt: function(str) { 76 | // Taken from StackOverflow post, http://stackoverflow.com/questions/10834796/validate-that-a-string-is-a-positive-integer 77 | var n = ~~Number(str); 78 | return String(n) === str && n >= 0; 79 | } 80 | }); 81 | }); -------------------------------------------------------------------------------- /setting/css/style.css: -------------------------------------------------------------------------------- 1 | tr td:last-child { 2 | width: 70%; 3 | } 4 | tr td:last-child input { 5 | width: 100%; 6 | } --------------------------------------------------------------------------------