├── .csslintrc ├── .eslintrc ├── .gitignore ├── LICENSE.md ├── README.md ├── README.md.orig └── widgets ├── AppSettings.js ├── AppSettings ├── README.md ├── _extentMixin.js ├── _layerMixin.js ├── _shareMixin.js ├── appSettings.png ├── php │ ├── dbHelper.php │ └── index.php └── templates │ └── AppSettings.html ├── FeatureTableWrapper.js ├── FeatureTableWrapper └── css │ └── style.css ├── Filter.js ├── Filter ├── README.md ├── css │ └── filter.css ├── docs │ └── filter.png ├── nls │ └── filter.js └── templates │ └── filter.html ├── HeatMap.js ├── HeatMap ├── README.md └── heatmap.png ├── LabelLayer.js ├── LabelLayer ├── README.md ├── docs │ ├── advanced.png │ └── basic.png ├── nls │ └── LabelLayer.js └── templates │ └── LabelLayer.html ├── LoginCookie.js ├── LoginCookie └── README.md ├── MetadataDialog.js ├── MetadataDialog ├── README.md ├── css │ └── MetadataDialog.css ├── metadatadialog.png └── templates │ └── dialogContent.html ├── PDFGenerator.js ├── PDFGenerator └── nls │ └── PDFGenerator.js ├── RelationshipTable ├── README.md ├── RelationshipTable.js ├── identify │ └── factory.js ├── relatedRecords.png └── templates │ └── RelatedRecordTable.html ├── RelationshipTableTabs.js ├── Themes.js ├── TimeSlider.js └── TimeSlider ├── README.md └── timeSlider.png /.csslintrc: -------------------------------------------------------------------------------- 1 | /* https://github.com/CSSLint/csslint/wiki/Rules */ 2 | { 3 | "important": false, 4 | "adjoining-classes": false, 5 | "known-properties": false, 6 | "box-sizing": false, 7 | "box-model": false, 8 | "overqualified-elements": false, 9 | "display-property-grouping": false, 10 | "bulletproof-font-face": false, 11 | "compatible-vendor-prefixes": false, 12 | "regex-selectors": false, 13 | "errors": true, 14 | "duplicate-background-images": false, 15 | "duplicate-properties": false, 16 | "empty-rules": false, 17 | "selector-max-approaching": false, 18 | "gradients": false, 19 | "fallback-colors": false, 20 | "font-sizes": false, 21 | "font-faces": false, 22 | "floats": false, 23 | "star-property-hack": false, 24 | "outline-none": false, 25 | "import": false, 26 | "ids": false, 27 | "underscore-property-hack": false, 28 | "rules-count": false, 29 | "qualified-headings": false, 30 | "selector-max": false, 31 | "shorthand": false, 32 | "text-indent": false, 33 | "unique-headings": false, 34 | "universal-selector": false, 35 | "unqualified-attributes": false, 36 | "vendor-prefix": false, 37 | "zero-units": false 38 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | /* https://github.com/eslint/eslint/tree/master/docs/rules */ 2 | { 3 | "rules": { 4 | 5 | /* 6 | Possible Errors 7 | The follow rules point out areas where you 8 | might have made mistakes. 9 | */ 10 | "comma-dangle": [2, "never"], 11 | "no-cond-assign": 2, 12 | "no-console": 2, 13 | "no-constant-condition": 2, 14 | "no-control-regex": 2, 15 | "no-debugger": 1, 16 | "no-dupe-args": 2, 17 | "no-dupe-keys": 2, 18 | "no-duplicate-case": 2, 19 | "no-empty-character-class": 2, 20 | "no-empty": 2, 21 | "no-ex-assign": 2, 22 | "no-extra-boolean-cast": 2, 23 | "no-extra-semi": 2, 24 | "no-func-assign": 2, 25 | "no-inner-declarations": 2, 26 | "no-invalid-regexp": 2, 27 | "no-irregular-whitespace": 2, 28 | "no-negated-in-lhs": 2, 29 | "no-obj-calls": 2, 30 | "no-regex-spaces": 2, 31 | "no-sparse-arrays": 2, 32 | "no-unexpected-multiline": 2, 33 | "no-unreachable": 2, 34 | "use-isnan": 2, 35 | "valid-jsdoc": 0, 36 | "valid-typeof": 2, 37 | 38 | // ignored possible errors 39 | "no-extra-parens": 0, 40 | 41 | 42 | /* 43 | Best Practices 44 | These are rules designed to prevent you from making 45 | mistakes. They either prescribe a better way of 46 | doing something or help you avoid footguns. 47 | */ 48 | "accessor-pairs": [2, { 49 | "getWithoutSet": true 50 | }], 51 | "block-scoped-var": 2, 52 | "complexity": [2, 20], /** TODO should try to get this lower **/ 53 | "consistent-return": 2, 54 | "curly": 2, 55 | "default-case": 2, 56 | "dot-location": [2, "property"], 57 | "dot-notation": 2, 58 | "eqeqeq": 2, 59 | "guard-for-in": 2, 60 | "max-statements": [1, 30, {"ignoreTopLevelFunctions": true}], 61 | "no-alert": 2, 62 | "no-caller": 2, 63 | "no-div-regex": 2, 64 | "no-empty-pattern": 2, 65 | "no-eq-null": 2, 66 | "no-eval": 2, 67 | "no-extend-native": 2, 68 | "no-extra-bind": 2, 69 | "no-fallthrough": 2, 70 | "no-floating-decimal": 2, 71 | "no-implicit-coercion": 2, 72 | "no-implied-eval": 2, 73 | "no-iterator": 2, 74 | "no-labels": 2, 75 | "no-lone-blocks": 2, 76 | "no-loop-func": 2, 77 | "no-multi-spaces": 2, 78 | "no-multi-str": 2, 79 | "no-native-reassign": 2, 80 | "no-new-func": 2, 81 | "no-new-wrappers": 2, 82 | "no-new": 2, 83 | "no-octal-escape": 2, 84 | "no-octal": 2, 85 | "no-process-env": 2, 86 | "no-proto": 2, 87 | "no-redeclare": 2, 88 | "no-return-assign": 2, 89 | "no-script-url": 2, 90 | "no-self-compare": 2, 91 | "no-sequences": 2, 92 | "no-throw-literal": 2, 93 | "no-unused-expressions": 2, 94 | "no-useless-call": 2, 95 | "no-useless-concat": 2, 96 | "no-void": 2, 97 | "no-with": 2, 98 | "radix": 2, 99 | "wrap-iife": [2, "inside"], 100 | "yoda": 2, 101 | 102 | // ignored best practices rules 103 | "no-else-return": 0, 104 | "no-invalid-this": 0, 105 | "no-magic-numbers": 0, 106 | "no-param-reassign": 0, 107 | "no-warning-comments": 0, 108 | "vars-on-top": 0, 109 | 110 | 111 | /* 112 | Strict Mode 113 | */ 114 | "strict": 2, 115 | 116 | 117 | /* 118 | Variables 119 | These rules have to do with variable declarations. 120 | */ 121 | "no-catch-shadow": 2, 122 | "no-delete-var": 2, 123 | "no-label-var": 2, 124 | "no-shadow-restricted-names": 2, 125 | "no-shadow": 2, 126 | "no-undef-init": 2, 127 | "no-undef": 2, 128 | "no-unused-vars": 2, 129 | "no-use-before-define": 2, 130 | 131 | // ignore variable rules 132 | "init-declarations": 0, 133 | "no-undefined": 0, 134 | 135 | 136 | /* 137 | Stylistic Issues 138 | These rules are purely matters of style and 139 | are quite subjective. 140 | */ 141 | "array-bracket-spacing": [2, "never"], 142 | "block-spacing": [2, "always"], 143 | "brace-style": [2, "1tbs", { 144 | "allowSingleLine": false 145 | }], 146 | "camelcase": 2, 147 | "comma-spacing": [2, { 148 | "before": false, 149 | "after": true 150 | }], 151 | "comma-style": 2, 152 | "computed-property-spacing": [2, "never"], 153 | "consistent-this": [2, "self"], 154 | "func-style": [2, "declaration"], 155 | "indent": [2, 4], 156 | "key-spacing": [2, { 157 | "beforeColon": false, 158 | "afterColon": true, 159 | "mode": "strict" 160 | }], 161 | "keyword-spacing": [2, {"before": true, "after": true, "overrides": {}}], 162 | "max-nested-callbacks": [2, 4], 163 | "new-cap": 2, 164 | "new-parens": 2, 165 | "no-array-constructor": 2, 166 | "no-continue": 2, 167 | "no-lonely-if": 2, 168 | "no-mixed-spaces-and-tabs": 2, 169 | "no-multiple-empty-lines": 2, 170 | "object-curly-spacing": [2, "never"], 171 | "operator-assignment": 2, 172 | "operator-linebreak": 2, 173 | "quotes": [2, "single"], 174 | "semi-spacing": [2, { 175 | "before": false, 176 | "after": true 177 | }], 178 | "semi": [2, "always"], 179 | "space-before-blocks": [2, "always"], 180 | "space-before-function-paren": [2, "always"], 181 | "space-in-parens": [2, "never"], 182 | "space-infix-ops": 2, 183 | "wrap-regex": 2, 184 | 185 | // ignored stylistic rules 186 | "eol-last": 0, 187 | "func-names": 0, 188 | "id-match": 0, 189 | "jsx-quotes": 0, 190 | "lines-around-comment": 0, 191 | "linebreak-style": 0, 192 | "newline-after-var": 0, 193 | "no-inline-comments": 0, 194 | "no-negated-condition": 0, 195 | "no-underscore-dangle": 0, 196 | "one-var": 0, 197 | "padded-blocks": 0, 198 | "quote-props": 0, 199 | "require-jsdoc": 0, 200 | "space-unary-ops": 0, 201 | "spaced-comment": 0, 202 | 203 | 204 | /* 205 | ECMAScript 6 206 | These rules are only relevant to ES6 environments. 207 | */ 208 | "arrow-parens": [2, "always"], 209 | "arrow-spacing": [2, { 210 | "before": true, 211 | "after": true 212 | }], 213 | "constructor-super": 2, 214 | "generator-star-spacing": [2, { 215 | "before": true, 216 | "after": true 217 | }], 218 | "no-class-assign": 2, 219 | "no-const-assign": 2, 220 | "no-dupe-class-members": 2, 221 | "no-this-before-super": 2, 222 | "prefer-const": 2, 223 | "prefer-spread": 2, 224 | "require-yield": 2, 225 | 226 | // ignored ECMA6 rules 227 | "no-arrow-condition": 0, 228 | "no-var": 0, 229 | "object-shorthand": 0, 230 | "prefer-arrow-callback": 0, 231 | "prefer-reflect": 0, 232 | "prefer-template": 0 233 | 234 | }, 235 | "env": { 236 | "amd": true, 237 | "es6": true, 238 | "browser": true 239 | }, 240 | "globals": { 241 | "define": true, 242 | "require": true 243 | }, 244 | "parser": "babel-eslint", 245 | "extends": "eslint:recommended" 246 | } 247 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/private/ 2 | *.project 3 | *.xml 4 | *.sqlite3 5 | *.sqlite 6 | *.directory 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gregg Roemhildt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CMV Widgets 2 | =========== 3 | 4 | [![Join the chat at https://gitter.im/roemhildtg/CMV_Widgets](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/roemhildtg/CMV_Widgets?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | Dojo Widgets to extend the functionality of the Esri Javascript API and [CMV](https://github.com/cmv/cmv-app). Documentation on how to use these widgets can be found in their respective folder. While designed to work specifically for CMV, they should function just as well outside of CMV in a different Dojo or Esri API app. 7 | 8 | ### Demo 9 | 10 | * Check out the [demo](http://roemhildtg.github.io/cmv-widgets/) for a fully functional demo. 11 | * View the [source code](https://github.com/roemhildtg/cmv-widgets/tree/gh-pages) in the gh-pages branch. 12 | 13 | ### AppSettings 14 | 15 | * Store and share map and application state. 16 | * [View the Documentation](widgets/AppSettings/): 17 | 18 | ![URL Field](widgets/AppSettings/appSettings.png) 19 | 20 | ### Filter 21 | 22 | * Apply user defined filters to map and attributes table 23 | * [View the Documentation](widgets/Filter/) 24 | 25 | ![URL Field](widgets/Filter/docs/filter.png) 26 | 27 | 28 | ### RelatedRecordsTable 29 | 30 | * Query and display the layers' related records 31 | * [View the Documentation](widgets/RelationshipTable/) 32 | 33 | ![URL Field](widgets/RelationshipTable/relatedRecords.png) 34 | 35 | ### MetadataDialog 36 | 37 | * Query the layer's rest page and display it's description 38 | * [View the Documentation](widgets/MetadataDialog/) 39 | 40 | ![URL Field](widgets/MetadataDialog/metadatadialog.png) 41 | 42 | ### HeatMap Layer 43 | 44 | * Toggle a dynamic heatmap renderer on point layers 45 | * [View the Documentation](widgets/HeatMap/) 46 | 47 | ![URL Field](widgets/HeatMap/heatmap.png) 48 | 49 | ### Label Layer Creator 50 | 51 | * Add labels to dynamic map layers 52 | * [View the Documentation](widgets/LabelLayer/) 53 | 54 | ![widgets/LabelLayer/docs/label.png](widgets/LabelLayer/docs/basic.png) 55 | 56 | ### TimeSlider 57 | 58 | * Control the current display of all time enabled layers ont he map 59 | * [View the Documentation](widgets/TimeSlider) 60 | 61 | ![URL Field](widgets/TimeSlider/timeSlider.png) 62 | 63 | ### LoginCookie 64 | 65 | * Cache logins in a cookie or local storage and reload them when the app starts 66 | * [View the Documentation](widgets/LoginCookie) 67 | -------------------------------------------------------------------------------- /README.md.orig: -------------------------------------------------------------------------------- 1 | CMV Widgets 2 | =========== 3 | 4 | Download this branch for a fully functional demo. Note: The demo uses the PHP proxy from esri, and the app settings version that shortens the url using a php script. 5 | If you do not want to install php, configure a different proxy, and modify the `viewer.js` to not use the php script. 6 | 7 | [![Join the chat at https://gitter.im/roemhildtg/CMV_Widgets](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/roemhildtg/CMV_Widgets?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | Dojo Widgets to extend the functionality of the Esri Javascript API and [CMV](https://github.com/cmv/cmv-app). Documentation on how to use these widgets can be found in their respective folder. While designed to work specifically for CMV, they should function just as well outside of CMV in a different Dojo or Esri API app. 10 | 11 | <<<<<<< HEAD 12 | ======= 13 | ### Demo 14 | 15 | * Check out the [demo](http://roemhildtg.github.io/cmv-widgets/) for a fully functional demo. 16 | * View the [source code](https://github.com/roemhildtg/cmv-widgets/tree/gh-pages) in the gh-pages branch. 17 | 18 | >>>>>>> master 19 | ### AppSettings 20 | 21 | * Store and share map and application state. 22 | * [View the Documentation](widgets/AppSettings/): 23 | 24 | ![URL Field](widgets/AppSettings/appSettings.png) 25 | 26 | ### RelatedRecordsTable 27 | 28 | * Query and display the layers' related records 29 | * [View the Documentation](widgets/RelationshipTable/) 30 | 31 | ![URL Field](widgets/RelationshipTable/relatedRecords.png) 32 | 33 | ### MetadataDialog 34 | 35 | * Query the layer's rest page and display it's description 36 | * [View the Documentation](widgets/MetadataDialog/) 37 | 38 | ![URL Field](widgets/MetadataDialog/metadatadialog.png) 39 | 40 | ### HeatMap Layer 41 | 42 | * Toggle a dynamic heatmap renderer on point layers 43 | * [View the Documentation](widgets/HeatMap/) 44 | 45 | ![URL Field](widgets/HeatMap/heatmap.png) 46 | 47 | ### Label Layer Creator 48 | 49 | * Add labels to dynamic map layers 50 | * [View the Documentation](widgets/LabelLayer/) 51 | 52 | ![URL Field](widgets/LabelLayer/docs/label.png) 53 | 54 | ### TimeSlider 55 | 56 | * Control the current display of all time enabled layers ont he map 57 | * [View the Documentation](widgets/TimeSlider) 58 | 59 | ![URL Field](widgets/TimeSlider/timeSlider.png) 60 | 61 | ### LoginCookie 62 | 63 | * Cache logins in a cookie or local storage and reload them when the app starts 64 | * [View the Documentation](widgets/LoginCookie) 65 | -------------------------------------------------------------------------------- /widgets/AppSettings.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dijit/_WidgetBase', 4 | 'dijit/_TemplatedMixin', 5 | 'dijit/_WidgetsInTemplateMixin', 6 | 'dojo/dom-construct', 7 | 'dojo/topic', 8 | 'dojo/json', 9 | 'dojo/_base/lang', 10 | 'dojo/ready', 11 | 'dijit/form/CheckBox', 12 | './AppSettings/_extentMixin', 13 | './AppSettings/_layerMixin', 14 | './AppSettings/_shareMixin', 15 | 'dojo/text!./AppSettings/templates/AppSettings.html', 16 | 'dijit/form/Button' 17 | ], function (declare, _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, 18 | DomConstruct, topic, json, lang, ready, Checkbox, 19 | _extentMixin, _layerMixin, _shareMixin, appSettingsTemplate) { 20 | //this widget uses several mixins to add additional functionality 21 | //additional mixins may be added 22 | return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, 23 | _extentMixin, _layerMixin, _shareMixin 24 | ], { 25 | /* params */ 26 | /** 27 | * each app setting may have the following properties: 28 | * save - boolean, whether or not to save this setting 29 | * value: object, the currently saved value 30 | * checkbox: boolean, whether or not to display a user checkbox 31 | * label: string, a checkbox label 32 | * urlLoad: whether or not a value has been loaded via url - this is set by this widget 33 | */ 34 | appSettings: {}, 35 | parameterName: 'cmvSettings', 36 | /* private */ 37 | widgetsInTemplate: true, 38 | templateString: appSettingsTemplate, 39 | _defaultAppSettings: {}, 40 | _appSettings: null, 41 | baseClass: 'appSettings', 42 | postCreate: function () { 43 | this.inherited(arguments); 44 | //mixin additional default properties 45 | lang.mixin(this._defaultAppSettings, this.appSettings); 46 | //initialize the object 47 | this._appSettings = lang.clone(this._defaultAppSettings); 48 | this.loadAppSettings(); 49 | }, 50 | /** 51 | * this method is called after settings are loaded 52 | * mixins may use this method to apply loaded settings 53 | * mixins init method must call this.inherited(arguments) to ensure 54 | * other mixins load properly 55 | */ 56 | init: function () { 57 | this._initialized = true; 58 | this.inherited(arguments); 59 | //call mixins init method 60 | this._setCheckboxHandles(); 61 | //needs to wait for other widgets to load 62 | //so they can subscribe to topic 63 | ready(10, this, '_handletopics'); 64 | }, 65 | /** 66 | * loads the settings from localStorage and overrides the loaded settings 67 | * with values in the url parameters 68 | * Up to one additional mixin may use this method 69 | * This method is responsible for calling the init method 70 | */ 71 | loadAppSettings: function () { 72 | //mixin local storage 73 | if (localStorage[this.parameterName]) { 74 | //this may fail if json is invalid 75 | try { 76 | //parse the settings 77 | var loadedSettings = json.parse(localStorage.getItem(this.parameterName)); 78 | //store each setting that was loaded 79 | //override the default 80 | for (var setting in loadedSettings) { 81 | if (loadedSettings.hasOwnProperty(setting) && 82 | this._appSettings.hasOwnProperty(setting)) { 83 | //mixin the setting 84 | lang.mixin(this._appSettings[setting], loadedSettings[setting]); 85 | } 86 | } 87 | } catch (error) { 88 | this._error('_loadLocalStorage: ' + error); 89 | } 90 | } 91 | if (!this._initialized) { 92 | this.init(); 93 | } 94 | }, 95 | /** 96 | * if the checkbox property is set to true, calls the _addCheckbox function 97 | */ 98 | _setCheckboxHandles: function () { 99 | for (var setting in this._appSettings) { 100 | if (this._appSettings.hasOwnProperty(setting) && this._appSettings[setting].checkbox) { 101 | this._addCheckbox(setting); 102 | } 103 | } 104 | }, 105 | /** 106 | * creates a checkbox and sets the event handlers 107 | * @param {object} setting 108 | */ 109 | _addCheckbox: function (setting) { 110 | var li = DomConstruct.create('li', null, this.settingsList); 111 | this._appSettings[setting]._checkboxNode = new Checkbox({ 112 | id: setting, 113 | checked: this._appSettings[setting].save, 114 | onChange: lang.hitch(this, (function (setting) { 115 | return function (checked) { 116 | //optional, publish a google analytics event 117 | topic.publish('googleAnalytics/events', { 118 | category: 'AppSettings', 119 | action: 'checkbox', 120 | label: setting, 121 | value: checked ? 1 : 0 122 | }); 123 | this._appSettings[setting].save = checked; 124 | this._saveAppSettings(); 125 | }; 126 | }(setting))) 127 | }); 128 | this._appSettings[setting]._checkboxNode.placeAt(li); 129 | DomConstruct.create('label', { 130 | innerHTML: this._appSettings[setting].label, 131 | 'for': setting 132 | }, li); 133 | }, 134 | /** 135 | * publishes and subscribes to the AppSettings topics 136 | */ 137 | _handletopics: function () { 138 | this.own(topic.subscribe('AppSettings/setValue', lang.hitch(this, function (key, value) { 139 | this._setValue(key, value); 140 | }))); 141 | console.log(this._appSettings); 142 | topic.publish('AppSettings/onSettingsLoad', lang.clone(this._appSettings)); 143 | }, 144 | /* 145 | * used to modify settings programatically (without checkboxes) 146 | * @param {type} key string to identify setting to set 147 | * @param {object} value value to mixin as setting 148 | * @returns {undefined} 149 | */ 150 | _setValue: function (key, settingsObj) { 151 | if (this._appSettings.hasOwnProperty(key)) { 152 | lang.mixin(this._appSettings[key], settingsObj); 153 | } else { 154 | this._appSettings[key] = settingsObj; 155 | } 156 | this._saveAppSettings(); 157 | this._refreshView(); 158 | 159 | }, 160 | /** 161 | * saves the current value of this._appSettings to localStorage 162 | */ 163 | _saveAppSettings: function () { 164 | localStorage.setItem(this.parameterName, this._settingsToJSON()); 165 | }, 166 | /** 167 | * returns a simplified _appSettings object encoded in JSON 168 | * @param {object} settings 169 | * @returns {object } simplified settings object 170 | */ 171 | _settingsToJSON: function (settings) { 172 | if (!settings) { 173 | settings = {}; 174 | } 175 | for (var setting in this._appSettings) { 176 | if (this._appSettings.hasOwnProperty(setting)) { 177 | settings[setting] = { 178 | save: this._appSettings[setting].save, 179 | value: lang.clone(this._appSettings[setting].value) 180 | }; 181 | } 182 | } 183 | return json.stringify(settings); 184 | }, 185 | /** 186 | * in case something goes wrong, the settings are reset. 187 | * the user has the option to reset without manually clearing their cache 188 | */ 189 | _clearCache: function () { 190 | for (var setting in this._appSettings) { 191 | if (this._defaultAppSettings.hasOwnProperty(setting)) { 192 | lang.mixin(this._appSettings[setting], this._defaultAppSettings[setting]); 193 | } else { 194 | delete this._appSettings[setting]; 195 | } 196 | } 197 | this._saveAppSettings(); 198 | this._refreshView(); 199 | }, 200 | /** 201 | * in case something changes programatically, this can be called to update 202 | * the checkboxes 203 | */ 204 | _refreshView: function () { 205 | for (var setting in this._appSettings) { 206 | if (this._appSettings.hasOwnProperty(setting) && 207 | this._appSettings[setting].checkbox) { 208 | this._appSettings[setting]._checkboxNode.set('checked', 209 | this._appSettings[setting].save); 210 | } 211 | } 212 | }, 213 | /* 214 | * a helper error logging function 215 | */ 216 | _error: function (e) { 217 | //if an error occurs local storage corruption 218 | // is probably the issue 219 | topic.publish('viewer/handleError', { 220 | source: 'AppSettings', 221 | error: e 222 | }); 223 | topic.publish('growler/growl', { 224 | title: 'Settings', 225 | message: ['Something went wrong..and your saved settings were cleared.', 226 | 'To re-enable them click Help/Settings' 227 | ].join(' '), 228 | timeout: 7000 229 | }); 230 | this._clearCache(); 231 | } 232 | 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /widgets/AppSettings/README.md: -------------------------------------------------------------------------------- 1 | # AppSettings 2 | 3 | A widget designed for use in dojo applications like CMV (v1.3.3) which allows the user to save the current state of the application including map extent and visible layers. It also provides functionality to share the current state of the application via a url. 4 | 5 | ## Features: 6 | 7 | - Allow the user to save the current state of the map extent and visible layers using html5 localStorage. 8 | - Allow the user to 'share' a current snapshot of the map state with others via email client. 9 | - Integrate with other widgets to allow additional properties to be saved in the application and shared 10 | - Includes a simple PHP script which shortens urls 11 | 12 | ## Usage 13 | 14 | Copy the AppSettings.js and AppSettings folder into your relevent directory. (In CMV, this is `gis/dijit/..`) 15 | 16 | In viewer.js (see below for descriptions of parameters): 17 | 18 | ```javascript 19 | settings: { 20 | include: true, 21 | id: 'settings', 22 | position: 10, 23 | type: 'titlePane', 24 | path: 'path/AppSettings', 25 | title: 'Save/Share Current Map', 26 | options: { 27 | 28 | //these options are required: 29 | map: true, 30 | layerControlLayerInfos: true, 31 | 32 | //not required unless you want to add additional settings to save 33 | appSettings: { 34 | yourSetting: { 35 | label: 'Your Other Setting to save', 36 | checkbox: true, 37 | save: true 38 | } 39 | }, 40 | 41 | //these options are not required (defaults are shown): 42 | parametername: 'cmvSettings', 43 | mapRightClickMenu: true, 44 | address: 'email@email.com', 45 | subject: 'Share Map', 46 | body: 'Check out this map!
', 47 | emailSettings: ['saveMapExtent', 'saveLayerVisibility'], 48 | shareNode: null, 49 | shareTemplate: 'Share Map', 50 | server: null, //setting this may require a proxy 51 | extentWaitForReady: true, // wait for dojo ready before loading the extent 52 | layersWaitForReady: true // wait for dojo ready before setting layer visibility 53 | } 54 | } 55 | ``` 56 | 57 | If you're not using the CMV App, configure `dojoConfig` variable and create the widget using a constructor: 58 | 59 | ```javascript 60 | require(["esri/map", "esri/layers/ArcGISDynamicMapServiceLayer", 'widgets/AppSettings', "dojo/domReady!"], function (Map, Dynamic, Settings) { 61 | var map = new Map("map", { 62 | basemap: "topo", //For full list of pre-defined basemaps, navigate to http://arcg.is/1JVo6Wd 63 | center: [-94.75290067627297, 39.034671990514816], // long, lat 64 | zoom: 12, 65 | }); 66 | var demographicsLayerURL = "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer"; 67 | var demographicsLayerOptions = { 68 | "id": "demographicsLayer", 69 | "opacity": 0.8, 70 | "showAttribution": false 71 | }; 72 | 73 | var demographicsLayer = new Dynamic(demographicsLayerURL, demographicsLayerOptions); 74 | map.addLayer(demographicsLayer); 75 | 76 | //here we create a Settings widget in the domNode '#settings' 77 | //by passing the layerInfos and the map object 78 | var settings = new Settings({ 79 | layerInfos: [{ 80 | layer: demographicsLayer 81 | }], 82 | map: map 83 | //other options 84 | }, 'settings'); 85 | }); 86 | ``` 87 | 88 | ## Options Parameters: 89 | 90 | Key | Type | Default | Description 91 | ----------------- | -------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- 92 | map | - | - | In CMV config set to true to include map 93 | layerInfos | - | - | In CMV config, set layerControlLayerInfos to true to include layerInfos 94 | mapRightClickMenu | - | - | In CMV config, set to true to add a right click menu option for sharing the map 95 | appSettings | object | `{}` | Additional app settings to be stored, by default map extent and layer visibility are saved, widget developers may add more (see below) 96 | parameterName | string | 'cmvSettings' | Name of the parameter stored in localStorage and via url 97 | emailSettings | array | `['saveMapExtent', 'saveLayerVisibility']` | Keys of the settings that will be included in the Share Map email link 98 | shareNode | string or domNode | '' | an optional domnode to place a link to share map 99 | shareTemplate | html template string | `Share Map` | The html template that will be placed at the shareNode 100 | address | string | '' | the default email address in the Share Map email 101 | subject | string | 'Share map' | the default subject in the Share Map email 102 | body | string | '' | the default body message in the Share Map email 103 | server | string | null | URL to a save/get json script. [See below](#avoiding-long-urls-when-sharing-the-map) 104 | 105 | ### appSettings Parameter: 106 | 107 | Key | Type | Default | Description 108 | -------- | ------- | ------- | ------------------------------------------------------------------ 109 | save | boolean | - | a flag to identify whether or not this value should be saved 110 | value | object | - | the current value that is saved for this setting 111 | checkbox | boolean | - | whether the user should have a checkbox displayed for this setting 112 | label | string | - | the checkbox label 113 | 114 | ## Developing 115 | 116 | Storing a custom value in the appSettings widget can be used to set and retrieve values in localStorage and via url when the user clicks 'Share Map'. If the url becomes too long, a web server url should be provided, such as the attached php script. 117 | 118 | - First, see to find out how dojo/topic works. 119 | - Add key to appSettings option parameter array 120 | - In your widget, use topic.subscribe (example below) in the postCreate or startup function to load the values that were saved. 121 | - In your widget, use topic.publish (example below) to save your value any time it changes. 122 | - To add your custom saved value to the email link, add the key to the emailSettings option parameter array (also include the default ones). Note: this may result in a very long url depending on how big your setting value is. 123 | 124 | ### Storing a setting: 125 | 126 | ```javascript 127 | //writes the object value to the appSettings[key] and stores it in localStorage 128 | Topic.publish('AppSettings/setValue', key, value); 129 | ``` 130 | 131 | ### Retrieving a value when the widget is loaded: 132 | 133 | ```javascript 134 | //waits for the settings to be loaded and prints the appSettings object 135 | Topic.subscribe('AppSettings/onSettingsLoad', Lang.hitch(this, function (appSettings) { 136 | //appSettings is a clone of the entire data structure, if your setting is saved 137 | //it will be directly accessible via appSettings.mySetting 138 | 139 | //if your using the checkbox, then the following is relevant 140 | if(appSettings.mySetting && //make sure it exists 141 | (appSettings.mySetting.save || //the user had the checkbox checked 142 | appSettings.mySetting.urlLoad) //the user has loaded a url with this setting in it 143 | ) { 144 | console.log(appSettings.mySetting); 145 | } 146 | })); 147 | ``` 148 | 149 | Note: the `appSettings` object is a clone of the internal data structure 150 | 151 | ### Avoiding long urls when sharing the map 152 | 153 | A php script has been included in the php folder that allows for retrieving and setting values via POST or GET requests. AppSettings widget can now accept a `server: 'http://pathtoscript/index.php'` option which will be used to generate a shorter url and allow for more information to be saved when the user clicks "Share Map". 154 | 155 | This script is currently set up to store saved maps in a sqlite3 database. This requires the enabling allowing read/write permissions on the php folder. 156 | 157 | IMPORTANT: This php script has not been tested for security purposes and it is not recommended to place this in any public facing server. Use at your own risk! 158 | 159 | ## Changes 160 | 161 | 8/1/2016: 162 | 163 | - Check for setVisibleLayers function on a layer. Tiled layers have visibleLayers property, but not the setVisibleLayers function. 164 | 165 | 4/27/2015: 166 | 167 | - When sharing via url, the length of the url may become extremely long. This widget is now configured to handle sharing and retrieval via POST using a web server. A sample php script is packaged in the php folder 168 | 169 | 12/19/2014: 170 | 171 | - Settings will now load after other widgets are ready. IE 9 was throwing errors when the settings were loaded before. 172 | - Continuous url saving has been removed to allow for compatibility with the navigation hash widget, and other widgets using the hash. To share via url, the share map button can be used. 173 | 174 | License: MIT 175 | -------------------------------------------------------------------------------- /widgets/AppSettings/_extentMixin.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dojo/_base/lang', 4 | 'dojo/ready', 5 | 'dojo/sniff', 6 | 'esri/SpatialReference', 7 | 'esri/geometry/Point', 8 | 'dojo/topic' 9 | ], function (declare, lang, ready, has, SpatialReference, Point, topic) { 10 | return declare(null, { 11 | extentWaitForReady: true, 12 | postCreate: function () { 13 | this.inherited(arguments); 14 | if (!this.map) { 15 | topic.publish('viewer/handleError', { 16 | source: 'AppSettings', 17 | error: 'map is required' 18 | }); 19 | return; 20 | } 21 | this._defaultAppSettings.mapExtent = { 22 | save: false, 23 | value: {}, 24 | checkbox: true, 25 | label: 'Save Map Extent', 26 | urlLoad: false 27 | }; 28 | }, 29 | init: function () { 30 | this.inherited(arguments); 31 | if (!this._appSettings.mapExtent) { 32 | return; 33 | } 34 | if (this._appSettings.mapExtent.save || 35 | this._appSettings.mapExtent.urlLoad) { 36 | //once the saved map has finished zooming, set the handle 37 | var handle = this.map.on('zoom-end', lang.hitch(this, function () { 38 | handle.remove(); 39 | this._setExtentHandles(); 40 | })); 41 | //other widgets need to be ready to listen to extent 42 | //changes in the map 43 | if (this.extentWaitForReady) { 44 | ready(2, this, '_loadSavedExtent'); 45 | } else { 46 | this._loadSavedExtent(); 47 | } 48 | } else { 49 | this._setExtentHandles(); 50 | } 51 | }, 52 | /** 53 | * recovers the saved extent from the _appSettings object 54 | * if the settings's save or urlLoad property is true 55 | */ 56 | _loadSavedExtent: function () { 57 | //load map extent 58 | var center = this._appSettings.mapExtent.value.center; 59 | var point = new Point(center.x, center.y, new SpatialReference({ 60 | wkid: center.spatialReference.wkid 61 | })); 62 | if (has('ie')) { 63 | //work around an ie bug 64 | setTimeout(lang.hitch(this, function () { 65 | this.map.centerAndZoom(point, 66 | this._appSettings.mapExtent.value.zoom); 67 | }), 800); 68 | } else { 69 | this.map.centerAndZoom(point, 70 | this._appSettings.mapExtent.value.zoom); 71 | } 72 | //reset url flag 73 | this._appSettings.mapExtent.urlLoad = false; 74 | }, 75 | _setExtentHandles: function () { 76 | this._appSettings.mapExtent.value = {}; 77 | this.own(this.map.on('extent-change', lang.hitch(this, function () { 78 | this._appSettings.mapExtent.value.center = this.map.extent.getCenter(); 79 | this._appSettings.mapExtent.value.zoom = this.map.getZoom(); 80 | this._saveAppSettings(); 81 | }))); 82 | } 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /widgets/AppSettings/_layerMixin.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dojo/_base/lang', 4 | 'dojo/_base/array', 5 | 'dojo/topic', 6 | 'dojo/ready' 7 | ], function (declare, lang, array, topic, ready) { 8 | return declare(null, { 9 | layersWaitForReady: true, 10 | postCreate: function () { 11 | this.inherited(arguments); 12 | if (!this.layerInfos) { 13 | topic.publish('viewer/handleError', { 14 | source: 'AppSettings', 15 | error: 'layerInfos are required' 16 | }); 17 | return; 18 | } 19 | this._defaultAppSettings.layerVisibility = { 20 | save: false, 21 | value: {}, 22 | checkbox: true, 23 | label: 'Save Layer Visibility', 24 | urlLoad: false 25 | }; 26 | }, 27 | init: function () { 28 | this.inherited(arguments); 29 | if (!this._appSettings.layerVisibility) { 30 | return; 31 | } 32 | if (this._appSettings.layerVisibility.save || 33 | this._appSettings.layerVisibility.urlLoad) { 34 | //needs to be ready so other widgets can update layers 35 | //accordingly 36 | 37 | if (this.layersWaitForReady) { 38 | ready(3, this, '_loadSavedLayers'); 39 | } else { 40 | this._loadSavedLayers(); 41 | } 42 | } 43 | //needs to come after the loadSavedLayers function 44 | //so also needs to be ready 45 | ready(4, this, '_setLayerVisibilityHandles'); 46 | }, 47 | /** 48 | * sets the visibility of the loaded layers if save or urlLoad is true 49 | */ 50 | _loadSavedLayers: function () { 51 | var layers = this._appSettings.layerVisibility.value; 52 | //load visible layers 53 | array.forEach(this.layerInfos, lang.hitch(this, function (layer) { 54 | if (layers.hasOwnProperty(layer.layer.id)) { 55 | if (layers[layer.layer.id].visibleLayers && 56 | layer.layer.setVisibleLayers) { 57 | layer.layer.setVisibleLayers(layers[layer.layer.id].visibleLayers); 58 | topic.publish('layerControl/setVisibleLayers', { 59 | id: layer.layer.id, 60 | visibleLayers: layers[layer.layer.id] 61 | .visibleLayers 62 | }); 63 | } 64 | if (layers[layer.layer.id].visible !== null) { 65 | layer.layer.setVisibility(layers[layer.layer.id].visible); 66 | } 67 | } 68 | })); 69 | //reset url flag 70 | this._appSettings.layerVisibility.urlLoad = false; 71 | }, 72 | _setLayerVisibilityHandles: function () { 73 | var setting = this._appSettings.layerVisibility; 74 | setting.value = {}; 75 | //since the javascript api visibleLayers property starts 76 | //with a different set of layers than what is actually turned 77 | //on, we need to iterate through, find the parent layers, 78 | array.forEach(this.layerInfos, lang.hitch(this, '_setLayerHandle')); 79 | this.own(topic.subscribe('layerControl/setVisibleLayers', lang.hitch(this, function (layer) { 80 | setting.value[layer.id].visibleLayers = layer.visibleLayers; 81 | this._saveAppSettings(); 82 | }))); 83 | this.own(topic.subscribe('layerControl/layerToggle', lang.hitch(this, function (layer) { 84 | setting.value[layer.id].visible = layer.visible; 85 | this._saveAppSettings(); 86 | }))); 87 | this.own(topic.subscribe('layerControl/addLayerControls', lang.hitch(this, '_handleLayerAdds'))); 88 | }, 89 | _setLayerHandle: function (layer) { 90 | var setting = this._appSettings.layerVisibility; 91 | var id = layer.layer.id; 92 | var visibleLayers; 93 | if (layer.layer.hasOwnProperty('visibleLayers')) { 94 | visibleLayers = []; 95 | array.forEach(layer.layer.visibleLayers, lang.hitch(this, function (subLayerId) { 96 | var layerInfo = this.getLayerInfo(layer.layer.layerInfos, subLayerId); 97 | if (layerInfo) { 98 | visibleLayers.push(subLayerId); 99 | } 100 | })); 101 | if (visibleLayers.length === 0) { 102 | visibleLayers.push(-1); 103 | } 104 | } 105 | setting.value[id] = { 106 | visible: layer.layer.visible, 107 | visibleLayers: visibleLayers 108 | }; 109 | }, 110 | _handleLayerAdds: function (layerInfos) { 111 | layerInfos.forEach(lang.hitch(this, '_setLayerHandle')); 112 | }, 113 | getLayerInfo: function (layerInfos, id) { 114 | 115 | if (!layerInfos || !layerInfos.length || id === -1) { 116 | return false; 117 | } 118 | 119 | var info = array.filter(layerInfos, function (inf) { 120 | return inf.id === id; 121 | }); 122 | 123 | return info[0]; 124 | } 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /widgets/AppSettings/_shareMixin.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dojo/_base/lang', 4 | 'dojo/topic', 5 | 'dojo/json', 6 | 'dojo/dom-construct', 7 | 'dijit/Dialog', 8 | 'dojo/on', 9 | 'dojo/_base/array', 10 | 'esri/request', 11 | 'dijit/Menu', 12 | 'dijit/MenuItem' 13 | ], function (declare, lang, topic, json, domConstruct, Dialog, on, array, request, 14 | Menu, MenuItem) { 15 | return declare(null, { 16 | //email settings 17 | shareNode: null, 18 | shareTemplate: 'Share Map', 19 | shareDialogTemplate: '

Right click the link below and choose Copy Link or Copy Shortcut:

Share this map

', 20 | loadingDialogTemplate: '

Loading...

', 21 | /* settings to share via email */ 22 | emailSettings: ['mapExtent', 'layerVisibility'], 23 | address: '', 24 | subject: 'Share Map', 25 | body: '', 26 | //a url to use as a server for sharing urls 27 | server: '', 28 | //whether to show the share dialog 29 | showShareDialog: true, 30 | shareProperty: null, 31 | /** 32 | * creates the domnode for the share button 33 | */ 34 | postCreate: function () { 35 | this.inherited(arguments); 36 | this.shareProperty = this.parameterName; 37 | //place share button/link 38 | if (this.shareNode !== null) { 39 | var share = domConstruct.place(this.shareTemplate, this.shareNode); 40 | this.own(on(share, 'click', lang.hitch(this, 'emailLink'))); 41 | } 42 | 43 | this.own(on(this.defaultShareButton, 'click', lang.hitch(this, 'emailLink'))); 44 | if (this.mapRightClickMenu) { 45 | this.addRightClickMenu(); 46 | } 47 | if (window.location.search.indexOf(this.shareProperty) !== -1) { 48 | //override the default loadAppSettings function 49 | this.set('loadAppSettings', this.loadAppSettingsOverride); 50 | } 51 | }, 52 | loadAppSettingsOverride: function () { 53 | var settings = this.getQueryStringParameter(this.parameterName); 54 | if (this.server) { 55 | this.requestSettingsFromServer(settings).then(lang.hitch(this, 'handleServerData')); 56 | } else { 57 | this.loadSettingsFromParameter(decodeURIComponent(settings)); 58 | this.init(); 59 | } 60 | //don't override the user's settings and instead use a temp location 61 | //in local storage 62 | this.parameterName += '_urlLoad'; 63 | }, 64 | handleServerData: function (data) { 65 | if (data.Value) { 66 | this.loadSettingsFromParameter(data.Value); 67 | } 68 | this.init(); 69 | }, 70 | /** 71 | * creates the right click map menu 72 | */ 73 | addRightClickMenu: function () { 74 | this.menu = new Menu(); 75 | this.mapRightClickMenu.addChild(new MenuItem({ 76 | label: 'Share Map', 77 | onClick: lang.hitch(this, 'emailLink') 78 | })); 79 | }, 80 | /** 81 | * handles the opening of a new email and displays a temporary dialog 82 | * in case the email fails to open 83 | */ 84 | emailLink: function () { 85 | this.showDialog(this.loadingDialogTemplate); 86 | var settings = {}; 87 | array.forEach(this.emailSettings, lang.hitch(this, function (setting) { 88 | if (this._appSettings.hasOwnProperty(setting)) { 89 | settings[setting] = this._appSettings[setting].value; 90 | } 91 | })); 92 | if (this.server) { 93 | this.saveSettingsOnServer(settings); 94 | } else if (this.showShareDialog) { 95 | this.showDialog(lang.replace(this.shareDialogTemplate, [this.settingsToURL(settings)])); 96 | } 97 | }, 98 | saveSettingsOnServer: function (settings) { 99 | new request({ 100 | url: this.server, 101 | content: { 102 | action: 'set', 103 | value: json.stringify(settings) 104 | }, 105 | handleAs: 'json', 106 | usePost: true 107 | }).then(lang.hitch(this, 'handleSaveResults')); 108 | }, 109 | handleSaveResults: function (data) { 110 | if (data.ID) { 111 | var link = this.buildLink(data.ID); 112 | try { 113 | window.open('mailto:' + this.address + '?subject=' + this.subject + 114 | '&body=' + this.body + ' ' + link, '_self'); 115 | } catch (e) { 116 | this._error('emailLink: ' + e); 117 | } 118 | if (this.showShareDialog) { 119 | this.showDialog(lang.replace(this.shareDialogTemplate, [link])); 120 | } 121 | //optional google analytics event 122 | topic.publish('googleAnalytics/events', { 123 | category: 'AppSettings', 124 | action: 'map-share' 125 | }); 126 | } else { 127 | this._error('an error occurred fetching the id'); 128 | } 129 | }, 130 | requestSettingsFromServer: function (settings) { 131 | return new request({ 132 | url: this.server, 133 | content: { 134 | action: 'get', 135 | id: settings 136 | }, 137 | handleAs: 'json', 138 | usePost: true 139 | }); 140 | }, 141 | /** 142 | * 143 | * @param {type} parameters 144 | * @returns {undefined} 145 | */ 146 | loadSettingsFromParameter: function (parameters) { 147 | try { 148 | var settings = json.parse(parameters); 149 | for (var s in settings) { 150 | if (!this._appSettings.hasOwnProperty(s)) { 151 | this._appSettings[s] = {}; 152 | } 153 | if (settings.hasOwnProperty(s)) { 154 | this._appSettings[s].value = settings[s]; 155 | //set urlLoad flag override 156 | //this tells the widget that the settings were loaded via 157 | //url and should be loaded regardless of the user's checkbox 158 | this._appSettings[s].urlLoad = true; 159 | } 160 | } 161 | } catch (error) { 162 | this._error('_loadURLParameters' + error); 163 | } 164 | }, 165 | showDialog: function (content) { 166 | if (!this.shareDialog) { 167 | this.shareDialog = new Dialog({ 168 | title: 'Share Map', 169 | content: content, 170 | style: 'width: 300px; overflow:hidden;' 171 | }); 172 | } else { 173 | this.shareDialog.set('content', content); 174 | } 175 | this.shareDialog.show(); 176 | }, 177 | /** 178 | * creates a share url form the settings 179 | * @param {object} settings - the settings to generate a url from 180 | * @returns {string} url 181 | */ 182 | settingsToURL: function (settings) { 183 | var jsonString = encodeURIComponent(json.stringify(settings)); 184 | return this.buildLink(jsonString); 185 | }, 186 | buildLink: function (value) { 187 | var queryString; 188 | if (window.location.search !== '') { 189 | if (window.location.search.indexOf(this.shareProperty) !== -1) { 190 | 191 | queryString = this.replaceUrlParameter( 192 | window.location.search, 193 | this.shareProperty, 194 | value 195 | ); 196 | } else { 197 | queryString = [window.location.search, 198 | '&', 199 | this.shareProperty, 200 | '=', 201 | value 202 | ].join(''); 203 | 204 | } 205 | } else { 206 | queryString = ['?', this.shareProperty, '=', value].join(''); 207 | 208 | } 209 | //build url using window.location 210 | return [window.location.protocol + '//', 211 | window.location.host, 212 | window.location.pathname, 213 | queryString 214 | ].join(''); 215 | }, 216 | replaceUrlParameter: function (url, param, value) { 217 | var regex = new RegExp('([?|&]' + param + '=)[^\&]+'); 218 | return url.replace(regex, '$1' + value); 219 | }, 220 | /** 221 | * 222 | * @param {type} parameter 223 | * @returns {Boolean} 224 | */ 225 | getQueryStringParameter: function (parameter) { 226 | var search = decodeURI(window.location.search), 227 | parameters; 228 | if (search.indexOf(parameter) !== -1) { 229 | if (search.indexOf('&') !== -1) { 230 | parameters = search.split('&'); 231 | } else { 232 | parameters = [search]; 233 | } 234 | for (var i in parameters) { 235 | if (parameters[i].indexOf(parameter) !== -1) { 236 | return parameters[i].split('=')[1]; 237 | } 238 | } 239 | } 240 | return false; 241 | } 242 | }); 243 | }); 244 | -------------------------------------------------------------------------------- /widgets/AppSettings/appSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/cmv-widgets/90d5cd08a949fdfdcdcc15dfc9a394a07fa2b2e5/widgets/AppSettings/appSettings.png -------------------------------------------------------------------------------- /widgets/AppSettings/php/dbHelper.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 16 | $this->table = $table; 17 | } 18 | 19 | /** 20 | * checks to make sure the table exists and if not creates it 21 | */ 22 | function checkTableExists() { 23 | $checkTableExistsSql = 24 | "CREATE TABLE IF NOT EXISTS " 25 | . "`$this->table` ( " 26 | . "`ID` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, " 27 | . "`Value` TEXT " 28 | . ");"; 29 | $this->conn->query($checkTableExistsSql); 30 | } 31 | 32 | /** 33 | * queries the table for a matching record by id and returns the record 34 | * @param integer $id 35 | * @return object 36 | */ 37 | function getItemById($id) { 38 | $sql = "SELECT * " 39 | . "FROM `$this->table` " 40 | . "WHERE ID=? LIMIT 1;"; 41 | $stmt = $this->conn->prepare($sql); 42 | $stmt->execute(array($id)); 43 | return $stmt->fetch(); 44 | } 45 | 46 | /** 47 | * selects the last id and returns the record 48 | * @return object 49 | */ 50 | function getLastItem() { 51 | $sql = "SELECT *, MAX(ID) " 52 | . "FROM `$this->table`;"; 53 | $stmt = $this->conn->prepare($sql); 54 | $stmt->execute(); 55 | return $stmt->fetch(); 56 | } 57 | 58 | /** 59 | * inserts a new item 60 | * @param string $value - the string to insert into the table 61 | */ 62 | function insertItem($value) { 63 | $sql = "INSERT INTO `$this->table` (ID, Value) " 64 | . "VALUES (null, ?);"; 65 | $stmt = $this->conn->prepare($sql); 66 | $stmt->execute(array($value)); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /widgets/AppSettings/php/index.php: -------------------------------------------------------------------------------- 1 | true) 10 | ); 11 | 12 | function getRequestData($key) { 13 | if (isset($_POST[$key])) { 14 | return $_POST[$key]; 15 | } else if (isset($_GET[$key])) { 16 | return $_GET[$key]; 17 | } 18 | return null; 19 | } 20 | 21 | $action = getRequestData('action'); 22 | $id = getRequestData('id'); 23 | $value = getRequestData('value'); 24 | 25 | $db = new dbHelper($conn, $table); 26 | $db->checkTableExists(); 27 | 28 | if ($action === 'get') { 29 | if ($id) { 30 | echo json_encode($db->getItemById($id)); 31 | } else { 32 | echo json_encode(array( 33 | "error" => "id is required" 34 | )); 35 | } 36 | } else if ($action === 'set') { 37 | if ($value) { 38 | $db->insertItem($value); 39 | echo json_encode($db->getLastItem('ID')); 40 | } else { 41 | echo json_encode(array( 42 | "error" => "value is required" 43 | )); 44 | } 45 | } else { 46 | echo json_encode(array( 47 | "error" => "action is required and options are 'get' or 'set'" 48 | )); 49 | } 50 | ?> -------------------------------------------------------------------------------- /widgets/AppSettings/templates/AppSettings.html: -------------------------------------------------------------------------------- 1 |
2 |

Application Settings:

3 |
    5 |
    6 |

    9 |

    13 |
    14 |
    15 | -------------------------------------------------------------------------------- /widgets/FeatureTableWrapper.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dijit/_WidgetBase', 4 | 'dijit/_TemplatedMixin', 5 | 'dojo/_base/array', 6 | 'dojo/_base/lang', 7 | 'esri/dijit/FeatureTable', 8 | 'esri/layers/FeatureLayer', 9 | 'esri/tasks/QueryTask', 10 | 'esri/tasks/query', 11 | 'dojo/dom-class', 12 | 'xstyle/css!./FeatureTableWrapper/css/style.css' 13 | ], function (declare, _WidgetBase, _TemplatedMixin, array, lang, FeatureTable, FeatureLayer, QueryTask, Query, domClass) { 14 | return declare([_WidgetBase, _TemplatedMixin], { 15 | tableOptions: {}, 16 | layerId: '', 17 | templateString: '
    ', 18 | postCreate: function () { 19 | this.inherited(arguments); 20 | domClass.add(this.parentWidget.domNode, 'featureTableWidget'); 21 | array.forEach(this.layerInfos, lang.hitch(this, function (layerInfo) { 22 | if (this.layerId === layerInfo.layer.id) { 23 | this.fl = layerInfo.layer; 24 | } 25 | })); 26 | this.tableOptions = lang.mixin({ 27 | map: this.map, 28 | featureLayer: this.fl, 29 | allowSelectAll: true, 30 | dateOptions: { 31 | timeEnabled: false, 32 | datePattern: 'YYYY-MM-DD' 33 | } 34 | }, this.tableOptions); 35 | }, 36 | startup: function () { 37 | this.inherited(arguments); 38 | this.ft = new FeatureTable(this.tableOptions, 'featureTableNode'); 39 | this.ft.on('dgrid-select', lang.hitch(this, '_centerOnSelection')); 40 | this.fl.on("click", lang.hitch(this, '_selectFromGrid')); 41 | this.ft.startup(); 42 | 43 | }, 44 | _centerOnSelection: function (e) { 45 | this._removeHighlight(); 46 | // select the feature 47 | var query = new Query(); 48 | query.objectIds = array.map(e, lang.hitch(this, function (row) { 49 | return parseInt(row.id); 50 | })); 51 | this.fl.selectFeatures(query, FeatureLayer.SELECTION_NEW, lang.hitch(this, function (result) { 52 | if (result.length) { 53 | // re-center the map to the selected feature 54 | this.map.centerAt(result[0].geometry.getExtent().getCenter()); 55 | } else { 56 | //console.log("Feature Layer query returned no features... ", result); 57 | } 58 | })); 59 | }, 60 | _selectFromGrid: function (e) { 61 | this.ft.grid.clearSelection(); 62 | this._removeHighlight(); 63 | var id = e.graphic.attributes.OBJECTID; 64 | // select the feature that was clicked 65 | var query = new Query(); 66 | query.objectIds = [id]; 67 | 68 | this.fl.selectFeatures(query, FeatureLayer.SELECTION_NEW); 69 | // highlight the corresponding row in the grid 70 | // and make sure it is in view 71 | this.selectedElement = this.ft.grid.row(id).element; 72 | this.selectedElement.scrollIntoView(); 73 | domClass.add(this.selectedElement, 'highlighted'); 74 | }, 75 | resize: function () { 76 | this.inherited(arguments); 77 | this.ft.resize(); 78 | }, 79 | _removeHighlight: function () { 80 | if (this.selectedElement) { 81 | domClass.remove(this.selectedElement, 'highlighted'); 82 | } 83 | } 84 | 85 | }); 86 | }); -------------------------------------------------------------------------------- /widgets/FeatureTableWrapper/css/style.css: -------------------------------------------------------------------------------- 1 | .featureTableWidget {width:100%;height:100%;} 2 | 3 | .esriFeatureTable 4 | .esriFeatureTable_bc 5 | .esriFeatureTable_cp 6 | .esriFeatureTable_Table 7 | .dgrid-row.highlighted { 8 | background: #AEC7E3; 9 | } -------------------------------------------------------------------------------- /widgets/Filter.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dijit/_WidgetBase', 4 | 'dijit/_Templated', 5 | 'dojo/store/Memory', 6 | 'dojo/_base/lang', 7 | 'esri/request', 8 | 'dojo/topic', 9 | 'dojo/dom-style', 10 | 'esri/tasks/query', 11 | 'esri/tasks/QueryTask', 12 | 13 | 'dojo/text!./Filter/templates/filter.html', 14 | 'dojo/i18n!./Filter/nls/filter', 15 | 'xstyle/css!./Filter/css/filter.css', 16 | 17 | 'dijit/form/FilteringSelect', 18 | 'dijit/form/ValidationTextBox', 19 | 'dijit/form/Button', 20 | 'dijit/Toolbar' 21 | ], 22 | function (declare, _WidgetBase, _Templated, Memory, lang, request, topic, domStyle, Query, QueryTask, templateString, i18n) { 23 | 24 | function stringConvertor (val, op) { 25 | if (op.id === 'contains') { 26 | val = '%' + val + '%'; 27 | } 28 | return '\'' + val + '\''; 29 | } 30 | 31 | var CONVERTORS = { 32 | esriFieldTypeInteger: parseInt, 33 | esriFieldTypeOID: parseInt, 34 | esriFieldTypeSmallInteger: parseInt, 35 | esriFieldTypeDate: Date, 36 | esriFieldTypeDouble: parseFloat, 37 | esriFieldTypeSingle: parseFloat, 38 | esriFieldTypeString: stringConvertor, 39 | default: stringConvertor 40 | }; 41 | 42 | var EXCLUDE_TYPES = [ 43 | 'esriFieldTypeBlob', 44 | 'esriFieldTypeGeometry', 45 | 'esriFieldTypeRaster', 46 | 'esriFieldTypeGUID', 47 | 'esriFieldTypeGlobalID' 48 | ]; 49 | 50 | var OPERATORS = [{ 51 | name: 'Equals', 52 | id: 'equal', 53 | operator: '=' 54 | }, { 55 | name: 'Contains', 56 | id: 'contains', 57 | operator: 'LIKE' 58 | }, { 59 | name: 'Greater Than', 60 | id: 'gt', 61 | operator: '>' 62 | }, { 63 | name: 'Less Than', 64 | id: 'lt', 65 | operator: '<' 66 | }]; 67 | 68 | return declare([_WidgetBase, _Templated], { 69 | templateString: templateString, 70 | widgetsInTemplate: true, 71 | i18n: i18n, 72 | layerStore: null, 73 | fieldStore: null, 74 | opStore: null, 75 | operators: OPERATORS, 76 | defaultOperator: 'equal', 77 | convertors: CONVERTORS, 78 | showMapButton: true, 79 | _setShowMapButtonpAttr: function (show) { 80 | this.mapButton.domNode.style = show ? '' : 'display:none'; 81 | this.showMapButton = show; 82 | }, 83 | showTableButton: true, 84 | _setShowTableButtonAttr: function (show) { 85 | this.tableButton.domNode.style = show ? '' : 'display:none'; 86 | this.showTableButton = show; 87 | }, 88 | /** 89 | * The layer infos objects array, use the set method to update 90 | * @property {Array} 91 | */ 92 | layerInfos: [], 93 | _setLayerInfosAttr: function (layerInfos) { 94 | var store = this.layerStore; 95 | layerInfos.forEach(function (l) { 96 | if (l.layer.layerInfos) { 97 | l.layer.layerInfos.filter(function (sub) { 98 | return sub.subLayerIds === null; 99 | }).forEach(function (sub) { 100 | store.put({ 101 | id: l.layer.id + '_' + sub.id, 102 | name: sub.name, 103 | layer: l.layer, 104 | sublayer: sub.id 105 | }); 106 | }); 107 | } 108 | }); 109 | }, 110 | /** 111 | * the selected field 112 | * @property {Object} 113 | */ 114 | selectedField: null, 115 | _setSelectedFieldAttr: function (field) { 116 | this.selectedField = field; 117 | 118 | //reset values list 119 | this.set('values', []); 120 | }, 121 | /** 122 | * The currently selected layer objects properties. 123 | * @property {Object} 124 | */ 125 | selectedLayer: null, 126 | _setSelectedLayerAttr: function (l) { 127 | var def = request({ 128 | url: l.layer.url + '/' + l.sublayer, 129 | content: { 130 | 'f': 'json' 131 | } 132 | }); 133 | def.then(lang.hitch(this, function (data) { 134 | this.selectedLayer.name = data.name; 135 | this.set('layerMetadata', data); 136 | })); 137 | this.selectedLayer = l; 138 | }, 139 | selectedValue: '', 140 | _setSelectedValueAttr: function (value) { 141 | this.selectedValue = value; 142 | }, 143 | /** 144 | * The selected layers metadata object. Use the set() method to update 145 | * @property {Object} 146 | */ 147 | layerMetadata: {}, 148 | _setLayerMetadataAttr: function (layerProps) { 149 | this.layerMetadata = layerProps; 150 | 151 | this.fieldSelect.set('value', ''); 152 | 153 | //update the field store 154 | var store = this.fieldStore; 155 | var convertors = this.convertors; 156 | this.emptyStore(store); 157 | 158 | if (!layerProps.fields) { 159 | return; 160 | } 161 | 162 | //exclude fields 163 | layerProps.fields.filter(function (f) { 164 | return EXCLUDE_TYPES.indexOf(f.type) === -1; 165 | }).forEach(function (f) { 166 | store.put({ 167 | id: f.name, 168 | name: f.alias, 169 | convert: convertors[f.type || 'default'] 170 | }); 171 | }); 172 | }, 173 | /** 174 | * Whether or not to display the value select dropdown 175 | * This is automatically updated when values is set 176 | * @property {Boolean} 177 | */ 178 | valueSelectVisible: false, 179 | _setValueSelectVisibleAttr: function (visible) { 180 | this.valueSelectVisible = visible; 181 | 182 | //hide the text box and button 183 | domStyle.set(this.valueListButton.domNode, 'display', visible ? 'none' : ''); 184 | domStyle.set(this.valueText.domNode, 'display', visible ? 'none' : ''); 185 | 186 | //show the value select dropdown 187 | domStyle.set(this.valueSelect.domNode, 'display', visible ? 'block' : 'none'); 188 | }, 189 | /** 190 | * The list of values available to filter on. 191 | * Use set method to fill this value: 192 | * `this.set('values', [1, 3, 4]);` 193 | * @property {Array} 194 | */ 195 | values: [], 196 | _setValuesAttr: function (values) { 197 | this.values = values; 198 | 199 | //update visibility of value entry form 200 | this.set('valueSelectVisible', Boolean(values.length)); 201 | 202 | // update the store 203 | var store = this.valueStore; 204 | this.emptyStore(store); 205 | values.forEach(function (val) { 206 | store.put({ 207 | name: val || i18n.values.noValue, 208 | id: val 209 | }); 210 | }); 211 | }, 212 | /** 213 | * the default constructor, performs initialization of stores 214 | * @return {[type]} [description] 215 | */ 216 | constructor: function () { 217 | this.inherited(arguments); 218 | this.layerStore = new Memory({ 219 | data: [] 220 | }); 221 | this.fieldStore = new Memory({ 222 | data: [] 223 | }); 224 | this.opStore = new Memory({ 225 | data: this.operators 226 | }); 227 | this.valueStore = new Memory({ 228 | data: [] 229 | }); 230 | 231 | }, 232 | postCreate: function () { 233 | this.inherited(arguments); 234 | this.opSelect.set('value', this.defaultOperator); 235 | 236 | // event handles 237 | // layer select dropdown 238 | this.own(this.layerSelect.on('change', lang.hitch(this, function (id) { 239 | this.set('selectedLayer', this.layerStore.get(id)); 240 | }))); 241 | 242 | //field select dropdown 243 | this.own(this.fieldSelect.on('change', lang.hitch(this, function (id) { 244 | this.set('selectedField', this.fieldStore.get(id)); 245 | }))); 246 | 247 | //operator select dropdwn 248 | this.own(this.opSelect.on('change', lang.hitch(this, function (id) { 249 | this.set('selectedOperator', this.opStore.get(id)); 250 | }))); 251 | 252 | //value select dropdown and textbox 253 | this.own(this.valueText.on('change', lang.hitch(this, function (val) { 254 | this.set('selectedValue', val); 255 | }))); 256 | this.own(this.valueSelect.on('change', lang.hitch(this, function (val) { 257 | this.set('selectedValue', val); 258 | }))); 259 | 260 | }, 261 | /** 262 | * empties a store 263 | */ 264 | emptyStore: function (store) { 265 | store.query().forEach(function (item) { 266 | store.remove(item.id); 267 | }); 268 | }, 269 | /** 270 | * selects distinct values for the current layer and field 271 | */ 272 | fetchDistinctValues: function () { 273 | if (!this.selectedField) { 274 | return; 275 | } 276 | var query = lang.mixin(new Query(), { 277 | returnGeometry: false, 278 | returnDistinctValues: true, 279 | outFields: [this.selectedField.id], 280 | where: '1=1' 281 | }); 282 | 283 | var _this = this; 284 | new QueryTask(this.selectedLayer.layer.url + '/' + this.selectedLayer.sublayer) 285 | .execute(query) 286 | .then(function (data) { 287 | _this.set('values', data.features.map(function (f) { 288 | return f.attributes[_this.selectedField.id]; 289 | })); 290 | }).otherwise(function (e) {}); 291 | }, 292 | /** 293 | * builds and returns a where clause based on values in the widget 294 | * @return {String} The where clause 295 | */ 296 | buildWhereClause: function () { 297 | var value = this.selectedField.convert(this.selectedValue, this.selectedOperator); 298 | 299 | return lang.replace('{field} {operator} {value}', { 300 | field: this.selectedField.id, 301 | operator: this.selectedOperator.operator, 302 | value: value 303 | }); 304 | }, 305 | /** 306 | * publishes a topic to open an attributes table 307 | */ 308 | openTable: function () { 309 | topic.publish('attributesContainer/addTable', { 310 | // title for tab 311 | title: this.selectedLayer.name, 312 | 313 | // unique topicID so it doesn't collide with 314 | // other instances of attributes table 315 | topicID: this.selectedLayer.layer.id + this.selectedLayer.sublayer, 316 | 317 | // allow tabs to be closed 318 | // confirm tab closure 319 | closable: true, 320 | confirmClose: true, 321 | 322 | queryOptions: { 323 | // parameters for the query 324 | queryParameters: { 325 | url: this.selectedLayer.layer.url + '/' + this.selectedLayer.sublayer, 326 | maxAllowableOffset: 100, 327 | where: this.buildWhereClause() 328 | } 329 | } 330 | }); 331 | }, 332 | setLayerDefinitions: function () { 333 | var def = this.selectedLayer.layerDefinitions || []; 334 | def[this.selectedLayer.sublayer] = this.buildWhereClause(); 335 | this.selectedLayer.layer.setLayerDefinitions(def); 336 | } 337 | }); 338 | }); 339 | -------------------------------------------------------------------------------- /widgets/Filter/README.md: -------------------------------------------------------------------------------- 1 | # Filter 2 | 3 | A drop in widget to allow the user to filter any layer by any field and value. 4 | 5 | ![./docs/filter.png](./docs/filter.png) 6 | 7 | ## Demo: 8 | 9 | A demo is available at the [gh-pages branch using the config=filter-config](http://roemhildtg.github.io/cmv-widgets/?config=filter-config) 10 | 11 | ## Features: 12 | 13 | - Allow the user to build custom filters using a series of dropdown menus 14 | - Filter dynamic layers on the map 15 | - Open the cmv attribute table with the filter 16 | 17 | ## Usage 18 | 19 | In viewer.js (see below for descriptions of parameters): 20 | 21 | ```javascript 22 | filter: { 23 | include: true, 24 | id: 'filter', 25 | position: 10, 26 | type: 'titlePane', 27 | path: 'path/widgets/Filter', 28 | title: 'Filter Data', 29 | options: { 30 | layerControlLayerInfos: true 31 | } 32 | } 33 | ``` 34 | 35 | ## Options Parameters: 36 | 37 | Key | Type | Default | Description 38 | ---------- | -------- | ------- | ----------------------------------------------------------------------- 39 | layerInfos | `Object` | - | In CMV config, set layerControlLayerInfos to true to include layerInfos 40 | 41 | ## Limitations 42 | - Only one filter is currently supported per layer or table 43 | - Options are not configureable at the moment, future enhancements may include excluding layers and fields 44 | 45 | ## Changes 46 | 47 | 5/1/2017: Initial publish 48 | -------------------------------------------------------------------------------- /widgets/Filter/css/filter.css: -------------------------------------------------------------------------------- 1 | .filter-widget hidden { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /widgets/Filter/docs/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/cmv-widgets/90d5cd08a949fdfdcdcc15dfc9a394a07fa2b2e5/widgets/Filter/docs/filter.png -------------------------------------------------------------------------------- /widgets/Filter/nls/filter.js: -------------------------------------------------------------------------------- 1 | define({ 2 | root: { 3 | layerSelect: { 4 | label: 'Choose a layer' 5 | }, 6 | fieldSelect: { 7 | label: 'Choose a field' 8 | }, 9 | opSelect: { 10 | label: 'Choose a filter' 11 | }, 12 | valueText: { 13 | label: 'Enter a search value', 14 | listButton: '', 15 | listButtonTitle: 'Show Options' 16 | }, 17 | filterToolbar: { 18 | header: 'Apply filter to', 19 | tableButton: 'Table', 20 | mapButton: 'Map' 21 | }, 22 | values: { 23 | noValue: 'No Value' 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /widgets/Filter/templates/filter.html: -------------------------------------------------------------------------------- 1 |
    2 | 4 |
    5 | 12 |
    13 | 14 | 16 |
    17 | 24 |
    25 | 27 |
    28 | 35 |
    36 | 37 | 39 |
    40 | 41 | 44 | 51 |
    52 | 53 |

    ${i18n.filterToolbar.header}:

    54 | 55 |
    ${i18n.filterToolbar.tableButton}
    56 |
    ${i18n.filterToolbar.mapButton}
    57 | 58 |
    59 | -------------------------------------------------------------------------------- /widgets/HeatMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * simple heatmap widget 3 | * credits to 4 | * @bmadden and @ERS-Long 5 | * https://github.com/ERS-Long/HeatMap 6 | * 7 | */ 8 | 9 | define([ 10 | 'dojo/_base/declare', 11 | 'dijit/_WidgetBase', 12 | 'dojo/topic', 13 | 'dojo/_base/lang', 14 | 'esri/layers/FeatureLayer', 15 | 'esri/renderers/HeatmapRenderer', 16 | 'dojo/dom-class' 17 | ], function (declare, _WidgetBase, topic, lang, FeatureLayer, HeatmapRenderer, domClass) { 18 | return declare([_WidgetBase], { 19 | map: null, 20 | cssClasses: ['fa', 'fa-fire'], 21 | topic: 'layerControl/heatMap', 22 | colors: [ 23 | 'rgba(0,0,0,0.1)', 24 | 'rgba(0,0,255,0.7)', 25 | 'rgba(0,255,255,0.7)', 26 | 'rgba(0,255,0,0.7)', 27 | 'rgba(255,255,0,0.7)', 28 | 'rgba(255,0,0,0.7)' 29 | ], 30 | _heatMapLayers: {}, 31 | postCreate: function () { 32 | this.inherited(arguments); 33 | topic.subscribe(this.topic, lang.hitch(this, 'initHeatMap')); 34 | }, 35 | initHeatMap: function (r) { 36 | var layerId = [ 37 | r.layer.id, 38 | (r.subLayer ? r.subLayer.id : 'feature'), 39 | 'heatmap' 40 | ].join('-'); 41 | if (!this._heatMapLayers[layerId]) { 42 | var serviceURL = r.layer.url + (r.subLayer ? '/' + r.subLayer.id : ((r.layer.layerInfos.length === 1) ? '/0' : '')); 43 | var heatmapFeatureLayerOptions = { 44 | mode: FeatureLayer.MODE_SNAPSHOT, 45 | outFields: ['*'], 46 | id: layerId 47 | }; 48 | var heatmapRenderer = new HeatmapRenderer({ 49 | colors: this.colors 50 | }); 51 | this._heatMapLayers[layerId] = new FeatureLayer(serviceURL, heatmapFeatureLayerOptions); 52 | this._heatMapLayers[layerId].setRenderer(heatmapRenderer); 53 | this.map.addLayer(this._heatMapLayers[layerId]); 54 | } else { 55 | //toggle visibility 56 | this._heatMapLayers[layerId].setVisibility(!this._heatMapLayers[layerId].visible); 57 | } 58 | //modify the iconNode to show that a heatmap is enabled on this layer 59 | if (r.iconNode) { 60 | if (domClass.contains(r.iconNode, 'fa-fire')) { 61 | domClass.remove(r.iconNode, this.cssClasses); 62 | } else { 63 | domClass.add(r.iconNode, this.cssClasses); 64 | } 65 | } 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /widgets/HeatMap/README.md: -------------------------------------------------------------------------------- 1 | HeatMap 2 | ======= 3 | 4 | Dynamically renders heatmaps on point layers. For usage in CMV (v1.3.4) with [sublayer menu option on the LayerControl](http://docs.cmv.io/en/latest/widgets/LayerControl/). Credit to [@ERS-Long](https://github.com/ERS-Long/HeatMap) 5 | 6 | ## Requirements: 7 | 8 | * Layer must be type point 9 | * Layer must have a field of type `esriFieldTypeGeometry` 10 | 11 | ## Configuration: 12 | 13 | ```JavaScript 14 | //layerControl widget options: 15 | subLayerMenu: { 16 | dynamic: [{ 17 | label: 'Toggle Heatmap...', 18 | iconClass: 'fa fa-fire fa-fw', 19 | topic: 'heatMap' 20 | }] 21 | } 22 | ``` 23 | 24 | ```JavaScript 25 | //widget config 26 | heatmap: { 27 | include: true, 28 | id: 'heatmap', 29 | type: 'invisible', 30 | path: 'gis/widgets/HeatMap', 31 | options: { 32 | map: true 33 | } 34 | } 35 | ``` 36 | 37 | ## Alternative configuration: 38 | 39 | Widget developers can use this widget by publishing the following topic: `LayerControl/heatMap` and passing the correct parameters. 40 | 41 | ```JavaScript 42 | topic.publish('LayerControl/heatMap', { 43 | layer: dynamicMapServiceLayer, 44 | subLayer: dynamicMapServiceSubLayer, 45 | iconNode: optionalIconDomNode 46 | }); 47 | ``` 48 | -------------------------------------------------------------------------------- /widgets/HeatMap/heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/cmv-widgets/90d5cd08a949fdfdcdcc15dfc9a394a07fa2b2e5/widgets/HeatMap/heatmap.png -------------------------------------------------------------------------------- /widgets/LabelLayer.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dijit/_WidgetBase', 4 | 'dijit/_Templated', 5 | 'dojo/topic', 6 | 'dojo/_base/lang', 7 | 'esri/layers/FeatureLayer', 8 | 'esri/renderers/SimpleRenderer', 9 | 'dojo/dom-class', 10 | 'dojo/store/Memory', 11 | 'esri/request', 12 | 'esri/layers/LabelClass', 13 | 'esri/symbols/TextSymbol', 14 | 'esri/Color', 15 | 'dojo/text!./LabelLayer/templates/LabelLayer.html', 16 | 'dojo/i18n!./LabelLayer/nls/LabelLayer', 17 | 'dijit/CheckedMenuItem', 18 | 'dijit/form/Button', 19 | 'dijit/form/FilteringSelect', 20 | 'dijit/form/ComboBox', 21 | 'dijit/layout/TabContainer', 22 | 'dijit/layout/ContentPane', 23 | 'dijit/form/CheckBox' 24 | ], function (declare, _WidgetBase, _Templated, topic, lang, FeatureLayer, SimpleRenderer, 25 | domClass, Memory, request, LabelClass, TextSymbol, Color, templateString, i18n) { 26 | 27 | 28 | var EXCLUDE_TYPES = [ 29 | 'esriFieldTypeBlob', 30 | 'esriFieldTypeGeometry', 31 | 'esriFieldTypeRaster', 32 | 'esriFieldTypeGUID', 33 | 'esriFieldTypeOID', 34 | 'esriFieldTypeGlobalID' 35 | ]; 36 | 37 | var EXCLUDE_NAMES = [ 38 | 'Enabled' 39 | ]; 40 | 41 | var idCounter = 0; 42 | 43 | return declare([_WidgetBase, _Templated], { 44 | templateString: templateString, 45 | widgetsInTemplate: true, 46 | i18n: i18n, 47 | 48 | // the map object 49 | map: null, 50 | 51 | // label layer options to exclude layers and exclude fields 52 | // similar to cmv identify config 53 | // { 54 | // : { // a string like 'assets' 55 | // exclude: false, // exclude this entire layer 56 | // : { // a number representign the map service layer id 57 | // exclude: false, // set the entire layer to be excluded from the label widget 58 | // fields: [{ 59 | // alias: 'Field Label', 60 | // name: 'field_name' 61 | // }], 62 | // selections: [{ // sublayer id 63 | // name: 'Diameter - Material', //displayed to user 64 | // value: '{diameter}" {material}' //label string 65 | // }] 66 | // } 67 | // } 68 | // } 69 | labelInfos: {}, 70 | 71 | // automatically created labels 72 | // [{ 73 | // layer: 'layer_id', 74 | // sublayer: 13, // only for dynamic layers 75 | // visible: true, 76 | // name: 'Diameter - Material', //displayed to user 77 | // expression: '{diameter}" {material}' //label string 78 | // color: '#000', 79 | // fontSize: 8, 80 | // url: 'url to feature layer ' //if we want to create it, 81 | // title: 'layer title', 82 | // }] 83 | defaultLabels: [], 84 | _setDefaultLabelsAttr: function (labels) { 85 | labels.forEach(lang.hitch(this, function (label) { 86 | var layer = this.map.getLayer(label.layer); 87 | 88 | // if the layer doesn't exist, create it 89 | if (!layer) { 90 | layer = this.createFeatureLayer(label); 91 | this.layerStore.put({ 92 | id: layer.id, 93 | name: label.title, 94 | layer: layer 95 | }); 96 | } 97 | var labelLayer; 98 | if (this.labelLayers[layer.id]) { 99 | labelLayer = this.labelLayers[layer.id]; 100 | } else if (layer.declaredClass === 'esri.layers.ArcGISDynamicMapServiceLayer') { 101 | labelLayer = this.createLayerFromDynamic(layer, label.sublayer); 102 | } else if (layer.declaredClass === 'esri.layers.FeatureLayer') { 103 | labelLayer = layer; 104 | } else { 105 | return; 106 | } 107 | this.setLabel(labelLayer, { 108 | color: label.color || this.color, 109 | size: label.fontSize || this.fontSize, 110 | expression: label.expression 111 | }); 112 | labelLayer.setVisibility(label.visible); 113 | this.labelLayers[labelLayer.id] = labelLayer; 114 | })); 115 | }, 116 | 117 | // the default topics 118 | topics: { 119 | show: 'layerControl/showLabelPicker' 120 | }, 121 | 122 | // colors name is displayed to user, id is a valid dojo/Color string 123 | colors: [{ 124 | name: 'Black', 125 | id: '#000' 126 | }, { 127 | name: 'White', 128 | id: '#fff' 129 | }, { 130 | name: 'Red', 131 | id: 'red' 132 | }, { 133 | name: 'Blue', 134 | id: 'blue' 135 | }, { 136 | name: 'Orange', 137 | id: '#ffb900' 138 | }, { 139 | name: 'Purple', 140 | id: 'purple' 141 | }, { 142 | name: 'Green', 143 | id: 'green' 144 | }, { 145 | name: 'Yellow', 146 | id: 'yellow' 147 | }], 148 | 149 | // default color id 150 | color: '#000', 151 | 152 | //default font size 153 | fontSize: 8, 154 | 155 | append: true, 156 | /** 157 | * The layer infos objects array, use the set method to update 158 | * @type {Array} 159 | */ 160 | layerInfos: [], 161 | _setLayerInfosAttr: function (layerInfos) { 162 | this.layerInfos = layerInfos; 163 | if (!layerInfos.length) { 164 | return; 165 | } 166 | var store = this.layerStore; 167 | layerInfos.forEach(lang.hitch(this, function (l) { 168 | 169 | // check for exclusions of entire layer id 170 | if (this.labelInfos.hasOwnProperty(l.layer.id) && 171 | this.labelInfos[l.layer.id].exclude) { 172 | return; 173 | } 174 | 175 | if (l.layer.declaredClass === 'esri.layers.FeatureLayer') { 176 | store.put({ 177 | id: l.layer.id, 178 | name: l.title, 179 | layer: l.layer 180 | }); 181 | } else if (l.layer.declaredClass === 'esri.layers.ArcGISDynamicMapServiceLayer') { 182 | 183 | l.layer.layerInfos.filter(lang.hitch(this, function (sub) { 184 | 185 | // check for sublayer exclude 186 | if (this.labelInfos.hasOwnProperty(l.layer.id) && 187 | this.labelInfos[l.layer.id].hasOwnProperty(sub.id) && 188 | this.labelInfos[l.layer.id][sub.id].exclude) { 189 | return false; 190 | } 191 | 192 | // filter out group layers 193 | return sub.subLayerIds === null; 194 | })).forEach(function (sub) { 195 | store.put({ 196 | id: l.layer.id + '_' + sub.id, 197 | name: sub.name, 198 | layer: l.layer, 199 | sublayer: sub.id 200 | }); 201 | }); 202 | } 203 | })); 204 | }, 205 | activeLayer: {}, 206 | _setActiveLayerAttr: function (l) { 207 | if (!l || !l.layer) { 208 | return; 209 | } 210 | 211 | if (this.activeLayer === l) { 212 | return; 213 | } 214 | this.activeLayer = l; 215 | 216 | // set default label select 217 | this.setLabelSelections(this.activeLayer); 218 | 219 | 220 | var fields; 221 | // if fields are provided use those 222 | if (this.labelInfos.hasOwnProperty(l.layer.id) && 223 | this.labelInfos[l.layer.id].hasOwnProperty(l.sublayer) && 224 | this.labelInfos[l.layer.id][l.sublayer].fields) { 225 | fields = this.labelInfos[l.layer.id][l.sublayer].fields; 226 | this._setFields(fields); 227 | this.set('fieldsLoading', false); 228 | } else { 229 | 230 | // display a spinner 231 | this.set('fieldsLoading', true); 232 | 233 | // get the layer's fields 234 | var def = request({ 235 | url: l.layer.url + (l.sublayer ? '/' + l.sublayer : ''), 236 | content: { 237 | 'f': 'json' 238 | } 239 | }); 240 | def.then(lang.hitch(this, function (layerProps) { 241 | 242 | if (!layerProps.fields) { 243 | return; 244 | } 245 | 246 | // otherwise use the rest service results 247 | // exclude esri object id types 248 | fields = layerProps.fields.filter(function (f) { 249 | return EXCLUDE_TYPES.indexOf(f.type) === -1 && 250 | EXCLUDE_NAMES.indexOf(f.name) === -1; 251 | }); 252 | this._setFields(fields); 253 | this.set('fieldsLoading', false); 254 | 255 | })).otherwise(lang.hitch(this, function(){ 256 | this.set('fieldsLoading', false); 257 | })); 258 | } 259 | }, 260 | setLabelSelections: function (layer) { 261 | var layerId = layer.layer.id, 262 | sublayer = layer.sublayer, 263 | count = 1; 264 | var hasSelections = this.labelInfos[layerId] && 265 | this.labelInfos[layerId][sublayer] && 266 | this.labelInfos[layerId][sublayer].selections; 267 | this.tabContainer.selectChild(this.tabBasic); 268 | if (hasSelections) { 269 | this.set('hasLabels', true); 270 | this.emptyStore(this.labelSelectionStore); 271 | this.labelInfos[layerId][sublayer].selections.forEach(lang.hitch(this, function (labelObj) { 272 | labelObj.id = '_' + count++; 273 | labelObj.label = labelObj.name; 274 | this.labelSelectionStore.put(labelObj); 275 | })); 276 | this.labelSelect.set('value', '_' + 1); 277 | this.labelTextbox.set('value', this.labelSelectionStore.get('_' + 1).value); 278 | this.addSelectedLabels(); 279 | } else { 280 | this.set('hasLabels', false); 281 | } 282 | }, 283 | _setFields: function (fields) { 284 | if (!fields.length) { 285 | return; 286 | } 287 | this.set('hasLabels', true); 288 | 289 | fields.forEach(lang.hitch(this, function (f) { 290 | this.labelSelectionStore.put({ 291 | id: f.name, 292 | name: f.alias || f.name, 293 | value: '{' + f.name + '}' 294 | }); 295 | })); 296 | }, 297 | hasLabels: false, 298 | _setHasLabelsAttr: function (labels) { 299 | domClass[labels ? 'remove' : 'add'](this.fieldSpinner, 'dijitHidden'); 300 | }, 301 | /** 302 | * Display a field loading spinner 303 | * set using `this.set('fieldsLoading', true)` 304 | * @type {Boolean} fieldsLoading 305 | */ 306 | fieldsLoading: false, 307 | _setFieldsLoadingAttr: function (loading) { 308 | this.fieldsLoading = loading; 309 | domClass[loading ? 'remove' : 'add'](this.fieldSpinner, 'dijitHidden'); 310 | }, 311 | labelLayers: {}, 312 | constructor: function () { 313 | this.inherited(arguments); 314 | 315 | this.labelSelectionStore = new Memory({ 316 | data: [] 317 | }); 318 | 319 | this.layerStore = new Memory({ 320 | data: [] 321 | }); 322 | 323 | this.colorStore = new Memory({ 324 | data: this.colors 325 | }); 326 | }, 327 | postCreate: function () { 328 | this.inherited(arguments); 329 | 330 | // topics we subscribe to 331 | topic.subscribe(this.topics.show, lang.hitch(this, 'showParent')); 332 | 333 | this.own(this.parentWidget.on('show', lang.hitch(this, function () { 334 | this.tabContainer.resize(); 335 | }))); 336 | 337 | this.own(this.layerSelect.on('change', lang.hitch(this, function (id) { 338 | var layer = this.layerStore.get(id); 339 | this.set('activeLayer', layer); 340 | }))); 341 | 342 | this.colorSelect.set('value', this.color); 343 | 344 | this.own(this.labelSelect.on('change', lang.hitch(this, function (id) { 345 | if (!id) { 346 | return; 347 | } 348 | var newLabel = this.labelSelectionStore.get(id).value; 349 | if (this.appendCheckbox.checked) { 350 | newLabel = this.labelTextbox.value + ' ' + newLabel; 351 | } 352 | this.labelTextbox.set('value', newLabel); 353 | }))); 354 | 355 | //update labels when stuff changes 356 | var items = [this.colorSelect, this.labelTextbox, this.fontTextbox]; 357 | items.forEach(lang.hitch(this, function (item) { 358 | this.own(item.on('change', lang.hitch(this, function () { 359 | this.addSelectedLabels(); 360 | }))); 361 | 362 | })); 363 | }, 364 | showParent: function (event) { 365 | var layer = lang.mixin({ 366 | id: event.layer.id + (event.subLayer ? '_' + event.subLayer.id : '') 367 | }, event); 368 | 369 | // set dropdown values 370 | this.set('activeLayer', this.layerStore.get(layer.id)); 371 | this.layerSelect.set('value', layer.id); 372 | 373 | // toggle parent 374 | if (this.parentWidget) { 375 | if (!this.parentWidget.open && this.parentWidget.toggle) { 376 | this.parentWidget.toggle(); 377 | } else if (this.parentWidget.show) { 378 | this.parentWidget.show(); 379 | this.parentWidget.set('style', 'position: absolute; opacity: 1; left: 350px; top: 190px; z-index: 950;'); 380 | } 381 | } 382 | }, 383 | addSelectedLabels: function () { 384 | var layerId = this.activeLayer.id; 385 | if (!layerId) { 386 | return; 387 | } 388 | 389 | var layer; 390 | if (this.labelLayers[layerId]) { 391 | layer = this.labelLayers[layerId]; 392 | } else if (this.activeLayer.layer.declaredClass === 'esri.layers.ArcGISDynamicMapServiceLayer') { 393 | layer = this.createLayerFromDynamic(this.activeLayer.layer, this.activeLayer.sublayer); 394 | } else if (this.activeLayer.layer.declaredClass === 'esri.layers.FeatureLayer') { 395 | layer = this.activeLayer.layer; 396 | } else { 397 | return; 398 | } 399 | 400 | var labelInfo = { 401 | expression: this.labelTextbox.value, 402 | size: this.fontTextbox.value, 403 | color: this.colorSelect.value 404 | }; 405 | 406 | this.setLabel(layer, labelInfo); 407 | layer.setVisibility(true); 408 | this.labelLayers[layerId] = layer; 409 | }, 410 | setLabel: function (layer, labelInfo) { 411 | 412 | var label = new LabelClass({ 413 | labelExpressionInfo: { 414 | value: labelInfo.expression 415 | }, 416 | useCodedValues: true, 417 | labelPlacement: 'above-center' 418 | }); 419 | var symbol = new TextSymbol(); 420 | symbol.font.setSize(labelInfo.size + 'pt'); 421 | symbol.font.setFamily('Corbel'); 422 | symbol.setColor(new Color(labelInfo.color.toLowerCase())); 423 | label.symbol = symbol; 424 | 425 | layer.setLabelingInfo([label]); 426 | }, 427 | createLayerFromDynamic: function (layer, sublayerId) { 428 | 429 | var serviceURL = layer.url + '/' + sublayerId; 430 | 431 | // generate a unique layer id 432 | var layerId = layer.id + '_' + sublayerId; 433 | 434 | // get a nice layer title 435 | var layerInfos = layer.layerInfos.filter(lang.hitch(this, function (l) { 436 | return l.id === sublayerId; 437 | })); 438 | var title = layerInfos.length ? layerInfos[0].name + ' Labels' : 'Labels'; 439 | 440 | return this.createFeatureLayer({id: layerId, url: serviceURL, title: title}); 441 | }, 442 | createFeatureLayer: function (args) { 443 | var layerOptions = { 444 | mode: FeatureLayer.MODE_ONDEMAND, 445 | outFields: ['*'], 446 | id: args.id || args.layer || 'labels-' + idCounter++, 447 | visible: true, 448 | title: args.title || 'Labels', 449 | opacity: 0 450 | }; 451 | var layer = new FeatureLayer(args.url, layerOptions); 452 | this.map.addLayer(layer); 453 | 454 | // notify layer control and identify 455 | // wait for async layer loads 456 | layer.on('load', lang.hitch(this, function () { 457 | ['layerControl/addLayerControls', 'identify/addLayerInfos'].forEach(function (t) { 458 | topic.publish(t, [{ 459 | type: 'feature', 460 | layer: layer, 461 | title: args.title 462 | }]); 463 | }); 464 | })); 465 | return layer; 466 | }, 467 | /** 468 | * empties a store 469 | */ 470 | emptyStore: function (store) { 471 | store.query().forEach(function (item) { 472 | store.remove(item.id); 473 | }); 474 | } 475 | }); 476 | }); 477 | -------------------------------------------------------------------------------- /widgets/LabelLayer/README.md: -------------------------------------------------------------------------------- 1 | # Label Layer Widget 2 | 3 | Create and modify client side label layers using existing dynamic and feature map layers. Features 4 | automatic indexing of all dynamic map layers in `layerInfos` and minor font 5 | modification capabilities planned (currently supports color). 6 | 7 | Useful in either any of the cmv types, and uses `dojo.topic` to open or show itself 8 | when the topic is published. 9 | 10 | In addition, supports automatically added labels. 11 | 12 | Configure predefined labels: 13 | 14 | ![./docs/label-basic.png](./docs/basic.png) 15 | 16 | Allow users to build complex labels: 17 | 18 | ![./docs/label-advanced.png](./docs/advanced.png) 19 | 20 | ## CMV config 21 | 22 | **Map Options** 23 | 24 | This widget requires the map option `showLabels: true` enabled in order for 25 | labels to show up in your map. 26 | 27 | **Title Pane** 28 | 29 | ```javascript 30 | labelLayer: { 31 | title: 'Map Labels', 32 | id: 'labelLayer', 33 | include: true, 34 | type: 'titlePane', 35 | position: 15, 36 | path: 'roemhildtg/LabelLayer', 37 | options: { 38 | // required! 39 | map: true, 40 | layerControlLayerInfos: true, 41 | 42 | 43 | // OPTIONAL!! 44 | // label layer options to exclude layers and exclude fields 45 | // similar to cmv identify config 46 | labelInfos: { 47 | : { // a string like 'assets' 48 | exclude: false, // exclude this entire layer 49 | : { // a number representign the map service layer id 50 | exclude: false, // set the sublayer to be excluded from the label widget 51 | fields: [{ 52 | alias: 'Field Label', 53 | name: 'field_name' 54 | }], 55 | 56 | // the available select dropdowns for each layer 57 | selections: [{ // sublayer id 58 | name: 'Diameter - Material', //displayed to user 59 | value: '{diameter}" {material}' //label string 60 | }] 61 | } 62 | } 63 | }, 64 | // 65 | // automatically created labels 66 | defaultLabels: [{ 67 | layer: 'layer_id', 68 | sublayer: 13, // only for dynamic layers 69 | visible: true, 70 | name: 'Diameter - Material', //displayed to user 71 | expression: '{diameter}" {material}' //label string 72 | color: '#000', 73 | fontSize: 8, 74 | url: 'url to feature layer ' //if we want to create it, 75 | title: 'layer title', 76 | }], 77 | // 78 | // 79 | // // override the default colors 80 | // colors: [{ 81 | // name: 'Black', 82 | // id: '#000' 83 | // }], 84 | // 85 | // // set the default color choice using the id 86 | // color: '#000', 87 | // 88 | // //default font size 89 | // fontSize: 8, 90 | } 91 | }, 92 | ``` 93 | 94 | **Floating Style** 95 | 96 | ```javascript 97 | labelLayer: { 98 | title: 'Map Labels', 99 | id: 'labelLayer', 100 | include: true, 101 | type: 'floating', 102 | position: 15, 103 | path: 'roemhildtg/LabelLayer', 104 | options: { 105 | //options here (see above) 106 | } 107 | }, 108 | ``` 109 | 110 | Use layer control to publish the topic when the layer a layer's menu is selected: 111 | 112 | ```javascript 113 | layerControl: { 114 | include: true, 115 | id: 'layerControl', 116 | type: 'titlePane', 117 | path: 'gis/dijit/LayerControl', 118 | title: 'Layers', 119 | open: true, 120 | position: 0, 121 | options: { 122 | 123 | // add a menu option to feature layers 124 | menu: { 125 | feature: [{ 126 | label: 'Labels', 127 | topic: 'showLabelPicker', 128 | iconClass: 'fa fa-font fa-fw' 129 | }] 130 | }, 131 | 132 | // add a sublayer menu for dynamic layers 133 | subLayerMenu: { 134 | dynamic: [{ 135 | label: 'Labels', 136 | topic: 'showLabelPicker', 137 | iconClass: 'fa fa-font fa-fw' 138 | }] 139 | }, 140 | map: true, 141 | layerControlLayerInfos: true, 142 | separated: true, 143 | vectorReorder: true, 144 | overlayReorder: true 145 | } 146 | } 147 | 148 | ``` 149 | 150 | ## Outside of CMV use 151 | 152 | ```javascript 153 | require(['roemhildtg/LabelLayer'], function(LabelLayer){ 154 | new LabelLayer({ 155 | layerInfos: [{ 156 | //cmv layer info 157 | }], 158 | map: mapObject, //esri map object 159 | }, 'domNode'); 160 | }); 161 | ``` 162 | 163 | ## Changelog 164 | 165 | #### 4/27/2017: 166 | 167 | **Change Notes:** 168 | 169 | * Added the ability to exclude layers by id and sublayer id 170 | * Added the ability to exclude fields from a layer by overriding the field config 171 | * Fixed an issue with the creation of default label layers 172 | * Added a loading icon when fields are being retrieved from the server 173 | 174 | **New Config API:** 175 | Changes to the config were made to simplify excluding layers and fields. As a 176 | result, existing configs will have to be migrated. 177 | 178 | * `labelInfos` is the new primary config property. This is similar to the cmv identify config. Each label infos consts of a nested object 179 | * `labelSelections` has been changed to a property on the `labelInfos` called `selections` 180 | * Note: `defaultLabels` has not changed its position in the config 181 | 182 | Old Config: 183 | ```javascript 184 | labelSelections: [{ 185 | name: 'My label name', 186 | value: '{label_field} - {other_field}' 187 | }] 188 | ``` 189 | 190 | New Config: 191 | ```javascript 192 | layerId: { 193 | 0: { 194 | selections: [{ 195 | name: 'My label name', 196 | value: '{label_field} - {other_field}' 197 | }] 198 | } 199 | } 200 | ``` 201 | -------------------------------------------------------------------------------- /widgets/LabelLayer/docs/advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/cmv-widgets/90d5cd08a949fdfdcdcc15dfc9a394a07fa2b2e5/widgets/LabelLayer/docs/advanced.png -------------------------------------------------------------------------------- /widgets/LabelLayer/docs/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/cmv-widgets/90d5cd08a949fdfdcdcc15dfc9a394a07fa2b2e5/widgets/LabelLayer/docs/basic.png -------------------------------------------------------------------------------- /widgets/LabelLayer/nls/LabelLayer.js: -------------------------------------------------------------------------------- 1 | define({ 2 | root: { 3 | appendCheckbox: { 4 | label: 'Add to existing label' 5 | }, 6 | layerSelect: { 7 | label: 'Choose a layer' 8 | }, 9 | colorSelect: { 10 | label: 'Label color' 11 | }, 12 | addButton: { 13 | label: 'Apply' 14 | }, 15 | removeButton: { 16 | label: 'Remove' 17 | }, 18 | labelSelect: { 19 | label: 'Choose a Label' 20 | }, 21 | fontSize: { 22 | label: 'Label size' 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /widgets/LabelLayer/templates/LabelLayer.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 5 |
    6 | 13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 | 20 | Loading fields... 21 |
    22 | 24 |
    25 | 32 | 33 |
    34 | 35 | 36 | 37 |
    38 |
    39 |
    40 | 41 | 42 | 43 | 44 | 45 |
    46 | 47 | 48 | 49 | 50 | 51 | 52 |
    53 | 54 | 55 | 56 | 58 | 65 | 66 |
    67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
    75 |
    76 |
    77 | -------------------------------------------------------------------------------- /widgets/LoginCookie.js: -------------------------------------------------------------------------------- 1 | /* https://developers.arcgis.com/javascript/3/jssamples/widget_identitymanager_client_side.html */ 2 | define(['dojo/_base/declare', 3 | 'dojo/_base/window', 4 | 'dojo/_base/lang', 5 | 'dojo/cookie', 6 | 'esri/IdentityManager' 7 | ], function (declare, window, lang, cookie, esriId) { 8 | var global = window.global; 9 | return declare(null, { 10 | /** 11 | * the local storage or cookie key name 12 | * @type {String} 13 | */ 14 | key: 'esri_cred', 15 | /** 16 | * initializes the event listener to store credentials and 17 | * loads existing credentials into the identity manager 18 | */ 19 | constructor: function (params) { 20 | this.inherited(arguments); 21 | lang.mixin(this, params); 22 | 23 | //remember credentials once they're created 24 | esriId.on('credential-create', lang.hitch(this, 'storeCredentials')); 25 | 26 | //initialize our saved credentials 27 | this.loadCredentials(); 28 | }, 29 | /** 30 | * Store the credentials in local storage for next time 31 | */ 32 | storeCredentials: function () { 33 | 34 | // make sure there are some credentials to persist 35 | if (esriId.credentials.length === 0) { 36 | return; 37 | } 38 | 39 | // serialize the ID manager state to a string 40 | var idString = JSON.stringify(esriId.toJson()); 41 | // store it client side 42 | if (this.supportsLocalStorage()) { 43 | // use local storage 44 | global.localStorage.setItem(this.key, idString); 45 | // console.log("wrote to local storage"); 46 | } else { 47 | // use a cookie 48 | cookie(this.key, idString, { 49 | expires: 1 50 | }); 51 | // console.log("wrote a cookie :-/"); 52 | } 53 | }, 54 | /** 55 | * initialize the esri identity manager with credentials if they were stored 56 | */ 57 | loadCredentials: function () { 58 | if (this.credentials) { 59 | esriId.initialize(this.credentials); 60 | // console.log('provided credentials were initialized'); 61 | return; 62 | } 63 | var idJson, idObject; 64 | 65 | if (this.supportsLocalStorage()) { 66 | // read from local storage 67 | idJson = global.localStorage.getItem(this.key); 68 | } else { 69 | // read from a cookie 70 | idJson = cookie(this.key); 71 | } 72 | 73 | if (idJson && idJson != 'null' && idJson.length > 4) { 74 | try { 75 | idObject = JSON.parse(idJson); 76 | esriId.initialize(idObject); 77 | // console.log('creds loaded from local storage', idObject); 78 | } catch (e) { 79 | //TODO: growl 80 | // console.log(e); 81 | } 82 | } else { 83 | // console.log('didn\'t find anything to load :('); 84 | } 85 | }, 86 | /** 87 | * Checks for local storage support 88 | * @return {Boolean} whether or not local storage is supported 89 | */ 90 | supportsLocalStorage: function () { 91 | try { 92 | return 'localStorage' in global && global.localStorage !== null; 93 | } catch (e) { 94 | return false; 95 | } 96 | }, 97 | /** 98 | * clears existing credentials from local storage or cookie. Essentially 99 | * like a logout function, except it doesn't remove credentials from memory 100 | * in the identity manager 101 | */ 102 | clearCredentials: function () { 103 | if (this.supportsLocalStorage()) { 104 | global.localStorage.removeItem(this.key); 105 | } else { 106 | cookie(this.key, '', {}); 107 | } 108 | } 109 | 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /widgets/LoginCookie/README.md: -------------------------------------------------------------------------------- 1 | # Login Cookie 2 | 3 | A simple local storage or cookie cache for Esri Identity Manager credentials. This dojo class listens for new credentials added to an esri identity manager and caches them in local storage or a cookie if local storage is not supported. When the widget is constructed, it automatically checks for stored credentials in the cookie and if it finds them, will preload them into the Identity Manager. This will save your users from having to login every time they visit the app. 4 | 5 | ## Usage 6 | 7 | ```javascript 8 | //put this somewhere before your app starts trying to load layers 9 | require(['widgets/LoginCookie'], function(LoginCookie){ 10 | (new LoginCookie({ 11 | //optional: specify the key for local storage or cookie name 12 | //key: 'my_credentials' 13 | })); 14 | }); 15 | 16 | //or inside a module: 17 | define(['widgets/LoginCookie'], function(LoginCookie){ 18 | (new LoginCookie({ 19 | //optional: specify the key for local storage or cookie name 20 | //key: 'my_credentials' 21 | })); 22 | 23 | return {}; 24 | }); 25 | ``` 26 | -------------------------------------------------------------------------------- /widgets/MetadataDialog.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dojo/_base/lang', 4 | 'dijit/_WidgetBase', 5 | 'dijit/Dialog', 6 | 'dijit/DialogUnderlay', 7 | 'dijit/_WidgetsInTemplateMixin', 8 | 'dgrid/Grid', 9 | 'dojo/topic', 10 | 'esri/request', 11 | 'dojo/text!./MetadataDialog/templates/dialogContent.html', 12 | 'xstyle/css!./MetadataDialog/css/MetadataDialog.css', 13 | 'dijit/layout/TabContainer', 14 | 'dijit/layout/ContentPane' 15 | ], function(declare, lang, _WidgetBase, Dialog, DialogUnderlay, _WidgetsInTemplateMixin, 16 | Grid, topic, request, dialogContent) { 17 | return declare([_WidgetBase, Dialog, _WidgetsInTemplateMixin], { 18 | topic: 'layerControl/showMetadata', 19 | templateString: dialogContent, 20 | style: 'width:500px;height:450px;', 21 | widgetsInTemplate: true, 22 | title: '', 23 | description: '', 24 | _setDescriptionAttr: { 25 | node: 'descriptionNode', 26 | type: 'innerHTML' 27 | }, 28 | details: '', 29 | detailsTemplate: ['
    • ID: {id}
    • ', 30 | '
    • Parent Layer: {parentLayer.name} ({parentLayer.id})
    • ', 31 | '
    • Capabilities: {capabilities}
    • ', 32 | '
    • Metadata page
    • ', 33 | '
    ' 34 | ].join(''), 35 | _setDetailsAttr: { 36 | node: 'detailsNode', 37 | type: 'innerHTML' 38 | }, 39 | fields: [], 40 | open: false, 41 | metadata: { 42 | name: '' 43 | }, 44 | /** 45 | * initializes the widget's field grid and subscribes to the topic 46 | * `layerControl/showMetadata` 47 | */ 48 | postCreate: function() { 49 | this.inherited(arguments); 50 | this.dgrid = new Grid({ 51 | columns: { 52 | name: 'Field', 53 | alias: 'Name', 54 | type: 'Type' 55 | } 56 | }, this.fieldsNode); 57 | topic.subscribe('layerControl/showMetadata', lang.hitch(this, '_fetchMetadata')); 58 | }, 59 | /** 60 | * resize the tabcontainer when the dialog is resized 61 | */ 62 | resize: function() { 63 | this.inherited(arguments); 64 | this.tabContainer.resize(); 65 | }, 66 | /** 67 | * fetches metadata from a published topic event where the topic 68 | * must have the following properties: 69 | * - layer.url - the url to the layer's rest page `'.../Mapserver'` 70 | * - subLayer.id - The sublayer id number 71 | * @param {[type]} event [description] 72 | * @return {[type]} [description] 73 | */ 74 | _fetchMetadata: function(event) { 75 | var url = event.layer.url + (event.subLayer ? '/' + event.subLayer.id : ((event.layer.layerInfos.length === 1) ? '/0' : '')); 76 | new request({ 77 | url: url, 78 | content: { 79 | f: 'json' 80 | } 81 | }) 82 | .then(lang.hitch(this, '_showMetadata', url)) 83 | .otherwise(lang.hitch(this, '_handleError')); 84 | }, 85 | /** 86 | * Displays a given metadata using fields, name, and description 87 | * @param {object} data The raw json object from a rest page 88 | */ 89 | _showMetadata: function(url, data) { 90 | data.url = url; 91 | this.set({ 92 | 'title': data.name, 93 | description: data.description, 94 | fields: data.fields, 95 | details: lang.replace(this.detailsTemplate, data) 96 | }); 97 | this.dgrid.renderArray(this.fields); 98 | this.show(); 99 | //TODO: this causes an error when the dialog actually closes... 100 | //need to override this.hide() with a custom method 101 | // DialogUnderlay.hide(); 102 | }, 103 | /** 104 | * handles rest retrieval errors usually caused by not having a proxy or cors set up 105 | * @param {Error} e The error 106 | */ 107 | _handleError: function(e) { 108 | this._showMetadata({ 109 | id: e, 110 | name: 'Error', 111 | description: 'The query could not execute. Is a proxy configured?' 112 | }); 113 | } 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /widgets/MetadataDialog/README.md: -------------------------------------------------------------------------------- 1 | MetadataDialog 2 | ============== 3 | 4 | Queries the rest endpoint of a sublayer and displays its service description. In some cases, public access to the rest endpoint of the server is not accessible to read the metadata, so this widget works around that issue. For usage in CMV (v1.3.4) with [sublayer menu option on the LayerControl](http://docs.cmv.io/en/latest/widgets/LayerControl/). 5 | 6 | ## Configuration: 7 | 8 | ```JavaScript 9 | //layerControl widget options: 10 | subLayerMenu: { 11 | dynamic: [{ 12 | label: 'Show Metadata...', 13 | iconClass: 'fa fa-info fa-fw', 14 | topic: 'showMetadata' 15 | }] 16 | } 17 | ``` 18 | 19 | ```JavaScript 20 | //widget config 21 | metadata: { 22 | include: true, 23 | id: 'metadata', 24 | type: 'invisible', 25 | path: 'gis/widgets/MetadataDialog', 26 | options: {} 27 | } 28 | ``` 29 | 30 | ## Alternative configuration: 31 | 32 | Widget developers can use this widget by publishing the following topic: `LayerControl/heatMap` and passing the correct parameters. 33 | 34 | ```JavaScript 35 | topic.publish('LayerControl/showMetadata', { 36 | layer: dynamicMapServiceLayer, 37 | subLayer: dynamicMapServiceSubLayer 38 | }); 39 | ``` 40 | -------------------------------------------------------------------------------- /widgets/MetadataDialog/css/MetadataDialog.css: -------------------------------------------------------------------------------- 1 | .metadataDescription { 2 | width:100%; 3 | height:400px; 4 | overflow-y: auto; 5 | } 6 | 7 | .metadataDescription h1 { 8 | font-size:14px; 9 | } 10 | 11 | .metadataDescription p { 12 | font-size:12px; 13 | font-weight:bold; 14 | } 15 | 16 | .metadataDescription div { 17 | font-size: 12px; 18 | } 19 | 20 | .metadataDescription ul, ol { 21 | margin: 0; 22 | padding:0 15px; 23 | } 24 | .metadataDescription li { 25 | margin:10px 0; 26 | padding:0 5px 0 0 27 | } 28 | 29 | -------------------------------------------------------------------------------- /widgets/MetadataDialog/metadatadialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/cmv-widgets/90d5cd08a949fdfdcdcc15dfc9a394a07fa2b2e5/widgets/MetadataDialog/metadatadialog.png -------------------------------------------------------------------------------- /widgets/MetadataDialog/templates/dialogContent.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /widgets/PDFGenerator.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dijit/_WidgetBase', 4 | 'dijit/_Templated', 5 | 'dojo/topic', 6 | 'dojo/_base/lang', 7 | 'dijit/form/Button', 8 | 'pdfmake/pdfmake', 9 | 'dojo/i18n!./PDFGenerator/nls/PDFGenerator' 10 | 11 | ], function (declare, _WidgetBase, _Templated, topic, lang, Button, pdfmake, i18n) { 12 | 13 | // make sure pdfmake is loaded then import the font vs 14 | require(['pdfmake/vfs_fonts']); 15 | 16 | var defIndex = 0; 17 | 18 | return declare([_WidgetBase], { 19 | i18n: i18n, 20 | attributesTableTopic: 'attributesContainer/tableAdded', 21 | topic: 'pdf/generate', 22 | pdfDefinitions: [{ 23 | id: 'parcel', 24 | // optional attributes tab title 25 | title: 'Parcels', 26 | // the template to place in the pdf for each row 27 | rowTemplate: '{DEEDHOLD}\n' + 28 | '{MAILADDR}\n' + 29 | '{MAILCITY}, {MAILSTATE} {MAILZIP}\n\n', 30 | 31 | // the button to add to the attributes table toolbar 32 | button: { 33 | label: ' Mailing Labels' 34 | }, 35 | 36 | //the number of columns in the pdf document 37 | columns: 3 38 | 39 | // the default pdf options to override, i.e. fontSize 40 | // pdfDefaults: {defaultStyle: { fontSize: 8 }} 41 | }], 42 | _setPdfDefinitionsAttr: function (defs) { 43 | defs.forEach(function (d) { 44 | if (!d.id) { 45 | d.id = 'pdf-' + defIndex ++; 46 | } 47 | }); 48 | this.pdfDefinitions = defs; 49 | }, 50 | tabsWithButtons: {}, 51 | postCreate: function () { 52 | this.inherited(arguments); 53 | var _this = this; 54 | //subscribe a topic to modify the attributes container add tab 55 | topic.subscribe(this.attributesTableTopic, function (tab) { 56 | if (_this.tabsWithButtons[tab.title]) { 57 | return; 58 | } 59 | _this.pdfDefinitions.forEach(function (def) { 60 | if (def.tab === tab.title) { 61 | _this.addButton(tab, def); 62 | } 63 | }); 64 | _this.tabsWithButtons[tab.title] = true; 65 | }); 66 | 67 | // subscribe to our own topic 68 | topic.subscribe(this.topic, lang.hitch(this, 'generatePDF')); 69 | }, 70 | addButton: function (tab, options) { 71 | tab.attributesTableToolbarDijit.addChild(new Button(lang.mixin(options.button, { 72 | tab: tab, 73 | generator: this, 74 | onClick: function () { 75 | this.generator.generatePDF(tab.grid.store.data, options.id); 76 | } 77 | }))); 78 | }, 79 | getDefinition: function (id) { 80 | var result = this.pdfDefinitions.filter(function (def) { 81 | return def.id === id; 82 | }); 83 | 84 | if (result.length) { 85 | return result[0]; 86 | } 87 | return null; 88 | }, 89 | generatePDF: function (data, id) { 90 | var def = this.getDefinition(id); 91 | if (!def) { 92 | throw Error('The specified definition could not be found: ' + id); 93 | } 94 | var dd = lang.mixin({ 95 | content: { 96 | columns: [] 97 | }, 98 | styles: { 99 | header: { 100 | fontSize: 18, 101 | bold: true 102 | }, 103 | bigger: { 104 | fontSize: 15, 105 | italics: true 106 | } 107 | }, 108 | defaultStyle: { 109 | columnGap: 20, 110 | fontSize: 10 111 | } 112 | }, def.pdfDefaults || {}); 113 | 114 | 115 | for (var i = 0; i < def.columns; i ++) { 116 | dd.content.columns.push({ 117 | text: [] 118 | }); 119 | } 120 | 121 | for (i = 0; i < data.length; i ++) { 122 | for (var j = 0; j < def.columns && i < data.length; j ++) { 123 | dd.content.columns[j].text.push(lang.replace(def.rowTemplate, data[i])); 124 | i ++; 125 | } 126 | } 127 | 128 | window.pdfMake.createPdf(dd).open(); 129 | } 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /widgets/PDFGenerator/nls/PDFGenerator.js: -------------------------------------------------------------------------------- 1 | define({ 2 | root: { 3 | } 4 | }); 5 | -------------------------------------------------------------------------------- /widgets/RelationshipTable/README.md: -------------------------------------------------------------------------------- 1 | # Relationship Table Tabs 2 | 3 | A tabbed widget for displaying and interacting with tables related to feature layers. Includes a [child widget RelationshipTable/RelationshipTable](#relationship-table-widget) to use in other situations, like the identify popup (see below). 4 | 5 | Intended for use in [CMV v1.3.3](https://github.com/cmv/cmv-app/) 6 | 7 | ## Requirements 8 | 9 | - Layer mode should be set to `ON_DEMAND` or `SNAPSHOT`. `SELECTION` will not work currently. 10 | - Ensure [a proxy](https://github.com/Esri/resource-proxy) is configured and working 11 | 12 | ## Config 13 | 14 | ### CMV pane and widget config 15 | 16 | ```javascript 17 | panes: { 18 | left: { 19 | splitter: true 20 | } 21 | bottom: { 22 | id: 'sidebarBottom', 23 | placeAt: 'outer', 24 | splitter: true, 25 | collapsible: true, 26 | region: 'bottom', 27 | style: 'height:300px;display:none;', 28 | content: '
    ' 29 | } 30 | } 31 | ``` 32 | 33 | ```javascript 34 | widgets: { 35 | //.... 36 | relatedRecords: { 37 | include: true, 38 | id: 'relatedRecords', 39 | type: 'domNode', 40 | srcNodeRef: 'relatedRecords', 41 | path: 'gis/CMV_Widgets/widgets/RelationshipTableTabs', 42 | title: 'Related Records', 43 | options: { 44 | //required option 45 | layerControlLayerInfos: true, 46 | 47 | //optional relationships property 48 | relationships: { 49 | : { //layerID (string) key refers to featurelayer id in the operationalLayers array 50 | : { //relationshipID (integer) key referrs to the relationship id on the rest services page 51 | //relationship tab title 52 | title: 'Inspections', 53 | 54 | //set exclude to true to skip this relationship 55 | exclude: false, 56 | 57 | //other dgrid options like columns may be included 58 | } 59 | } 60 | } 61 | } 62 | } 63 | //..... 64 | } 65 | ``` 66 | 67 | ## Widget Options: 68 | 69 | Key | Type | Default | Description 70 | ---------------- | ---------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 71 | `layerInfos` | `boolean` | `null` | set layerControlLayerInfos to true, a layerInfos object is required 72 | `relationships` | `{object}` | `{}` | An object describing each relationship. Details below 73 | `tabPosition` | `string` | `'top'` | dijit tabContainer.tabPosition property: `'top'`, `'left-h'`, Not working due to layout issues: `'bottom'`, `'right-h'` 74 | `tabContainerId` | `string` | `null` | An optional id of a widget that is a tabContainer or contains a tabContainer property. If specified, this widgets tables will be added to an existing tabContainer like the one developed by tmcgee [here](https://github.com/tmcgee/cmv-widgets/blob/master/widgets/AttributesTable/README.md) instead of creating a new one. 75 | 76 | ### `relationships` property 77 | 78 | Each relationship object may have the following properties as well as [properties used by dgrid](https://github.com/SitePen/dgrid/blob/master/doc/components/core-components/OnDemandList-and-OnDemandGrid.md) 79 | 80 | Key | Type | Default | Description 81 | --------- | --------- | ------------------------------ | --------------------------------------------------------------------------- 82 | `title` | `string` | layer.name - relationship.name | the title for the relationship tab 83 | `exclude` | `boolean` | `null` | by default all relationships are included. set exclude: false to avoid this 84 | 85 | You may also use this widget in a programatic matter: 86 | 87 | ```javascript 88 | new RelationshipTableTabs({ 89 | layerInfos: [{ 90 | id: 'demographics', 91 | layer: demographicsLayer 92 | }] 93 | //other options 94 | }, 'domNodeId'); 95 | ``` 96 | 97 | # Relationship Table Widget 98 | 99 | This class can be used standalone in situations where you don't want tabbed tables, like the identify widget. 100 | 101 | ```javascript 102 | new RelationshipTable({ 103 | attributes: data.attributes, 104 | style: 'width:100%;', 105 | title: 'Bridge Links', 106 | objectIdField: 'OBJECTID', 107 | relationshipId: 0, 108 | url: '/arcgis/rest/services/Apps/RelatedRecords/MapServer/0', 109 | columns: [{ 110 | label: 'Link', 111 | field: 'Link_URL', 112 | formatter: formatters.url 113 | }, { 114 | label: 'Category', 115 | field: 'Category' 116 | }] 117 | }, 'domNodeId'); 118 | ``` 119 | 120 | ## Constructor Options 121 | 122 | In addition to the options below, RelationshipTable inherits from `_WidgetBase`, `OnDemandGrid`, `ColumnHider` and `DijitRegistry`. 123 | 124 | [See dgrid docs for additional useful properties](https://github.com/SitePen/dgrid/blob/v1.1.0/doc/components/core-components/OnDemandList-and-OnDemandGrid.md) 125 | 126 | Key | Type | Default | Description 127 | ---------------------- | ---------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- 128 | `objectIdField` | `String` | `null` | The object id field for this feature layer. This is typically the `layer.objectIdField` property. If you have an alias on the object id field, use the alias. 129 | `attributes` | `{object}` | `null` | A map of attributes returned from an identify result. This is typically the feature.attributes property. It must have an property with the name of the `objectIdField`. 130 | `url` | `String` | `''` | The url for the feature layer. 131 | `relationshipId` | `Number` | `0` | The id of the relationship to use. This is found on the rest services page for the feature layer, by adding `?f=json` to the url. 132 | `defaultNoDataMessage` | `String` | `'No results.'` | The default message to display when there is no data. 133 | `errorDataMessage` | `String` | `'The query could not be executed. Is a proxy configured?'` | The default error message to display. 134 | `loadingMessage` | `String` | `'Loading...'` | The default error message to display. 135 | 136 | ## Include in identify popup: 137 | 138 | Related Records comes with a simple content formatter for cmv. Add the required file to your `identify.js` and use it to include relationship queries in your popup. 139 | 140 | The factory is a function where you pass an array of objects. Each object needs the properties defined in the constructor options above. 141 | 142 | ```javascript 143 | define([ 144 | 'roemhildtg/RelationshipTable/identify/factory' 145 | ], function (factory) { 146 | 147 | 148 | //.........other identify.js stuff 149 | identifies: { 150 | layerId: { // operational layers layer `id` 151 | 0: { 152 | title: 'Bridge Inventory', 153 | content: factory([{ 154 | title: 'Bridge Links', 155 | objectIdField: 'OBJECTID', 156 | relationshipId: 0, 157 | url: '/arcgis/rest/services/Apps/RelatedRecords/MapServer/0', 158 | columns: [{ 159 | label: 'Link', 160 | field: 'Link_URL' 161 | }, { 162 | label: 'Category', 163 | field: 'Category' 164 | }] 165 | }// , { ... more relationships } if your dynamic layer has more relationships, you can add more than one 166 | ]) 167 | } 168 | } 169 | } 170 | ``` 171 | 172 | ## Changes: 173 | 174 | 3/6/2017: Add an identify content factory 175 | 176 | 5/30/2015: Major widget changes 177 | 178 | - moved majority of relationship querying and display code to a self-contained class RelationshipTable 179 | - renamed class to RelationshipTableTabs to better describe the widget 180 | - renamed widget folder to RelationshipTable 181 | - renamed columnInfos property to relationships 182 | 183 | **RelationshipTable Class** 184 | 185 | - Can be used standalone (like in identify popup) or with the RelatedRecordTabs 186 | - Inherits all properties, methods and events from `OnDemandGrid`, `ColumnHider`, and `DijitRegistry` 187 | -------------------------------------------------------------------------------- /widgets/RelationshipTable/RelationshipTable.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dijit/_WidgetBase', 4 | 'dojo/_base/array', 5 | 'dojo/_base/lang', 6 | 'esri/request', 7 | 'dojo/store/Memory', 8 | 'dgrid/OnDemandGrid', 9 | 'dgrid/extensions/ColumnHider', 10 | 'dgrid/extensions/DijitRegistry', 11 | 'dojo/topic' 12 | ], function (declare, _WidgetBase, array, lang, request, Memory, OnDemandGrid, ColumnHider, DijitRegistry, topic) { 13 | return declare([_WidgetBase, OnDemandGrid, ColumnHider, DijitRegistry], { 14 | //object id field for the feature layer (use field alias) 15 | objectIdField: null, 16 | //field value mappings of attributes from the feature layer to query related records from 17 | //should have a field with the same name as objetIdField 18 | attributes: null, 19 | //the url to the feature service 20 | url: '', 21 | //the relationship id for the feature service relationship 22 | relationshipId: 0, 23 | //default message to display when no results are returned 24 | defaultNoDataMessage: 'No results.', 25 | //default error message 26 | errorDataMessage: 'The query could not be executed. Is a proxy configured?', 27 | //default message when the query is being executed 28 | loadingMessage: 'Loading...', 29 | baseClass: 'RelationshipTable', 30 | postCreate: function () { 31 | this.inherited(arguments); 32 | if (!this.objectIdField) { 33 | topic.publish('viewer/handleError', { 34 | source: 'RelationshipTable', 35 | error: 'This widget requires an objetIdField' 36 | }); 37 | this.destroy(); 38 | } 39 | this.store = new Memory({ 40 | idProperty: this.objectIdField 41 | }); 42 | this.noDataMessage = this.defaultNoDataMessage; 43 | if (this.attributes) { 44 | this.getRelatedRecords(this.attributes); 45 | } 46 | }, 47 | getRelatedRecords: function (attributes) { 48 | if (this.deferred) { 49 | this.deferred.cancel(); 50 | } 51 | //reset the grid's data 52 | this.store.setData([]); 53 | this.noDataMessage = this.loadingMessage; 54 | this.refresh(); 55 | //get the objectID 56 | var objectID = attributes[this.objectIdField]; 57 | if (!objectID) { 58 | topic.publish('viewer/handleError', { 59 | source: 'RelationshipTable', 60 | error: this.objectIdField + ' ObjectIDField was not found in attributes' 61 | }); 62 | return; 63 | } 64 | //build a query 65 | var query = { 66 | url: this.url, 67 | objectIds: [objectID], 68 | outFields: ['*'], 69 | relationshipId: this.relationshipId 70 | }; 71 | this.deferred = this._queryRelatedRecords(query); 72 | this.deferred 73 | .then(lang.hitch(this, '_handleRecords')) 74 | .otherwise(lang.hitch(this, '_handleError')); 75 | return this.deferred.promise; 76 | }, 77 | _handleRecords: function (results) { 78 | this.deferred = null; 79 | this.noDataMessage = this.defaultNoDataMessage; 80 | //if we don't have columns set yet 81 | if (!this.get('columns').length) { 82 | this.set('columns', array.map(results.fields, lang.hitch(this, function (field) { 83 | return { 84 | label: field.alias, 85 | field: field.name 86 | }; 87 | }))); 88 | } 89 | if (results.relatedRecordGroups.length > 0) { 90 | array.forEach(results.relatedRecordGroups[0].relatedRecords, lang.hitch(this, '_addRecord')); 91 | } 92 | this.refresh(); 93 | }, 94 | _handleError: function (e) { 95 | this.noDataMessage = this.errorDataMessage; 96 | this.refresh(); 97 | }, 98 | _addRecord: function (record) { 99 | this.store.put(record.attributes); 100 | }, 101 | /* 102 | * custom queryRelatedRecords function 103 | * layer.queryRelatedRecords doesn't return the field 104 | * properties such as alias. 105 | * @param {object} query - object with the query properties 106 | * @param function callback - function(responseFields, relatedRecordGroups) 107 | * query properties: 108 | * - url: the url of the featureLayer 109 | * - objectIds: [object IDs] 110 | * - outFields: ['*'], 111 | * - relationshipId: integer 112 | */ 113 | _queryRelatedRecords: function (query) { 114 | var deferred = new request({ 115 | url: query.url + '/queryRelatedRecords', 116 | content: { 117 | returnGeometry: false, 118 | objectIDs: query.objectIds, 119 | outFields: query.outFields, 120 | relationshipId: query.relationshipId, 121 | f: 'json' 122 | }, 123 | handleAs: 'json' 124 | }); 125 | return deferred; 126 | }, 127 | destroy: function () { 128 | if (this.deferred) { 129 | this.deferred.cancel(); 130 | this.deferred = null; 131 | } 132 | this.inherited(arguments); 133 | }, 134 | resize: function () { 135 | this.inherited(arguments); 136 | } 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /widgets/RelationshipTable/identify/factory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines a content formatter for showing relationships in a popup 3 | * 4 | * Usage: 5 | * 6 | * ```javascript 7 | * 8 | define([roemhildtg/RelationshipTable/identify/factory], function(factory){ 9 | 10 | identifies: { 11 | layerId: { 12 | 0: { 13 | content: factory([{ 14 | title: 'Bridge Links', 15 | objectIdField: 'OBJECTID', 16 | relationshipId: 0, 17 | url: '/arcgis/rest/services/Apps/RelatedRecords/MapServer/0', 18 | columns: [{ 19 | label: 'Link', 20 | field: 'Link_URL', 21 | formatter: formatters.url 22 | }, { 23 | label: 'Category', 24 | field: 'Category' 25 | }] 26 | }]) 27 | } 28 | } 29 | } 30 | 31 | }); 32 | * 33 | * ``` 34 | */ 35 | 36 | define([ 37 | 'dojo/dom-construct', 38 | 'dojo/_base/lang', 39 | 'dijit/layout/TabContainer', 40 | 'dijit/layout/ContentPane', 41 | '../RelationshipTable' 42 | ], function (domConstruct, lang, TabContainer, ContentPane, RelationshipTable) { 43 | 44 | function attributes (identifyResults) { 45 | //set up our templates for lang.replace 46 | var row = '{field}{value}'; 47 | var table = '{rows}
    '; 48 | var rows = []; 49 | for (var a in identifyResults.attributes) { 50 | if (identifyResults.attributes.hasOwnProperty(a)) { 51 | rows.push(lang.replace(row, { 52 | field: a, 53 | value: identifyResults.attributes[a] 54 | })); 55 | } 56 | } 57 | var html = lang.replace(table, { 58 | rows: rows.join('') 59 | }); 60 | return html; 61 | } 62 | 63 | function factory (relationships) { 64 | return function (data) { 65 | 66 | var container = new TabContainer({ 67 | style: 'width:100%;height:280px;' 68 | }, domConstruct.create('div')); 69 | 70 | // add a basic attributes table 71 | container.addChild(new ContentPane({ 72 | title: 'Properties', 73 | content: attributes(data) 74 | })); 75 | 76 | // create new relationship tables for each relationship passed 77 | relationships.forEach(function (rel) { 78 | container.addChild(new RelationshipTable(lang.mixin({ 79 | attributes: data.attributes, 80 | title: 'Related Records', 81 | style: 'width:100%;' 82 | }, rel))); 83 | }); 84 | setTimeout(function () { 85 | container.resize(); 86 | }, 200); 87 | return container.domNode; 88 | }; 89 | } 90 | 91 | return factory; 92 | }); 93 | -------------------------------------------------------------------------------- /widgets/RelationshipTable/relatedRecords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/cmv-widgets/90d5cd08a949fdfdcdcc15dfc9a394a07fa2b2e5/widgets/RelationshipTable/relatedRecords.png -------------------------------------------------------------------------------- /widgets/RelationshipTable/templates/RelatedRecordTable.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    -------------------------------------------------------------------------------- /widgets/RelationshipTableTabs.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dijit/_WidgetBase', 4 | 'dojo/_base/array', 5 | 'dojo/_base/lang', 6 | 'dojo/ready', 7 | 'dijit/registry', 8 | 'dijit/_Container', 9 | 'dijit/layout/TabContainer', 10 | 'dojo/topic', 11 | './RelationshipTable/RelationshipTable' 12 | ], function (declare, _WidgetBase, array, lang, ready, registry, _Container, TabContainer, topic, RelationshipTable) { 13 | return declare([_WidgetBase, _Container], { 14 | //tab position for the TabContainer 15 | tabPosition: 'top', 16 | //optional relationships information 17 | relationships: {}, 18 | //dom element class 19 | baseClass: 'RelationshipTableTabs', 20 | //this id can be the dijit id of a tabcontainer or 21 | //a widget that has a tabContainer property, like tmgee's attribute table widget 22 | tabContainerId: null, 23 | postCreate: function () { 24 | this.inherited(arguments); 25 | if (this.tabContainerId) { 26 | ready(10, this, '_init'); 27 | } else { 28 | this.tabContainer = new TabContainer({ 29 | tabPosition: this.tabPosition, 30 | style: 'height:100%;width:100%;' 31 | }); 32 | this.addChild(this.tabContainer); 33 | this.tabContainer.startup(); 34 | this._init(); 35 | } 36 | }, 37 | resize: function () { 38 | this.inherited(arguments); 39 | this.tabContainer.resize(); 40 | }, 41 | _init: function () { 42 | if (!this.tabContainer) { 43 | this.tabContainer = registry.byId(this.tabContainerId); 44 | //allow user to reference a widget with a tab container by id 45 | if (this.tabContainer && this.tabContainer.hasOwnProperty('tabContainer')) { 46 | this.tabContainer = this.tabContainer.tabContainer; 47 | } 48 | } 49 | if (!this.tabContainer) { 50 | topic.publish('viewer/handleError', { 51 | source: 'RelatedRecordTable', 52 | error: 'tab container could not be found' 53 | }); 54 | return; 55 | } 56 | if (this.layerInfos.length > 0) { 57 | array.forEach(this.layerInfos, lang.hitch(this, function (l) { 58 | array.forEach(l.layer.relationships, lang.hitch(this, function (r) { 59 | if (l.layer.relationships && 60 | l.layer.relationships.length > 0) { 61 | var props = { 62 | title: l.layer.name + ' - ' + r.name, 63 | url: l.layer.url, 64 | objectIdField: l.layer.objectIdField, 65 | relationshipId: r.id 66 | }; 67 | if (this.relationships[l.layer.id] && 68 | this.relationships[l.layer.id][r.id]) { 69 | if (this.relationships[l.layer.id][r.id].exclude) { 70 | return; 71 | } 72 | props = lang.mixin(props, this.relationships[l.layer.id][r.id]); 73 | } 74 | var table = new RelationshipTable(props); 75 | this.tabContainer.addChild(table); 76 | l.layer.on('click', lang.hitch(this, function (e) { 77 | table.getRelatedRecords(e.graphic.attributes); 78 | })); 79 | } 80 | })); 81 | })); 82 | } 83 | this.resize(); 84 | } 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /widgets/Themes.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dojo/_base/array', 4 | 'dojo/_base/lang', 5 | 'dijit/form/FilteringSelect', 6 | 'dojo/store/Memory', 7 | 'dojo/topic' 8 | ], function (declare, array, lang, FilteringSelect, Memory, topic) { 9 | return declare([FilteringSelect], { 10 | /** 11 | * Array of theme objects like: 12 | * { 13 | * name: 'Theme Name', 14 | * id: 'myTheme', 15 | * layers: { 16 | * layerId: { 17 | * visible: true, 18 | * visibleLayers: [8,9] 19 | * } 20 | * } 21 | * } 22 | */ 23 | themes: [], 24 | _setThemesAttr: function (themes) { 25 | themes = [{name: '- Select Theme -', selected: true, id: 'none'}].concat(themes); 26 | this.themes = themes; 27 | this.store.setData(themes); 28 | this.set('value', 'none'); 29 | }, 30 | /** 31 | * Layer infos: 32 | * { 33 | * layer: 34 | * } 35 | */ 36 | layerInfos: [], 37 | name: 'themeSelect', 38 | id: 'themeSelect', 39 | style: 'width:100%;', 40 | value: 'none', 41 | _setValueAttr: function (val) { 42 | this.inherited(arguments); 43 | var themeObj = this.store.get(val); 44 | if (!themeObj || val === 'none') { 45 | return; 46 | } 47 | array.forEach(this.layerInfos, function (info) { 48 | if (themeObj.layers.hasOwnProperty(info.layer.id)) { 49 | if (info.layer.hasOwnProperty('visible') && 50 | themeObj.layers[info.layer.id].hasOwnProperty('visible')) { 51 | info.layer.setVisibility(themeObj.layers[info.layer.id].visible); 52 | topic.publish('layerControl/layerToggle', { 53 | id: info.layer.id, 54 | visible: themeObj.layers[info.layer.id].visible 55 | }); 56 | } 57 | if (info.layer.hasOwnProperty('visibleLayers') && 58 | themeObj.layers[info.layer.id].hasOwnProperty('visibleLayers')) { 59 | info.layer.setVisibleLayers(themeObj.layers[info.layer.id].visibleLayers); 60 | topic.publish('layerControl/setVisibleLayers', { 61 | id: info.layer.id, 62 | visibleLayers: themeObj.layers[info.layer.id].visibleLayers 63 | }); 64 | } 65 | } else { 66 | info.layer.setVisibility(false); 67 | } 68 | }); 69 | }, 70 | store: null, 71 | constructor: function () { 72 | this.inherited(arguments); 73 | this.set({ 74 | store: new Memory({ 75 | data: this.themes 76 | }) 77 | }); 78 | }, 79 | postCreate: function () { 80 | this.inherited(arguments); 81 | if (!this.layerInfos) { 82 | this.destroyRecursive(); 83 | topic.publish('viewer/handleError', { 84 | source: 'Themes', 85 | error: 'layerInfos is required' 86 | }); 87 | } 88 | } 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /widgets/TimeSlider.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dijit/_WidgetBase', 4 | 'dojo/_base/lang', 5 | 'dojo/_base/array', 6 | 'esri/TimeExtent', 7 | 'esri/dijit/TimeSlider', 8 | 'dijit/_Container', 9 | 'dojo/on' 10 | ], function(declare, _WidgetBase, lang, array, TimeExtent, TimeSlider, _Container, on) { 11 | return declare([_WidgetBase, _Container], { 12 | map: null, 13 | startTime: new Date('1/1/1921'), 14 | endTime: new Date('12/31/2016'), 15 | timeSlider: null, 16 | timeExtent: null, 17 | timeInterval: 2, 18 | timeIntervalUnits: 'esriTimeUnitsYears', 19 | timeSliderProperties: {}, 20 | postCreate: function() { 21 | this.inherited(arguments); 22 | if(!this.map){ 23 | return false; 24 | } 25 | this.timeSlider = new TimeSlider(lang.mixin({ 26 | style: 'width:100%;' 27 | }, this.timeSliderProperties)); 28 | this.map.setTimeSlider(this.timeSlider); 29 | this.timeExtent = lang.mixin(new TimeExtent(), { 30 | startTime: this.startTime, 31 | endTime: this.endTime 32 | }); 33 | this.timeSlider.setThumbCount(2); 34 | this.timeSlider.createTimeStopsByTimeInterval( 35 | this.timeExtent, 36 | this.timeInterval, 37 | this.timeIntervalUnits 38 | ); 39 | this.timeSlider.setThumbIndexes([0,1]); 40 | this.timeSlider.setThumbMovingRate(2000); 41 | this.addChild(this.timeSlider); 42 | }, 43 | startup: function() { 44 | this.inherited(arguments); 45 | this.timeSlider.startup(); 46 | this.setLabels(); 47 | if (this.getParent) { 48 | var parent = this.getParent(); 49 | if (parent) { 50 | this.own(on(parent, 'hide', lang.hitch(this, function () { 51 | this.timeSlider.setThumbIndexes([0, this.timeSlider.timeStops.length - 1]); 52 | }))); 53 | } 54 | } 55 | }, 56 | setLabels: function() { 57 | //add labels for every other time stop 58 | var labels = array.map(this.timeSlider.timeStops, function(timeStop, i) { 59 | if ( i % 2 === 0 ) { 60 | return timeStop.getUTCFullYear(); 61 | } else { 62 | return ""; 63 | } 64 | }); 65 | this.timeSlider.setLabels(labels); 66 | 67 | } 68 | }); 69 | }); -------------------------------------------------------------------------------- /widgets/TimeSlider/README.md: -------------------------------------------------------------------------------- 1 | TimeSlider 2 | ============== 3 | 4 | Places a simple esri timeslider widget in a node. Controls the view of all time enabled layers in the map. 5 | 6 | ##Configuration: 7 | 8 | ```JavaScript 9 | //enable the bottom pane and give it a node 10 | bottom: { 11 | id: 'sidebarBottom', 12 | placeAt: 'outer', 13 | splitter: true, 14 | collapsible: true, 15 | region: 'bottom', 16 | style: 'height:300px;display:block;', 17 | content: '
    ' 18 | }, 19 | ``` 20 | 21 | ```JavaScript 22 | //widget config 23 | timeSlider: { 24 | include: true, 25 | id: 'timeSlider', 26 | type: 'domNode', 27 | srcNodeRef: 'timeSlider', 28 | path: 'gis/roemhildtg/widgets/TimeSlider', 29 | title: 'Time Slider', 30 | options: { 31 | map: true 32 | } 33 | } 34 | ``` 35 | 36 | ###Options 37 | Key | Type | Default | Description 38 | ---|---|---|--- 39 | `map` | `esri.map` | `null` | Required. In CMV set `map: true` so the app passes the map in 40 | `startTime` | `Date` | `new Date('1/1/1921')` | Optional. The start date for the timeslider 41 | `endTime` | `Date` | `new Date('12/31/2016')` | Optional. The end date for the timeslider 42 | `timeInterval` | `Integer` | `2` | Optional. The interval for the timeslider 43 | `timeIntervalUnits` | `String` | `esriTimeUnitsYears` | Optional. The interval units for the timeslider 44 | `timeSliderProperties` | `Object` | `{}` | Optional. Additional properties to use for the timeslider. [See the ArcGIS Javascript API](https://developers.arcgis.com/javascript/jsapi/timeslider-amd.html) -------------------------------------------------------------------------------- /widgets/TimeSlider/timeSlider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/green3g/cmv-widgets/90d5cd08a949fdfdcdcc15dfc9a394a07fa2b2e5/widgets/TimeSlider/timeSlider.png --------------------------------------------------------------------------------