Greasy Fork is available in English.

WME GIS Layers

Adds Paraguay GIS layers in WME

  1. /* eslint-disable camelcase */
  2. /* eslint-disable brace-style, curly, nonblock-statement-body-position, no-template-curly-in-string, func-names */
  3. // ==UserScript==
  4. // @name WME GIS Layers
  5. // @namespace https://greasyfork.org/users/324334
  6. // @version 2023.09.27.001-py028
  7. // @description Adds Paraguay GIS layers in WME
  8. // @author MapOMatic
  9. // @match *://*.waze.com/*editor*
  10. // @exclude *://*.waze.com/user/editor*
  11. // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/Turf.js/4.7.3/turf.min.js
  13. // @grant GM_xmlhttpRequest
  14. // @connect greasyfork.org
  15. // @grant GM_info
  16. // @license GNU GPLv3
  17. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  18. // @connect *
  19. // @connect www.asuncion.gov.py
  20. // @connect analisis.stp.gov.py
  21. // @connect www.arcgis.com
  22. // @connect services1.arcgis.com
  23. // @connect services2.arcgis.com
  24. // @connect services3.arcgis.com
  25. // @connect services5.arcgis.com
  26. // @connect services6.arcgis.com
  27. // @connect services8.arcgis.com
  28. // @connect services9.arcgis.com
  29. // @connect geohidroinformatica.itaipu.gov.py
  30. // @connect geobosques.pti.org.py
  31. // @connect catastro.gov.py
  32. // @connect geo1.skycop.com.py
  33. // @connect sigcosiplan.unasursg.org
  34. // @connect snmf.infona.gov.py
  35. // @connect 190.52.167.121
  36. // @connect sedac.ciesin.columbia.edu
  37. // @connect 190.128.205.76
  38. // @connect wwf-sight-maps.org
  39. // @connect www.geosur.info
  40. // @connect a.mapillary.com
  41. // @connect geoshape.unasursg.org
  42. // @connect geo-ide.carto.com
  43. // @connect 201.217.59.143
  44. // @connect pese.pti.org.py
  45. // @connect geo.pti.org.py
  46. // @connect www.mapadeasentamientos.org.py
  47. // @connect gis-gfw.wri.org
  48. // @connect opengeo.pol.una.py
  49. // @connect gis.mic.gov.py
  50. // @connect vigisalud.gov.py
  51. // @connect mapaescolar.mec.gov.py
  52. // @connect apps.mades.gov.py
  53. // @connect www.mopc.gov.py
  54. // ==/UserScript==
  55.  
  56. // This version is for Paraguay Only, modified by ancho85
  57. /* global OpenLayers */
  58. /* global W */
  59. /* global WazeWrap */
  60. /* global _ */
  61. /* global turf */
  62.  
  63. (function main() {
  64. 'use strict';
  65.  
  66. // **************************************************************************************************************
  67. // IMPORTANT: Update this when releasing a new version of script that includes changes to the spreadsheet format
  68. // that may cause old code to break. This # should match the version listed in the spreadsheet
  69. // i.e. update them at the same time.
  70.  
  71. // const LAYER_DEF_VERSION = '2018.04.27.001'; // NOT ACTUALLY USED YET
  72.  
  73. // **************************************************************************************************************
  74. // const UPDATE_MESSAGE = 'Bug fix due to WME update';
  75. // const UPDATE_MESSAGE = `<ul>${[
  76. // 'Added ability to shift layers. Right click a layer in the list to bring up the layer settings window.'
  77. // ].map(item => `<li>${item}</li>`).join('')}</ul><br>`;
  78. const GF_URL = 'https://greasyfork.org/en/scripts/388277-wme-paraguay-gis-layers';
  79. // Used in tooltips to tell people who to report issues to. Update if a new author takes ownership of this script.
  80. const SCRIPT_AUTHOR = 'ancho85'; // MapOMatic is the original author, but he won't fix any Paraguay related issues
  81. // const LAYER_INFO_URL = 'https://spreadsheets.google.com/feeds/list/1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw/o7gusx3/public/values?alt=json';
  82. const LAYER_DEF_SPREADSHEET_URL = 'https://sheets.googleapis.com/v4/spreadsheets/1aePOmux2IBxE_2CGPOequGnubr9g4hWr1wH_qAjcM24/values/layerDefs';
  83. const API_KEY = 'UVVsNllWTjVSSEJvYm5sQ05FdElNa3BqV1RBMFZtZHRSMDFRYm5Ca1ZURkZNRGRIYUVkbg==';
  84. const REQUEST_FORM_URL = 'https://docs.google.com/forms/d/e/1FAIpQLSfMhBxF0P6bn8dFfOoNTAF1LHBFXr5w9oXvzqsii_TfA-_Bmw/viewform?usp=pp_url&entry.831784226={username}';
  85. const DEC = s => atob(atob(s));
  86. const PRIVATE_LAYERS = { 'nc-henderson-sl-signs': ['the_cre8r', 'mapomatic'] }; // case sensitive -- use all lower case
  87. // const COUNTRIES = {
  88. // 'United States': {
  89. // sheetId: '1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw',
  90. // sheetLayerRange: 'layerDefs'
  91. // }
  92. // };
  93. const DEFAULT_STYLE = {
  94. fillColor: '#000',
  95. pointRadius: 4,
  96. label: '${label}',
  97. strokeColor: '#ffa500',
  98. strokeOpacity: '0.95',
  99. strokeWidth: 1.5,
  100. fontColor: '#ffc520',
  101. fontSize: '13',
  102. labelOutlineColor: 'black',
  103. labelOutlineWidth: 3
  104. };
  105. const LAYER_STYLES = {
  106. cities: {
  107. fillOpacity: 0.3,
  108. fillColor: '#f65',
  109. strokeColor: '#f65',
  110. fontColor: '#f62'
  111. },
  112. forests_parks: {
  113. fillOpacity: 0.4,
  114. fillColor: '#585',
  115. strokeColor: '#484',
  116. fontColor: '#8b8'
  117. },
  118. milemarkers: {
  119. strokeColor: '#fff',
  120. fontColor: '#fff',
  121. fontWeight: 'bold',
  122. fillOpacity: 0,
  123. labelYOffset: 10,
  124. pointRadius: 2,
  125. fontSize: 12
  126. },
  127. parcels: {
  128. fillOpacity: 0,
  129. fillColor: '#ffa500'
  130. },
  131. points: {
  132. strokeColor: '#000',
  133. fontColor: '#0ff',
  134. fillColor: '#0ff',
  135. labelYOffset: -10,
  136. labelAlign: 'ct'
  137. },
  138. post_offices: {
  139. strokeColor: '#000',
  140. fontColor: '#f84',
  141. fillColor: '#f84',
  142. fontWeight: 'bold',
  143. labelYOffset: -10,
  144. labelAlign: 'ct'
  145. },
  146. state_parcels: {
  147. fillOpacity: 0,
  148. strokeColor: '#e62',
  149. fillColor: '#e62',
  150. fontColor: '#e73'
  151. },
  152. state_points: {
  153. strokeColor: '#000',
  154. fontColor: '#3cf',
  155. fillColor: '#3cf',
  156. labelYOffset: -10,
  157. labelAlign: 'ct'
  158. },
  159. road_labels: {
  160. strokeOpacity: 0,
  161. fillOpacity: 0,
  162. fontColor: '#faf'
  163. },
  164. structures: {
  165. fillOpacity: 0,
  166. strokeColor: '#f7f',
  167. fontColor: '#f7f'
  168. },
  169. water: {
  170. fillOpacity: 1,
  171. strokeColor: '#13a1dd',
  172. fillColor: '#13a1dd',
  173. fontColor: '#13a1dd',
  174. fontWeight: 'bold'
  175. }
  176. };
  177. let ROAD_STYLE;
  178. function initRoadStyle() {
  179. ROAD_STYLE = new OpenLayers.Style({
  180. pointRadius: 12,
  181. fillColor: '#369',
  182. pathLabel: '${label}',
  183. label: '',
  184. fontColor: '#faf',
  185. labelSelect: true,
  186. pathLabelYOffset: '${getOffset}',
  187. pathLabelCurve: '${getSmooth}',
  188. pathLabelReadable: '${getReadable}',
  189. labelAlign: '${getAlign}',
  190. labelOutlineWidth: 3,
  191. labelOutlineColor: '#000',
  192. strokeWidth: 3,
  193. stroke: true,
  194. strokeColor: '#f0f',
  195. strokeOpacity: 0.4,
  196. fontWeight: 'bold',
  197. fontSize: 11
  198. }, {
  199. context: {
  200. getOffset() { return -(W.map.getZoom() + 5); },
  201. getSmooth() { return ''; },
  202. getReadable() { return '1'; },
  203. getAlign() { return 'cb'; }
  204. }
  205. });
  206. }
  207.  
  208. // eslint-disable-next-line no-unused-vars
  209. const _regexReplace = {
  210. // Strip leading zeros or blank full label for any label starting with a non-digit or
  211. // is a Zero Address, use with '' as replace.
  212. r0: /^(0+(\s.*)?|\D.*)/,
  213. // Strip Everything After Street Type to end of the string by use $1 and $2 capture
  214. // groups, use with replace '$1$2'
  215. // eslint-disable-next-line max-len
  216. r1: /^(.* )(Ave(nue)?|Dr(ive)?|St(reet)?|C(our)?t|Cir(cle)?|Blvd|Boulevard|Pl(ace)?|Ln|Lane|Fwy|Freeway|R(oa)?d|Ter(r|race)?|Tr(ai)?l|Way|Rte \d+|Route \d+)\b.*/gi,
  217. // Strip SPACE 5 Digits from end of string, use with replace ''
  218. r2: /\s\d{5}$/,
  219. // Strip Everything after a "~", ",", ";" to the end of the string, use with replace ''
  220. r3: /(~|,|;|\s?\r\n).*$/,
  221. // Move the digits after the last space to before the rest of the string using, use with
  222. // replace '$2 $1'
  223. r4: /^(.*)\s(\d+).*/,
  224. // Insert newline between digits (including "-") and everything after the digits,
  225. // except(and before) a ",", use with replace '$1\n$2'
  226. r5: /^([-\d]+)\s+([^,]+).*/,
  227. // Insert newline between digits and everything after the digits, use with
  228. // replace '$1\n$2'
  229. r6: /^(\d+)\s+(.*)/
  230. };
  231.  
  232. let _gisLayers = [];
  233.  
  234. // const _layerRefinements = [
  235. // {
  236. // id: 'us-post-offices',
  237. // labelHeaderFields: ['LOCALE_NAME']
  238. // }
  239. // ];
  240.  
  241. const STATES = {
  242. _states: [
  243. ['PRY (Pais)', 'PRY', -1], ['Asuncion (Capital)', 'ASU', 0], ['Concepcion', 'CON', 1],
  244. ['San Pedro', 'SAN', 2], ['Cordillera', 'COR', 3], ['Guaira', 'GUA', 4],
  245. ['Caaguazu', 'CAG', 5], ['Caazapa', 'CAZ', 6], ['Itapua', 'ITA', 7],
  246. ['Misiones', 'MIS', 8], ['Paraguari', 'PAR', 9], ['Alto Parana', 'ANA', 10],
  247. ['Central', 'CEN', 11], ['Neembucu', 'NEE', 12], ['Amambay', 'AMA', 13], ['Canindeyu', 'CAN', 14],
  248. ['Presidente Hayes', 'PHA', 15], ['Boqueron', 'BOQ', 16], ['Alto Paraguay', 'AAY', 17],
  249. ],
  250. toAbbr(fullName) { return this._states.find(a => a[0] === fullName)[1]; },
  251. toFullName(abbr) { return this._states.find(a => a[1] === abbr)[0]; },
  252. toFullNameArray() { return this._states.map(a => a[0]); },
  253. toAbbrArray() { return this._states.map(a => a[1]); },
  254. fromId(id) { return this._states.find(a => a[2] === id); }
  255. };
  256. const DEFAULT_VISIBLE_AT_ZOOM = 6;
  257. const SETTINGS_STORE_NAME = 'wme_gis_layers';
  258. const COUNTIES_URL = 'https://analisis.stp.gov.py:443/user/ine/api/v2/';
  259. // const COUNTIES_URL2 = 'https://services2.arcgis.com/tnyi76ruua1nbtl3/ArcGIS/rest/services/Paraguay_Interactive/FeatureServer/0';
  260. const COUNTIES_URL2 = 'https://services2.arcgis.com/Xim64FzemN4fqY1y/ArcGIS/rest/services/PY_Departamentos_y_Municipios/FeatureServer/0';
  261. const ALERT_UPDATE = false;
  262. const SCRIPT_NAME = GM_info.script.name;
  263. const SCRIPT_VERSION = GM_info.script.version;
  264. const DOWNLOAD_URL = 'https://greasyfork.org/scripts/388277-wme-paraguay-gis-layers/code/WME%20Paraguay%20GIS%20Layers.user.js';
  265. const SCRIPT_VERSION_CHANGES = [];
  266. let _mapLayer = null;
  267. let _roadLayer = null;
  268. let _settings = {};
  269. let _ignoreFetch = false;
  270. let _lastToken = {};
  271.  
  272. const DEBUG = true;
  273. //function log(message) { console.log('PY GIS Layers:', message); }
  274. function logError(message) { console.error(`${SCRIPT_NAME}:`, message); }
  275. function logDebug(message) { if (DEBUG) console.debug(`${SCRIPT_NAME}:`, message); }
  276. // function logWarning(message) { console.warn('PY GIS Layers:', message); }
  277.  
  278. let _layerSettingsDialog;
  279.  
  280. class LayerSettingsDialog {
  281. constructor() {
  282. this._$titleText = $('<span>');
  283. this._$closeButton = $('<span>', {
  284. style: 'cursor:pointer;padding-left:4px;font-size:17px;color:#d6e6f3;float:right;',
  285. class: 'fa fa-window-close'
  286. }).click(() => this._onCloseButtonClick());
  287. this._$shiftUpButton = LayerSettingsDialog._createShiftButton('fa-angle-up').click(() => this._onShiftButtonClick(0, 1));
  288. this._$shiftLeftButton = LayerSettingsDialog._createShiftButton('fa-angle-left').click(() => this._onShiftButtonClick(-1, 0));
  289. this._$shiftRightButton = LayerSettingsDialog._createShiftButton('fa-angle-right').click(() => this._onShiftButtonClick(1, 0));
  290. this._$shiftDownButton = LayerSettingsDialog._createShiftButton('fa-angle-down').click(() => this._onShiftButtonClick(0, -1));
  291. this._$resetButton = $('<button>', {
  292. class: 'form-control',
  293. style: 'height: 24px; width: auto; padding: 2px 6px 0px 6px; display: inline-block; float: right;'
  294. }).text('Reset').click(() => this._onResetButtonClick());
  295.  
  296. this._dialogDiv = $('<div>', {
  297. style: 'position: fixed; top: 15%; left: 400px; width: 200px; z-index: 100; background-color: #73a9bd; border-width: 1px; border-style: solid;'
  298. + 'border-radius: 10px; box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.7); border-color: #50667b; padding: 4px;'
  299. }).append($('<div>').append( // The extra div is needed here. When the header text wraps, the main dialog div won't expand properly without it.
  300. // HEADER
  301. $('<div>', { style: 'border-radius:5px 5px 0px 0px; padding: 4px; color: #fff; font-weight: bold; text-align:left; cursor: default;' }).append(
  302. this._$closeButton,
  303. this._$titleText
  304. ),
  305. // BODY
  306. $('<div>', { style: 'border-radius: 5px; width: 100%; padding: 4px; background-color:#d6e6f3; display:inline-block; margin-right:5px;' }).append(
  307. this._$resetButton,
  308. $('<input>', {
  309. type: 'radio', id: 'gisLayerShiftAmt1', name: 'gisLayerShiftAmt', value: '1', checked: 'checked'
  310. }),
  311. $('<label>', { for: 'gisLayerShiftAmt1' }).text('1m'),
  312. $('<input>', {
  313. type: 'radio', id: 'gisLayerShiftAmt10', name: 'gisLayerShiftAmt', value: '10', style: 'margin-left: 6px'
  314. }),
  315. $('<label>', { for: 'gisLayerShiftAmt10' }).text('10m'),
  316. $('<div>', { style: 'padding: 4px' }).append(
  317. $('<table>', { style: 'table-layout:fixed; width:60px; height:84px; margin-left:auto;margin-right:auto;' }).append(
  318. $('<tr>', { style: 'width: 20px; height: 28px;' }).append(
  319. $('<td>', { align: 'center' }),
  320. $('<td>', { align: 'center' }).append(this._$shiftUpButton),
  321. $('<td>', { align: 'center' })
  322. ),
  323. $('<tr>', { style: 'width: 20px; height: 28px;' }).append(
  324. $('<td>', { align: 'center' }).append(this._$shiftLeftButton),
  325. $('<td>', { align: 'center' }),
  326. $('<td>', { align: 'center' }).append(this._$shiftRightButton)
  327. ),
  328. $('<tr>', { style: 'width: 20px; height: 28px;' }).append(
  329. $('<td>', { align: 'center' }),
  330. $('<td>', { align: 'center' }).append(this._$shiftDownButton),
  331. $('<td>', { align: 'center' })
  332. )
  333. )
  334. )
  335. )
  336. ));
  337.  
  338. this.hide();
  339. this._dialogDiv.appendTo('body');
  340.  
  341. if (typeof jQuery.ui !== 'undefined') {
  342. const that = this;
  343. this._dialogDiv.draggable({
  344. // Gotta nuke the height setting the dragging inserts otherwise the panel cannot dynamically resize
  345. stop() { that._dialogDiv.css('height', ''); }
  346. });
  347. }
  348. }
  349.  
  350. get gisLayer() {
  351. return this._gisLayer;
  352. }
  353.  
  354. set gisLayer(value) {
  355. if (value !== this._gisLayer) {
  356. this._gisLayer = value;
  357. this.title = value.name;
  358. }
  359. }
  360.  
  361. get title() {
  362. return this._$titleText.text();
  363. }
  364.  
  365. set title(value) {
  366. this._$titleText.text(value);
  367. }
  368.  
  369. // eslint-disable-next-line class-methods-use-this
  370. getShiftAmount() {
  371. return $('input[name=gisLayerShiftAmt]:checked').val();
  372. }
  373.  
  374. show() {
  375. this._dialogDiv.show();
  376. }
  377.  
  378. hide() {
  379. this._dialogDiv.hide();
  380. }
  381.  
  382. _onCloseButtonClick() {
  383. this.hide();
  384. }
  385.  
  386. _onShiftButtonClick(x, y) {
  387. const shiftAmount = this.getShiftAmount();
  388. x *= shiftAmount;
  389. y *= shiftAmount;
  390. this._shiftLayerFeatures(x, y);
  391. const { id } = this._gisLayer;
  392. let offset = _settings.getLayerSetting(id, 'offset');
  393. if (!offset) {
  394. offset = { x: 0, y: 0 };
  395. _settings.setLayerSetting(id, 'offset', offset);
  396. }
  397. offset.x += x;
  398. offset.y += y;
  399. saveSettingsToStorage();
  400. }
  401.  
  402. _onResetButtonClick() {
  403. const offset = _settings.getLayerSetting(this._gisLayer.id, 'offset');
  404. if (offset) {
  405. this._shiftLayerFeatures(offset.x * -1, offset.y * -1);
  406. delete _settings.layers[this._gisLayer.id].offset;
  407. saveSettingsToStorage();
  408. }
  409. }
  410.  
  411. _shiftLayerFeatures(x, y) {
  412. const layer = this.gisLayer.isRoadLayer ? _roadLayer : _mapLayer;
  413. layer.getFeaturesByAttribute('layerID', this.gisLayer.id).forEach(f => f.geometry.move(x, y));
  414. layer.redraw();
  415. }
  416.  
  417. static _createShiftButton(fontAwesomeClass) {
  418. return $('<button>', {
  419. class: 'form-control',
  420. style: 'cursor:pointer;font-size:14px;padding: 3px;border-radius: 5px;width: 21px;height: 21px;'
  421. }).append(
  422. $('<i>', { class: 'fa', style: 'vertical-align: super' }).addClass(fontAwesomeClass)
  423. );
  424. }
  425. }
  426.  
  427. function loadSettingsFromStorage() {
  428. const loadedSettings = $.parseJSON(localStorage.getItem(SETTINGS_STORE_NAME));
  429. const defaultSettings = {
  430. lastVersion: null,
  431. visibleLayers: [],
  432. onlyShowApplicableLayers: false,
  433. selectedStates: [],
  434. enabled: true,
  435. fillParcels: false,
  436. toggleHnsOnlyShortcut: '',
  437. oneTimeAlerts: {},
  438. layers: {}
  439. };
  440. _settings = loadedSettings || defaultSettings;
  441. Object.keys(defaultSettings).forEach(prop => {
  442. if (!_settings.hasOwnProperty(prop)) {
  443. _settings[prop] = defaultSettings[prop];
  444. }
  445. });
  446.  
  447. _settings.getLayerSetting = function getLayerSetting(layerID, settingName) {
  448. const layerSettings = this.layers[layerID];
  449. if (!layerSettings) {
  450. return undefined;
  451. }
  452. return layerSettings[settingName];
  453. };
  454. _settings.setLayerSetting = function setLayerSetting(layerID, settingName, value) {
  455. let layerSettings = this.layers[layerID];
  456. if (!layerSettings) {
  457. layerSettings = {};
  458. this.layers[layerID] = layerSettings;
  459. }
  460. layerSettings[settingName] = value;
  461. };
  462. }
  463.  
  464. function saveSettingsToStorage() {
  465. // Check for existance of action first, due to WME beta issue.
  466. if (W.accelerators.Actions.GisLayersAddrDisplay) {
  467. let keys = '';
  468. const { shortcut } = W.accelerators.Actions.GisLayersAddrDisplay;
  469. if (shortcut) {
  470. if (shortcut.altKey) keys += 'A';
  471. if (shortcut.shiftKey) keys += 'S';
  472. if (shortcut.ctrlKey) keys += 'C';
  473. if (keys.length) keys += '+';
  474. if (shortcut.keyCode) keys += shortcut.keyCode;
  475. }
  476. _settings.toggleHnsOnlyShortcut = keys;
  477. }
  478. _settings.lastVersion = SCRIPT_VERSION;
  479. localStorage.setItem(SETTINGS_STORE_NAME, JSON.stringify(_settings));
  480. logDebug('Configuracion guardada');
  481. }
  482.  
  483. function getUrl(extent, gisLayer) {
  484. if (gisLayer.spatialReference) {
  485. const proj = new OpenLayers.Projection(`EPSG:${gisLayer.spatialReference}`);
  486. let new_extent = extent.clone();
  487. new_extent.transform(W.map.getProjectionObject(), proj); // do not transform original extent
  488. extent = new_extent;
  489. }
  490. let layerOffset = _settings.getLayerSetting(gisLayer.id, 'offset');
  491. if (!layerOffset) {
  492. layerOffset = { x: 0, y: 0 };
  493. }
  494. const geometry = {
  495. xmin: extent.left - layerOffset.x,
  496. ymin: extent.bottom - layerOffset.y,
  497. xmax: extent.right - layerOffset.x,
  498. ymax: extent.top - layerOffset.y,
  499. spatialReference: {
  500. wkid: gisLayer.spatialReference ? gisLayer.spatialReference : 102100,
  501. latestWkid: gisLayer.spatialReference ? gisLayer.spatialReference : 3857
  502. }
  503. };
  504. const geometryStr = JSON.stringify(geometry);
  505. let fields = gisLayer.labelFields.filter(function (e) { return e != ""});
  506. if (gisLayer.labelHeaderFields) {
  507. fields = fields.concat(gisLayer.labelHeaderFields);
  508. }
  509. if (gisLayer.distinctFields) {
  510. fields = fields.concat(gisLayer.distinctFields);
  511. }
  512. let url = ""
  513. if (gisLayer.isFeatureSet) {
  514. url = gisLayer.url; // no extra filters for this resource (caching)
  515. } else if (gisLayer.serverType == "GeoNode"){
  516. url = gisLayer.url;
  517. url += `&CRS=EPSG:${geometry.spatialReference.latestWkid}`;
  518. if (gisLayer.where){
  519. var geom_field = gisLayer.cql_the_geom ? gisLayer.cql_the_geom : "the_geom"; //some geom fields are called simply 'geom'
  520. var where = `(bbox(${geom_field},${geometry.xmin},${geometry.ymin},${geometry.xmax},${geometry.ymax},'EPSG:${geometry.spatialReference.latestWkid}') and ${gisLayer.where})`;
  521. url += `&cql_filter=${encodeURIComponent(where)}`;
  522. } else {
  523. url += `&bbox=${geometry.xmin},${geometry.ymin},${geometry.xmax},${geometry.ymax},EPSG:${geometry.spatialReference.latestWkid}`;
  524. }
  525. url += `&srsName=EPSG:${geometry.spatialReference.latestWkid}&outputFormat=${gisLayer.output? gisLayer.output : "application/json"}`;
  526. } else if (gisLayer.serverType == "CartoDB"){
  527. // url with query format 'SELECT the_geom_webmercator AS the_geom FROM user.table_name'
  528. url =`${gisLayer.url} WHERE ST_Intersects(ST_SetSRID(ST_MakeBox2D(ST_Point(${extent.left},${extent.top}),ST_Point(${extent.right},${extent.bottom})),3857),the_geom_webmercator)`;
  529. if (fields.length){
  530. url = url.replace("the_geom_webmercator AS the_geom", `the_geom_webmercator AS the_geom%2C${encodeURIComponent(fields.join(','))}`)
  531. }
  532. if (gisLayer.where){
  533. url += `AND ${gisLayer.where}`;
  534. }
  535. url += '&format=GeoJSON'
  536. } else { //default ArcGIS server
  537. url = `${gisLayer.url}/query?geometry=${encodeURIComponent(geometryStr)}`;
  538. url += gisLayer.token ? `&token=${gisLayer.token}` : '';
  539. url += `&outFields=${encodeURIComponent(fields.join(','))}`;
  540. url += '&returnGeometry=true&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope';
  541. url += `&inSR=${gisLayer.spatialReference ? gisLayer.spatialReference : '102100'}`;
  542. url += '&outSR=3857&f=json';
  543. url += gisLayer.where ? `&where=${encodeURIComponent(gisLayer.where)}` : '';
  544. }
  545.  
  546. logDebug(`Request URL: ${url}`);
  547. return url;
  548. }
  549.  
  550. function hashString(value) {
  551. let hash = 0;
  552. if (value.length === 0) return hash;
  553. for (let i = 0; i < value.length; i++) {
  554. const chr = value.charCodeAt(i);
  555. // eslint-disable-next-line no-bitwise
  556. hash = ((hash << 5) - hash) + chr;
  557. // eslint-disable-next-line no-bitwise
  558. hash |= 0; // Convert to 32bit integer
  559. }
  560. return hash;
  561. }
  562.  
  563. function getCountiesUrl(extent) {
  564. const geometry = {
  565. xmin: extent.left,
  566. ymin: extent.bottom,
  567. xmax: extent.right,
  568. ymax: extent.top,
  569. spatialReference: { wkid: 102100, latestWkid: 3857 }
  570. };
  571. const url = `${COUNTIES_URL2}/query?geometry=${encodeURIComponent(JSON.stringify(geometry))}`;
  572. return `${url}&outFields=NAME as BASENAME%2CCODE as STATE&returnGeometry=false&spatialRel=esriSpatialRelIntersects`
  573. + '&geometryType=esriGeometryEnvelope&inSR=102100&outSR=3857&f=json';
  574.  
  575. /*const url = `${COUNTIES_URL}sql?q=SELECT dist_desc_ AS BASENAME, dpto AS STATE FROM ine.paraguay_2019_distritos `;
  576. var gps1 = WazeWrap.Geometry.ConvertTo4326(extent.left, extent.top);
  577. var gps2 = WazeWrap.Geometry.ConvertTo4326(extent.right, extent.bottom);
  578. return `${url} WHERE ST_Intersects(
  579. ST_SetSRID(
  580. ST_MakeBox2D(
  581. ST_Point(${gps1.lon},${gps1.lat}),
  582. ST_Point(${gps2.lon},${gps2.lat})
  583. ),
  584. 4326
  585. ),
  586. the_geom)`;*/
  587. }
  588.  
  589. let _countiesInExtent = [];
  590. let _statesInExtent = [];
  591.  
  592. function getFetchableLayers(getInvisible) {
  593. if (W.map.getZoom() < 12 - 12) return []; //TODO: CHECK THIS LINE
  594. return _gisLayers.filter(gisLayer => {
  595. const isValidUrl = gisLayer.url && gisLayer.url.trim().length > 0;
  596. const isVisible = (getInvisible || _settings.visibleLayers.includes(gisLayer.id))
  597. && _settings.selectedStates.includes(gisLayer.state);
  598. const isInState = gisLayer.state === 'PRY' || _countiesInExtent.some(county => county.stateInfo[1] === gisLayer.state);
  599. // Be sure to use hasOwnProperty when checking this, since 0 is a valid value.
  600. const isValidZoom = getInvisible || W.map.getZoom() - 12 >= (gisLayer.hasOwnProperty('visibleAtZoom')
  601. ? gisLayer.visibleAtZoom : DEFAULT_VISIBLE_AT_ZOOM);
  602. return isValidUrl && isInState && isVisible && isValidZoom;
  603. });
  604. }
  605.  
  606. function filterLayerCheckboxes() {
  607. const applicableLayers = getFetchableLayers(true).filter(layer => {
  608. const hasCounties = layer.hasOwnProperty('counties');
  609. return (hasCounties && layer.counties.some(countyName => _countiesInExtent.some(county => county.name === countyName.toLowerCase()
  610. && layer.state === county.stateInfo[1]))) || !hasCounties;
  611. });
  612. const statesToHide = STATES.toAbbrArray();
  613.  
  614. _gisLayers.forEach(gisLayer => {
  615. const id = `#gis-layer-${gisLayer.id}-container`;
  616. if (!_settings.onlyShowApplicableLayers || applicableLayers.includes(gisLayer)) {
  617. $(id).show();
  618. $(`#gis-layers-for-${gisLayer.state}`).show();
  619. const idx = statesToHide.indexOf(gisLayer.state);
  620. if (idx > -1) statesToHide.splice(idx, 1);
  621. } else {
  622. $(id).hide();
  623. }
  624. });
  625. if (_settings.onlyShowApplicableLayers) {
  626. statesToHide.forEach(st => $(`#gis-layers-for-${st}`).hide());
  627. }
  628. }
  629.  
  630. function convertFeatureGeometry(gisLayer, featureGeometry) {
  631. if (gisLayer.spatialReference) {
  632. const proj = new OpenLayers.Projection(`EPSG:${gisLayer.spatialReference}`);
  633. featureGeometry.transform(proj, W.map.getProjectionObject());
  634. }
  635. return featureGeometry;
  636. }
  637.  
  638. function setStateFullAddress() {
  639. if (document.getElementsByClassName("location-info")){
  640. var full = document.getElementsByClassName("location-info")[0];
  641. if (full != undefined){
  642. var yy = full.innerText;
  643. if (yy.includes("Paraguay")){
  644. var deptos = _statesInExtent.join(', ');
  645. yy = yy.replace(/\[.*\]/g, '');
  646. yy += " [" + deptos + "]";
  647. document.getElementsByClassName("location-info")[0].innerText = yy;
  648. }
  649. }
  650. }
  651. }
  652.  
  653. const ROAD_ABBR = [
  654. [/\bAVDA./gi, 'Av.'], [/\bAVENIDA/gi, 'Av.'], [/\bCOURT$/, 'CT'], [/\bDRIVE$/, 'DR'],
  655. [/\bLANE$/, 'LN'], [/\bPARK$/, 'PK'], [/\bPLACE$/, 'PL'], [/\bROAD$/, 'RD'], [/\bSTREET$/, 'ST'],
  656. [/\bTERRACE$/, 'TER']
  657. ];
  658. function processFeatures(data, token, gisLayer) {
  659. const features = [];
  660. if (data.skipIt) {
  661. // do nothing
  662. } else if (data.error) {
  663. logError(`Error in layer "${gisLayer.name}": ${data.error.message}`);
  664. } else {
  665. let items = {}
  666. if (gisLayer.isFeatureSet){
  667. // storing result as cache if not already there
  668. if (!sessionStorage.getItem(gisLayer.id)){
  669. sessionStorage.setItem(gisLayer.id, JSON.stringify(data));
  670. }
  671. if (gisLayer.isFeatureSet == 1) {
  672. items = data.layers[0].featureSet.features;
  673. } else if (gisLayer.isFeatureSet == 2){ // 2 is for GeoNode
  674. items = data.features;
  675. } else if (gisLayer.isFeatureSet == 3){ // RawData
  676. items = data;
  677. }
  678. } else {
  679. items = data.features || [];
  680. }
  681. if (!token.cancel) {
  682. let error = false;
  683. const distinctValues = [];
  684. items.forEach(item => {
  685. let skipIt = false;
  686. if (!token.cancel && !error) {
  687. let feature;
  688. let featureGeometry;
  689. let area;
  690. if (gisLayer.distinctFields) {
  691. if (distinctValues.some(v => gisLayer.distinctFields.every(
  692. fld => v[fld] === item.attributes[fld]
  693. ))) {
  694. skipIt = true;
  695. } else {
  696. const dist = {};
  697. gisLayer.distinctFields.forEach(fld => (dist[fld] = item.attributes[fld]));
  698. distinctValues.push(dist);
  699. }
  700. }
  701. if (!skipIt) {
  702. let isPolyLine = false;
  703. let layerOffset = _settings.getLayerSetting(gisLayer.id, 'offset');
  704. if (!layerOffset) {
  705. layerOffset = { x: 0, y: 0 };
  706. }
  707. // Special handling for this layer, because it doesn't have a geometry property.
  708. // Coordinates are stored in the attributes.
  709. // if (gisLayer.id === 'nc-richmond-co-pts') {
  710. // const pt = new OpenLayers.Geometry.Point(item.attributes.XCOOR, item.attributes.YCOOR);
  711. // pt.transform(W.map.getOLMap().displayProjection, W.map.getProjectionObject());
  712. // item.geometry = pt;
  713. // }
  714. if (!item.geometry && ["RawPointData",].indexOf(gisLayer.serverType) >= 0){
  715. item.geometry = "RawPointData"
  716. }
  717. if (item.geometry) {
  718. if (item.geometry.x) {
  719. featureGeometry = new OpenLayers.Geometry.Point(
  720. item.geometry.x + layerOffset.x,
  721. item.geometry.y + layerOffset.y
  722. );
  723. } else if (item.geometry.points) {
  724. // @TODO Fix for multiple points instead of just grabbing first.
  725. featureGeometry = new OpenLayers.Geometry.Point(
  726. item.geometry.points[0][0] + layerOffset.x,
  727. item.geometry.points[0][1] + layerOffset.y
  728. );
  729. } else if (item.geometry.rings) {
  730. const rings = [];
  731. item.geometry.rings.forEach(ringIn => {
  732. const pnts = [];
  733. for (let i = 0; i < ringIn.length; i++) {
  734. pnts.push(new OpenLayers.Geometry.Point(
  735. ringIn[i][0] + layerOffset.x,
  736. ringIn[i][1] + layerOffset.y
  737. ));
  738. }
  739. rings.push(new OpenLayers.Geometry.LinearRing(pnts));
  740. });
  741. featureGeometry = new OpenLayers.Geometry.Polygon(rings);
  742. if (gisLayer.areaToPoint) {
  743. featureGeometry = featureGeometry.getCentroid();
  744. } else {
  745. area = featureGeometry.getArea();
  746. }
  747. } else if (data.geometryType === 'esriGeometryPolyline') {
  748. // We have to handle polylines differently since each item can have multiple features.
  749. // In terms of ArcGIS, each feature's geometry can have multiple paths. For instance
  750. // a single road can be broken into parts that are physically not connected to each other.
  751. let label = '';
  752. const hasVisibleAtZoom = gisLayer.hasOwnProperty('visibleAtZoom');
  753. const hasLabelsVisibleAtZoom = gisLayer.hasOwnProperty('labelsVisibleAtZoom');
  754. const displayLabelsAtZoom = hasLabelsVisibleAtZoom ? gisLayer.labelsVisibleAtZoom
  755. : (hasVisibleAtZoom ? gisLayer.visibleAtZoom : DEFAULT_VISIBLE_AT_ZOOM) + 1;
  756. if (gisLayer.labelHeaderFields) {
  757. label = `${gisLayer.labelHeaderFields.map(
  758. fieldName => item.attributes[fieldName]
  759. ).join(' ').trim()}\n`;
  760. }
  761. if (W.map.getZoom() - 12 >= displayLabelsAtZoom || area >= 5000) { //TODO: CHECK THIS LINE
  762. label += gisLayer.labelFields.map(
  763. fieldName => item.attributes[fieldName]
  764. ).join(' ').trim();
  765. if (gisLayer.processLabel) {
  766. label = gisLayer.processLabel(label, item.attributes);
  767. label = label ? label.trim() : '';
  768. }
  769. }
  770.  
  771. // Use Turf library to clip the geometry to the screen bounds.
  772. // This allows labels to stay in view on very long roads.
  773. const mls = turf.multiLineString(item.geometry.paths);
  774. const e = W.map.getExtent();
  775. const bbox = [e.left, e.bottom, e.right, e.top];
  776. const clipped = turf.bboxClip(mls, bbox);
  777. if (clipped.geometry.type === 'LineString') {
  778. item.geometry.paths = [clipped.geometry.coordinates];
  779. } else if (clipped.geometry.type === 'MultiLineString') {
  780. item.geometry.paths = clipped.geometry.coordinates;
  781. }
  782.  
  783. item.geometry.paths.forEach(path => {
  784. const pointList = [];
  785. path.forEach(point => pointList.push(new OpenLayers.Geometry.Point(
  786. point[0] + layerOffset.x,
  787. point[1] + layerOffset.y
  788. )));
  789. featureGeometry = new OpenLayers.Geometry.LineString(pointList);
  790. featureGeometry.skipDupeCheck = true;
  791.  
  792. const attributes = {
  793. layerID: gisLayer.id,
  794. label
  795. };
  796.  
  797. const lineFeature = new OpenLayers.Feature.Vector(featureGeometry, attributes);
  798. features.push(lineFeature);
  799. });
  800. isPolyLine = true;
  801. } else if (["GeoNode", "CartoDB"].indexOf(gisLayer.serverType) >= 0){
  802. if (item.geometry.type == "GeometryCollection") {
  803. let props = item.properties;
  804. item = item.geometry.geometries[0];
  805. item.geometry = item;
  806. item.properties = props;
  807. }
  808. if (item.geometry.type == "Point") {
  809. featureGeometry = new OpenLayers.Geometry.Point(
  810. item.geometry.coordinates[0] + layerOffset.x,
  811. item.geometry.coordinates[1] + layerOffset.y
  812. );
  813. } else if (item.geometry.type == "MultiPoint") {
  814. const rings = [];
  815. const pnts = [];
  816. item.geometry.coordinates.forEach(ringIn => {
  817. pnts.push(new OpenLayers.Geometry.Point(
  818. ringIn[0] + layerOffset.x,
  819. ringIn[1] + layerOffset.y
  820. ));
  821. });
  822. rings.push(new OpenLayers.Geometry.LinearRing(pnts));
  823. featureGeometry = new OpenLayers.Geometry.Polygon(rings);
  824. } else if (item.geometry.type == "Polygon") {
  825. const rings = [];
  826. item.geometry.coordinates.forEach(ringIn => {
  827. const pnts = [];
  828. for (let i = 0; i < ringIn.length; i++) {
  829. pnts.push(new OpenLayers.Geometry.Point(
  830. ringIn[i][0] + layerOffset.x,
  831. ringIn[i][1] + layerOffset.y
  832. ));
  833. }
  834. rings.push(new OpenLayers.Geometry.LinearRing(pnts));
  835. });
  836. featureGeometry = new OpenLayers.Geometry.Polygon(rings);
  837. if (gisLayer.areaToPoint) {
  838. featureGeometry = featureGeometry.getCentroid();
  839. } else {
  840. area = featureGeometry.getArea();
  841. }
  842. } else if (item.geometry.type == "MultiPolygon") {
  843. const source = item.geometry.coordinates[0];
  844. const polygonList = [];
  845. for (var i = 0; i < source.length; i += 1) {
  846. const pointList = [];
  847. for (var j = 0; j < source[i].length; j += 1) {
  848. var point = new OpenLayers.Geometry.Point(
  849. source[i][j][0],
  850. source[i][j][1]
  851. );
  852. pointList.push(point);
  853. }
  854. var linearRing = new OpenLayers.Geometry.LinearRing(pointList);
  855. var polygon = new OpenLayers.Geometry.Polygon([linearRing]);
  856. polygonList.push(polygon);
  857. }
  858. featureGeometry = new OpenLayers.Geometry.MultiPolygon(polygonList);
  859. } else if (item.geometry.type == "MultiLineString") {
  860. const pointList = [];
  861. item.geometry.coordinates.forEach(path => {
  862. path.forEach(point => pointList.push(new OpenLayers.Geometry.Point(
  863. point[0] + layerOffset.x,
  864. point[1] + layerOffset.y
  865. )));
  866. });
  867. featureGeometry = new OpenLayers.Geometry.LineString(pointList);
  868. featureGeometry.skipDupeCheck = true;
  869. } else if (item.geometry.type == "LineString") {
  870. const pointList = [];
  871. item.geometry.coordinates.forEach(point => {
  872. pointList.push(new OpenLayers.Geometry.Point(
  873. point[0] + layerOffset.x,
  874. point[1] + layerOffset.y
  875. ));
  876. });
  877. featureGeometry = new OpenLayers.Geometry.LineString(pointList);
  878. }
  879. featureGeometry = convertFeatureGeometry(gisLayer, featureGeometry);
  880. } else if (["RawPointData",].indexOf(gisLayer.serverType) >= 0){
  881. featureGeometry = new OpenLayers.Geometry.Point(item[`${gisLayer.processLon}`] + layerOffset.x, item[`${gisLayer.processLat}`] + layerOffset.y);
  882. featureGeometry = convertFeatureGeometry(gisLayer, featureGeometry);
  883. } else {
  884. logDebug(`Unexpected feature type in layer: ${JSON.stringify(item)}`);
  885. logError(`Error: Unexpected feature type in layer "${gisLayer.name}"`);
  886. error = true;
  887. }
  888. if (!error && !isPolyLine) {
  889. const hasVisibleAtZoom = gisLayer.hasOwnProperty('visibleAtZoom');
  890. const hasLabelsVisibleAtZoom = gisLayer.hasOwnProperty('labelsVisibleAtZoom');
  891. const displayLabelsAtZoom = hasLabelsVisibleAtZoom ? gisLayer.labelsVisibleAtZoom
  892. : (hasVisibleAtZoom ? gisLayer.visibleAtZoom : DEFAULT_VISIBLE_AT_ZOOM) + 1;
  893. let label = '';
  894. let attrs = [];
  895. if (["GeoNode", "CartoDB"].indexOf(gisLayer.serverType) >= 0){
  896. attrs = item.properties;
  897. } else if (["RawPointData"].indexOf(gisLayer.serverType) >= 0) {
  898. attrs = item;
  899. } else {
  900. attrs = item.attributes;
  901. }
  902. if (gisLayer.labelHeaderFields) {
  903. label = `${gisLayer.labelHeaderFields.map(
  904. fieldName => attrs[fieldName]
  905. ).join(' ').trim()}\n`;
  906. }
  907. if (W.map.getZoom() - 12 >= displayLabelsAtZoom || area >= 5000) {
  908. label += gisLayer.labelFields.map(
  909. fieldName => attrs[fieldName]
  910. ).join(' ').trim();
  911. if (gisLayer.processLabel) {
  912.  
  913. label = gisLayer.processLabel(label, attrs);
  914. label = label ? label.trim() : '';
  915. }
  916. }
  917. if (label && [
  918. LAYER_STYLES.points, LAYER_STYLES.parcels, LAYER_STYLES.state_points,
  919. LAYER_STYLES.state_parcels
  920. ].includes(gisLayer.style)) {
  921. if (_settings.addrLabelDisplay === 'hn') {
  922. const m = label.match(/^\d+/);
  923. label = m ? m[0] : '';
  924. } else if (_settings.addrLabelDisplay === 'street') {
  925. const m = label.match(/^(?:\d+\s)?(.*)/);
  926. label = m ? m[1].trim() : '';
  927. }
  928. else if (_settings.addrLabelDisplay === 'none') {
  929. label = '';
  930. }
  931. }
  932. const attributes = {
  933. layerID: gisLayer.id,
  934. label
  935. };
  936. if (gisLayer.isFeatureSet){
  937. // avoid drawing features that are not in extent
  938. const isFeatureInExtent = W.map.getExtent().intersectsBounds(featureGeometry.getBounds());
  939. if (!isFeatureInExtent) return;
  940. }
  941. feature = new OpenLayers.Feature.Vector(featureGeometry, attributes);
  942. features.push(feature);
  943. }
  944. }
  945. }
  946. }
  947. });
  948. }
  949. }
  950. if (!token.cancel) {
  951. // Check for duplicate geometries.
  952. for (let i = 0; i < features.length; i++) {
  953. const f1 = features[i];
  954. let labels = [f1.attributes.label];
  955. if (!f1.geometry.skipDupeCheck) {
  956. const c1 = f1.geometry.getCentroid();
  957.  
  958. for (let j = i + 1; j < features.length; j++) {
  959. const f2 = features[j];
  960. if (!f2.geometry.skipDupeCheck && f2.geometry.getCentroid().distanceTo(c1) < 1) {
  961. features.splice(j, 1);
  962. labels.push(f2.attributes.label);
  963. j--;
  964. }
  965. }
  966. }
  967. labels = _.uniq(labels);
  968. if (labels.length > 1) {
  969. labels.forEach((label, idx) => {
  970. label = label.replace(/\n/g, ' ').replace(/\s{2,}/, ' ').replace(/\bUNIT\s.{1,5}$/i, '').trim();
  971. ROAD_ABBR.forEach(abbr => (label = label.replace(abbr[0], abbr[1])));
  972. labels[idx] = label;
  973. });
  974. labels = _.uniq(labels);
  975. labels.sort();
  976. if (labels.length > 12) {
  977. const len = labels.length;
  978. labels = labels.slice(0, 10);
  979. labels.push(`(${len - 10} more...)`);
  980. }
  981. f1.attributes.label = _.uniq(labels).join('\n');
  982. } else {
  983. let { label } = f1.attributes;
  984. ROAD_ABBR.forEach(abbr => (label = label.replace(abbr[0], abbr[1])));
  985. f1.attributes.label = label;
  986. }
  987. }
  988.  
  989. const layer = gisLayer.isRoadLayer ? _roadLayer : _mapLayer;
  990. layer.removeFeatures(layer.getFeaturesByAttribute('layerID', gisLayer.id));
  991. layer.addFeatures(features);
  992.  
  993. if (features.length) {
  994. $(`label[for="gis-layer-${gisLayer.id}"]`).css({ color: '#00a009' });
  995. }
  996. }
  997. } // END processFeatures()
  998.  
  999. function fetchFeatures() {
  1000. if (!_settings.enabled) return;
  1001. if (_ignoreFetch) return;
  1002. if (W.map.getZoom() < 12 - 12) {// TODO: CHECK THIS LINE
  1003. filterLayerCheckboxes();
  1004. return;
  1005. }
  1006. _lastToken.cancel = true;
  1007. _lastToken = { cancel: false, features: [], layersProcessed: 0 };
  1008. $('.gis-state-layer-label').css({ color: '#777' });
  1009.  
  1010. let _layersCleared = false;
  1011.  
  1012. // if (layersToFetch.length) {
  1013. const extent = W.map.getExtent();
  1014. GM_xmlhttpRequest({
  1015. url: getCountiesUrl(extent),
  1016. method: 'GET',
  1017. onload(res) {
  1018. if (res.status < 400) {
  1019. const data = $.parseJSON(res.responseText);
  1020. if (data.error) {
  1021. logError(`Error in PY Census counties data: ${data.error.message}`);
  1022. } else {
  1023. _countiesInExtent = data.features.map(feature => {
  1024. const name = feature.attributes.BASENAME.toLowerCase();
  1025. const stateInfo = STATES.fromId(parseInt(feature.attributes.STATE, 10));
  1026. return { name, stateInfo };
  1027. });
  1028. logDebug(`PY Census counties: ${_countiesInExtent.map(c => `${c.name} ${c.stateInfo[1]}`).join(', ')}`);
  1029. _statesInExtent = _.uniq(data.features.map(
  1030. // eslint-disable-next-line radix
  1031. feature => STATES.fromId(parseInt(feature.attributes.STATE, 10))[0]
  1032. ));
  1033. setStateFullAddress();
  1034. let layersToFetch;
  1035. if (!_layersCleared) {
  1036. _layersCleared = true;
  1037. layersToFetch = getFetchableLayers();
  1038.  
  1039. // Remove features of any layers that won't be mapped.
  1040. _gisLayers.forEach(gisLayer => {
  1041. if (!layersToFetch.includes(gisLayer)) {
  1042. _mapLayer.removeFeatures(_mapLayer.getFeaturesByAttribute('layerID', gisLayer.id));
  1043. _roadLayer.removeFeatures(_roadLayer.getFeaturesByAttribute('layerID', gisLayer.id));
  1044. }
  1045. });
  1046. }
  1047.  
  1048. layersToFetch = layersToFetch.filter(layer => !layer.hasOwnProperty('counties')
  1049. || layer.counties.some(countyName => _countiesInExtent.some(county => county.name === countyName.toLowerCase()
  1050. && layer.state === county.stateInfo[1])));
  1051. filterLayerCheckboxes();
  1052. logDebug(`Fetching ${layersToFetch.length} layers...`);
  1053. logDebug(layersToFetch);
  1054. layersToFetch.forEach(gisLayer => {
  1055. const url = getUrl(extent, gisLayer);
  1056. if (gisLayer.isFeatureSet){ // trying to retrieve cached data from sessionStorage
  1057. let sessionValue = sessionStorage.getItem(gisLayer.id);
  1058. if (sessionValue){
  1059. logDebug(`Processing features of ${gisLayer.id} from storage (RawData)...`);
  1060. processFeatures($.parseJSON(sessionValue), {}, gisLayer);
  1061. return;
  1062. }
  1063. }
  1064. GM_xmlhttpRequest({
  1065. url,
  1066. context: _lastToken,
  1067. method: 'GET',
  1068. headers: (gisLayer.customHeaders) ? $.parseJSON(gisLayer.customHeaders): {},
  1069. onload(res2) {
  1070. if (res2.status < 400) { // Handle stupid issue where http 4## is considered success
  1071. processFeatures($.parseJSON(res2.responseText), res2.context, gisLayer);
  1072. } else {
  1073. logDebug(`HTTP request error: ${JSON.stringify(res2)}`);
  1074. logError(`Could not fetch layer "${gisLayer.id}". Request returned ${res2.status}`);
  1075. $(`label[for="gis-layer-${gisLayer.id}"]`).css({ color: '#ff0000' });
  1076. }
  1077. },
  1078. onerror(res3) {
  1079. logDebug(`xmlhttpRequest error:${JSON.stringify(res3)}`);
  1080. logError(`Could not fetch layer "${gisLayer.id}". An error was thrown.`);
  1081. $(`label[for="gis-layer-${gisLayer.id}"]`).css({ color: '#ff0000' });
  1082. }
  1083. });
  1084. });
  1085. }
  1086. } else {
  1087. logDebug(`HTTP request error: ${JSON.stringify(res)}`);
  1088. logError(`Could not fetch counties from PY Census site. Request returned ${res.status}`);
  1089. }
  1090. },
  1091. onerror(res) {
  1092. logDebug(`xmlhttpRequest error:${JSON.stringify(res)}`);
  1093. logError('Could not fetch counties from PY Census site. An error was thrown.');
  1094. }
  1095. });
  1096. }
  1097.  
  1098. function showScriptInfoAlert() {
  1099. /* Check version and alert on update */
  1100. if (ALERT_UPDATE && SCRIPT_VERSION !== _settings.lastVersion) {
  1101. // alert(SCRIPT_VERSION_CHANGES);
  1102. let releaseNotes = '';
  1103. releaseNotes += '<p>What\'s New:</p>';
  1104. if (SCRIPT_VERSION_CHANGES.length > 0) {
  1105. releaseNotes += '<ul>';
  1106. for (let idx = 0; idx < SCRIPT_VERSION_CHANGES.length; idx++)
  1107. releaseNotes += `<li>${SCRIPT_VERSION_CHANGES[idx]}`;
  1108. releaseNotes += '</ul>';
  1109. }
  1110. else {
  1111. releaseNotes += '<ul><li>Nothing major.</ul>';
  1112. }
  1113. WazeWrap.Interface.ShowScriptUpdate(GM_info.script.name, SCRIPT_VERSION, releaseNotes, GF_URL);
  1114. }
  1115. }
  1116.  
  1117. function setEnabled(value) {
  1118. _settings.enabled = value;
  1119. saveSettingsToStorage();
  1120. _mapLayer.setVisibility(value);
  1121. _roadLayer.setVisibility(value);
  1122. const color = value ? '#00bd00' : '#ccc';
  1123. $('span#gis-layers-power-btn').css({ color });
  1124. if (value) fetchFeatures();
  1125. $('#layer-switcher-item_gis_layers').prop('checked', value);
  1126. }
  1127.  
  1128. function onGisLayerToggleChanged() {
  1129. const checked = $(this).is(':checked');
  1130. const layerId = $(this).data('layer-id');
  1131. const idx = _settings.visibleLayers.indexOf(layerId);
  1132. if (checked) {
  1133. const gisLayer = _gisLayers.find(l => l.id === layerId);
  1134. if (gisLayer.oneTimeAlert) {
  1135. const lastAlertHash = _settings.oneTimeAlerts[layerId];
  1136. const newAlertHash = hashString(gisLayer.oneTimeAlert);
  1137. if (lastAlertHash !== newAlertHash) {
  1138. // alert(`Layer: ${gisLayer.name}\n\nMessage:\n${gisLayer.oneTimeAlert}`);
  1139. WazeWrap.Alerts.info(GM_info.script.name, `Layer: ${gisLayer.name}<br><br>Message:<br>${gisLayer.oneTimeAlert}`);
  1140. _settings.oneTimeAlerts[layerId] = newAlertHash;
  1141. saveSettingsToStorage();
  1142. }
  1143. }
  1144. if (idx === -1) _settings.visibleLayers.push(layerId);
  1145. } else if (idx > -1) _settings.visibleLayers.splice(idx, 1);
  1146. if (!_ignoreFetch) {
  1147. saveSettingsToStorage();
  1148. fetchFeatures();
  1149. }
  1150. }
  1151.  
  1152. function onOnlyShowApplicableLayersChanged() {
  1153. _settings.onlyShowApplicableLayers = $(this).is(':checked');
  1154. saveSettingsToStorage();
  1155. fetchFeatures();
  1156. }
  1157.  
  1158. function onStateCheckChanged(evt) {
  1159. const state = evt.data;
  1160. const idx = _settings.selectedStates.indexOf(state);
  1161. if (evt.target.checked) {
  1162. if (idx === -1) _settings.selectedStates.push(state);
  1163. } else if (idx > -1) _settings.selectedStates.splice(idx, 1);
  1164. if (!_ignoreFetch) {
  1165. saveSettingsToStorage();
  1166. initLayersTab();
  1167. fetchFeatures();
  1168. }
  1169. }
  1170.  
  1171. function onLayerCheckboxChanged(checked) {
  1172. setEnabled(checked);
  1173. }
  1174.  
  1175. function setFillParcels(doFill) {
  1176. [LAYER_STYLES.parcels, LAYER_STYLES.state_parcels].forEach(style => {
  1177. style.fillOpacity = doFill ? 0.2 : 0;
  1178. });
  1179. }
  1180.  
  1181. function onFillParcelsCheckedChanged(evt) {
  1182. const { checked } = evt.target;
  1183. setFillParcels(checked);
  1184. _settings.fillParcels = checked;
  1185. saveSettingsToStorage();
  1186. fetchFeatures();
  1187. }
  1188.  
  1189. function onMapMove() {
  1190. if (_settings.enabled) fetchFeatures();
  1191. }
  1192.  
  1193. function onRefreshLayersClick() {
  1194. const $btn = $('#gis-layers-refresh');
  1195. if (!$btn.hasClass('fa-spin')) {
  1196. $btn.css({ cursor: 'auto' });
  1197. $btn.addClass('fa-spin');
  1198. init(false);
  1199. }
  1200. }
  1201.  
  1202. function onChevronClick(evt) {
  1203. const $target = $(evt.currentTarget);
  1204. $($target.children()[0])
  1205. .toggleClass('fa fa-fw fa-chevron-down')
  1206. .toggleClass('fa fa-fw fa-chevron-right');
  1207. $($target.siblings()[0]).toggleClass('collapse');
  1208. }
  1209.  
  1210. function doToggleABunch(evt, checkState) {
  1211. _ignoreFetch = true;
  1212. $(evt.target).closest('fieldset').find('input').prop('checked', !checkState).trigger('click');
  1213. _ignoreFetch = false;
  1214. saveSettingsToStorage();
  1215. if (evt.data) initLayersTab();
  1216. fetchFeatures();
  1217. }
  1218.  
  1219. function onSelectAllClick(evt) {
  1220. doToggleABunch(evt, true);
  1221. }
  1222.  
  1223. function onSelectNoneClick(evt) {
  1224. doToggleABunch(evt, false);
  1225. }
  1226.  
  1227. function onGisAddrDisplayChange(evt) {
  1228. _settings.addrLabelDisplay = evt.target.value;
  1229. saveSettingsToStorage();
  1230. fetchFeatures();
  1231. }
  1232.  
  1233. function onAddressDisplayShortcutKey() {
  1234. if (!$('#gisAddrDisplay-hn').is(':checked')) {
  1235. $('#gisAddrDisplay-hn').click();
  1236. } else {
  1237. $('#gisAddrDisplay-all').click();
  1238. }
  1239. }
  1240.  
  1241. function initLayer() {
  1242. const rules = _gisLayers.map(gisLayer => new OpenLayers.Rule({
  1243. filter: new OpenLayers.Filter.Comparison({
  1244. type: OpenLayers.Filter.Comparison.EQUAL_TO,
  1245. property: 'layerID',
  1246. value: gisLayer.id
  1247. }),
  1248. symbolizer: gisLayer.style
  1249. }));
  1250.  
  1251. setFillParcels(_settings.fillParcels);
  1252.  
  1253. const style = new OpenLayers.Style(DEFAULT_STYLE, { rules });
  1254. let existingLayer;
  1255. let uniqueName;
  1256.  
  1257. uniqueName = 'wmeGISLayersDefault';
  1258. existingLayer = W.map.layers.find(l => l.uniqueName === uniqueName); // Note: W.map.getLayerByUniqueName(...) isn't working.
  1259. if (existingLayer) W.map.removeLayer(existingLayer);
  1260. _mapLayer = new OpenLayers.Layer.Vector('PY GIS Layers - Default', {
  1261. uniqueName,
  1262. styleMap: new OpenLayers.StyleMap(style)
  1263. });
  1264.  
  1265. uniqueName = 'wmeGISLayersRoads';
  1266. existingLayer = W.map.layers.find(l => l.uniqueName === uniqueName); // Note: W.map.getLayerByUniqueName(...) isn't wworking.
  1267. if (existingLayer) W.map.removeLayer(existingLayer);
  1268. _roadLayer = new OpenLayers.Layer.Vector('PY GIS Layers - Roads', {
  1269. uniqueName,
  1270. styleMap: new OpenLayers.StyleMap(ROAD_STYLE)
  1271. });
  1272.  
  1273. _mapLayer.setVisibility(_settings.enabled);
  1274. _roadLayer.setVisibility(_settings.enabled);
  1275.  
  1276. W.map.addLayers([_roadLayer, _mapLayer]);
  1277. } // END InitLayer
  1278.  
  1279. function initLayersTab() {
  1280. const user = W.loginManager.user.attributes.userName.toLowerCase();
  1281. const states = _.uniq(_gisLayers.map(l => l.state)).filter(st => _settings.selectedStates.includes(st));
  1282.  
  1283. $('#panel-gis-state-layers').empty().append(
  1284. $('<div>', { class: 'controls-container' }).css({ 'padding-top': '0px' }).append(
  1285. $('<input>', { type: 'checkbox', id: 'only-show-applicable-gis-layers' }).change(
  1286. onOnlyShowApplicableLayersChanged
  1287. ).prop('checked', _settings.onlyShowApplicableLayers),
  1288. $('<label>', { for: 'only-show-applicable-gis-layers' })
  1289. .css({ 'white-space': 'pre-line' }).text('Solo mostrar capas aplicables')
  1290. ),
  1291. $('.gis-layers-state-checkbox:checked').length === 0
  1292. ? $('<div>').text('Marcar categoria de capas en solapa Configuraciones')
  1293. : states.map(st => $('<fieldset>', {
  1294. id: `gis-layers-for-${st}`,
  1295. style: 'border:1px solid silver;padding:4px;border-radius:4px;-webkit-padding-before: 0;'
  1296. }).append(
  1297. $('<legend>', { style: 'margin-bottom:0px;border-bottom-style:none;width:auto;' })
  1298. .click(onChevronClick).append(
  1299. $('<i>', {
  1300. class: 'fa fa-fw fa-chevron-down',
  1301. style: 'cursor: pointer;font-size: 12px;margin-right: 4px'
  1302. }),
  1303. $('<span>', {
  1304. style: 'font-size:14px;font-weight:600;text-transform: uppercase; cursor: pointer'
  1305. }).text(STATES.toFullName(st))
  1306. ),
  1307. $('<div>', { id: `${st}_body` }).append(
  1308. $('<div>').css({ 'font-size': '11px' }).append(
  1309. $('<span>').append(
  1310. 'Select ',
  1311. $('<a>', { href: '#' })
  1312. .text('Todos')
  1313. .click(onSelectAllClick),
  1314. ' / ',
  1315. $('<a>', { href: '#' })
  1316. .text('Ninguno')
  1317. .click(onSelectNoneClick)
  1318. )
  1319. ),
  1320. $('<div>', { class: 'controls-container', style: 'padding-top:0px;' }).append(
  1321. _gisLayers.filter(l => (l.state === st && (!PRIVATE_LAYERS.hasOwnProperty(l.id)
  1322. || PRIVATE_LAYERS[l.id].includes(user))))
  1323. .map(gisLayer => {
  1324. const id = `gis-layer-${gisLayer.id}`;
  1325. return $('<div>', { class: 'controls-container', id: `${id}-container` })
  1326. .css({ 'padding-top': '0px', display: 'block' })
  1327. .append(
  1328. $('<input>', { type: 'checkbox', id })
  1329. .data('layer-id', gisLayer.id)
  1330. .change(onGisLayerToggleChanged)
  1331. .prop('checked', _settings.visibleLayers.includes(gisLayer.id)),
  1332. $('<label>', { for: id, class: 'gis-state-layer-label' })
  1333. .css({ 'white-space': 'pre-line' })
  1334. .text(`${gisLayer.name}${gisLayer.restrictTo ? ' *' : ''}`)
  1335. .attr('title', gisLayer.restrictTo ? `Restringido a: ${gisLayer.restrictTo}` : '')
  1336. .contextmenu(evt => {
  1337. evt.preventDefault();
  1338. // TODO - enable the layer if it isn't already.
  1339. // Tried using click function on the evt target, but that didn't work.
  1340. _layerSettingsDialog.gisLayer = gisLayer;
  1341. _layerSettingsDialog.show();
  1342. })
  1343. );
  1344. })
  1345. )
  1346. )
  1347. ))
  1348. );
  1349. }
  1350.  
  1351. function initSettingsTab() {
  1352. const states = _.uniq(_gisLayers.map(l => l.state));
  1353. const createRadioBtn = (name, value, text, checked) => {
  1354. const id = `${name}-${value}`;
  1355. return [$('<input>', {
  1356. type: 'radio', id, name, value
  1357. }).prop('checked', checked), $('<label>', { for: id }).text(text).css({
  1358. paddingLeft: '15px', marginRight: '4px'
  1359. })];
  1360. };
  1361. $('#panel-gis-layers-settings').empty().append(
  1362. $('<fieldset>', {
  1363. style: 'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;margin-top:-8px;'
  1364. }).append(
  1365. $('<legend>', {
  1366. style: 'margin-bottom:0px;border-bottom-style:none;width:auto;'
  1367. }).append($('<span>', {
  1368. style: 'font-size:14px;font-weight:600;text-transform: uppercase;'
  1369. }).text('Etiquetas')),
  1370. $('<div>', { id: 'labelSettings' }).append(
  1371. $('<div>', { class: 'controls-container' }).css({ 'padding-top': '2px' }).append(
  1372. $('<label>', { style: 'font-weight:normal;' }).text('Addresses:'),
  1373. createRadioBtn('gisAddrDisplay', 'hn', 'Nro Casa', _settings.addrLabelDisplay === 'hn'),
  1374. createRadioBtn('gisAddrDisplay', 'street', 'Calle', _settings.addrLabelDisplay === 'street'),
  1375. createRadioBtn('gisAddrDisplay', 'all', 'Ambos', _settings.addrLabelDisplay === 'all'),
  1376. createRadioBtn('gisAddrDisplay', 'none', 'None', _settings.addrLabelDisplay === 'none'),
  1377. $('<i>', {
  1378. class: 'waze-tooltip',
  1379. id: 'gisAddrDisplayInfo',
  1380. 'data-toggle': 'tooltip',
  1381. style: 'margin-left:8px; font-size:12px',
  1382. 'data-placement': 'bottom',
  1383. title: `This may not work properly for all layers. Please report issues to ${SCRIPT_AUTHOR}.`
  1384. }).tooltip()
  1385. )
  1386. )
  1387. ),
  1388. $('<fieldset>', {
  1389. style: 'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;'
  1390. }).append(
  1391. $('<legend>', {
  1392. style: 'margin-bottom:0px;border-bottom-style:none;width:auto;'
  1393. }).append($('<span>', {
  1394. style: 'font-size:14px;font-weight:600;text-transform: uppercase;'
  1395. }).text('Categoria de Capas')),
  1396. $('<div>', { id: 'states_body' }).append(
  1397. $('<div>').css({ 'font-size': '11px' }).append(
  1398. $('<span>').append(
  1399. 'Select ',
  1400. $('<a>', { href: '#' }).text('All').click(true, onSelectAllClick),
  1401. ' / ',
  1402. $('<a>', { href: '#' }).text('None').click(true, onSelectNoneClick)
  1403. )
  1404. ),
  1405. $('<div>', { class: 'controls-container', style: 'padding-top:0px;' }).append(
  1406. states.map(st => {
  1407. const fullName = STATES.toFullName(st);
  1408. const id = `gis-layer-enable-state-${st}`;
  1409. return $('<div>', { class: 'controls-container' })
  1410. .css({ 'padding-top': '0px', display: 'block' })
  1411. .append(
  1412. $('<input>', { type: 'checkbox', id, class: 'gis-layers-state-checkbox' })
  1413. .change(st, onStateCheckChanged)
  1414. .prop('checked', _settings.selectedStates.includes(st)),
  1415. $('<label>', { for: id }).css({ 'white-space': 'pre-line', color: '#777' }).text(fullName)
  1416. );
  1417. })
  1418. )
  1419. )
  1420. )
  1421. );
  1422. $('#panel-gis-layers-settings').append(
  1423. $('<fieldset>', { style: 'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;' })
  1424. .append(
  1425. $('<legend>', { style: 'margin-bottom:0px;border-bottom-style:none;width:auto;' })
  1426. .append(
  1427. $('<span>', { style: 'font-size:14px;font-weight:600;text-transform: uppercase;' })
  1428. .text('Apariencia')
  1429. ),
  1430. $('<div>', { class: 'controls-container' }).css({ 'padding-top': '2px' }).append(
  1431. $('<input>', { type: 'checkbox', id: 'fill-parcels' })
  1432. .change(onFillParcelsCheckedChanged)
  1433. .prop('checked', _settings.fillParcels),
  1434. $('<label>', { for: 'fill-parcels' }).css({ 'white-space': 'pre-line', color: '#777' }).text('Llenar parcelas')
  1435. )
  1436. )
  1437. );
  1438. $('input[name=gisAddrDisplay]').change(onGisAddrDisplayChange);
  1439. }
  1440.  
  1441. async function initTab(firstCall = true) {
  1442. if (firstCall) {
  1443. const { user } = W.loginManager;
  1444. const content = $('<div>').append(
  1445. $('<span>', { style: 'font-size:14px;font-weight:600' }).text('Paraguay GIS Layers'),
  1446. $('<span>', { style: 'font-size:11px;margin-left:10px;color:#aaa;' }).text(GM_info.script.version),
  1447. // <a href="https://docs.google.com/forms/d/e/1FAIpQLSfMhBxF0P6bn8dFfOoNTAF1LHBFXr5w9oXvzqsii_TfA-_Bmw/viewform?usp=pp_url&entry.831784226=test" target="_blank" style="color: #6290b7;font-size: 12px;margin-left: 8px;" title="Report broken layers, bugs, request new layers, script features">Report an issue</a>
  1448. $('<a>', {
  1449. href: REQUEST_FORM_URL.replace('{username}', user.userName),
  1450. target: '_blank',
  1451. style: 'color: #6290b7;font-size: 12px;margin-left: 8px;',
  1452. title: 'Reportar capas rotas, bugs, solicitar nuevas capas, nuevas caracteristicas'
  1453. }).text('Enviar una solicitud'),
  1454. $('<span>', {
  1455. id: 'gis-layers-refresh',
  1456. class: 'fa fa-refresh',
  1457. style: 'float: right;',
  1458. 'data-toggle': 'tooltip',
  1459. title: 'Obtener nuevas informaciones del planilla primaria y refrescar todas las capas.'
  1460. }),
  1461. '<ul class="nav nav-tabs">'
  1462. + '<li class="active"><a data-toggle="tab" href="#panel-gis-state-layers" aria-expanded="true">'
  1463. + 'Capas'
  1464. + '</a></li>'
  1465. + '<li><a data-toggle="tab" href="#panel-gis-layers-settings" aria-expanded="true">'
  1466. + 'Configuracion'
  1467. + '</a></li> '
  1468. + '</ul>',
  1469. $('<div>', { class: 'tab-content', style: 'padding:8px;padding-top:2px' }).append(
  1470. $('<div>', { class: 'tab-pane active', id: 'panel-gis-state-layers', style: 'padding: 4px 0px 0px 0px; width: auto' }),
  1471. $('<div>', { class: 'tab-pane', id: 'panel-gis-layers-settings', style: 'padding: 4px 0px 0px 0px; width: auto' })
  1472. )
  1473. ).html();
  1474.  
  1475. const powerButtonColor = _settings.enabled ? '#00bd00' : '#ccc';
  1476. const labelText = $('<div>').append(
  1477. $('<span>', {
  1478. class: 'fa fa-power-off',
  1479. id: 'gis-layers-power-btn',
  1480. style: `margin-right: 5px;cursor: pointer;color: ${powerButtonColor};font-size: 13px;`,
  1481. title: 'Activar/Desactivar Paraguay GIS Layers'
  1482. }),
  1483. $('<span>', { title: 'PY GIS Layers' }).text('PY GIS-L')
  1484. ).html();
  1485.  
  1486. const { tabLabel, tabPane } = W.userscripts.registerSidebarTab('PY GIS-L');
  1487. tabLabel.innerHTML = labelText;
  1488. tabPane.innerHTML = content;
  1489. // Fix tab content div spacing.
  1490. $(tabPane).parent().css({ width: 'auto', padding: '6px' });
  1491.  
  1492. await W.userscripts.waitForElementConnected(tabPane);
  1493. $('#gis-layers-power-btn').click(evt => {
  1494. evt.stopPropagation();
  1495. setEnabled(!_settings.enabled);
  1496. });
  1497. $('#gis-layers-refresh').click(onRefreshLayersClick);
  1498. }
  1499.  
  1500. initSettingsTab();
  1501. initLayersTab();
  1502. }
  1503.  
  1504. function initGui(firstCall = true) {
  1505. initLayer();
  1506.  
  1507. if (firstCall) {
  1508. initTab(true);
  1509.  
  1510. WazeWrap.Interface.AddLayerCheckbox('Display', 'PY GIS Layers', _settings.enabled, onLayerCheckboxChanged);
  1511. // W.map.events.register('moveend', null, onMapMove);
  1512. WazeWrap.Events.register('moveend', null, onMapMove);
  1513. showScriptInfoAlert();
  1514. } else {
  1515. initTab(firstCall);
  1516. }
  1517. }
  1518.  
  1519. async function loadSpreadsheetAsync() {
  1520. let data;
  1521. try {
  1522. data = await $.getJSON(`${LAYER_DEF_SPREADSHEET_URL}?key=${DEC(API_KEY)}`);
  1523. } catch (err) {
  1524. throw new Error(`Spreadsheet call failed. (${err.status}: ${err.statusText})`);
  1525. }
  1526. const [[minVersion], fieldNames, ...layerDefRows] = data.values;
  1527. const REQUIRED_FIELD_NAMES = [
  1528. 'state', 'name', 'id', 'counties', 'url', 'where', 'labelFields',
  1529. 'processLabel', 'style', 'visibleAtZoom', 'labelsVisibleAtZoom', 'enabled',
  1530. 'restrictTo', 'oneTimeAlert', "areaToPoint", "isFeatureSet", "serverType"
  1531. ];
  1532. const result = { error: null };
  1533. const checkFieldNames = fldName => fieldNames.includes(fldName);
  1534.  
  1535. if (SCRIPT_VERSION < minVersion) {
  1536. result.error = `Script must be updated to at least version ${
  1537. minVersion} before layer definitions can be loaded.`;
  1538. } else if (fieldNames.length < REQUIRED_FIELD_NAMES.length) {
  1539. result.error = `Expected ${
  1540. REQUIRED_FIELD_NAMES.length} columns in layer definition data. Spreadsheet returned ${
  1541. fieldNames.length}.`;
  1542. } else if (!REQUIRED_FIELD_NAMES.every(fldName => checkFieldNames(fldName))) {
  1543. result.error = 'Script expected to see the following column names in the layer '
  1544. + `definition spreadsheet:\n${REQUIRED_FIELD_NAMES.join(', ')}\n`
  1545. + `But the spreadsheet returned these:\n${fieldNames.join(', ')}`;
  1546. }
  1547. if (!result.error) {
  1548. layerDefRows.filter(row => row.length).forEach(layerDefRow => {
  1549. const layerDef = { enabled: '0' };
  1550. fieldNames.forEach((fldName, fldIdx) => {
  1551. let value = layerDefRow[fldIdx];
  1552. if (value !== undefined && value.trim().length > 0) {
  1553. value = value.trim();
  1554. if (fldName === 'counties' || fldName === 'labelFields') {
  1555. value = value.split(',').map(item => item.trim());
  1556. } else if (fldName === 'processLabel') {
  1557. try {
  1558. // eslint-disable-next-line no-eval
  1559. value = eval(`(function(label, fieldValues){${value}})`);
  1560. } catch (ex) {
  1561. logError(`Error loading label processing function for layer "${
  1562. layerDef.id}".`);
  1563. logDebug(ex);
  1564. }
  1565. } else if (fldName === 'style') {
  1566. layerDef.isRoadLayer = value === 'roads';
  1567. if (LAYER_STYLES.hasOwnProperty(value)) {
  1568. value = LAYER_STYLES[value];
  1569. } else if (!layerDef.isRoadLayer) {
  1570. // If style is not defined, try to read in as JSON (custom style)
  1571. try {
  1572. value = JSON.parse(value);
  1573. } catch (ex) {
  1574. // ignore error
  1575. }
  1576. }
  1577. } else if (fldName === 'state') {
  1578. value = value ? value.toUpperCase() : value;
  1579. } else if (fldName === 'restrictTo') {
  1580. try {
  1581. const { user } = W.loginManager;
  1582. const values = value.split(',').map(v => v.trim().toLowerCase());
  1583. layerDef.notAllowed = !values.some(entry => {
  1584. const rankMatch = entry.match(/^r(\d)(\+am)?$/);
  1585. if (rankMatch) {
  1586. if (rankMatch[1] <= (user.attributes.rank + 1) && (!rankMatch[2] || user.attributes.isAreaManager)) {
  1587. return true;
  1588. }
  1589. } else if (entry === 'am' && user.attributes.isAreaManager) {
  1590. return true;
  1591. } else if (entry === user.attributes.userName.toLowerCase()) {
  1592. return true;
  1593. }
  1594. return false;
  1595. });
  1596. } catch (ex) {
  1597. logError(ex);
  1598. }
  1599. }
  1600. layerDef[fldName] = value;
  1601. } else if (fldName === 'labelFields') {
  1602. layerDef[fldName] = [''];
  1603. }
  1604. });
  1605. const enabled = layerDef.enabled && !['0', 'false', 'no', 'n'].includes(layerDef.enabled.toString().trim().toLowerCase());
  1606. if (!layerDef.notAllowed && (enabled || layerDef.restrictTo)) {
  1607. _gisLayers.push(layerDef);
  1608. }
  1609. });
  1610. }
  1611.  
  1612. return result;
  1613. }
  1614.  
  1615. function loadScriptUpdateMonitor() {
  1616. try {
  1617. const updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(SCRIPT_NAME, SCRIPT_VERSION, DOWNLOAD_URL, GM_xmlhttpRequest);
  1618. updateMonitor.start();
  1619. } catch (ex) {
  1620. // Report, but don't stop if ScriptUpdateMonitor fails.
  1621. logError(ex);
  1622. }
  1623. }
  1624.  
  1625. async function init(firstCall = true) {
  1626. _gisLayers = [];
  1627. if (firstCall) {
  1628. loadScriptUpdateMonitor();
  1629. initRoadStyle();
  1630. loadSettingsFromStorage();
  1631. installPathFollowingLabels();
  1632. // W.accelerators.events.listeners was removed in WME beta, so check for it here before calling WazeWrap.Interface.Shortcut
  1633. // Hopefully there will be a fix or workaround for this issue.
  1634. if (W.accelerators.events.listeners) {
  1635. new WazeWrap.Interface.Shortcut(
  1636. 'GisLayersAddrDisplay',
  1637. 'Activar/desactivar etiquetas/direcciones solo con numero casa (Paraguay GIS Layers)',
  1638. 'layers',
  1639. 'layersToggleGisAddressLabelDisplay',
  1640. _settings.toggleHnsOnlyShortcut,
  1641. onAddressDisplayShortcutKey,
  1642. null
  1643. ).add();
  1644. }
  1645. window.addEventListener('beforeunload', saveSettingsToStorage, false);
  1646. _layerSettingsDialog = new LayerSettingsDialog();
  1647. }
  1648. const t0 = performance.now();
  1649. try {
  1650. const result = await loadSpreadsheetAsync();
  1651. if (result.error) {
  1652. logError(result.error);
  1653. return;
  1654. }
  1655. // _layerRefinements.forEach(layerRefinement => {
  1656. // const layerDef = _gisLayers.find(layerDef2 => layerDef2.id === layerRefinement.id);
  1657. // if (layerDef) {
  1658. // Object.keys(layerRefinement).forEach(fldName => {
  1659. // const value = layerRefinement[fldName];
  1660. // if (fldName !== 'id' && layerDef.hasOwnProperty(fldName)) {
  1661. // logDebug(`The "${fldName}" property of layer "${
  1662. // layerDef.id}" has a value hardcoded in the script, and also defined in the spreadsheet.`
  1663. // + ' The spreadsheet value takes precedence.');
  1664. // } else if (value) layerDef[fldName] = value;
  1665. // });
  1666. // } else {
  1667. // logDebug(`Refined layer "${layerRefinement.id}" does not have a corresponding layer defined`
  1668. // + ' in the spreadsheet. It can probably be removed from the script.');
  1669. // }
  1670. // });
  1671. logDebug(`Loaded ${_gisLayers.length} layer definitions in ${Math.round(performance.now() - t0)} ms.`);
  1672. initGui(firstCall);
  1673. fetchFeatures();
  1674. $('#gis-layers-refresh').removeClass('fa-spin').css({ cursor: 'pointer' });
  1675. logDebug('Inicializado.');
  1676. } catch (err) {
  1677. logError(err);
  1678. }
  1679. }
  1680.  
  1681. function onWmeReady() {
  1682. if (WazeWrap && WazeWrap.Ready) {
  1683. logDebug('Inicializando...');
  1684. init();
  1685. } else {
  1686. logDebug('Bootstrap ha fallado. Reintentando...');
  1687. setTimeout(onWmeReady, 100);
  1688. }
  1689. }
  1690.  
  1691. function bootstrap() {
  1692. if (typeof W === 'object' && W.userscripts?.state.isReady) {
  1693. onWmeReady();
  1694. } else {
  1695. document.addEventListener('wme-ready', onWmeReady, { once: true });
  1696. }
  1697. }
  1698.  
  1699. bootstrap();
  1700.  
  1701. /*eslint-disable*/
  1702. function installPathFollowingLabels() {
  1703. // Copyright (c) 2015 by Jean-Marc.Viglino [at]ign.fr
  1704. // Dual-licensed under the CeCILL-B Licence (http://www.cecill.info/)
  1705. // and the Beerware license (http://en.wikipedia.org/wiki/Beerware),
  1706. // feel free to use and abuse it in your projects (the code, not the beer ;-).
  1707. //
  1708. //* Overwrite the SVG function to allow text along a path
  1709. //* setStyle function
  1710. //*
  1711. //* Add new options to the Openlayers.Style
  1712.  
  1713. // pathLabel: {String} Label to draw on the path
  1714. // pathLabelXOffset: {String} Offset along the line to start drawing text in pixel or %, default: "50%"
  1715. // pathLabelYOffset: {Number} Distance of the line to draw the text
  1716. // pathLabelCurve: {String} Smooth the line the label is drawn on (empty string for no)
  1717. // pathLabelReadable: {String} Make the label readable (empty string for no)
  1718.  
  1719. // * Extra standard values : all label and text values
  1720.  
  1721.  
  1722. // *
  1723. // * Method: removeChildById
  1724. // * Remove child in a node.
  1725. // *
  1726.  
  1727. function removeChildById(node, id) {
  1728. if (node.querySelector) {
  1729. var c = node.querySelector('#' + id);
  1730. if (c) node.removeChild(c);
  1731. return;
  1732. }
  1733. // For old browsers
  1734. var c = node.childNodes;
  1735. if (c) for (var i = 0; i < c.length; i++) {
  1736. if (c[i].id === id) {
  1737. node.removeChild(c[i]);
  1738. return;
  1739. }
  1740. }
  1741. }
  1742.  
  1743.  
  1744. // *
  1745. // * Method: setStyle
  1746. // * Use to set all the style attributes to a SVG node.
  1747. // *
  1748. // * Takes care to adjust stroke width and point radius to be
  1749. // * resolution-relative
  1750. // *
  1751. // * Parameters:
  1752. // * node - {SVGDomElement} An SVG element to decorate
  1753. // * style - {Object}
  1754. // * options - {Object} Currently supported options include
  1755. // * 'isFilled' {Boolean} and
  1756. // * 'isStroked' {Boolean}
  1757.  
  1758. var setStyle = OpenLayers.Renderer.SVG.prototype.setStyle;
  1759. OpenLayers.Renderer.SVG.LABEL_STARTOFFSET = { 'l': '0%', 'r': '100%', 'm': '50%' };
  1760.  
  1761. OpenLayers.Renderer.SVG.prototype.pathText = function (node, style, suffix) {
  1762. var label = this.nodeFactory(null, 'text');
  1763. label.setAttribute('id', node._featureId + '_' + suffix);
  1764. if (style.fontColor) label.setAttributeNS(null, 'fill', style.fontColor);
  1765. if (style.fontStrokeColor) label.setAttributeNS(null, 'stroke', style.fontStrokeColor);
  1766. if (style.fontStrokeWidth) label.setAttributeNS(null, 'stroke-width', style.fontStrokeWidth);
  1767. if (style.fontOpacity) label.setAttributeNS(null, 'opacity', style.fontOpacity);
  1768. if (style.fontFamily) label.setAttributeNS(null, 'font-family', style.fontFamily);
  1769. if (style.fontSize) label.setAttributeNS(null, 'font-size', style.fontSize);
  1770. if (style.fontWeight) label.setAttributeNS(null, 'font-weight', style.fontWeight);
  1771. if (style.fontStyle) label.setAttributeNS(null, 'font-style', style.fontStyle);
  1772. if (style.labelSelect === true) {
  1773. label.setAttributeNS(null, 'pointer-events', 'visible');
  1774. label._featureId = node._featureId;
  1775. } else {
  1776. label.setAttributeNS(null, 'pointer-events', 'none');
  1777. }
  1778.  
  1779. function getpath(pathStr, readeable) {
  1780. var npath = pathStr.split(',');
  1781. var pts = [];
  1782. if (!readeable || Number(npath[0]) - Number(npath[npath.length - 2]) < 0) {
  1783. while (npath.length) pts.push({ x: Number(npath.shift()), y: Number(npath.shift()) });
  1784. } else {
  1785. while (npath.length) pts.unshift({ x: Number(npath.shift()), y: Number(npath.shift()) });
  1786. }
  1787. return pts;
  1788. }
  1789.  
  1790. var path = this.nodeFactory(null, 'path');
  1791. var tpid = node._featureId + '_t' + suffix;
  1792. var tpath = node.getAttribute('points');
  1793. if (style.pathLabelCurve) {
  1794. var pts = getpath(tpath, style.pathLabelReadable);
  1795. var p = pts[0].x + ' ' + pts[0].y;
  1796. var dx, dy, s1, s2;
  1797. dx = (pts[0].x - pts[1].x) / 4;
  1798. dy = (pts[0].y - pts[1].y) / 4;
  1799. for (var i = 1; i < pts.length - 1; i++) {
  1800. p += ' C ' + (pts[i - 1].x - dx) + ' ' + (pts[i - 1].y - dy);
  1801. dx = (pts[i - 1].x - pts[i + 1].x) / 4;
  1802. dy = (pts[i - 1].y - pts[i + 1].y) / 4;
  1803. s1 = Math.sqrt(Math.pow(pts[i - 1].x - pts[i].x, 2) + Math.pow(pts[i - 1].y - pts[i].y, 2));
  1804. s2 = Math.sqrt(Math.pow(pts[i + 1].x - pts[i].x, 2) + Math.pow(pts[i + 1].y - pts[i].y, 2));
  1805. p += ' ' + (pts[i].x + s1 * dx / s2) + ' ' + (pts[i].y + s1 * dy / s2);
  1806. dx *= s2 / s1;
  1807. dy *= s2 / s1;
  1808. p += ' ' + pts[i].x + ' ' + pts[i].y;
  1809. }
  1810. p += ' C ' + (pts[i - 1].x - dx) + ' ' + (pts[i - 1].y - dy);
  1811. dx = (pts[i - 1].x - pts[i].x) / 4;
  1812. dy = (pts[i - 1].y - pts[i].y) / 4;
  1813. p += ' ' + (pts[i].x + dx) + ' ' + (pts[i].y + dy);
  1814. p += ' ' + pts[i].x + ' ' + pts[i].y;
  1815.  
  1816. path.setAttribute('d', 'M ' + p);
  1817. } else {
  1818. if (style.pathLabelReadable) {
  1819. var pts = getpath(tpath, style.pathLabelReadable);
  1820. var p = '';
  1821. for (var i = 0; i < pts.length; i++) p += ' ' + pts[i].x + ' ' + pts[i].y;
  1822. path.setAttribute('d', 'M ' + p);
  1823. } else path.setAttribute('d', 'M ' + tpath);
  1824. }
  1825. path.setAttribute('id', tpid);
  1826.  
  1827. var defs = this.createDefs();
  1828. removeChildById(defs, tpid);
  1829. defs.appendChild(path);
  1830.  
  1831. var textPath = this.nodeFactory(null, 'textPath');
  1832. textPath.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + tpid);
  1833. var align = style.labelAlign || OpenLayers.Renderer.defaultSymbolizer.labelAlign;
  1834. label.setAttributeNS(null, 'text-anchor', OpenLayers.Renderer.SVG.LABEL_ALIGN[align[0]] || 'middle');
  1835. textPath.setAttribute('startOffset', style.pathLabelXOffset || OpenLayers.Renderer.SVG.LABEL_STARTOFFSET[align[0]] || '50%');
  1836. label.setAttributeNS(null, 'dominant-baseline', OpenLayers.Renderer.SVG.LABEL_ALIGN[align[1]] || 'central');
  1837. if (style.pathLabelYOffset) label.setAttribute('dy', style.pathLabelYOffset);
  1838. //textPath.setAttribute('method','stretch');
  1839. //textPath.setAttribute('spacing','auto');
  1840.  
  1841. textPath.textContent = style.pathLabel;
  1842. label.appendChild(textPath);
  1843.  
  1844. removeChildById(this.textRoot, node._featureId + '_' + suffix);
  1845. this.textRoot.appendChild(label);
  1846. };
  1847.  
  1848. OpenLayers.Renderer.SVG.prototype.setStyle = function (node, style, options) {
  1849. if (node._geometryClass === 'OpenLayers.Geometry.LineString' && style.pathLabel) {
  1850. if (node._geometryClass === 'OpenLayers.Geometry.LineString' && style.pathLabel) {
  1851. var drawOutline = (!!style.labelOutlineWidth);
  1852. // First draw text in halo color and size and overlay the
  1853. // normal text afterwards
  1854. if (drawOutline) {
  1855. var outlineStyle = OpenLayers.Util.extend({}, style);
  1856. outlineStyle.fontColor = outlineStyle.labelOutlineColor;
  1857. outlineStyle.fontStrokeColor = outlineStyle.labelOutlineColor;
  1858. outlineStyle.fontStrokeWidth = style.labelOutlineWidth;
  1859. if (style.labelOutlineOpacity) outlineStyle.fontOpacity = style.labelOutlineOpacity;
  1860. delete outlineStyle.labelOutlineWidth;
  1861. this.pathText(node, outlineStyle, 'txtpath0');
  1862. }
  1863. this.pathText(node, style, 'txtpath');
  1864. setStyle.apply(this, arguments);
  1865. }
  1866. } else setStyle.apply(this, arguments);
  1867. return node;
  1868. };
  1869.  
  1870. // *
  1871. // * Method: drawGeometry
  1872. // * Remove the textpath if no geometry is drawn.
  1873. // *
  1874. // * Parameters:
  1875. // * geometry - {<OpenLayers.Geometry>}
  1876. // * style - {Object}
  1877. // * featureId - {String}
  1878. // *
  1879. // * Returns:
  1880. // * {Boolean} true if the geometry has been drawn completely; null if
  1881. // * incomplete; false otherwise
  1882.  
  1883. var drawGeometry = OpenLayers.Renderer.SVG.prototype.drawGeometry;
  1884. OpenLayers.Renderer.SVG.prototype.drawGeometry = function (geometry, style, id) {
  1885. var rendered = drawGeometry.apply(this, arguments);
  1886. if (rendered === false) {
  1887. removeChildById(this.textRoot, id + '_txtpath');
  1888. removeChildById(this.textRoot, id + '_txtpath0');
  1889. }
  1890. return rendered;
  1891. };
  1892.  
  1893. // *
  1894. // * Method: eraseGeometry
  1895. // * Erase a geometry from the renderer. In the case of a multi-geometry,
  1896. // * we cycle through and recurse on ourselves. Otherwise, we look for a
  1897. // * node with the geometry.id, destroy its geometry, and remove it from
  1898. // * the DOM.
  1899. // *
  1900. // * Parameters:
  1901. // * geometry - {<OpenLayers.Geometry>}
  1902. // * featureId - {String}
  1903.  
  1904. var eraseGeometry = OpenLayers.Renderer.SVG.prototype.eraseGeometry;
  1905. OpenLayers.Renderer.SVG.prototype.eraseGeometry = function (geometry, featureId) {
  1906. eraseGeometry.apply(this, arguments);
  1907. removeChildById(this.textRoot, featureId + '_txtpath');
  1908. removeChildById(this.textRoot, featureId + '_txtpath0');
  1909. };
  1910.  
  1911. }
  1912. })();