WME GIS Layers

Adds GIS layers in WME

当前为 2018-07-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name WME GIS Layers
  3. // @namespace https://greasyfork.org/users/45389
  4. // @version 2018.07.11.001
  5. // @description Adds GIS layers in WME
  6. // @author MapOMatic
  7. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  8. // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_info
  11. // @license GNU GPLv3
  12. // @connect *
  13. // ==/UserScript==
  14.  
  15. /* global OL */
  16. /* global W */
  17. /* global GM_info */
  18. /* global WazeWrap */
  19. /* global _ */
  20. /* global $ */
  21.  
  22. (function() {
  23. // **************************************************************************************************************
  24. // IMPORTANT: Update this when releasing a new version of script that includes changes to the spreadsheet format
  25. // that may cause old code to break. This # should match the version listed in the spreadsheet
  26. // i.e. update them at the same time.
  27.  
  28. const LAYER_DEF_VERSION = '2018.04.27.001';
  29.  
  30. // **************************************************************************************************************
  31.  
  32. const SCRIPT_AUTHOR = 'MapOMatic'; // Used in tooltips to tell people who to report issues to. Update if a new author takes ownership of this script.
  33. const LAYER_INFO_URL = 'https://spreadsheets.google.com/feeds/list/1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw/o7gusx3/public/values?alt=json';
  34. const LAYER_DEF_URL = 'https://spreadsheets.google.com/feeds/list/1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw/oj7k5j6/public/values?alt=json';
  35. const PRIVATE_LAYERS = {'nc-henderson-sl-signs': ['the_cre8r','mapomatic']}; // case sensitive -- use all lower case
  36.  
  37. const DEFAULT_STYLE = {
  38. fillColor: '#000',
  39. pointRadius: 4,
  40. label : '${label}',
  41. strokeColor: '#ffa500',
  42. strokeOpacity: '0.95',
  43. strokeWidth: 1.5,
  44. fontColor: '#ffc520',
  45. fontSize: '13',
  46. labelOutlineColor: 'black',
  47. labelOutlineWidth: 3
  48. };
  49.  
  50. const LAYER_STYLES = {
  51. cities: {
  52. fillOpacity: 0.3,
  53. fillColor: '#f65',
  54. strokeColor: '#f65',
  55. fontColor: '#f62'
  56. },
  57. forests_parks: {
  58. fillOpacity: 0.4,
  59. fillColor: '#585',
  60. strokeColor: '#484',
  61. fontColor: '#8b8'
  62. },
  63. milemarkers: {
  64. strokeColor: '#fff',
  65. fontColor: '#fff',
  66. fontWeight: 'bold',
  67. fillOpacity: 0,
  68. labelYOffset: 10,
  69. pointRadius: 2,
  70. fontSize: 12
  71. },
  72. parcels: {
  73. fillOpacity: 0,
  74. fillColor: '#ffa500'
  75. },
  76. points: {
  77. strokeColor: '#000',
  78. fontColor: '#0ff',
  79. fillColor: '#0ff',
  80. labelYOffset: -10,
  81. labelAlign: 'ct'
  82. },
  83. post_offices: {
  84. strokeColor: '#000',
  85. fontColor: '#f84',
  86. fillColor: '#f84',
  87. fontWeight: 'bold',
  88. labelYOffset: -10,
  89. labelAlign: 'ct'
  90. },
  91. state_parcels: {
  92. fillOpacity: 0,
  93. strokeColor: '#e62',
  94. fillColor: '#e62',
  95. fontColor: '#e73'
  96. },
  97. state_points: {
  98. strokeColor: '#000',
  99. fontColor: '#3cf',
  100. fillColor: '#3cf',
  101. labelYOffset: -10,
  102. labelAlign: 'ct'
  103. },
  104. structures: {
  105. fillOpacity: 0,
  106. strokeColor: '#f7f',
  107. fontColor: '#f7f'
  108. }
  109. };
  110.  
  111. const ROAD_STYLE = new OL.Style(
  112. {
  113. pointRadius: 12,
  114. fillColor:'#369',
  115. pathLabel: '${label}',
  116. label:'',
  117. fontColor:'#faf',
  118. labelSelect: true,
  119. pathLabelYOffset:'${getOffset}',
  120. pathLabelCurve: '${getSmooth}',
  121. pathLabelReadable: '${getReadable}',
  122. labelAlign: '${getAlign}',
  123. labelOutlineWidth: 3,
  124. labelOutlineColor: '#000',
  125. strokeWidth:3,
  126. stroke:true,
  127. strokeColor:'#f0f',
  128. strokeOpacity: 0.4,
  129. fontWeight: 'bold',
  130. fontSize: 11
  131. }, {
  132. context: {
  133. getOffset: function() { return -(W.map.getZoom()+5); },
  134. getSmooth: function() { return ''; },
  135. getReadable: function() { return '1'; },
  136. getAlign: function() { return 'cb'; }
  137. }
  138. }
  139. );
  140.  
  141. let _regexReplace = {
  142. // Strip leading zeros or blank full label for any label starting with a non-digit or is a Zero Address, use with '' as replace.
  143. r0: /^(0+(\s.*)?|\D.*)/,
  144. // Strip Everything After Street Type to end of the string by use $1 and $2 capture groups, use with replace '$1$2'
  145. 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,
  146. // Strip SPACE 5 Digits from end of string, use with replace ''
  147. r2: /\s\d{5}$/,
  148. // Strip Everything after a "~", ",", ";" to the end of the string, use with replace ''
  149. r3: /(~|,|;|\s?\r\n).*$/,
  150. // Move the digits after the last space to before the rest of the string using, use with replace '$2 $1'
  151. r4: /^(.*)\s(\d+).*/,
  152. // Insert newline between digits (including "-") and everything after the digits, except (and before) a ",", use with replace '$1\n$2'
  153. r5: /^([-\d]+)\s+([^,]+).*/,
  154. // Insert newline between digits and everything after the digits, use with replace '$1\n$2'
  155. r6: /^(\d+)\s+(.*)/
  156. };
  157.  
  158. let _gisLayers = [];
  159.  
  160. let _layerRefinements = [
  161. {id: 'us-post-offices',
  162. labelHeaderFields: ['LOCALE_NAME']
  163. },
  164.  
  165. {id: 'ky-warren-co-wku-structures',
  166. labelHeaderFields: ['Bldg_Name']
  167. }
  168. ];
  169.  
  170. const STATES = {
  171. _states:[
  172. ['US (Country)','US',-1],['Alabama','AL',1],['Alaska','AK',2],['American Samoa','AS',60],['Arizona','AZ',4],['Arkansas','AR',5],['California','CA',6],['Colorado','CO',8],['Connecticut','CT',9],['Delaware','DE',10],['District of Columbia','DC',11],
  173. ['Florida','FL',12],['Georgia','GA',13],['Guam','GU',66],['Hawaii','HI',15],['Idaho','ID',16],['Illinois','IL',17],['Indiana','IN',18],['Iowa','IA',19],['Kansas','KS',20],
  174. ['Kentucky','KY',21],['Louisiana','LA',22],['Maine','ME',23],['Maryland','MD',24],['Massachusetts','MA',25],['Michigan','MI',26],['Minnesota','MN',27],['Mississippi','MS',28],['Missouri','MO',29],
  175. ['Montana','MT',30],['Nebraska','NE',31],['Nevada','NV',32],['New Hampshire','NH',33],['New Jersey','NJ',34],['New Mexico','NM',35],['New York','NY',36],['North Carolina','NC',37],['North Dakota','ND',38],
  176. ['Northern Mariana Islands','MP',69],['Ohio','OH',39],['Oklahoma','OK',40],['Oregon','OR',41],['Pennsylvania','PA',42],['Puerto Rico','PR',72],['Rhode Island','RI',44],['South Carolina','SC',45],
  177. ['South Dakota','SD',46],['Tennessee','TN',47],['Texas','TX',48],['Utah','UT',49],['Vermont','VT',50],['Virgin Islands','VI',78],['Virginia','VA',51],['Washington','WA',53],['West Virginia','WV',54],['Wisconsin','WI',55],['Wyoming','WY',56]
  178. ],
  179. toAbbr: function(fullName) { return this._states.find(a => a[0] === fullName)[1]; },
  180. toFullName: function(abbr) { return this._states.find(a => a[1] === abbr)[0]; },
  181. toFullNameArray: function() { return this._states.map(a => a[0]); },
  182. toAbbrArray: function() { return this._states.map(a => a[1]); },
  183. fromId: function(id) { return this._states.find(a => a[2] === id); }
  184. };
  185. const DEFAULT_VISIBLE_AT_ZOOM = 6;
  186. const SETTINGS_STORE_NAME = 'wme_gis_layers_fl';
  187. const COUNTIES_URL = 'https://tigerweb.geo.census.gov/arcgis/rest/services/Census2010/State_County/MapServer/1/';
  188. const ALERT_UPDATE = false;
  189. const SCRIPT_VERSION = GM_info.script.version;
  190. const SCRIPT_VERSION_CHANGES = [
  191. GM_info.script.name + '\nv' + SCRIPT_VERSION + '\n\nWhat\'s New\n------------------------------\n',
  192. '\n- Update for new WME layers menu.'
  193. ].join('');
  194. let _mapLayer = null;
  195. let _roadLayer = null;
  196. let _settings = {};
  197. let _ignoreFetch = false;
  198. let _lastToken = {};
  199.  
  200. const DEBUG = true;
  201. function log(message) { console.log('GIS Layers:', message); }
  202. function logError(message) { console.error('GIS Layers:', message); }
  203. function logDebug(message) { if (DEBUG) console.debug('GIS Layers:', message); }
  204. function logWarning(message) { console.warn('GIS Layers:', message); }
  205.  
  206. function loadSettingsFromStorage() {
  207. let loadedSettings = $.parseJSON(localStorage.getItem(SETTINGS_STORE_NAME));
  208. let defaultSettings = {
  209. lastVersion: null,
  210. visibleLayers: [],
  211. onlyShowApplicableLayers: false,
  212. selectedStates: [],
  213. enabled: true,
  214. fillParcels: false,
  215. addrLabelDisplay: 'all'
  216. };
  217. _settings = loadedSettings ? loadedSettings : defaultSettings;
  218. for (let prop in defaultSettings) {
  219. if (!_settings.hasOwnProperty(prop)) {
  220. _settings[prop] = defaultSettings[prop];
  221. }
  222. }
  223. }
  224.  
  225. function saveSettingsToStorage() {
  226. if (localStorage) {
  227. _settings.lastVersion = SCRIPT_VERSION;
  228. localStorage.setItem(SETTINGS_STORE_NAME, JSON.stringify(_settings));
  229. log('Settings saved');
  230. }
  231. }
  232.  
  233. function getUrl(extent, gisLayer) {
  234. if (gisLayer.spatialReference) {
  235. let proj = new OL.Projection('EPSG:' + gisLayer.spatialReference);
  236. extent.transform(W.map.getProjection(), proj);
  237. }
  238. let geometry = { xmin:extent.left, ymin:extent.bottom, xmax:extent.right, ymax:extent.top, spatialReference: {wkid: gisLayer.spatialReference ? gisLayer.spatialReference : 102100, latestWkid: gisLayer.spatialReference ? gisLayer.spatialReference : 3857} };
  239. let geometryStr = JSON.stringify(geometry);
  240. let url = gisLayer.url + '/query?geometry=' + encodeURIComponent(geometryStr);
  241. let fields = gisLayer.labelFields;
  242. if (gisLayer.labelHeaderFields) {
  243. fields = fields.concat(gisLayer.labelHeaderFields);
  244. }
  245. if (gisLayer.distinctFields) {
  246. fields = fields.concat(gisLayer.distinctFields);
  247. }
  248. url += gisLayer.token ? '&token=' + gisLayer.token : '';
  249. url += '&outFields=' + encodeURIComponent(fields.join(','));
  250. url += '&returnGeometry=true';
  251. url += '&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=' + (gisLayer.spatialReference ? gisLayer.spatialReference : '102100') + '&outSR=3857&f=json';
  252. if (gisLayer.where) {
  253. url += '&where=' + encodeURIComponent(gisLayer.where);
  254. }
  255. logDebug('Request URL: ' + url);
  256. return url;
  257. }
  258.  
  259. function getCountiesUrl(extent) {
  260. let geometry = { xmin:extent.left, ymin:extent.bottom, xmax:extent.right, ymax:extent.top, spatialReference: {wkid: 102100, latestWkid: 3857} };
  261. let url = COUNTIES_URL + '/query?geometry=' + encodeURIComponent(JSON.stringify(geometry));
  262. return url + '&outFields=BASENAME%2CSTATE&returnGeometry=false&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=102100&outSR=3857&f=json';
  263. }
  264.  
  265. let _countiesInExtent = [];
  266. let _statesInExtent = [];
  267.  
  268. function getFetchableLayers(getInvisible) {
  269. return _gisLayers.filter(gisLayer => {
  270. let isValidUrl = gisLayer.url && gisLayer.url.trim().length > 0;
  271. let isVisible = (getInvisible || _settings.visibleLayers.indexOf(gisLayer.id) > -1) && _settings.selectedStates.indexOf(gisLayer.state) > -1;
  272. let isInState = gisLayer.state === 'US' || _statesInExtent.indexOf(STATES.toFullName(gisLayer.state)) > -1;
  273. // Be sure to use hasOwnProperty when checking this, since 0 is a valid value.
  274. let isValidZoom = getInvisible || W.map.getZoom() >= (gisLayer.hasOwnProperty('visibleAtZoom') ? gisLayer.visibleAtZoom : DEFAULT_VISIBLE_AT_ZOOM);
  275. return isValidUrl && isInState && isVisible && isValidZoom;
  276. });
  277. }
  278.  
  279. function filterLayerCheckboxes() {
  280. let applicableLayers = getFetchableLayers(true).filter(layer => {
  281. let hasCounties = layer.hasOwnProperty('counties');
  282. return (hasCounties && layer.counties.some(county => _countiesInExtent.indexOf(county.toLowerCase()) > -1)) || !hasCounties;
  283. });
  284. var statesToHide = STATES.toAbbrArray();
  285.  
  286. _gisLayers.forEach(gisLayer => {
  287. let id = '#gis-layer-' + gisLayer.id + '-container';
  288. if(!_settings.onlyShowApplicableLayers || applicableLayers.indexOf(gisLayer) > -1){
  289. $(id).show();
  290. $('#gis-layers-for-' + gisLayer.state).show();
  291. let idx = statesToHide.indexOf(gisLayer.state);
  292. if (idx > -1) statesToHide.splice(idx, 1);
  293. } else {
  294. $(id).hide();
  295. }
  296. });
  297. if (_settings.onlyShowApplicableLayers) {
  298. statesToHide.forEach(st => $('#gis-layers-for-' + st).hide());
  299. }
  300. }
  301.  
  302. const ROAD_ABBR = [[/\bAVENUE$/,'AVE'], [/\bCIRCLE$/,'CIR'], [/\bCOURT$/,'CT'], [/\bDRIVE$/,'DR'], [/\bLANE$/,'LN'], [/\bPARK$/,'PK'], [/\bPLACE$/,'PL'], [/\bROAD$/,'RD'], [/\bSTREET$/,'ST'], [/\bTERRACE$/,'TER']];
  303. function processFeatures(data, token, gisLayer) {
  304. let features = [];
  305. if (data.skipIt) {
  306. // do nothing
  307. } else if (data.error) {
  308. logError('Error in layer "' + gisLayer.name + '": ' + data.error.message);
  309. } else {
  310. let items = data.features;
  311. if (!token.cancel) {
  312. let error = false;
  313. let distinctValues = [];
  314. items.forEach(item => {
  315. let skipIt = false;
  316. if (!token.cancel && !error) {
  317. let feature;
  318. let featureGeometry;
  319. let area;
  320. if (gisLayer.distinctFields) {
  321. if (distinctValues.some( v => gisLayer.distinctFields.every(fld => v[fld] === item.attributes[fld]) )) {
  322. skipIt = true;
  323. } else {
  324. let dist = {};
  325. gisLayer.distinctFields.forEach(fld => dist[fld] = item.attributes[fld]);
  326. distinctValues.push(dist);
  327. }
  328. }
  329. if (!skipIt) {
  330. let layerOffset = gisLayer.layerOffset ? gisLayer.layerOffset : {x: 0, y: 0};
  331. // Special handling for this layer, because it doesn't have a geometry property. Coordinates are stored in the attributes.
  332. if (gisLayer.id === 'nc-richmond-co-pts') {
  333. let pt = new OL.Geometry.Point(item.attributes.XCOOR, item.attributes.YCOOR);
  334. pt.transform(W.map.displayProjection, W.map.projection);
  335. item.geometry = pt;
  336. }
  337. if (item.geometry) {
  338. if (item.geometry.x) {
  339. featureGeometry = new OL.Geometry.Point(item.geometry.x + layerOffset.x, item.geometry.y + layerOffset.y);
  340. } else if (item.geometry.points) {
  341. // @TODO Fix for multiple points instead of just grabbing first.
  342. featureGeometry = new OL.Geometry.Point(item.geometry.points[0][0] + layerOffset.x, item.geometry.points[0][1] + layerOffset.y);
  343. } else if (item.geometry.rings) {
  344. let rings = [];
  345. item.geometry.rings.forEach(function(ringIn) {
  346. let pnts= [];
  347. for(let i=0;i<ringIn.length;i++){
  348. pnts.push(new OL.Geometry.Point(ringIn[i][0] + layerOffset.x, ringIn[i][1] + layerOffset.y));
  349. }
  350. rings.push(new OL.Geometry.LinearRing(pnts));
  351. });
  352. featureGeometry = new OL.Geometry.Polygon(rings);
  353. if (gisLayer.areaToPoint) {
  354. featureGeometry = featureGeometry.getCentroid();
  355. } else {
  356. area = featureGeometry.getArea();
  357. }
  358. } else if (data.geometryType === 'esriGeometryPolyline') {
  359. let pointList = [];
  360. item.geometry.paths.forEach(function(path){
  361. path.forEach(point => pointList.push(new OL.Geometry.Point(point[0] + layerOffset.x, point[1] + layerOffset.y)));
  362. });
  363. featureGeometry = new OL.Geometry.LineString(pointList);
  364. featureGeometry.skipDupeCheck = true;
  365. } else {
  366. logDebug('Unexpected feature type in layer: ' + JSON.stringify(item));
  367. logError('Error: Unexpected feature type in layer "' + gisLayer.name + '"');
  368. error = true;
  369. }
  370. if (!error) {
  371. let hasVisibleAtZoom = gisLayer.hasOwnProperty('visibleAtZoom');
  372. let hasLabelsVisibleAtZoom = gisLayer.hasOwnProperty('labelsVisibleAtZoom');
  373. let displayLabelsAtZoom = hasLabelsVisibleAtZoom ? gisLayer.labelsVisibleAtZoom : (hasVisibleAtZoom ? gisLayer.visibleAtZoom : DEFAULT_VISIBLE_AT_ZOOM) + 1;
  374. let label = '';
  375. if (gisLayer.labelHeaderFields) {
  376. label = gisLayer.labelHeaderFields.map(fieldName => item.attributes[fieldName]).join(' ').trim() + '\n';
  377. }
  378. if (W.map.getZoom() >= displayLabelsAtZoom || area >= 5000) {
  379. label += gisLayer.labelFields.map(fieldName => item.attributes[fieldName]).join(' ').trim();
  380. if (gisLayer.processLabel) label = gisLayer.processLabel(label, item.attributes).trim();
  381. }
  382. if (label && [LAYER_STYLES.points, LAYER_STYLES.parcels, LAYER_STYLES.state_points, LAYER_STYLES.state_parcels].indexOf(gisLayer.style) > -1) {
  383. if (_settings.addrLabelDisplay === 'hn') {
  384. let m = label.match(/^\d+/);
  385. label = m ? m[0] : '';
  386. } else if (_settings.addrLabelDisplay === 'street') {
  387. let m = label.match(/^(?:\d+\s)?(.*)/);
  388. label = m ? m[1].trim() : '';
  389. }
  390. }
  391. let attributes = {
  392. layerID: gisLayer.id,
  393. label: label
  394. };
  395. feature = new OL.Feature.Vector(featureGeometry,attributes);
  396. features.push(feature);
  397. }
  398. }
  399. }
  400. }
  401. });
  402. }
  403. }
  404. if (!token.cancel) {
  405. // Check for duplicate geometries.
  406. for (let i=0; i<features.length; i++) {
  407. let f1 = features[i];
  408. if (!f1.geometry.skipDupeCheck) {
  409. let c1 = f1.geometry.getCentroid();
  410. let labels = [f1.attributes.label];
  411. for (let j=i+1; j<features.length; j++) {
  412. let f2 = features[j];
  413. if (!f2.geometry.skipDupeCheck && f2.geometry.getCentroid().distanceTo(c1) < 1) {
  414. features.splice(j,1);
  415. labels.push(f2.attributes.label);
  416. j--;
  417. }
  418. }
  419. labels = _.unique(labels);
  420. if (labels.length > 1) {
  421. labels.forEach((label, idx) => {
  422. label = label.replace(/\n/g,' ').replace(/\s{2,}/,' ').replace(/\bUNIT\s.{1,5}$/i,'').trim();
  423. ROAD_ABBR.forEach(abbr => label = label.replace(abbr[0], abbr[1]));
  424. labels[idx] = label;
  425. });
  426. labels = _.unique(labels);
  427. labels.sort();
  428. if (labels.length > 12) {
  429. let len = labels.length;
  430. labels = labels.slice(0,10);
  431. labels.push('(' + (len - 10) + ' more...)');
  432. }
  433. f1.attributes.label = _.unique(labels).join('\n');
  434. } else {
  435. let label = f1.attributes.label;
  436. ROAD_ABBR.forEach(abbr => label = label.replace(abbr[0], abbr[1]));
  437. f1.attributes.label = label;
  438. }
  439. }
  440. }
  441.  
  442. let layer = gisLayer.isRoadLayer ? _roadLayer : _mapLayer;
  443. layer.removeFeatures(layer.getFeaturesByAttribute('layerID', gisLayer.id));
  444. layer.addFeatures(features);
  445.  
  446. if (features.length) {
  447. $('label[for="gis-layer-' + gisLayer.id + '"]').css({color:'#00a009'});
  448. }
  449. }
  450. } // END processFeatures()
  451.  
  452. function fetchFeatures() {
  453. if (_ignoreFetch) return;
  454. _lastToken.cancel = true;
  455. _lastToken = {cancel: false, features: [], layersProcessed: 0};
  456. $('.gis-state-layer-label').css({'color':'#777'});
  457.  
  458. let _layersCleared = false;
  459.  
  460. //if (layersToFetch.length) {
  461. let extent = W.map.getExtent();
  462. GM_xmlhttpRequest({
  463. url: getCountiesUrl(extent),
  464. method: 'GET',
  465. onload: function(res) {
  466. if (res.status < 400) {
  467. let data = $.parseJSON(res.responseText);
  468. if (data.error) {
  469. logError('Error in US Census counties data: ' + data.error.message);
  470. } else {
  471. _countiesInExtent = data.features.map(feature => feature.attributes.BASENAME.toLowerCase());
  472. logDebug('US Census counties: ' + _countiesInExtent.join(', '));
  473. _statesInExtent = _.unique(data.features.map(feature => STATES.fromId(parseInt(feature.attributes.STATE))[0]));
  474.  
  475. let layersToFetch;
  476. if (!_layersCleared) {
  477. _layersCleared = true;
  478. layersToFetch = getFetchableLayers();
  479.  
  480. // Remove features of any layers that won't be mapped.
  481. _gisLayers.forEach(gisLayer => {
  482. if (layersToFetch.indexOf(gisLayer) === -1) {
  483. _mapLayer.removeFeatures(_mapLayer.getFeaturesByAttribute('layerID', gisLayer.id));
  484. _roadLayer.removeFeatures(_roadLayer.getFeaturesByAttribute('layerID', gisLayer.id));
  485. }
  486. });
  487. }
  488.  
  489. layersToFetch = layersToFetch.filter( layer => !layer.hasOwnProperty('counties') || layer.counties.some(county => _countiesInExtent.indexOf(county.toLowerCase()) > -1) );
  490. filterLayerCheckboxes();
  491. logDebug('Fetching ' + layersToFetch.length + ' layers...');
  492. logDebug(layersToFetch);
  493. layersToFetch.forEach(gisLayer => {
  494. let url = getUrl(extent, gisLayer);
  495. GM_xmlhttpRequest({
  496. url: url,
  497. context: _lastToken,
  498. method: 'GET',
  499. onload: function(res) {
  500. if (res.status < 400) { // Handle stupid issue where http 4## is considered success //
  501. processFeatures($.parseJSON(res.responseText), res.context, gisLayer);
  502. } else {
  503. logDebug('HTTP request error: ' + JSON.stringify(res));
  504. logError('Could not fetch layer "' + gisLayer.id + '". Request returned ' + res.status);
  505. }},
  506. onerror: function(res) {
  507. logDebug('xmlhttpRequest error:' + JSON.stringify(res));
  508. logError('Could not fetch layer "' + gisLayer.id + '". An error was thrown.');
  509. }
  510. });
  511. });
  512. }
  513. } else {
  514. logDebug('HTTP request error: ' + JSON.stringify(res));
  515. logError('Could not fetch counties from US Census site. Request returned ' + res.status);
  516. }
  517. },
  518. onerror: function(res) {
  519. logDebug('xmlhttpRequest error:' + JSON.stringify(res));
  520. logError('Could not fetch counties from US Census site. An error was thrown.');
  521. }
  522. });
  523. //} else {
  524. // filterLayerCheckboxes();
  525. //}
  526. }
  527.  
  528. function showScriptInfoAlert() {
  529. /* Check version and alert on update */
  530. if (ALERT_UPDATE && SCRIPT_VERSION !== _settings.lastVersion) {
  531. alert(SCRIPT_VERSION_CHANGES);
  532. }
  533. }
  534.  
  535. function setEnabled(value) {
  536. _settings.enabled = value;
  537. saveSettingsToStorage();
  538. _mapLayer.setVisibility(value);
  539. _roadLayer.setVisibility(value);
  540. let color = value ? '#00bd00' : '#ccc';
  541. $('span#gis-layers-power-btn').css({color:color});
  542. if (value) fetchFeatures();
  543. $('#layer-switcher-item_gis_layers').prop('checked',value);
  544. }
  545.  
  546. function onLayerToggleChanged(checked, layerID) {
  547. let idx = _settings.visibleLayers.indexOf(layerID);
  548. if (checked) {
  549. if (idx === -1) _settings.visibleLayers.push(layerID);
  550. } else {
  551. if (idx > -1) _settings.visibleLayers.splice(idx, 1);
  552. }
  553. if (!_ignoreFetch) {
  554. saveSettingsToStorage();
  555. fetchFeatures();
  556. }
  557. }
  558.  
  559. function onOnlyShowApplicableLayersChanged(checked) {
  560. _settings.onlyShowApplicableLayers = checked;
  561. saveSettingsToStorage();
  562. fetchFeatures();
  563. }
  564.  
  565. function onStateCheckChanged(checked, st) {
  566. let idx = _settings.selectedStates.indexOf(st);
  567. if (checked) {
  568. if (idx === -1) _settings.selectedStates.push(st);
  569. } else {
  570. if (idx > -1) _settings.selectedStates.splice(idx, 1);
  571. }
  572. if (!_ignoreFetch) {
  573. saveSettingsToStorage();
  574. initLayersTab();
  575. fetchFeatures();
  576. }
  577. }
  578.  
  579. function onLayerCheckboxChanged(checked) {
  580. setEnabled(checked);
  581. }
  582.  
  583. function setFillParcels(doFill) {
  584. [LAYER_STYLES.parcels, LAYER_STYLES.state_parcels].forEach(style => {
  585. style.fillOpacity = doFill ? 0.2 : 0;
  586. });
  587. }
  588.  
  589. function onFillParcelsCheckedChanged(checked) {
  590. setFillParcels(checked);
  591. _settings.fillParcels = checked;
  592. saveSettingsToStorage();
  593. fetchFeatures();
  594. }
  595.  
  596. function onMapMove() {
  597. if (_settings.enabled) fetchFeatures();
  598. }
  599.  
  600. function initLayer(){
  601. let rules = _gisLayers.map(gisLayer => {
  602. return new OL.Rule({
  603. filter: new OL.Filter.Comparison({
  604. type: OL.Filter.Comparison.EQUAL_TO,
  605. property: 'layerID',
  606. value: gisLayer.id
  607. }),
  608. symbolizer: gisLayer.style
  609. });
  610. });
  611.  
  612. setFillParcels(_settings.fillParcels);
  613.  
  614. let style = new OL.Style(DEFAULT_STYLE, { rules: rules } );
  615.  
  616. _mapLayer = new OL.Layer.Vector('GIS Layers - Default', {
  617. uniqueName: 'wmeGISLayersDefault',
  618. styleMap: new OL.StyleMap(style)
  619. });
  620.  
  621. _roadLayer = new OL.Layer.Vector('GIS Layers - Roads', {
  622. uniqueName: 'wmeGISLayersRoads',
  623. styleMap: new OL.StyleMap(ROAD_STYLE)
  624. });
  625.  
  626. _mapLayer.setVisibility(_settings.enabled);
  627. _roadLayer.setVisibility(_settings.enabled);
  628.  
  629. W.map.addLayer(_roadLayer);
  630. W.map.addLayer(_mapLayer);
  631.  
  632. } // END InitLayer
  633.  
  634. function initLayersTab() {
  635. let user = W.loginManager.user.userName.toLowerCase();
  636. let states = _.uniq(_gisLayers.map(l => l.state)).filter(st => _settings.selectedStates.indexOf(st) > -1);
  637. $('#panel-gis-state-layers').empty();
  638. $('#panel-gis-state-layers').append(
  639. $('<div>', {class: 'controls-container'}).css({'padding-top':'2px'}).append(
  640. $('<input>', {type:'checkbox', id:'only-show-applicable-gis-layers'}).change(function() { onOnlyShowApplicableLayersChanged($(this).is(':checked')); }).prop('checked', _settings.onlyShowApplicableLayers),
  641. $('<label>', {for:'only-show-applicable-gis-layers'}).css({'white-space':'pre-line'}).text('Only show applicable layers')
  642. ),
  643. $('.gis-layers-state-checkbox:checked').length === 0 ? $('<div>').text('Turn on layer categories in the Settings tab.') : states.map(st => {
  644. return $('<fieldset>', {id:'gis-layers-for-' + st, style:'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;'}).append(
  645. $('<legend>', {style:'margin-bottom:0px;border-bottom-style:none;width:auto;'}).append($('<i>', {class:'fa fa-fw fa-chevron-down', style:'cursor: pointer;font-size: 12px;margin-right: 4px'}).click(function() {
  646. $(this).toggleClass('fa fa-fw fa-chevron-down');
  647. $(this).toggleClass('fa fa-fw fa-chevron-right');
  648. $(`#${st}_body`).toggleClass('collapse');
  649. }), $('<span>', {style:'font-size:14px;font-weight:600;text-transform: uppercase;'}).text(STATES.toFullName(st))),
  650. $('<div>', {id:`${st}_body`}).append(
  651. $('<div>').css({'font-size':'11px'}).append(
  652. $('<span>').append(
  653. 'Select ',
  654. $('<a>', {href:'#'}).text('All').click(function(){
  655. _ignoreFetch = true;
  656. $(this).closest('fieldset').find('input').prop('checked', false).trigger('click');
  657. _ignoreFetch = false;
  658. saveSettingsToStorage();
  659. fetchFeatures();
  660. }),
  661. ' / ',
  662. $('<a>', {href:'#'}).text('None').click(function(){
  663. _ignoreFetch = true;
  664. $(this).closest('fieldset').find('input').prop('checked', true).trigger('click');
  665. _ignoreFetch = false;
  666. saveSettingsToStorage();
  667. fetchFeatures();
  668. })
  669. )
  670. ),
  671. $('<div>', {class:'controls-container', style:'padding-top:0px;'}).append(
  672. _gisLayers.filter(l => (l.state === st && (!PRIVATE_LAYERS.hasOwnProperty(l.id) || PRIVATE_LAYERS[l.id].indexOf(user) > -1))).map(gisLayer => {
  673. let id = 'gis-layer-' + gisLayer.id;
  674. return $('<div>', {class: 'controls-container', id: id+'-container'}).css({'padding-top':'2px', 'display':'block'}).append(
  675. $('<input>', {type:'checkbox', id:id}).change(function() { onLayerToggleChanged($(this).is(':checked'), gisLayer.id); }).prop('checked', _settings.visibleLayers.indexOf(gisLayer.id) > -1),
  676. $('<label>', {for:id, class:'gis-state-layer-label'}).css({'white-space':'pre-line'}).text(gisLayer.name)
  677. );
  678. })
  679. )
  680. )
  681. );
  682. })
  683. );
  684. }
  685.  
  686. function initSettingsTab() {
  687. let states = _.uniq(_gisLayers.map(l => l.state));
  688. let createRadioBtn = (name, value, text, checked) => {
  689. let id = `${name}-${value}`;
  690. return [$('<input>', {type:'radio', id:id, name:name, value:value}).prop('checked',checked),$('<label>', {for:id}).text(text).css({paddingLeft:'15px', marginRight:'4px'})];
  691. };
  692. $('#panel-gis-layers-settings').append(
  693. $('<fieldset>', {style:'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;'}).append(
  694. $('<legend>', {style:'margin-bottom:0px;border-bottom-style:none;width:auto;'}).append($('<span>', {style:'font-size:14px;font-weight:600;text-transform: uppercase;'}).text('Labels')),
  695. $('<div>', {id:'labelSettings'}).append(
  696. $('<div>', {class: 'controls-container'}).css({'padding-top':'2px'}).append(
  697. $('<label>',{style:'font-weight:normal;'}).text('Addresses:'),
  698. createRadioBtn('gisAddrDisplay', 'hn', 'HN', _settings.addrLabelDisplay === 'hn'),
  699. createRadioBtn('gisAddrDisplay', 'street', 'Street', _settings.addrLabelDisplay === 'street'),
  700. createRadioBtn('gisAddrDisplay', 'all', 'Both', _settings.addrLabelDisplay === 'all'),
  701. $('<i>', {class:'waze-tooltip', id:'gisAddrDisplayInfo', 'data-toggle':'tooltip', style:'margin-left:8px; font-size:12px', 'data-placement':'bottom',
  702. 'title':`This may not work properly for all layers. Please report issues to ${SCRIPT_AUTHOR}.`}).tooltip()
  703. )
  704. )
  705. ),
  706. $('<fieldset>', {style:'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;'}).append(
  707. $('<legend>', {style:'margin-bottom:0px;border-bottom-style:none;width:auto;'}).append($('<span>', {style:'font-size:14px;font-weight:600;text-transform: uppercase;'}).text('Layer Categories')),
  708. $('<div>', {id:'states_body'}).append(
  709. $('<div>').css({'font-size':'11px'}).append(
  710. $('<span>').append(
  711. 'Select ',
  712. $('<a>', {href:'#'}).text('All').click(function(){
  713. _ignoreFetch = true;
  714. $(this).closest('fieldset').find('input').prop('checked', false).trigger('click');
  715. _ignoreFetch = false;
  716. saveSettingsToStorage();
  717. initLayersTab();
  718. fetchFeatures();
  719. }),
  720. ' / ',
  721. $('<a>', {href:'#'}).text('None').click(function(){
  722. _ignoreFetch = true;
  723. $(this).closest('fieldset').find('input').prop('checked', true).trigger('click');
  724. _ignoreFetch = false;
  725. saveSettingsToStorage();
  726. initLayersTab();
  727. fetchFeatures();
  728. })
  729. )
  730. ),
  731. $('<div>', {class:'controls-container', style:'padding-top:0px;'}).append(
  732. states.map(st => {
  733. let fullName = STATES.toFullName(st);
  734. let id = 'gis-layer-enable-state-' + st;
  735. return $('<div>', {class: 'controls-container'}).css({'padding-top':'2px','display':'block'}).append(
  736. $('<input>', {type:'checkbox', id:id, class:'gis-layers-state-checkbox'}).change(function() { onStateCheckChanged($(this).is(':checked'), st); }).prop('checked', _settings.selectedStates.indexOf(st) > -1),
  737. $('<label>', {for:id}).css({'white-space':'pre-line'}).text(fullName)
  738. );
  739. })
  740. )
  741. )
  742. )
  743. );
  744. $('#panel-gis-layers-settings').append(
  745. $('<fieldset>', {style:'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;'}).append(
  746. $('<legend>', {style:'margin-bottom:0px;border-bottom-style:none;width:auto;'}).append($('<span>', {style:'font-size:14px;font-weight:600;text-transform: uppercase;'}).text('Appearance')),
  747. // $('<div>', {class:'controls-container', style:'padding-top:0px;'}).append(
  748. $('<div>', {class: 'controls-container'}).css({'padding-top':'2px'}).append(
  749. $('<input>', {type:'checkbox', id:'fill-parcels'}).change(function() { onFillParcelsCheckedChanged($(this).is(':checked')); }).prop('checked', _settings.fillParcels),
  750. $('<label>', {for:'fill-parcels'}).css({'white-space':'pre-line'}).text('Fill parcels')
  751. )
  752. // )
  753. )
  754. );
  755. $('input[name=gisAddrDisplay]').on('change', function() {
  756. _settings.addrLabelDisplay = $(this).val();
  757. saveSettingsToStorage();
  758. fetchFeatures();
  759. });
  760. }
  761.  
  762. function initTab() {
  763. initSettingsTab();
  764. initLayersTab();
  765. if (!$('#gis-layers-power-btn').length) {
  766. let color = _settings.enabled ? '#00bd00' : '#ccc';
  767. $('a[href="#sidepanel-gis-l"]').prepend(
  768. $('<span>', {class:'fa fa-power-off', id:'gis-layers-power-btn', style:'margin-right: 5px;cursor: pointer;color: ' + color + ';font-size: 13px;', title:'Toggle GIS Layers'}).click(function(evt) {
  769. evt.stopPropagation();
  770. setEnabled(!_settings.enabled);
  771. })
  772. );
  773. }
  774. }
  775.  
  776. function initGui() {
  777. initLayer();
  778.  
  779. let content = $('<div>').append(
  780. $('<span>', {style:'font-size:14px;font-weight:600'}).text('GIS Layers'),
  781. $('<span>', {style:'font-size:11px;margin-left:10px;color:#aaa;'}).text(GM_info.script.version),
  782. '<ul class="nav nav-tabs">' +
  783. '<li class="active"><a data-toggle="tab" href="#panel-gis-state-layers" aria-expanded="true">Layers</a></li>' +
  784. '<li><a data-toggle="tab" href="#panel-gis-layers-settings" aria-expanded="true">Settings</a></li>' +
  785. '</ul>',
  786. $('<div>', {class:'tab-content',style:'padding:8px;padding-top:2px'}).append(
  787. $('<div>', {class:'tab-pane active', id:'panel-gis-state-layers'}),
  788. $('<div>', {class:'tab-pane', id:'panel-gis-layers-settings'})
  789. )
  790. ).html();
  791.  
  792. new WazeWrap.Interface.Tab('GIS-L', content, initTab, null);
  793. WazeWrap.Interface.AddLayerCheckbox('Display', 'GIS Layers', _settings.enabled, onLayerCheckboxChanged);
  794. W.map.events.register('moveend',null,onMapMove);
  795. showScriptInfoAlert();
  796. }
  797.  
  798. function loadSpreadsheetAsync() {
  799. return new Promise((resolve, reject) => {
  800. $.get({
  801. url: LAYER_DEF_URL,
  802. success: function(data) {
  803. // Critical fields that must be present in the spreadsheet, or script cannot process the data correctly.
  804. // If any of these are still null after processing the fields entry, there's a problem.
  805. const EXPECTED_FIELD_NAMES = ['state','name','id','counties','url','where','labelFields','processLabel','style','visibleAtZoom','labelsVisibleAtZoom','enabled'];
  806. let ssFieldNames;
  807. let result = {error:null};
  808. let checkFieldNames = fldName => ssFieldNames.indexOf(fldName) > -1;
  809.  
  810. for(let entryIdx = 0; entryIdx < data.feed.entry.length && !result.error; entryIdx++) {
  811. let cellValue = data.feed.entry[entryIdx].title.$t;
  812. if (entryIdx === 0) {
  813. // The minimum script version that the spreadsheet supports.
  814. if (SCRIPT_VERSION < cellValue) {
  815. result.error = 'Script must be updated to at least version ' + cellValue + ' before layer definitions can be loaded.';
  816. }
  817. } else if (entryIdx === 1) {
  818. // Process field names
  819. ssFieldNames = cellValue.split('|').map(fldName => fldName.trim());
  820. if (ssFieldNames.length < EXPECTED_FIELD_NAMES.length) {
  821. result.error = 'Expected ' + EXPECTED_FIELD_NAMES.length + ' columns in layer definition data. Spreadsheet returned ' + ssFieldNames.length + '.';
  822. } else if (!EXPECTED_FIELD_NAMES.every(fldName => checkFieldNames(fldName))) {
  823. result.error = 'Script expected to see the following column names in the layer definition spreadsheet:\n' + EXPECTED_FIELD_NAMES.join(', ') + '\nBut the spreadsheet returned these:\n' + ssFieldNames.join(', ');
  824. }
  825. } else {
  826. let values = cellValue.split('|');
  827. if (values[ssFieldNames.indexOf('enabled')]) {
  828. let layerDef = {};
  829. ssFieldNames.forEach((fldName, fldIdx) => {
  830. let value = values[fldIdx];
  831. if (value.toString().length > 0) {
  832. if (fldName === 'counties' || fldName === 'labelFields') {
  833. value = value.split(',').map(item => item.trim());
  834. } else if (fldName === 'processLabel') {
  835. try {
  836. value = eval('(function(label, fieldValues){' + value + '})');
  837. } catch (ex) {
  838. logError('Error loading label processing function for layer "' + layerDef.id + '".');
  839. logDebug(ex);
  840. }
  841. } else if (fldName === 'style') {
  842. layerDef.isRoadLayer = value === 'roads';
  843. if (LAYER_STYLES.hasOwnProperty(value)) {
  844. value = LAYER_STYLES[value];
  845. }
  846. // If layer is not defined, allow the value to be set as-is because it could be a custom style.
  847. // *** THIS NEEDS TO BE TESTED ***
  848. }
  849. layerDef[fldName] = value;
  850. } else if (fldName === 'labelFields') {
  851. layerDef[fldName] = [''];
  852. }
  853. });
  854. if (layerDef.enabled && ['0','false','no','n'].indexOf(layerDef.enabled.toString().trim().toLowerCase()) === -1) {
  855. _gisLayers.push(layerDef);
  856. }
  857. }
  858. }
  859. }
  860. resolve(result);
  861. },
  862. error: function() {
  863. reject({message: 'An error occurred while loading the GIS layer definition spreadsheet.'});
  864. }
  865. });
  866. });
  867. }
  868.  
  869. function init() {
  870. installPathFollowingLabels();
  871. let t0 = performance.now();
  872. loadSpreadsheetAsync().then(result => {
  873. if (result.error) {
  874. logError(result.error);
  875. return;
  876. }
  877. _layerRefinements.forEach(layerRefinement => {
  878. let layerDef = _gisLayers.find(layerDef => layerDef.id === layerRefinement.id);
  879. if (layerDef) {
  880. Object.keys(layerRefinement).forEach((fldName) => {
  881. let value = layerRefinement[fldName];
  882. if (fldName !== 'id' && layerDef.hasOwnProperty(fldName)) {
  883. logDebug('The "' + fldName + '" property of layer "' + layerDef.id + '" has a value hardcoded in the script, and also defined in the spreadsheet. The spreadsheet value takes precedence.');
  884. } else {
  885. if (value) layerDef[fldName] = value;
  886. }
  887. });
  888. } else {
  889. logDebug('Refined layer "' + layerRefinement.id + '" does not have a corresponding layer defined in the spreadsheet. It can probably be removed from the script.');
  890. }
  891. });
  892. logDebug('Loaded ' + _gisLayers.length + ' layer definitions in ' + Math.round(performance.now() - t0) + ' ms.');
  893. loadSettingsFromStorage();
  894. initGui();
  895. fetchFeatures();
  896. log('Initialized.');
  897. }).catch(err => {
  898. let msg;
  899. if (err && err.message) {
  900. msg = err.message;
  901. } else {
  902. msg = err;
  903. }
  904. logError(msg);
  905. });
  906. }
  907.  
  908. function bootstrap() {
  909. if (W && W.loginManager && W.map && W.loginManager.isLoggedIn() && W.model && W.model.states && W.model.states.getObjectArray().length) {
  910. log('Initializing...');
  911. init();
  912. } else {
  913. log('Bootstrap failed. Trying again...');
  914. setTimeout(function () {
  915. bootstrap();
  916. }, 1000);
  917. }
  918. }
  919.  
  920. bootstrap();
  921.  
  922. function installPathFollowingLabels() {
  923. // Copyright (c) 2015 by Jean-Marc.Viglino [at]ign.fr
  924. // Dual-licensed under the CeCILL-B Licence (http://www.cecill.info/)
  925. // and the Beerware license (http://en.wikipedia.org/wiki/Beerware),
  926. // feel free to use and abuse it in your projects (the code, not the beer ;-).
  927. //
  928. //* Overwrite the SVG function to allow text along a path
  929. //* setStyle function
  930. //*
  931. //* Add new options to the Openlayers.Style
  932.  
  933. // pathLabel: {String} Label to draw on the path
  934. // pathLabelXOffset: {String} Offset along the line to start drawing text in pixel or %, default: "50%"
  935. // pathLabelYOffset: {Number} Distance of the line to draw the text
  936. // pathLabelCurve: {String} Smooth the line the label is drawn on (empty string for no)
  937. // pathLabelReadable: {String} Make the label readable (empty string for no)
  938.  
  939. // * Extra standard values : all label and text values
  940.  
  941.  
  942. // *
  943. // * Method: removeChildById
  944. // * Remove child in a node.
  945. // *
  946.  
  947. function removeChildById(node,id) {
  948. if (node.querySelector) {
  949. var c = node.querySelector('#'+id);
  950. if (c) node.removeChild(c);
  951. return;
  952. }
  953. // For old browsers
  954. var c = node.childNodes;
  955. if (c) for (var i=0; i<c.length; i++) {
  956. if (c[i].id === id) {
  957. node.removeChild(c[i]);
  958. return;
  959. }
  960. }
  961. }
  962.  
  963.  
  964. // *
  965. // * Method: setStyle
  966. // * Use to set all the style attributes to a SVG node.
  967. // *
  968. // * Takes care to adjust stroke width and point radius to be
  969. // * resolution-relative
  970. // *
  971. // * Parameters:
  972. // * node - {SVGDomElement} An SVG element to decorate
  973. // * style - {Object}
  974. // * options - {Object} Currently supported options include
  975. // * 'isFilled' {Boolean} and
  976. // * 'isStroked' {Boolean}
  977.  
  978. var setStyle = OL.Renderer.SVG.prototype.setStyle;
  979. OL.Renderer.SVG.LABEL_STARTOFFSET = { 'l':'0%', 'r':'100%', 'm':'50%' };
  980.  
  981. OL.Renderer.SVG.prototype.pathText = function (node, style, suffix) {
  982. var label = this.nodeFactory(null, 'text');
  983. label.setAttribute('id',node._featureId+'_'+suffix);
  984. if (style.fontColor) label.setAttributeNS(null, 'fill', style.fontColor);
  985. if (style.fontStrokeColor) label.setAttributeNS(null, 'stroke', style.fontStrokeColor);
  986. if (style.fontStrokeWidth) label.setAttributeNS(null, 'stroke-width', style.fontStrokeWidth);
  987. if (style.fontOpacity) label.setAttributeNS(null, 'opacity', style.fontOpacity);
  988. if (style.fontFamily) label.setAttributeNS(null, 'font-family', style.fontFamily);
  989. if (style.fontSize) label.setAttributeNS(null, 'font-size', style.fontSize);
  990. if (style.fontWeight) label.setAttributeNS(null, 'font-weight', style.fontWeight);
  991. if (style.fontStyle) label.setAttributeNS(null, 'font-style', style.fontStyle);
  992. if (style.labelSelect === true) {
  993. label.setAttributeNS(null, 'pointer-events', 'visible');
  994. label._featureId = node._featureId;
  995. } else {
  996. label.setAttributeNS(null, 'pointer-events', 'none');
  997. }
  998.  
  999. function getpath (pathStr, readeable) {
  1000. var npath = pathStr.split(',');
  1001. var pts = [];
  1002. if (!readeable || Number(npath[0]) - Number(npath[npath.length-2]) < 0) {
  1003. while (npath.length) pts.push ( { x:Number(npath.shift()), y:Number(npath.shift()) } );
  1004. } else {
  1005. while (npath.length) pts.unshift ( { x:Number(npath.shift()), y:Number(npath.shift()) } );
  1006. }
  1007. return pts;
  1008. }
  1009.  
  1010. var path = this.nodeFactory(null, 'path');
  1011. var tpid = node._featureId+'_t'+suffix;
  1012. var tpath = node.getAttribute('points');
  1013. if (style.pathLabelCurve) {
  1014. var pts = getpath (tpath, style.pathLabelReadable);
  1015. var p = pts[0].x+' '+pts[0].y;
  1016. var dx, dy, s1, s2;
  1017. dx = (pts[0].x-pts[1].x)/4;
  1018. dy = (pts[0].y-pts[1].y)/4;
  1019. for (var i=1; i<pts.length-1; i++) {
  1020. p += ' C '+(pts[i-1].x-dx)+' '+(pts[i-1].y-dy);
  1021. dx = (pts[i-1].x-pts[i+1].x)/4;
  1022. dy = (pts[i-1].y-pts[i+1].y)/4;
  1023. s1 = Math.sqrt( Math.pow(pts[i-1].x-pts[i].x,2)+ Math.pow(pts[i-1].y-pts[i].y,2) );
  1024. s2 = Math.sqrt( Math.pow(pts[i+1].x-pts[i].x,2)+ Math.pow(pts[i+1].y-pts[i].y,2) );
  1025. p += ' '+(pts[i].x+s1*dx/s2)+' '+(pts[i].y+s1*dy/s2);
  1026. dx *= s2/s1;
  1027. dy *= s2/s1;
  1028. p += ' '+pts[i].x+' '+pts[i].y;
  1029. }
  1030. p += ' C '+(pts[i-1].x-dx)+' '+(pts[i-1].y-dy);
  1031. dx = (pts[i-1].x-pts[i].x )/4;
  1032. dy = (pts[i-1].y-pts[i].y )/4;
  1033. p += ' '+(pts[i].x+dx)+' '+(pts[i].y+dy);
  1034. p += ' '+pts[i].x+' '+pts[i].y;
  1035.  
  1036. path.setAttribute('d','M '+p);
  1037. } else {
  1038. if (style.pathLabelReadable) {
  1039. var pts = getpath (tpath, style.pathLabelReadable);
  1040. var p='';
  1041. for (var i=0; i<pts.length; i++) p += ' '+pts[i].x+' '+pts[i].y;
  1042. path.setAttribute('d','M '+p);
  1043. } else path.setAttribute('d','M '+tpath);
  1044. }
  1045. path.setAttribute('id',tpid);
  1046.  
  1047. var defs = this.createDefs();
  1048. removeChildById (defs, tpid);
  1049. defs.appendChild(path);
  1050.  
  1051. var textPath = this.nodeFactory(null, 'textPath');
  1052. textPath.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#'+tpid);
  1053. var align = style.labelAlign || OL.Renderer.defaultSymbolizer.labelAlign;
  1054. label.setAttributeNS(null, 'text-anchor', OL.Renderer.SVG.LABEL_ALIGN[align[0]] || 'middle');
  1055. textPath.setAttribute('startOffset', style.pathLabelXOffset || OL.Renderer.SVG.LABEL_STARTOFFSET[align[0]] || '50%');
  1056. label.setAttributeNS(null, 'dominant-baseline', OL.Renderer.SVG.LABEL_ALIGN[align[1]] || 'central');
  1057. if (style.pathLabelYOffset) label.setAttribute('dy', style.pathLabelYOffset);
  1058. //textPath.setAttribute('method','stretch');
  1059. //textPath.setAttribute('spacing','auto');
  1060.  
  1061. textPath.textContent = style.pathLabel;
  1062. label.appendChild(textPath);
  1063.  
  1064. removeChildById (this.textRoot, node._featureId+'_'+suffix);
  1065. this.textRoot.appendChild(label);
  1066. };
  1067.  
  1068. OL.Renderer.SVG.prototype.setStyle = function(node, style, options) {
  1069. if (node._geometryClass === 'OpenLayers.Geometry.LineString' && style.pathLabel) {
  1070. if (node._geometryClass === 'OpenLayers.Geometry.LineString' && style.pathLabel) {
  1071. var drawOutline = (!!style.labelOutlineWidth);
  1072. // First draw text in halo color and size and overlay the
  1073. // normal text afterwards
  1074. if (drawOutline) {
  1075. var outlineStyle = OL.Util.extend({}, style);
  1076. outlineStyle.fontColor = outlineStyle.labelOutlineColor;
  1077. outlineStyle.fontStrokeColor = outlineStyle.labelOutlineColor;
  1078. outlineStyle.fontStrokeWidth = style.labelOutlineWidth;
  1079. if (style.labelOutlineOpacity) outlineStyle.fontOpacity = style.labelOutlineOpacity;
  1080. delete outlineStyle.labelOutlineWidth;
  1081. this.pathText(node, outlineStyle, 'txtpath0');
  1082. }
  1083. this.pathText(node, style, 'txtpath');
  1084. setStyle.apply(this,arguments);
  1085. }
  1086. } else setStyle.apply(this,arguments);
  1087. return node;
  1088. };
  1089.  
  1090. // *
  1091. // * Method: drawGeometry
  1092. // * Remove the textpath if no geometry is drawn.
  1093. // *
  1094. // * Parameters:
  1095. // * geometry - {<OpenLayers.Geometry>}
  1096. // * style - {Object}
  1097. // * featureId - {String}
  1098. // *
  1099. // * Returns:
  1100. // * {Boolean} true if the geometry has been drawn completely; null if
  1101. // * incomplete; false otherwise
  1102.  
  1103. var drawGeometry = OL.Renderer.SVG.prototype.drawGeometry;
  1104. OL.Renderer.SVG.prototype.drawGeometry = function(geometry, style, id) {
  1105. var rendered = drawGeometry.apply(this,arguments);
  1106. if (rendered === false) {
  1107. removeChildById(this.textRoot, id+'_txtpath');
  1108. removeChildById(this.textRoot, id+'_txtpath0');
  1109. }
  1110. return rendered;
  1111. };
  1112.  
  1113. // *
  1114. // * Method: eraseGeometry
  1115. // * Erase a geometry from the renderer. In the case of a multi-geometry,
  1116. // * we cycle through and recurse on ourselves. Otherwise, we look for a
  1117. // * node with the geometry.id, destroy its geometry, and remove it from
  1118. // * the DOM.
  1119. // *
  1120. // * Parameters:
  1121. // * geometry - {<OpenLayers.Geometry>}
  1122. // * featureId - {String}
  1123.  
  1124. var eraseGeometry = OL.Renderer.SVG.prototype.eraseGeometry;
  1125. OL.Renderer.SVG.prototype.eraseGeometry = function(geometry, featureId) {
  1126. eraseGeometry.apply(this,arguments);
  1127. removeChildById(this.textRoot, featureId+'_txtpath');
  1128. removeChildById(this.textRoot, featureId+'_txtpath0');
  1129. };
  1130.  
  1131. }
  1132. })();