├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── bower.json ├── closest-facility.html ├── debug ├── closest-facility.html ├── histogram.html ├── sample-async.html ├── sample-mapService.html └── sample.html ├── demos ├── vrp.geojson └── vrp.html ├── elevation.html ├── karma.conf.js ├── license.txt ├── package-lock.json ├── package.json ├── profiles ├── base.js ├── debug.js └── production.js ├── scripts └── release.sh ├── spec └── Tasks │ └── GPTaskSpec.js ├── src ├── EsriLeafletGP.js ├── Services │ └── Geoprocessing.js └── Tasks │ └── Geoprocessing.js └── support ├── Leaflet.Elevation-0.0.2.css └── Leaflet.Elevation-0.0.2.src.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | Icon 4 | ._* 5 | .Spotlight-V100 6 | 7 | # SublimeText 8 | /*.sublime-project 9 | *.sublime-workspace 10 | *.sublime-project 11 | .idea/* 12 | 13 | # Node 14 | node_modules 15 | npm-debug.log 16 | 17 | # Coverage 18 | coverage 19 | 20 | # Debugging Files 21 | debug/* 22 | !debug/sample.html 23 | !debug/sample-mapService.html 24 | !debug/sample-async.html 25 | !debug/closest-facility.html 26 | !debug/histogram.html 27 | 28 | # Built Files 29 | dist/ 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | Icon 4 | ._* 5 | .Spotlight-V100 6 | 7 | # SublimeText 8 | /*.sublime-project 9 | *.sublime-workspace 10 | *.sublime-project 11 | .idea/* 12 | 13 | # Node 14 | node_modules 15 | 16 | # Coverage 17 | coverage 18 | 19 | # Debugging Files 20 | debug.html -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased][unreleased] 4 | 5 | ## [3.0.0] 6 | 7 | ### Breaking Changes 8 | 9 | * Leaflet and Esri Leaflet are now peerDependencies so that consumers can install whichever version they want. 10 | 11 | ## [2.0.3] 12 | 13 | ### Added 14 | 15 | * support for `GPRecordSet` input parameters (feature collections with properties and no geometry) 16 | 17 | ## [2.0.2] 18 | 19 | ### Added 20 | 21 | * plugin now supports making GP requests to Image Servers that return histograms 22 | * plugin now supports Network Analyst style GP requests that return routes 23 | * parsed input parameters are now limited to a raw geometry (as opposed to an array of features) when `geometryType` was set previously 24 | 25 | ### Fixed 26 | 27 | * more server errors are now passed back to the developer 28 | * `GPLinearUnit` input parameters can now be passed as JSON object literals 29 | 30 | ### Changed 31 | 32 | * some previously undocumented public methods have been made private 33 | 34 | ## [2.0.1] 35 | 36 | ### Fixed 37 | 38 | * add `package.json` browser bundle pointing at built library. 39 | 40 | ### Changed 41 | 42 | * Build system refactored to use latest Rollup and Rollup plugins. 43 | * Reworked bundling directives for various modules systems to resolve and simplify various issues 44 | * WebPack users no longer have to use the Babel loader. 45 | * Babelify with Babel 6 now works 46 | 47 | ## [2.0.0] 48 | 49 | ### Changed 50 | 51 | * moving out of beta and into full-blown SemVer. 52 | 53 | ## [2.0.0-beta.1] 54 | 55 | ### Breaking 56 | * Requires the 2.0.0-beta.4 release of Esri Leaflet. 57 | * Requires the 1.0.0-beta.1 release of Leaflet. 58 | * Namespaces have changed all exports now sit directly under the `L.esri.GP` namespace. This mean that things like `L.esri.GP.Services.Geoprocessing` can now be accessed like `L.esri.GP.Service`. 59 | 60 | ### Added 61 | 62 | * Better build/test/release automation. 63 | * Support for JSPM in package.json. Now you can `import gpTask from 'esri-leaflet-gp/src/Tasks/Geoprocessing';` for more compact builds but, be aware of [caveats](http://blog.izs.me/post/44149270867/why-no-directories-lib-in-node-the-less-snarky) 64 | * Support for browserify in the package.json. Now you can `var gpTask = require('esri-leaflet-gp/src/Tasks/Geoprocessing');` for more compact builds, but be aware of [caveats](http://blog.izs.me/post/44149270867/why-no-directories-lib-in-node-the-less-snarky) 65 | 66 | 67 | ## [1.0.2] 68 | 69 | ### Changed 70 | - moved CDN to [jsdelivr](http://www.jsdelivr.com/#!leaflet.esri.gp) 71 | 72 | 73 | ## [1.0.1] 74 | 75 | ### Changed 76 | - refactored result parsing logic to return **all** output results in callback for synchronous services instead of just the first. 77 | 78 | ## [1.0.0] 79 | 80 | This is expected to be the last (and only) stable release of Esri Leaflet GP compatible with Leaflet 0.7.3. All future 1.0.X releases will be compatible with Leaflet 0.7.3 and contain only bug fixes. New features will only be added in Esri Leaflet GP 2.0.0 (which will require Leaflet 1.0.0). 81 | 82 | ### Changed 83 | - Added support for temporary map service output (async services only). 84 | 85 | ## [Beta 1] 86 | ### Changed 87 | - fixed two bugs that caused errors when calling asynchronous services 88 | - made the interval at which the plugin checks for async gp output configurable 89 | - added `jobId` to parsed async responses 90 | - fixed an edge case where async results could be passed to the client app more than once 91 | 92 | ## [Alpha 3] 93 | ### Breaking Changes 94 | - modified Services.Geoprocessing constructor to account for changes in esri leaflet core at [Release Candidate 5](https://github.com/Esri/esri-leaflet/blob/master/CHANGELOG.md#release-candidate-5) 95 | - added a generic setParam() method to replace previous setters 96 | 97 | ### Changed 98 | - in addition to L.GeoJSON geometries, L.LatLng, L.LatLngBounds, and L.Marker are now considered valid GP inputs 99 | 100 | ## Alpha 2 101 | 102 | ### Breaking Changes 103 | - reorganized logic of GP to inherit from services (to introduce better support for secure resources) 104 | 105 | ### Changed 106 | - added ability to set custom paths (which will allow for use with Network Analyst services and SOEs) 107 | - refactored code to follow pattern established by [esri-leaflet-geocoder](https://github.com/Esri/esri-leaflet-geocoder) 108 | - introduced ability to check properties of GP services that don't support CORS 109 | 110 | [unreleased]: https://github.com/jgravois/esri-leaflet-gp/compare/v3.0.0...HEAD 111 | [3.0.0]: https://github.com/jgravois/esri-leaflet-gp/compare/v2.0.3...v3.0.0 112 | [2.0.3]: https://github.com/jgravois/esri-leaflet-gp/compare/v2.0.2...v2.0.3 113 | [2.0.2]: https://github.com/jgravois/esri-leaflet-gp/compare/v2.0.1...v2.0.2 114 | [2.0.1]: https://github.com/jgravois/esri-leaflet-gp/compare/v2.0.0...v2.0.1 115 | [2.0.0]: https://github.com/jgravois/esri-leaflet-gp/compare/v2.0.0-beta.1...v2.0.0 116 | [2.0.0-beta.1]: https://github.com/jgravois/esri-leaflet-gp/compare/v1.0.2...v2.0.0-beta.1 117 | [1.0.2]: https://github.com/jgravois/esri-leaflet-gp/compare/v1.0.1...v1.0.2 118 | [1.0.1]: https://github.com/jgravois/esri-leaflet-gp/compare/v1.0.0...v1.0.1 119 | [1.0.0]: https://github.com/jgravois/esri-leaflet-gp/compare/v0.0.1-beta.1...v1.0.0 120 | [Beta 1]: https://github.com/jgravois/esri-leaflet-gp/compare/v0.0.1-alpha.3...v0.0.1-beta.1 121 | [Alpha 3]: https://github.com/jgravois/esri-leaflet-gp/compare/v0.0.1-alpha.3...v0.0.1-alpha.2 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Esri Leaflet GP 2 | 3 | > an Esri Leaflet plugin for interacting with geoprocessing services published with ArcGIS Server and the analysis services hosted in ArcGIS Online. 4 | 5 | Esri Leaflet GP relies on the minimal [Esri Leaflet](https://github.com/Esri/esri-leaflet) Core which handles abstraction for requests and authentication when necessary. 6 | 7 | ## Demos 8 | Note that the latest version of this plugin requires a minimum of esri-leaflet [2.0.0](https://github.com/Esri/esri-leaflet/releases/tag/v2.0.0). 9 | 10 | The demos below show the plugin in action 11 | 12 | * [calculate a drivetime polygon](http://esri.github.io/esri-leaflet/examples/gp-plugin.html) 13 | * [generate an elevation profile](https://jgravois.github.io/esri-leaflet-gp/elevation.html) 14 | * [route to the closest facility](https://jgravois.github.io/esri-leaflet-gp/closest-facility.html) 15 | * [solve a vehicle routing problem](https://jgravois.github.io/esri-leaflet-gp/demos/vrp.html) 16 | 17 | ## Example 18 | 19 | ```html 20 | 21 | 22 | 23 | 24 | gp drivetime 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 59 | 60 | 61 | 62 |
63 |
64 |
65 | 66 | 95 | 96 | 97 | 98 | ``` 99 | ## API Reference 100 | 101 | ### [`L.esri.GP.Service`](http://esri.github.io/esri-leaflet/api-reference/services/gp-service.html) 102 | ### [`L.esri.GP.Task`](http://esri.github.io/esri-leaflet/api-reference/tasks/gp-task.html) 103 | 104 | ## Development Instructions 105 | 106 | 1. [Fork and clone Esri Leaflet GP](https://help.github.com/articles/fork-a-repo) 107 | 2. `cd` into the `esri-leaflet-gp` folder and install the dependencies with `npm install` 108 | 3. Run `npm start` from the command line. This will compile minified source in a brand new `dist` directory, launch a tiny webserver and begin watching the raw source for changes. 109 | 4. The example at `debug/sample.html` *should* 'just work' 110 | 5. Make your changes and create a [pull request](https://help.github.com/articles/creating-a-pull-request) 111 | 112 | ## Dependencies 113 | 114 | Esri Leaflet GP relies on the minimal Esri Leaflet Core which handles abstraction for requests and authentication when necessary. You can find out more about Esri Leaflet from the [Esri Leaflet Quickstart](http://esri.github.io/esri-leaflet/examples/). 115 | 116 | ## Resources 117 | 118 | * [Geoprocessing Services Documentation](http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#/GP_Service/02r3000000rq000000/) 119 | * [ArcGIS for Developers](http://developers.arcgis.com) 120 | * [ArcGIS REST Services](http://resources.arcgis.com/en/help/arcgis-rest-api/) 121 | * [twitter@esri](http://twitter.com/esri) 122 | 123 | ## Issues 124 | 125 | Find a bug or want to request a new feature? Please let us know by submitting an issue. 126 | 127 | ## Contributing 128 | 129 | Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing](https://github.com/Esri/esri-leaflet/blob/master/CONTRIBUTING.md). 130 | 131 | ## Terms and Conditions 132 | 133 | Signup for an [ArcGIS for Developers account](https://developers.arcgis.com/en/plans) or purchase an [ArcGIS Online Organizational Subscription](http://www.arcgis.com/features/plans/pricing.html). 134 | 135 | 1. Once you have an account you are good to go. Thats it! 136 | 2. If you use this library in a revenue generating application or for government use you must upgrade to a paid account. You are not allowed to generate revenue while on a free plan. 137 | 138 | This information is from the [ArcGIS for Developers Terms of Use FAQ](https://developers.arcgis.com/en/terms/faq/) 139 | 140 | ## Licensing 141 | Copyright 2017 Esri 142 | 143 | Licensed under the Apache License, Version 2.0 (the "License"); 144 | you may not use this file except in compliance with the License. 145 | You may obtain a copy of the License at 146 | 147 | > http://www.apache.org/licenses/LICENSE-2.0 148 | 149 | Unless required by applicable law or agreed to in writing, software 150 | distributed under the License is distributed on an "AS IS" BASIS, 151 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 152 | See the License for the specific language governing permissions and 153 | limitations under the License. 154 | 155 | A copy of the license is available in the repository's [license.txt]( https://raw.github.com/Esri/esri-leaflet-geocoder/master/license.txt) file. 156 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esri-leaflet-gp", 3 | "main": "dist/esri-leaflet-gp.js", 4 | "ignore": [ 5 | "**/.*", 6 | "node_modules", 7 | "components", 8 | "spec", 9 | "debug" 10 | ] 11 | } -------------------------------------------------------------------------------- /closest-facility.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | route to the closest facility 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 32 | 33 | 34 | 35 |
36 |
37 | 40 |
41 | 42 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /debug/closest-facility.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Closest Facility 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 35 | 36 | 37 |
38 |
39 | 42 |
43 | 44 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /debug/histogram.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | histograms 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 32 | 33 | 34 |
35 |
36 | 39 |
40 | 41 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /debug/sample-async.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | drivetimes 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 32 | 33 | 34 |
35 |
36 | 39 |
40 | 41 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /debug/sample-mapService.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leverage a Geoprocessing Service 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 31 | 32 |
33 |
34 | 37 |
38 | 39 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /debug/sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | drivetimes 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 32 | 33 | 34 |
35 |
36 | 39 |
40 | 41 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /demos/vrp.geojson: -------------------------------------------------------------------------------- 1 | const orders = {"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.51,37.7724]},"properties":{"DeliveryQuantities":1706,"Name":"Store_1","ServiceTime":25,"TimeWindowStart1":1355245200000,"TimeWindowEnd1":1355274000000,"MaxViolationTime1":0}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.4889,37.7538]},"properties":{"DeliveryQuantities":1533,"Name":"Store_2","ServiceTime":23,"TimeWindowStart1":1355245200000,"TimeWindowEnd1":1355274000000,"MaxViolationTime1":0}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.4649,37.7747]},"properties":{"DeliveryQuantities":1580,"Name":"Store_3","ServiceTime":24,"TimeWindowStart1":1355245200000,"TimeWindowEnd1":1355274000000,"MaxViolationTime1":0}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.4739,37.7432]},"properties":{"DeliveryQuantities":1289,"Name":"Store_4","ServiceTime":20,"TimeWindowStart1":1355245200000,"TimeWindowEnd1":1355274000000,"MaxViolationTime1":0}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.4493,37.7315]},"properties":{"DeliveryQuantities":1302,"Name":"Store_5","ServiceTime":21,"TimeWindowStart1":1355245200000,"TimeWindowEnd1":1355274000000,"MaxViolationTime1":0}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.4917,37.6493]},"properties":{"DeliveryQuantities":1775,"Name":"Store_6","ServiceTime":26,"TimeWindowStart1":1355245200000,"TimeWindowEnd1":1355274000000,"MaxViolationTime1":0}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.4832,37.7012]},"properties":{"DeliveryQuantities":1014,"Name":"Store_7","ServiceTime":17,"TimeWindowStart1":1355245200000,"TimeWindowEnd1":1355274000000,"MaxViolationTime1":0}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.5301,37.8935]},"properties":{"DeliveryQuantities":1761,"Name":"Store_8","ServiceTime":26,"TimeWindowStart1":1355245200000,"TimeWindowEnd1":1355274000000,"MaxViolationTime1":0}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.2875,37.8909]},"properties":{"DeliveryQuantities":1815,"Name":"Store_9","ServiceTime":27,"TimeWindowStart1":1355245200000,"TimeWindowEnd1":1355274000000,"MaxViolationTime1":0}}]} 2 | 3 | const depots = {"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.3943,37.7967]},"properties":{"Name":"San Francisco","TimeWindowStart1":1355241600000,"TimeWindowEnd1":1355274000000}}]} 4 | 5 | // synchronous service maxes out at two input routes, async can accept many many more 6 | const routes = {"type":"FeatureCollection","features":[{"type":"Feature","geometry":null,"properties":{"Name":"Truck_1","StartDepotName":"San Francisco","EndDepotName":"San Francisco","StartDepotServiceTime":60,"EarliestStartTime":1355241600000,"LatestStartTime":1355241600000,"Capacities":"15000","CostPerUnitTime":0.2,"CostPerUnitDistance":1.5,"MaxOrderCount":5,"MaxTotalTime":360,"MaxTotalTravelTime":120,"MaxTotalDistance":80}},{"type":"Feature","geometry":null,"properties":{"Name":"Truck_2","StartDepotName":"San Francisco","EndDepotName":"San Francisco","StartDepotServiceTime":60,"EarliestStartTime":1355241600000,"LatestStartTime":1355241600000,"Capacities":"15000","CostPerUnitTime":0.2,"CostPerUnitDistance":1.5,"MaxOrderCount":5,"MaxTotalTime":360,"MaxTotalTravelTime":120,"MaxTotalDistance":80}}]} -------------------------------------------------------------------------------- /demos/vrp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | D3 Elevation 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 36 | 37 | 38 | 39 |
40 |
41 | 44 |
45 | 46 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /elevation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | D3 Elevation 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 57 | 58 | 59 | 60 |
61 |
62 | 63 |
64 | 65 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Generated on Fri May 30 2014 15:44:45 GMT-0400 (EDT) 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: '', 8 | 9 | // frameworks to use 10 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ['mocha', 'chai-sinon'], 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | 'node_modules/leaflet/dist/leaflet.css', 16 | 'node_modules/leaflet/dist/leaflet.js', 17 | 'node_modules/esri-leaflet/dist/esri-leaflet.js', 18 | 'dist/esri-leaflet-gp.js', 19 | 'spec/**/*Spec.js' 20 | ], 21 | 22 | // list of files to exclude 23 | exclude: [], 24 | 25 | // preprocess matching files before serving them to the browser 26 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 27 | preprocessors: { 28 | 'dist/**/*.js': ['sourcemap', 'coverage'] 29 | }, 30 | 31 | // test results reporter to use 32 | // possible values: 'dots', 'progress' 33 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 34 | reporters: ['mocha', 'coverage'], 35 | 36 | // web server port 37 | port: 9876, 38 | 39 | // enable / disable colors in the output (reporters and logs) 40 | colors: true, 41 | 42 | // level of logging 43 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 44 | logLevel: config.LOG_WARN, 45 | 46 | // enable / disable watching file and executing tests whenever any file changes 47 | autoWatch: true, 48 | 49 | // start these browsers 50 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 51 | browsers: [ 52 | 'Chrome' 53 | // 'ChromeCanary', 54 | // 'Firefox', 55 | // 'Safari', 56 | // 'PhantomJS' 57 | ], 58 | 59 | // Continuous Integration mode 60 | // if true, Karma captures browsers, runs the tests and exits 61 | singleRun: true, 62 | 63 | // Configure the coverage reporters 64 | coverageReporter: { 65 | instrumenters: { 66 | isparta: require('isparta') 67 | }, 68 | instrumenter: { 69 | 'src/**/*.js': 'isparta' 70 | }, 71 | reporters: [ 72 | { 73 | type: 'html', 74 | dir: 'coverage/' 75 | }, { 76 | type: 'text' 77 | } 78 | ] 79 | } 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Apache License – 2.0 2 | 3 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 4 | 5 | 1. Definitions. 6 | 7 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 8 | 9 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 10 | 11 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 12 | 13 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 14 | 15 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 16 | 17 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 18 | 19 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 20 | 21 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 22 | 23 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 24 | 25 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 26 | 27 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 28 | 29 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 30 | 31 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 32 | 33 | 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 34 | 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 35 | 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 36 | 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 37 | 38 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 39 | 40 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 41 | 42 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 43 | 44 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 45 | 46 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 47 | 48 | END OF TERMS AND CONDITIONS 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esri-leaflet-gp", 3 | "description": "A Leaflet plugin for interacting with ArcGIS geoprocessing services.", 4 | "version": "3.0.0", 5 | "author": "John Gravois (http://johngravois.com)", 6 | "browser": "dist/esri-leaflet-gp-debug.js", 7 | "bugs": { 8 | "url": "https://github.com/jgravois/esri-leaflet-gp/issues" 9 | }, 10 | "contributors": [ 11 | "John Gravois (http://johngravois.com)", 12 | "Nicholas Furness (http://nixta.github.io/)", 13 | "Patrick Arlt (http://patrickarlt.com)", 14 | "Rowan Winsemius" 15 | ], 16 | "peerDependencies": { 17 | "leaflet": "*", 18 | "esri-leaflet": "*" 19 | }, 20 | "devDependencies": { 21 | "@rollup/plugin-json": "^4.1.0", 22 | "@rollup/plugin-node-resolve": "^7.1.3", 23 | "chai": "2.3.0", 24 | "esri-leaflet": "^3.0.3", 25 | "gh-release": "^6.0.1", 26 | "highlight.js": "^11.3.1", 27 | "http-server": "^14.0.0", 28 | "isparta": "^4.1.1", 29 | "istanbul": "^0.4.2", 30 | "karma": "^6.3.8", 31 | "karma-chai-sinon": "^0.1.3", 32 | "karma-chrome-launcher": "^3.1.0", 33 | "karma-coverage": "^2.0.3", 34 | "karma-mocha": "^2.0.1", 35 | "karma-mocha-reporter": "^2", 36 | "karma-phantomjs-launcher": "^1.0.4", 37 | "karma-sourcemap-loader": "^0.3.5", 38 | "leaflet": "^1.0.0", 39 | "mkdirp": "^0.5.1", 40 | "mocha": "^9.1.3", 41 | "rollup": "^2.60.0", 42 | "rollup-plugin-json": "^2.0.0", 43 | "rollup-plugin-node-resolve": "^1.4.0", 44 | "rollup-plugin-uglify": "^6.0.4", 45 | "semistandard": "^16.0.1", 46 | "sinon": "^6.3.5", 47 | "sinon-chai": "^3.7.0", 48 | "snazzy": "^9.0.0", 49 | "uglify-js": "^2.6.1", 50 | "watch": "^1.0.2" 51 | }, 52 | "homepage": "https://github.com/jgravois/esri-leaflet-gp", 53 | "jsnext:main": "src/EsriLeafletGP.js", 54 | "jspm": { 55 | "registry": "npm", 56 | "format": "es6", 57 | "main": "src/EsriLeafletGP.js" 58 | }, 59 | "license": "Apache-2.0", 60 | "main": "dist/esri-leaflet-gp-debug.js", 61 | "readmeFilename": "README.md", 62 | "repository": { 63 | "type": "git", 64 | "url": "https://github.com/jgravois/esri-leaflet-gp.git" 65 | }, 66 | "scripts": { 67 | "prebuild": "mkdirp dist", 68 | "build": "rollup -c profiles/debug.js & rollup -c profiles/production.js", 69 | "lint": "semistandard src/**/*.js | snazzy", 70 | "prepublish": "npm run build", 71 | "pretest": "npm run build", 72 | "test": "npm run lint && karma start", 73 | "release": "./scripts/release.sh", 74 | "start": "watch 'npm run build' src & http-server -p 5000 -c-1 -o" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /profiles/base.js: -------------------------------------------------------------------------------- 1 | import config from '../node_modules/esri-leaflet/profiles/base.js'; 2 | 3 | config.input = 'src/EsriLeafletGP.js'; 4 | config.output.name = 'L.esri.GP'; 5 | 6 | export default config; 7 | -------------------------------------------------------------------------------- /profiles/debug.js: -------------------------------------------------------------------------------- 1 | import config from './base.js'; 2 | 3 | config.output.file = 'dist/esri-leaflet-gp-debug.js'; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /profiles/production.js: -------------------------------------------------------------------------------- 1 | import { uglify } from 'rollup-plugin-uglify'; 2 | import config from './base.js' 3 | 4 | config.output.file = 'dist/esri-leaflet-gp.js'; 5 | 6 | // use a Regex to preserve copyright text 7 | config.plugins.push(uglify({ output: { comments: /Institute, Inc/ } })); 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # config 4 | VERSION=$(node --eval "console.log(require('./package.json').version);") 5 | NAME=$(node --eval "console.log(require('./package.json').name);") 6 | 7 | # build and test 8 | npm test || exit 1 9 | 10 | # checkout temp branch for release 11 | git checkout -b gh-release 12 | 13 | # run prepublish to build files 14 | npm run prepublish 15 | 16 | # force add files 17 | git add dist -f 18 | 19 | # commit changes with a versioned commit message 20 | git commit -m "build $VERSION" 21 | 22 | # push commit so it exists on GitHub when we run gh-release 23 | git push origin gh-release 24 | 25 | # create a ZIP archive of the dist files 26 | zip -r $NAME-v$VERSION.zip dist 27 | 28 | # run gh-release to create the tag and push release to github 29 | gh-release --assets $NAME-v$VERSION.zip 30 | 31 | # checkout master and delete release branch locally and on GitHub 32 | git checkout master 33 | git branch -D gh-release 34 | git push origin :gh-release 35 | 36 | # publish release on NPM 37 | npm publish -------------------------------------------------------------------------------- /spec/Tasks/GPTaskSpec.js: -------------------------------------------------------------------------------- 1 | describe('L.esri.GP', function () { 2 | function createMap(){ 3 | // create container 4 | var container = document.createElement('div'); 5 | 6 | // give container a width/height 7 | container.setAttribute('style', 'width:500px; height: 500px;'); 8 | 9 | // add contianer to body 10 | document.body.appendChild(container); 11 | 12 | return L.map(container).setView([45.51, -122.66], 16); 13 | } 14 | 15 | var map = createMap(); 16 | 17 | var task; 18 | var service; 19 | 20 | var bounds = L.latLngBounds([[45.5, -122.66],[ 45.51, -122.65]]); 21 | var latlng = L.latLng(45.51, -122.66); 22 | var rawLatlng = [45.51, -122.66]; 23 | 24 | var rawBounds = [[45.5, -122.66],[ 45.51, -122.65]]; 25 | var rawLatLng = [45.51, -122.66]; 26 | 27 | var rawGeoJsonPolygon = { 28 | "type": "Polygon", 29 | "coordinates": [[ 30 | [-97,39],[-97,41],[-94,41],[-94,39],[-97,39] 31 | ]] 32 | }; 33 | 34 | var rawGeoJsonFeature = {"type": "Feature"} 35 | rawGeoJsonFeature.geometry = rawGeoJsonPolygon; 36 | 37 | var geoJsonPolygon = L.geoJson(rawGeoJsonPolygon); 38 | 39 | var gpServiceUrl = 'http://example.com/mock/arcgis/rest/services/Folder/MockService/GPServer/CoolAnalysis'; 40 | 41 | var sampleServiceMetadataResponse = { 42 | "name": "CoolAnalysis", 43 | "displayName": "Cool Analysis", 44 | "category": "", 45 | "helpUrl": "http://example.arcgisonline.com/arcgisoutput/whatever.htm", 46 | "executionType": "esriExecutionTypeSynchronous", 47 | "parameters": [{ 48 | "name": "Input_Location", 49 | "dataType": "GPFeatureRecordSetLayer", 50 | "displayName": "Input Location", 51 | "direction": "esriGPParameterDirectionInput", 52 | "defaultValue": { 53 | "geometryType": "esriGeometryPoint", 54 | "spatialReference": { 55 | "wkid": 4326 56 | }, 57 | "Fields": [{ 58 | "name": "FID", 59 | "type": "esriFieldTypeOID", 60 | "alias": "FID" 61 | }], 62 | "fields": [{ 63 | "name": "FID", 64 | "type": "esriFieldTypeOID", 65 | "alias": "FID" 66 | }] 67 | }, 68 | "parameterType": "esriGPParameterTypeRequired", 69 | "category": "", 70 | "choiceList": [] 71 | }, { 72 | "name": "Drive_Times", 73 | "dataType": "GPString", 74 | "displayName": "Drive Times", 75 | "direction": "esriGPParameterDirectionInput", 76 | "defaultValue": "5 10 15", 77 | "parameterType": "esriGPParameterTypeOptional", 78 | "category": "", 79 | "choiceList": [] 80 | }, { 81 | "name": "Output_Drive_Time_Polygons", 82 | "dataType": "GPFeatureRecordSetLayer", 83 | "displayName": "Output Drive Time Polygons", 84 | "direction": "esriGPParameterDirectionOutput", 85 | "defaultValue": { 86 | "geometryType": "esriGeometryPolygon", 87 | "spatialReference": { 88 | "wkid": 4326 89 | }, 90 | "Fields": [{ 91 | "name": "FID", 92 | "type": "esriFieldTypeOID", 93 | "alias": "FID" 94 | }, { 95 | "name": "FacilityID", 96 | "type": "esriFieldTypeInteger", 97 | "alias": "FacilityID" 98 | }], 99 | "fields": [{ 100 | "name": "FID", 101 | "type": "esriFieldTypeOID", 102 | "alias": "FID" 103 | }, { 104 | "name": "FacilityID", 105 | "type": "esriFieldTypeInteger", 106 | "alias": "FacilityID" 107 | }] 108 | }, 109 | "parameterType": "esriGPParameterTypeRequired", 110 | "category": "", 111 | "choiceList": [] 112 | }] 113 | } 114 | 115 | var sampleFeatureCollection = { 116 | 'type': 'FeatureCollection', 117 | 'features': [{ 118 | 'type': 'Feature', 119 | 'geometry': { 120 | 'type': 'Point', 121 | 'coordinates': [-122.81, 45.48] 122 | }, 123 | 'properties': { 124 | 'ObjectID': 1, 125 | 'Name': 'Site' 126 | }, 127 | 'id': 1 128 | }] 129 | }; 130 | 131 | beforeEach(function(){ 132 | server = sinon.fakeServer.create(); 133 | service = new L.esri.GP.Service({url: gpServiceUrl}); 134 | 135 | }); 136 | 137 | afterEach(function(){ 138 | server.restore(); 139 | service = null; 140 | }); 141 | 142 | it("should be able to instantiate a task from a service", function () { 143 | var gpTask = service.createTask(); 144 | expect(gpTask.options.useCors).to.be.eq(true); 145 | expect(gpTask.params).to.be.a('object'); 146 | }); 147 | 148 | it("should be able to determine if a service is sync or async", function () { 149 | 150 | //make sure requst URL operation name isn't malformed 151 | gpTask = service.createTask(); 152 | gpTask.on('initialized', function(){ 153 | expect(gpTask.options.async).to.be.eq(false); 154 | expect(gpTask.options.path).to.be.eq("execute"); 155 | }); 156 | }); 157 | 158 | it("should make appropriate requests when a custom path is supplied", function () { 159 | var gpTask = service.createTask(); 160 | expect(1).to.be.eq(1); 161 | }); 162 | 163 | it("should pass along individual primitive parameters in requests", function () { 164 | var gpTask = service.createTask(); 165 | expect(1).to.be.eq(1); 166 | }); 167 | 168 | it("should parse GeoJSON inputs and pass them as GeoServices JSON", function () { 169 | var gpTask = service.createTask(); 170 | expect(1).to.be.eq(1); 171 | }); 172 | 173 | it("should do the same with markers", function () { 174 | var gpTask = service.createTask(); 175 | expect(1).to.be.eq(1); 176 | }); 177 | 178 | it("and bounds", function () { 179 | var gpTask = service.createTask(); 180 | expect(1).to.be.eq(1); 181 | }); 182 | 183 | it("should parse GP service feature collection responses and present them as GeoJSON", function () { 184 | var gpTask = service.createTask(); 185 | expect(1).to.be.eq(1); 186 | }); 187 | 188 | it("should parse GP service responses and present file download urls", function () { 189 | var gpTask = service.createTask(); 190 | expect(1).to.be.eq(1); 191 | }); 192 | 193 | it("should make appropriate requests when a custom path is supplied", function () { 194 | var gpTask = service.createTask(); 195 | expect(1).to.be.eq(1); 196 | }); 197 | 198 | /* to do 199 | 200 | it("should poll async services and pass output when its baked", function () { 201 | var gpTask = L.esri.GP.Tasks.Geoprocessing(); 202 | expect(1).to.be.eq(1); 203 | }); 204 | 205 | */ 206 | 207 | }); -------------------------------------------------------------------------------- /src/EsriLeafletGP.js: -------------------------------------------------------------------------------- 1 | // export version 2 | export { version as VERSION } from '../package.json'; 3 | 4 | export { Task, task } from './Tasks/Geoprocessing.js'; 5 | export { Service, service } from './Services/Geoprocessing.js'; 6 | -------------------------------------------------------------------------------- /src/Services/Geoprocessing.js: -------------------------------------------------------------------------------- 1 | import { Service as BaseService } from 'esri-leaflet'; 2 | import { Task } from '../Tasks/Geoprocessing'; 3 | 4 | export const Service = BaseService.extend({ 5 | options: { 6 | asyncInterval: 1 7 | }, 8 | 9 | createTask: function () { 10 | return new Task(this, this.options); 11 | } 12 | 13 | }); 14 | 15 | export function service (options) { 16 | return new Service(options); 17 | } 18 | 19 | export default service; 20 | -------------------------------------------------------------------------------- /src/Tasks/Geoprocessing.js: -------------------------------------------------------------------------------- 1 | /* 2 | to do: 3 | setParam([]) 4 | */ 5 | 6 | import L from 'leaflet'; 7 | import { Task as BaseTask, Util } from 'esri-leaflet'; 8 | 9 | export const Task = BaseTask.extend({ 10 | 11 | includes: L.Evented.prototype, 12 | 13 | // setters: {}, we don't use these because we don't know the ParamName OR value of custom GP services 14 | params: {}, 15 | resultParams: {}, 16 | 17 | initialize: function (options) { 18 | // don't replace parent initialize 19 | BaseTask.prototype.initialize.call(this, options); 20 | 21 | // if no constuctor options are supplied try and determine if its sync or async and set path via metadata 22 | if (!this.options.path && typeof this.options.async === 'undefined') { 23 | // assume initially that the service is synchronous 24 | this.options.async = false; 25 | this.options.path = 'execute'; 26 | 27 | // the parameters below seem wonky to me, but work for both CORS and JSONP requests 28 | this._service.metadata(function (error, results) { 29 | if (!error) { 30 | if (results.executionType === 'esriExecutionTypeSynchronous') { 31 | this.options.async = false; 32 | this.options.path = 'execute'; 33 | } else { 34 | this.options.async = true; 35 | this.options.path = 'submitJob'; 36 | } 37 | this.fire('initialized'); 38 | } else { 39 | // abort 40 | 41 | } 42 | }, this); 43 | } else { 44 | // if async is set, but not path, default to submit job 45 | if (this.options.async) { 46 | this.options.path = this.options.path ? this.options.path : 'submitJob'; 47 | } 48 | if (!this.options.async) { 49 | this.options.path = this.options.path ? this.options.path : 'execute'; 50 | } 51 | } 52 | }, 53 | 54 | // doc for various GPInput types can be found here 55 | // http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#/GP_Result/02r3000000q7000000/ 56 | setParam: function (paramName, paramValue) { 57 | if (typeof paramValue === 'boolean' || typeof paramValue !== 'object') { 58 | // pass through booleans, numbers, strings 59 | this.params[paramName] = paramValue; 60 | } else if (typeof paramValue === 'object' && paramValue.units) { 61 | // pass through GPLinearUnit params unmolested also 62 | this.params[paramName] = paramValue; 63 | } else if (paramName === 'geometry') { 64 | // convert raw geojson geometries to esri geometries 65 | this.params[paramName] = this._setGeometry(paramValue); 66 | } else { 67 | // otherwise assume its latlng, marker, bounds or geojson and package up an array of esri features 68 | const geometryType = this._setGeometryType(paramValue); 69 | const esriFeatures = { 70 | features: [] 71 | }; 72 | 73 | if (geometryType) { 74 | esriFeatures.geometryType = geometryType; 75 | } 76 | if (paramValue.type === 'FeatureCollection' && paramValue.features[0].type === 'Feature') { 77 | for (let i = 0; i < paramValue.features.length; i++) { 78 | if (paramValue.features[i].type === 'Feature') { 79 | // pass through feature attributes and geometries 80 | esriFeatures.features.push(Util.geojsonToArcGIS(paramValue.features[i])); 81 | } else { 82 | // otherwise assume the array only contains geometries 83 | esriFeatures.features.push({ geometry: Util.geojsonToArcGIS(paramValue.features[i].geometry) }); 84 | } 85 | } 86 | } else { 87 | esriFeatures.features.push({ geometry: this._setGeometry(paramValue) }); 88 | } 89 | this.params[paramName] = esriFeatures; 90 | } 91 | }, 92 | 93 | // give developer opportunity to point out where the output is going to be available 94 | setOutputParam: function (paramName) { 95 | this.params.outputParam = paramName; 96 | }, 97 | 98 | /* async elevation services need resultParams in order to return Zs (unnecessarily confusing) */ 99 | gpAsyncResultParam: function (paramName, paramValue) { 100 | this.resultParams[paramName] = paramValue; 101 | }, 102 | 103 | // we currently expect a single geometry or feature (ported from: Tasks.Query._setGeometry) 104 | _setGeometry: function (geometry) { 105 | // convert bounds to extent and finish 106 | if (geometry instanceof L.LatLngBounds) { 107 | return L.esri.Util.boundsToExtent(geometry); 108 | } 109 | 110 | // convert L.Marker > L.LatLng 111 | if (geometry.getLatLng) { 112 | geometry = geometry.getLatLng(); 113 | } 114 | 115 | // convert L.LatLng to a geojson point and continue; 116 | if (geometry instanceof L.LatLng) { 117 | geometry = { 118 | type: 'Point', 119 | coordinates: [geometry.lng, geometry.lat] 120 | }; 121 | } 122 | 123 | // handle L.GeoJSON, pull out the first geometry 124 | if (geometry instanceof L.GeoJSON) { 125 | // reassign geometry to the GeoJSON value (we assume one feature is present) 126 | geometry = geometry.getLayers()[0].feature.geometry; 127 | // processedInput.geometryType = Util.geojsonTypeToArcGIS(geometry.type); 128 | return Util.geojsonToArcGIS(geometry); 129 | } 130 | 131 | // Handle L.Polyline and L.Polygon 132 | if (geometry.toGeoJSON) { 133 | geometry = geometry.toGeoJSON(); 134 | } 135 | 136 | // handle GeoJSON feature by pulling out the geometry 137 | if (geometry.type === 'Feature') { 138 | // get the geometry of the geojson feature 139 | geometry = geometry.geometry; 140 | } 141 | 142 | // confirm that our GeoJSON is a point, line or polygon 143 | if (geometry.type === 'Point' || geometry.type === 'LineString' || geometry.type === 'Polygon') { 144 | return Util.geojsonToArcGIS(geometry); 145 | // processedInput.geometryType = Util.geojsonTypeToArcGIS(geometry.type); 146 | } else { 147 | Util.warn('invalid geometry passed as GP input. Should be an L.LatLng, L.LatLngBounds, L.Marker or GeoJSON Point Line or Polygon object'); 148 | } 149 | }, 150 | 151 | _setGeometryType: function (geometry) { 152 | if (geometry instanceof L.LatLngBounds) { 153 | return 'esriGeometryEnvelope'; 154 | } 155 | 156 | // convert L.Marker > L.LatLng 157 | if (geometry.getLatLng || geometry instanceof L.LatLng) { 158 | return 'esriGeometryPoint'; 159 | } 160 | 161 | // handle L.GeoJSON, pull out the first geometry 162 | if (geometry instanceof L.GeoJSON) { 163 | geometry = geometry.getLayers()[0].feature.geometry; 164 | return Util.geojsonTypeToArcGIS(geometry.type); 165 | } 166 | 167 | // Handle L.Polyline and L.Polygon 168 | if (geometry.toGeoJSON) { 169 | geometry = geometry.toGeoJSON(); 170 | } 171 | 172 | // handle GeoJSON feature by pulling out the geometry 173 | if (geometry.type === 'Feature') { 174 | // get the geometry of the geojson feature 175 | geometry = geometry.geometry; 176 | } 177 | 178 | // confirm that our GeoJSON is a point, line or polygon 179 | if (geometry.type === 'Point' || geometry.type === 'LineString' || geometry.type === 'Polygon') { 180 | return Util.geojsonTypeToArcGIS(geometry.type); 181 | } else if (geometry.type === 'FeatureCollection') { 182 | return Util.geojsonTypeToArcGIS(geometry.features[0].type); 183 | } else { 184 | return null; 185 | } 186 | }, 187 | 188 | run: function (callback, context) { 189 | this._done = false; 190 | 191 | if (this.options.async === true) { 192 | /* eslint-disable */ 193 | this._service.request(this.options.path, this.params, function (error, response) { 194 | this._currentJobId = response.jobId; 195 | this.checkJob(this._currentJobId, callback, context); 196 | }, this); 197 | /* eslint-enable */ 198 | } else { 199 | return this._service.request(this.options.path, this.params, function (error, response) { 200 | if (!error) { 201 | if (response.results) { 202 | callback.call(context, error, (response && this._processGPOutput(response)), response); 203 | } else if (response.histograms) { 204 | callback.call(context, error, response, response); 205 | } else if (response.routes) { 206 | callback.call(context, error, (response && this._processNetworkAnalystOutput(response)), response); 207 | } 208 | } else { 209 | callback.call(context, error, null, null); 210 | } 211 | }, this); 212 | } 213 | }, 214 | 215 | getResult: function (jobId, output, callback, context) { 216 | this._service.request( 217 | 'jobs/' + jobId + '/results/' + output, 218 | this.resultParams, 219 | function processJobResult (error, response) { 220 | let result = null; 221 | const out = (response && this._processAsyncOutput(response)); 222 | 223 | if (output in out) { 224 | result = out[output]; 225 | } 226 | 227 | callback.call( 228 | context, 229 | error, 230 | result, 231 | response 232 | ); 233 | }, this); 234 | }, 235 | 236 | checkJob: function (jobId, callback, context) { 237 | const pollJob = function () { 238 | /* eslint-disable */ 239 | this._service.request('jobs/' + jobId, {}, function polledJob (error, response) { 240 | if (response.jobStatus === 'esriJobSucceeded') { 241 | if (!this._done) { 242 | this._done = true; 243 | // to do: 244 | // refactor to make an array of async requests for output 245 | this._service.request('jobs/' + jobId + '/results/' + this.params.outputParam, this.resultParams, function processJobResult (error, response) { 246 | callback.call(context, error, (response && this._processAsyncOutput(response)), response); 247 | }, this); 248 | } 249 | window.clearInterval(counter); 250 | } else if (response.jobStatus === 'esriJobFailed') { 251 | callback.call(context, 'Job Failed', null); 252 | window.clearInterval(counter); 253 | } 254 | }, this); 255 | /* eslint-enable */ 256 | }.bind(this); 257 | 258 | const counter = window.setInterval(pollJob, this._service.options.asyncInterval * 1000); 259 | }, 260 | 261 | _processGPOutput: function (response) { 262 | const processedResponse = {}; 263 | 264 | const results = response.results; 265 | // grab syncronous results 266 | if (this.options.async === false) { 267 | // loop through results and pass back, parsing esri json 268 | for (let i = 0; i < results.length; i++) { 269 | if (results[i].dataType === 'GPFeatureRecordSetLayer') { 270 | const featureCollection = Util.responseToFeatureCollection(results[i].value); 271 | processedResponse[results[i].paramName] = featureCollection; 272 | } else { 273 | processedResponse[results[i].paramName] = results[i].value; 274 | } 275 | } 276 | } else { // grab async results slightly differently 277 | processedResponse.jobId = this._currentJobId; 278 | // var responseValue = response.value; 279 | } 280 | 281 | // if output is a raster layer, we also need to stub out a MapService url using jobid 282 | if (this.options.async === true && response.dataType === 'GPRasterDataLayer') { 283 | const baseURL = this.options.url; 284 | const n = baseURL.indexOf('GPServer'); 285 | const serviceURL = baseURL.slice(0, n) + 'MapServer/'; 286 | processedResponse.outputMapService = serviceURL + 'jobs/' + this._currentJobId; 287 | } 288 | 289 | return processedResponse; 290 | }, 291 | 292 | _processNetworkAnalystOutput: function (response) { 293 | const processedResponse = {}; 294 | 295 | if (response.routes.features.length > 0) { 296 | const featureCollection = Util.responseToFeatureCollection(response.routes); 297 | processedResponse.routes = featureCollection; 298 | } 299 | 300 | return processedResponse; 301 | }, 302 | 303 | _processAsyncOutput: function (response) { 304 | const processedResponse = {}; 305 | processedResponse.jobId = this._currentJobId; 306 | 307 | // if output is a raster layer, we also need to stub out a MapService url using jobid 308 | if (this.options.async === true && response.dataType === 'GPRasterDataLayer') { 309 | const baseURL = this.options.url; 310 | const n = baseURL.indexOf('GPServer'); 311 | const serviceURL = baseURL.slice(0, n) + 'MapServer/'; 312 | processedResponse.outputMapService = serviceURL + 'jobs/' + this._currentJobId; 313 | } 314 | 315 | // if output is GPFeatureRecordSetLayer, convert to GeoJSON 316 | if (response.dataType === 'GPFeatureRecordSetLayer') { 317 | const featureCollection = Util.responseToFeatureCollection(response.value); 318 | processedResponse[response.paramName] = featureCollection; 319 | } else { 320 | processedResponse[response.paramName] = response.value; 321 | } 322 | 323 | return processedResponse; 324 | } 325 | 326 | }); 327 | 328 | export function task (options) { 329 | return new Task(options); 330 | } 331 | 332 | export default task; 333 | -------------------------------------------------------------------------------- /support/Leaflet.Elevation-0.0.2.css: -------------------------------------------------------------------------------- 1 | .lime-theme .leaflet-control.elevation .background{background-color:rgba(156,194,34,.2);-webkit-border-radius:5px;-moz-border-radius:5px;-ms-border-radius:5px;-o-border-radius:5px;border-radius:5px}.lime-theme .leaflet-control.elevation .axis line,.lime-theme .leaflet-control.elevation .axis path{fill:none;stroke:#566b13;stroke-width:2}.lime-theme .leaflet-control.elevation .area{fill:#9cc222}.lime-theme .leaflet-control.elevation .mouse-focus-line{pointer-events:none;stroke-width:1;stroke:#101404}.lime-theme .leaflet-control.elevation .elevation-toggle{cursor:pointer;box-shadow:0 1px 7px rgba(0,0,0,.4);-webkit-border-radius:5px;border-radius:5px;width:36px;height:36px;background:url(images/elevation.png) no-repeat center center #f8f8f9}.lime-theme .leaflet-control.elevation-collapsed .background{display:none}.lime-theme .leaflet-control.elevation-collapsed .elevation-toggle{display:block}.lime-theme .leaflet-control.elevation .mouse-drag{fill:rgba(99,126,11,.4)}.lime-theme .leaflet-overlay-pane .height-focus{stroke:#9cc222;fill:#9cc222}.lime-theme .leaflet-overlay-pane .height-focus.line{pointer-events:none;stroke-width:2}.steelblue-theme .leaflet-control.elevation .background{background-color:rgba(70,130,180,.2);-webkit-border-radius:5px;-moz-border-radius:5px;-ms-border-radius:5px;-o-border-radius:5px;border-radius:5px}.steelblue-theme .leaflet-control.elevation .axis line,.steelblue-theme .leaflet-control.elevation .axis path{fill:none;stroke:#0d1821;stroke-width:2}.steelblue-theme .leaflet-control.elevation .area{fill:#4682b4}.steelblue-theme .leaflet-control.elevation .mouse-focus-line{pointer-events:none;stroke-width:1;stroke:#0d1821}.steelblue-theme .leaflet-control.elevation .elevation-toggle{cursor:pointer;box-shadow:0 1px 7px rgba(0,0,0,.4);-webkit-border-radius:5px;border-radius:5px;width:36px;height:36px;background:url(images/elevation.png) no-repeat center center #f8f8f9}.steelblue-theme .leaflet-control.elevation-collapsed .background{display:none}.steelblue-theme .leaflet-control.elevation-collapsed .elevation-toggle{display:block}.steelblue-theme .leaflet-control.elevation .mouse-drag{fill:rgba(23,74,117,.4)}.steelblue-theme .leaflet-overlay-pane .height-focus{stroke:#4682b4;fill:#4682b4}.steelblue-theme .leaflet-overlay-pane .height-focus.line{pointer-events:none;stroke-width:2}.purple-theme .leaflet-control.elevation .background{background-color:rgba(115,44,123,.2);-webkit-border-radius:5px;-moz-border-radius:5px;-ms-border-radius:5px;-o-border-radius:5px;border-radius:5px}.purple-theme .leaflet-control.elevation .axis line,.purple-theme .leaflet-control.elevation .axis path{fill:none;stroke:#2d1130;stroke-width:2}.purple-theme .leaflet-control.elevation .area{fill:#732c7b}.purple-theme .leaflet-control.elevation .mouse-focus-line{pointer-events:none;stroke-width:1;stroke:#000}.purple-theme .leaflet-control.elevation .elevation-toggle{cursor:pointer;box-shadow:0 1px 7px rgba(0,0,0,.4);-webkit-border-radius:5px;border-radius:5px;width:36px;height:36px;background:url(images/elevation.png) no-repeat center center #f8f8f9}.purple-theme .leaflet-control.elevation-collapsed .background{display:none}.purple-theme .leaflet-control.elevation-collapsed .elevation-toggle{display:block}.purple-theme .leaflet-control.elevation .mouse-drag{fill:rgba(74,14,80,.4)}.purple-theme .leaflet-overlay-pane .height-focus{stroke:#732c7b;fill:#732c7b}.purple-theme .leaflet-overlay-pane .height-focus.line{pointer-events:none;stroke-width:2} -------------------------------------------------------------------------------- /support/Leaflet.Elevation-0.0.2.src.js: -------------------------------------------------------------------------------- 1 | L.Control.Elevation = L.Control.extend({ 2 | options: { 3 | position: "topright", 4 | theme: "lime-theme", 5 | width: 600, 6 | height: 175, 7 | margins: { 8 | top: 10, 9 | right: 20, 10 | bottom: 30, 11 | left: 60 12 | }, 13 | useHeightIndicator: true, 14 | interpolation: "linear", 15 | hoverNumber: { 16 | decimalsX: 3, 17 | decimalsY: 0, 18 | formatter: undefined 19 | }, 20 | xTicks: undefined, 21 | yTicks: undefined, 22 | collapsed: false, 23 | yAxisMin: undefined, 24 | yAxisMax: undefined, 25 | forceAxisBounds: false 26 | }, 27 | 28 | onRemove: function(map) { 29 | this._container = null; 30 | }, 31 | 32 | onAdd: function(map) { 33 | this._map = map; 34 | 35 | var opts = this.options; 36 | var margin = opts.margins; 37 | opts.xTicks = opts.xTicks || Math.round(this._width() / 75); 38 | opts.yTicks = opts.yTicks || Math.round(this._height() / 30); 39 | opts.hoverNumber.formatter = opts.hoverNumber.formatter || this._formatter; 40 | 41 | //append theme name on body 42 | d3.select("body").classed(opts.theme, true); 43 | 44 | var x = this._x = d3.scale.linear() 45 | .range([0, this._width()]); 46 | 47 | var y = this._y = d3.scale.linear() 48 | .range([this._height(), 0]); 49 | 50 | var area = this._area = d3.svg.area() 51 | .interpolate(opts.interpolation) 52 | .x(function(d) { 53 | var xDiagCoord = x(d.dist); 54 | d.xDiagCoord = xDiagCoord; 55 | return xDiagCoord; 56 | }) 57 | .y0(this._height()) 58 | .y1(function(d) { 59 | return y(d.altitude); 60 | }); 61 | 62 | var container = this._container = L.DomUtil.create("div", "elevation"); 63 | 64 | this._initToggle(); 65 | 66 | var cont = d3.select(container); 67 | cont.attr("width", opts.width); 68 | var svg = cont.append("svg"); 69 | svg.attr("width", opts.width) 70 | .attr("class", "background") 71 | .attr("height", opts.height) 72 | .append("g") 73 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 74 | 75 | var line = d3.svg.line(); 76 | line = line 77 | .x(function(d) { 78 | return d3.mouse(svg.select("g"))[0]; 79 | }) 80 | .y(function(d) { 81 | return this._height(); 82 | }); 83 | 84 | var g = d3.select(this._container).select("svg").select("g"); 85 | 86 | this._areapath = g.append("path") 87 | .attr("class", "area"); 88 | 89 | var background = this._background = g.append("rect") 90 | .attr("width", this._width()) 91 | .attr("height", this._height()) 92 | .style("fill", "none") 93 | .style("stroke", "none") 94 | .style("pointer-events", "all"); 95 | 96 | if (L.Browser.touch) { 97 | 98 | background.on("touchmove.drag", this._dragHandler.bind(this)). 99 | on("touchstart.drag", this._dragStartHandler.bind(this)). 100 | on("touchstart.focus", this._mousemoveHandler.bind(this)); 101 | L.DomEvent.on(this._container, 'touchend', this._dragEndHandler, this); 102 | 103 | } else { 104 | 105 | background.on("mousemove.focus", this._mousemoveHandler.bind(this)). 106 | on("mouseout.focus", this._mouseoutHandler.bind(this)). 107 | on("mousedown.drag", this._dragStartHandler.bind(this)). 108 | on("mousemove.drag", this._dragHandler.bind(this)); 109 | L.DomEvent.on(this._container, 'mouseup', this._dragEndHandler, this); 110 | 111 | } 112 | 113 | this._xaxisgraphicnode = g.append("g"); 114 | this._yaxisgraphicnode = g.append("g"); 115 | this._appendXaxis(this._xaxisgraphicnode); 116 | this._appendYaxis(this._yaxisgraphicnode); 117 | 118 | var focusG = this._focusG = g.append("g"); 119 | this._mousefocus = focusG.append('svg:line') 120 | .attr('class', 'mouse-focus-line') 121 | .attr('x2', '0') 122 | .attr('y2', '0') 123 | .attr('x1', '0') 124 | .attr('y1', '0'); 125 | this._focuslabelX = focusG.append("svg:text") 126 | .style("pointer-events", "none") 127 | .attr("class", "mouse-focus-label-x"); 128 | this._focuslabelY = focusG.append("svg:text") 129 | .style("pointer-events", "none") 130 | .attr("class", "mouse-focus-label-y"); 131 | 132 | if (this._data) { 133 | this._applyData(); 134 | } 135 | 136 | return container; 137 | }, 138 | 139 | _dragHandler: function() { 140 | 141 | //we don´t want map events to occur here 142 | d3.event.preventDefault(); 143 | d3.event.stopPropagation(); 144 | 145 | this._gotDragged = true; 146 | 147 | this._drawDragRectangle(); 148 | 149 | }, 150 | 151 | /* 152 | * Draws the currently dragged rectabgle over the chart. 153 | */ 154 | _drawDragRectangle: function() { 155 | 156 | if (!this._dragStartCoords) { 157 | return; 158 | } 159 | 160 | var dragEndCoords = this._dragCurrentCoords = d3.mouse(this._background.node()); 161 | 162 | var x1 = Math.min(this._dragStartCoords[0], dragEndCoords[0]), 163 | x2 = Math.max(this._dragStartCoords[0], dragEndCoords[0]); 164 | 165 | if (!this._dragRectangle && !this._dragRectangleG) { 166 | var g = d3.select(this._container).select("svg").select("g"); 167 | 168 | this._dragRectangleG = g.append("g"); 169 | 170 | this._dragRectangle = this._dragRectangleG.append("rect") 171 | .attr("width", x2 - x1) 172 | .attr("height", this._height()) 173 | .attr("x", x1) 174 | .attr('class', 'mouse-drag') 175 | .style("pointer-events", "none"); 176 | } else { 177 | this._dragRectangle.attr("width", x2 - x1) 178 | .attr("x", x1); 179 | } 180 | 181 | }, 182 | 183 | /* 184 | * Removes the drag rectangle and zoms back to the total extent of the data. 185 | */ 186 | _resetDrag: function() { 187 | 188 | if (this._dragRectangleG) { 189 | 190 | this._dragRectangleG.remove(); 191 | this._dragRectangleG = null; 192 | this._dragRectangle = null; 193 | 194 | this._hidePositionMarker(); 195 | 196 | this._map.fitBounds(this._fullExtent); 197 | 198 | } 199 | 200 | }, 201 | 202 | /* 203 | * Handles end of dragg operations. Zooms the map to the selected items extent. 204 | */ 205 | _dragEndHandler: function() { 206 | 207 | if (!this._dragStartCoords || !this._gotDragged) { 208 | this._dragStartCoords = null; 209 | this._gotDragged = false; 210 | this._resetDrag(); 211 | return; 212 | } 213 | 214 | this._hidePositionMarker(); 215 | 216 | var item1 = this._findItemForX(this._dragStartCoords[0]), 217 | item2 = this._findItemForX(this._dragCurrentCoords[0]); 218 | 219 | this._fitSection(item1, item2); 220 | 221 | this._dragStartCoords = null; 222 | this._gotDragged = false; 223 | 224 | }, 225 | 226 | _dragStartHandler: function() { 227 | 228 | d3.event.preventDefault(); 229 | d3.event.stopPropagation(); 230 | 231 | this._gotDragged = false; 232 | 233 | this._dragStartCoords = d3.mouse(this._background.node()); 234 | 235 | }, 236 | 237 | /* 238 | * Finds a data entry for a given x-coordinate of the diagram 239 | */ 240 | _findItemForX: function(x) { 241 | var bisect = d3.bisector(function(d) { 242 | return d.dist; 243 | }).left; 244 | var xinvert = this._x.invert(x); 245 | return bisect(this._data, xinvert); 246 | }, 247 | 248 | _findItemForLatLng: function(latlng) { 249 | var result = null, 250 | d = Infinity; 251 | this._data.forEach(function(item) { 252 | var dist = latlng.distanceTo(item.latlng); 253 | if (dist < d) { 254 | d = dist; 255 | result = item; 256 | } 257 | }); 258 | return result; 259 | }, 260 | 261 | /** Make the map fit the route section between given indexes. */ 262 | _fitSection: function(index1, index2) { 263 | 264 | var start = Math.min(index1, index2), 265 | end = Math.max(index1, index2); 266 | 267 | var ext = this._calculateFullExtent(this._data.slice(start, end)); 268 | 269 | this._map.fitBounds(ext); 270 | 271 | }, 272 | 273 | _initToggle: function() { 274 | 275 | /* inspired by L.Control.Layers */ 276 | 277 | var container = this._container; 278 | 279 | //Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released 280 | container.setAttribute('aria-haspopup', true); 281 | 282 | if (!L.Browser.touch) { 283 | L.DomEvent 284 | .disableClickPropagation(container); 285 | //.disableScrollPropagation(container); 286 | } else { 287 | L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation); 288 | } 289 | 290 | if (this.options.collapsed) { 291 | this._collapse(); 292 | 293 | if (!L.Browser.android) { 294 | L.DomEvent 295 | .on(container, 'mouseover', this._expand, this) 296 | .on(container, 'mouseout', this._collapse, this); 297 | } 298 | var link = this._button = L.DomUtil.create('a', 'elevation-toggle', container); 299 | link.href = '#'; 300 | link.title = 'Elevation'; 301 | 302 | if (L.Browser.touch) { 303 | L.DomEvent 304 | .on(link, 'click', L.DomEvent.stop) 305 | .on(link, 'click', this._expand, this); 306 | } else { 307 | L.DomEvent.on(link, 'focus', this._expand, this); 308 | } 309 | 310 | this._map.on('click', this._collapse, this); 311 | // TODO keyboard accessibility 312 | } 313 | }, 314 | 315 | _expand: function() { 316 | this._container.className = this._container.className.replace(' elevation-collapsed', ''); 317 | }, 318 | 319 | _collapse: function() { 320 | L.DomUtil.addClass(this._container, 'elevation-collapsed'); 321 | }, 322 | 323 | _width: function() { 324 | var opts = this.options; 325 | return opts.width - opts.margins.left - opts.margins.right; 326 | }, 327 | 328 | _height: function() { 329 | var opts = this.options; 330 | return opts.height - opts.margins.top - opts.margins.bottom; 331 | }, 332 | 333 | /* 334 | * Fromatting funciton using the given decimals and seperator 335 | */ 336 | _formatter: function(num, dec, sep) { 337 | var res; 338 | if (dec === 0) { 339 | res = Math.round(num) + ""; 340 | } else { 341 | res = L.Util.formatNum(num, dec) + ""; 342 | } 343 | var numbers = res.split("."); 344 | if (numbers[1]) { 345 | var d = dec - numbers[1].length; 346 | for (; d > 0; d--) { 347 | numbers[1] += "0"; 348 | } 349 | res = numbers.join(sep || "."); 350 | } 351 | return res; 352 | }, 353 | 354 | _appendYaxis: function(y) { 355 | y.attr("class", "y axis") 356 | .call(d3.svg.axis() 357 | .scale(this._y) 358 | .ticks(this.options.yTicks) 359 | .orient("left")) 360 | .append("text") 361 | .attr("x", -45) 362 | .attr("y", 3) 363 | .style("text-anchor", "end") 364 | .text("m"); 365 | }, 366 | 367 | _appendXaxis: function(x) { 368 | x.attr("class", "x axis") 369 | .attr("transform", "translate(0," + this._height() + ")") 370 | .call(d3.svg.axis() 371 | .scale(this._x) 372 | .ticks(this.options.xTicks) 373 | .orient("bottom")) 374 | .append("text") 375 | .attr("x", this._width() + 20) 376 | .attr("y", 15) 377 | .style("text-anchor", "end") 378 | .text("km"); 379 | }, 380 | 381 | _updateAxis: function() { 382 | this._xaxisgraphicnode.selectAll("g").remove(); 383 | this._xaxisgraphicnode.selectAll("path").remove(); 384 | this._xaxisgraphicnode.selectAll("text").remove(); 385 | this._yaxisgraphicnode.selectAll("g").remove(); 386 | this._yaxisgraphicnode.selectAll("path").remove(); 387 | this._yaxisgraphicnode.selectAll("text").remove(); 388 | this._appendXaxis(this._xaxisgraphicnode); 389 | this._appendYaxis(this._yaxisgraphicnode); 390 | }, 391 | 392 | _mouseoutHandler: function() { 393 | 394 | this._hidePositionMarker(); 395 | 396 | }, 397 | 398 | /* 399 | * Hides the position-/heigth indication marker drawn onto the map 400 | */ 401 | _hidePositionMarker: function() { 402 | 403 | if (this._marker) { 404 | this._map.removeLayer(this._marker); 405 | this._marker = null; 406 | } 407 | if (this._mouseHeightFocus) { 408 | this._mouseHeightFocus.style("visibility", "hidden"); 409 | this._mouseHeightFocusLabel.style("visibility", "hidden"); 410 | } 411 | if (this._pointG) { 412 | this._pointG.style("visibility", "hidden"); 413 | } 414 | this._focusG.style("visibility", "hidden"); 415 | 416 | }, 417 | 418 | /* 419 | * Handles the moueseover the chart and displays distance and altitude level 420 | */ 421 | _mousemoveHandler: function(d, i, ctx) { 422 | if (!this._data || this._data.length === 0) { 423 | return; 424 | } 425 | var coords = d3.mouse(this._background.node()); 426 | var opts = this.options; 427 | 428 | var item = this._data[this._findItemForX(coords[0])], 429 | alt = item.altitude, 430 | dist = item.dist, 431 | ll = item.latlng, 432 | numY = opts.hoverNumber.formatter(alt, opts.hoverNumber.decimalsY), 433 | numX = opts.hoverNumber.formatter(dist, opts.hoverNumber.decimalsX); 434 | 435 | this._showDiagramIndicator(item, coords[0]); 436 | 437 | var layerpoint = this._map.latLngToLayerPoint(ll); 438 | 439 | //if we use a height indicator we create one with SVG 440 | //otherwise we show a marker 441 | if (opts.useHeightIndicator) { 442 | 443 | if (!this._mouseHeightFocus) { 444 | 445 | var heightG = d3.select(".leaflet-overlay-pane svg") 446 | .append("g"); 447 | this._mouseHeightFocus = heightG.append('svg:line') 448 | .attr('class', 'height-focus line') 449 | .attr('x2', '0') 450 | .attr('y2', '0') 451 | .attr('x1', '0') 452 | .attr('y1', '0'); 453 | 454 | var pointG = this._pointG = heightG.append("g"); 455 | pointG.append("svg:circle") 456 | .attr("r", 6) 457 | .attr("cx", 0) 458 | .attr("cy", 0) 459 | .attr("class", "height-focus circle-lower"); 460 | 461 | this._mouseHeightFocusLabel = heightG.append("svg:text") 462 | .attr("class", "height-focus-label") 463 | .style("pointer-events", "none"); 464 | 465 | } 466 | 467 | var normalizedAlt = this._height() / this._maxElevation * alt; 468 | var normalizedY = layerpoint.y - normalizedAlt; 469 | this._mouseHeightFocus.attr("x1", layerpoint.x) 470 | .attr("x2", layerpoint.x) 471 | .attr("y1", layerpoint.y) 472 | .attr("y2", normalizedY) 473 | .style("visibility", "visible"); 474 | 475 | this._pointG.attr("transform", "translate(" + layerpoint.x + "," + layerpoint.y + ")") 476 | .style("visibility", "visible"); 477 | 478 | this._mouseHeightFocusLabel.attr("x", layerpoint.x) 479 | .attr("y", normalizedY) 480 | .text(numY + " m") 481 | .style("visibility", "visible"); 482 | 483 | } else { 484 | 485 | if (!this._marker) { 486 | 487 | this._marker = new L.Marker(ll).addTo(this._map); 488 | 489 | } else { 490 | 491 | this._marker.setLatLng(ll); 492 | 493 | } 494 | 495 | } 496 | 497 | }, 498 | 499 | /* 500 | * Parsing of GeoJSON data lines and their elevation in z-coordinate 501 | */ 502 | _addGeoJSONData: function(coords) { 503 | if (coords) { 504 | var data = this._data || []; 505 | var dist = this._dist || 0; 506 | var ele = this._maxElevation || 0; 507 | for (var i = 0; i < coords.length; i++) { 508 | var s = new L.LatLng(coords[i][1], coords[i][0]); 509 | var e = new L.LatLng(coords[i ? i - 1 : 0][1], coords[i ? i - 1 : 0][0]); 510 | var newdist = s.distanceTo(e); 511 | dist = dist + Math.round(newdist / 1000 * 100000) / 100000; 512 | ele = ele < coords[i][2] ? coords[i][2] : ele; 513 | data.push({ 514 | dist: dist, 515 | altitude: coords[i][2], 516 | x: coords[i][0], 517 | y: coords[i][1], 518 | latlng: s 519 | }); 520 | } 521 | this._dist = dist; 522 | this._data = data; 523 | this._maxElevation = ele; 524 | } 525 | }, 526 | 527 | /* 528 | * Parsing function for GPX data as used by https://github.com/mpetazzoni/leaflet-gpx 529 | */ 530 | _addGPXdata: function(coords) { 531 | if (coords) { 532 | var data = this._data || []; 533 | var dist = this._dist || 0; 534 | var ele = this._maxElevation || 0; 535 | for (var i = 0; i < coords.length; i++) { 536 | var s = coords[i]; 537 | var e = coords[i ? i - 1 : 0]; 538 | var newdist = s.distanceTo(e); 539 | dist = dist + Math.round(newdist / 1000 * 100000) / 100000; 540 | ele = ele < s.meta.ele ? s.meta.ele : ele; 541 | data.push({ 542 | dist: dist, 543 | altitude: s.meta.ele, 544 | x: s.lng, 545 | y: s.lat, 546 | latlng: s 547 | }); 548 | } 549 | this._dist = dist; 550 | this._data = data; 551 | this._maxElevation = ele; 552 | } 553 | }, 554 | 555 | _addData: function(d) { 556 | var geom = d && d.geometry && d.geometry; 557 | var i; 558 | 559 | if (geom) { 560 | switch (geom.type) { 561 | case 'LineString': 562 | this._addGeoJSONData(geom.coordinates); 563 | break; 564 | 565 | case 'MultiLineString': 566 | for (i = 0; i < geom.coordinates.length; i++) { 567 | this._addGeoJSONData(geom.coordinates[i]); 568 | } 569 | break; 570 | 571 | default: 572 | throw new Error('Invalid GeoJSON object.'); 573 | } 574 | } 575 | 576 | var feat = d && d.type === "FeatureCollection"; 577 | if (feat) { 578 | for (i = 0; i < d.features.length; i++) { 579 | this._addData(d.features[i]); 580 | } 581 | } 582 | 583 | if (d && d._latlngs) { 584 | this._addGPXdata(d._latlngs); 585 | } 586 | }, 587 | 588 | /* 589 | * Calculates the full extent of the data array 590 | */ 591 | _calculateFullExtent: function(data) { 592 | 593 | if (!data || data.length < 1) { 594 | throw new Error("no data in parameters"); 595 | } 596 | 597 | var ext = new L.latLngBounds(data[0].latlng, data[0].latlng); 598 | 599 | data.forEach(function(item) { 600 | ext.extend(item.latlng); 601 | }); 602 | 603 | return ext; 604 | 605 | }, 606 | 607 | /* 608 | * Add data to the diagram either from GPX or GeoJSON and 609 | * update the axis domain and data 610 | */ 611 | addData: function(d, layer) { 612 | this._addData(d); 613 | if (this._container) { 614 | this._applyData(); 615 | } 616 | layer.on("mousemove", this._handleLayerMouseOver.bind(this)); 617 | }, 618 | 619 | /* 620 | * Handles mouseover events of the data layers on the map. 621 | */ 622 | _handleLayerMouseOver: function(evt) { 623 | if (!this._data || this._data.length === 0) { 624 | return; 625 | } 626 | var latlng = evt.latlng; 627 | var item = this._findItemForLatLng(latlng); 628 | if (item) { 629 | var x = item.xDiagCoord; 630 | this._showDiagramIndicator(item, x); 631 | } 632 | }, 633 | 634 | _showDiagramIndicator: function(item, xCoordinate) { 635 | var opts = this.options; 636 | this._focusG.style("visibility", "visible"); 637 | this._mousefocus.attr('x1', xCoordinate) 638 | .attr('y1', 0) 639 | .attr('x2', xCoordinate) 640 | .attr('y2', this._height()) 641 | .classed('hidden', false); 642 | 643 | var alt = item.altitude, 644 | dist = item.dist, 645 | ll = item.latlng, 646 | numY = opts.hoverNumber.formatter(alt, opts.hoverNumber.decimalsY), 647 | numX = opts.hoverNumber.formatter(dist, opts.hoverNumber.decimalsX); 648 | 649 | this._focuslabelX.attr("x", xCoordinate) 650 | .text(numY + " m"); 651 | this._focuslabelY.attr("y", this._height() - 5) 652 | .attr("x", xCoordinate) 653 | .text(numX + " km"); 654 | }, 655 | 656 | _applyData: function() { 657 | var xdomain = d3.extent(this._data, function(d) { 658 | return d.dist; 659 | }); 660 | var ydomain = d3.extent(this._data, function(d) { 661 | return d.altitude; 662 | }); 663 | var opts = this.options; 664 | 665 | if (opts.yAxisMin !== undefined && (opts.yAxisMin < ydomain[0] || opts.forceAxisBounds)) { 666 | ydomain[0] = opts.yAxisMin; 667 | } 668 | if (opts.yAxisMax !== undefined && (opts.yAxisMax > ydomain[1] || opts.forceAxisBounds)) { 669 | ydomain[1] = opts.yAxisMax; 670 | } 671 | 672 | this._x.domain(xdomain); 673 | this._y.domain(ydomain); 674 | this._areapath.datum(this._data) 675 | .attr("d", this._area); 676 | this._updateAxis(); 677 | 678 | this._fullExtent = this._calculateFullExtent(this._data); 679 | }, 680 | 681 | /* 682 | * Reset data 683 | */ 684 | _clearData: function() { 685 | this._data = null; 686 | this._dist = null; 687 | this._maxElevation = null; 688 | }, 689 | 690 | /* 691 | * Reset data and display 692 | */ 693 | clear: function() { 694 | 695 | this._clearData(); 696 | 697 | if (!this._areapath) { 698 | return; 699 | } 700 | 701 | // workaround for 'Error: Problem parsing d=""' in Webkit when empty data 702 | // https://groups.google.com/d/msg/d3-js/7rFxpXKXFhI/HzIO_NPeDuMJ 703 | //this._areapath.datum(this._data).attr("d", this._area); 704 | this._areapath.attr("d", "M0 0"); 705 | 706 | this._x.domain([0, 1]); 707 | this._y.domain([0, 1]); 708 | this._updateAxis(); 709 | } 710 | 711 | }); 712 | 713 | L.control.elevation = function(options) { 714 | return new L.Control.Elevation(options); 715 | }; --------------------------------------------------------------------------------