├── 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 |
Navigation/Drawing Tools:
5 |
6 |
8 |
10 |
11 |
12 |
13 |
14 |
# of facilities to find:
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
32 |
33 |
34 |
35 |
Result Rank Symbols:
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
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 = '