WME FC Layer

Adds a Functional Class layer for states that publish ArcGIS FC data.

  1. // ==UserScript==
  2. // @name WME FC Layer
  3. // @namespace https://greatest.deepsurf.us/users/45389
  4. // @version 2025.04.24.000
  5. // @description Adds a Functional Class layer for states that publish ArcGIS FC data.
  6. // @author MapOMatic
  7. // @match *://*.waze.com/*editor*
  8. // @exclude *://*.waze.com/user/editor*
  9. // @license GNU GPLv3
  10. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  11. // @require https://greatest.deepsurf.us/scripts/39002-bluebird/code/Bluebird.js?version=255146
  12. // @require https://greatest.deepsurf.us/scripts/24851-wazewrap/code/WazeWrap.js
  13. // @require https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js
  14. // @require https://update.greatest.deepsurf.us/scripts/509664/WME%20Utils%20-%20Bootstrap.js
  15. // @connect greatest.deepsurf.us
  16. // @grant GM_xmlhttpRequest
  17. // @connect arcgis.com
  18. // @connect arkansas.gov
  19. // @connect azdot.gov
  20. // @connect ca.gov
  21. // @connect coloradodot.info
  22. // @connect delaware.gov
  23. // @connect dc.gov
  24. // @connect ga.gov
  25. // @connect uga.edu
  26. // @connect hawaii.gov
  27. // @connect idaho.gov
  28. // @connect in.gov
  29. // @connect iowadot.gov
  30. // @connect illinois.gov
  31. // @connect ksdot.org
  32. // @connect ky.gov
  33. // @connect la.gov
  34. // @connect maine.gov
  35. // @connect md.gov
  36. // @connect ma.us
  37. // @connect mn.us
  38. // @connect nv.gov
  39. // @connect memphistn.gov
  40. // @connect state.mi.us
  41. // @connect modot.org
  42. // @connect mt.gov
  43. // @connect unh.edu
  44. // @connect ny.gov
  45. // @connect ncdot.gov
  46. // @connect nd.gov
  47. // @connect oh.us
  48. // @connect or.us
  49. // @connect penndot.gov
  50. // @connect sd.gov
  51. // @connect shelbycountytn.gov
  52. // @connect utah.gov
  53. // @connect vermont.gov
  54. // @connect wa.gov
  55. // @connect wv.gov
  56. // @connect wyoroad.info
  57. // ==/UserScript==
  58.  
  59. /* global turf */
  60. /* global bootstrap */
  61.  
  62. (async function main() {
  63. 'use strict';
  64.  
  65. const settingsStoreName = 'wme_fc_layer';
  66. const debug = false;
  67. const scriptVersion = GM_info.script.version;
  68. const downloadUrl = 'https://greatest.deepsurf.us/scripts/369633-wme-fc-layer/code/WME%20FC%20Layer.user.js';
  69. const sdk = await bootstrap({ scriptUpdateMonitor: { downloadUrl } });
  70. const layerName = 'FC Layer';
  71. let isAM = false;
  72. let userNameLC;
  73. let settings = {};
  74. let rank;
  75. let MAP_LAYER_Z_INDEX;
  76. const MIN_ZOOM_LEVEL = 11;
  77. const STATES_HASH = {
  78. Alabama: 'AL',
  79. Alaska: 'AK',
  80. 'American Samoa': 'AS',
  81. Arizona: 'AZ',
  82. Arkansas: 'AR',
  83. California: 'CA',
  84. Colorado: 'CO',
  85. Connecticut: 'CT',
  86. Delaware: 'DE',
  87. 'District of Columbia': 'DC',
  88. 'Federated States Of Micronesia': 'FM',
  89. Florida: 'FL',
  90. Georgia: 'GA',
  91. Guam: 'GU',
  92. Hawaii: 'HI',
  93. Idaho: 'ID',
  94. Illinois: 'IL',
  95. Indiana: 'IN',
  96. Iowa: 'IA',
  97. Kansas: 'KS',
  98. Kentucky: 'KY',
  99. Louisiana: 'LA',
  100. Maine: 'ME',
  101. 'Marshall Islands': 'MH',
  102. Maryland: 'MD',
  103. Massachusetts: 'MA',
  104. Michigan: 'MI',
  105. Minnesota: 'MN',
  106. Mississippi: 'MS',
  107. Missouri: 'MO',
  108. Montana: 'MT',
  109. Nebraska: 'NE',
  110. Nevada: 'NV',
  111. 'New Hampshire': 'NH',
  112. 'New Jersey': 'NJ',
  113. 'New Mexico': 'NM',
  114. 'New York': 'NY',
  115. 'North Carolina': 'NC',
  116. 'North Dakota': 'ND',
  117. 'Northern Mariana Islands': 'MP',
  118. Ohio: 'OH',
  119. Oklahoma: 'OK',
  120. Oregon: 'OR',
  121. Palau: 'PW',
  122. Pennsylvania: 'PA',
  123. 'Puerto Rico': 'PR',
  124. 'Rhode Island': 'RI',
  125. 'South Carolina': 'SC',
  126. 'South Dakota': 'SD',
  127. Tennessee: 'TN',
  128. Texas: 'TX',
  129. Utah: 'UT',
  130. Vermont: 'VT',
  131. 'Virgin Islands': 'VI',
  132. Virginia: 'VA',
  133. Washington: 'WA',
  134. 'West Virginia': 'WV',
  135. Wisconsin: 'WI',
  136. Wyoming: 'WY'
  137. };
  138.  
  139. function reverseStatesHash(stateAbbr) {
  140. // eslint-disable-next-line no-restricted-syntax
  141. for (const stateName in STATES_HASH) {
  142. if (STATES_HASH[stateName] === stateAbbr) return stateName;
  143. }
  144. throw new Error(`FC Layer: reverseStatesHash function did not return a value for ${stateAbbr}.`);
  145. }
  146.  
  147. const STATE_SETTINGS = {
  148. global: {
  149. roadTypes: ['St', 'PS', 'PS2', 'mH', 'MH', 'Ew', 'Rmp', 'Fw'], // Ew = Expressway. For FC's that make it uncertain if they should be MH or FW.
  150. getFeatureRoadType(feature, layer) {
  151. const fc = feature.attributes[layer.fcPropName];
  152. return this.getRoadTypeFromFC(fc, layer);
  153. },
  154. getRoadTypeFromFC(fc, layer) {
  155. return Object.keys(layer.roadTypeMap).find(rt => layer.roadTypeMap[rt].indexOf(fc) !== -1);
  156. },
  157. isPermitted(stateAbbr) {
  158. const state = STATE_SETTINGS[stateAbbr];
  159. if (state.isPermitted) return state.isPermitted();
  160. return (rank >= 3 && isAM) || (rank >= 4);
  161. },
  162. getMapLayer(stateAbbr, layerID) {
  163. let returnValue;
  164. STATE_SETTINGS[stateAbbr].fcMapLayers.forEach(layer => {
  165. if (layer.layerID === layerID) {
  166. returnValue = layer;
  167. }
  168. });
  169. return returnValue;
  170. }
  171. },
  172. AL: {
  173. baseUrl: 'https://services.arcgis.com/LZzQi3xDiclG6XvQ/arcgis/rest/services/HPMS_Year2017_F_System_Data/FeatureServer/',
  174. defaultColors: {
  175. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  176. },
  177. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  178. fcMapLayers: [
  179. {
  180. layerID: 0,
  181. fcPropName: 'F_SYSTEM_V',
  182. idPropName: 'OBJECTID',
  183. outFields: ['FID', 'F_SYSTEM_V', 'State_Sys'],
  184. roadTypeMap: {
  185. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  186. },
  187. maxRecordCount: 1000,
  188. supportsPagination: false
  189. }
  190. ],
  191. isPermitted() { return rank >= 3; },
  192. information: { Source: 'ALDOT', Permission: 'Visible to R3+', Description: 'Federal and State highways set to a minimum of mH.' },
  193. getWhereClause(context) {
  194. if (context.mapContext.zoom < 16) {
  195. return `${context.layer.fcPropName} <> 7`;
  196. }
  197. return null;
  198. },
  199. getFeatureRoadType(feature, layer) {
  200. let fc = parseInt(feature.attributes[layer.fcPropName], 10);
  201. if (fc > 4 && feature.attributes.State_Sys === 'YES') { fc = 4; }
  202. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  203. }
  204. },
  205. AK: {
  206. baseUrl: 'https://services.arcgis.com/r4A0V7UzH9fcLVvv/ArcGIS/rest/services/AKDOTPF_Route_Data/FeatureServer/',
  207. defaultColors: {
  208. Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  209. },
  210. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  211. fcMapLayers: [
  212. {
  213. layerID: 13,
  214. fcPropName: 'Functional_Class',
  215. idPropName: 'OBJECTID',
  216. outFields: ['OBJECTID', 'Functional_Class'],
  217. roadTypeMap: {
  218. Ew: [1, 2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  219. },
  220. maxRecordCount: 1000,
  221. supportsPagination: false
  222. }
  223. ],
  224. information: { Source: 'Alaska DOT&PF', Permission: 'Visible to R4+ or R3-AM', Description: 'Raw unmodified FC data.' },
  225. getWhereClause(context) {
  226. if (context.mapContext.zoom < 16) {
  227. return `${context.layer.fcPropName} <> 7`;
  228. }
  229. return null;
  230. },
  231. getFeatureRoadType(feature, layer) {
  232. if (layer.getFeatureRoadType) {
  233. return layer.getFeatureRoadType(feature);
  234. }
  235. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  236. }
  237. },
  238. AZ: {
  239. baseUrl: 'https://services1.arcgis.com/XAiBIVuto7zeZj1B/arcgis/rest/services/ATIS_prod_gdb_1/FeatureServer/',
  240. defaultColors: {
  241. Fw: '#ff00c5', Ew: '#ff00c5', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  242. },
  243. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1] },
  244. fcMapLayers: [
  245. {
  246. layerID: 38,
  247. fcPropName: 'FunctionalClass',
  248. idPropName: 'OBJECTID',
  249. outFields: ['OBJECTID', 'FunctionalClass', 'RouteId'],
  250. roadTypeMap: {
  251. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  252. },
  253. maxRecordCount: 1000,
  254. supportsPagination: false
  255. }
  256. ],
  257. information: { Source: 'ADOT', Permission: 'Visible to R4+ or R3-AM' },
  258. getWhereClause() {
  259. return null;
  260. },
  261. getFeatureRoadType(feature, layer) {
  262. const attr = feature.attributes;
  263. const roadID = attr.RouteId.trim().replace(/ +/g, ' ');
  264. const roadNum = parseInt(roadID.substring(2, 5), 10);
  265. let fc = attr[layer.fcPropName];
  266. switch (fc) {
  267. case 'Rural Principal Arterial - Interstate':
  268. case 'Urban Principal Arterial - Interstate': fc = 1; break;
  269. case 'Rural Principal Arterial - Other Fwys & Expwys':
  270. case 'Urban Principal Arterial - Other Fwys & Expwys': fc = 2; break;
  271. case 'Rural Principal Arterial - Other':
  272. case 'Urban Principal Arterial - Other': fc = 3; break;
  273. case 'Rural Minor Arterial':
  274. case 'Urban Minor Arterial': fc = 4; break;
  275. case 'Rural Major Collector':
  276. case 'Urban Major Collector': fc = 5; break;
  277. case 'Rural Minor Collector':
  278. case 'Urban Minor Collector': fc = 6; break;
  279. default: fc = 7;
  280. }
  281. const azIH = [8, 10, 11, 17, 19, 40]; // Interstate hwys in AZ
  282. const isUS = /^U\D\d{3}\b/.test(roadID);
  283. const isState = /^S\D\d{3}\b/.test(roadID);
  284. const isBiz = /^SB\d{3}\b/.test(roadID);
  285. if (fc > 4 && isState && azIH.includes(roadNum) && isBiz) fc = 4;
  286. else if (fc > 4 && isUS) fc = isBiz ? 6 : 4;
  287. else if (fc > 6 && isState) fc = isBiz ? 7 : 6;
  288. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  289. }
  290. },
  291. AR: {
  292. baseUrl: 'https://gis.arkansas.gov/arcgis/rest/services/FEATURESERVICES/Transportation/FeatureServer/',
  293. defaultColors: {
  294. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  295. },
  296. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  297. fcMapLayers: [
  298. {
  299. layerID: 8,
  300. fcPropName: 'functionalclass',
  301. idPropName: 'objectid',
  302. outFields: ['objectid', 'functionalclass', 'ah_route', 'ah_section'],
  303. roadTypeMap: {
  304. Fw: [1, 2], Ew: [], MH: [3], mH: [4], PS: [5, 6], St: [7]
  305. },
  306. maxRecordCount: 1000,
  307. supportsPagination: false
  308. }
  309. ],
  310. information: { Source: 'ARDOT', Permission: 'Visible to R4+ or R3-AM' },
  311. getWhereClause() {
  312. return null;
  313. },
  314. getFeatureRoadType(feature, layer) {
  315. const attr = feature.attributes;
  316. let fc = parseInt(attr[layer.fcPropName], 10);
  317. const roadID = parseInt(attr.ah_route, 10);
  318. const usHwys = [49, 59, 61, 62, 63, 64, 65, 67, 70, 71, 79, 82, 165, 167, 270, 271, 278, 371, 412, 425];
  319. const isUS = usHwys.includes(roadID);
  320. const isState = roadID < 613;
  321. const isBiz = attr.ah_section[attr.ah_section.length - 1] === 'B';
  322. if (fc > 3 && isUS) fc = isBiz ? 4 : 3;
  323. else if (fc > 4 && isState) fc = isBiz ? 5 : 4;
  324. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  325. }
  326. },
  327. CA: {
  328. baseUrl: 'https://caltrans-gis.dot.ca.gov/arcgis/rest/services/CHhighway/CRS_Functional_Classification/FeatureServer/',
  329. defaultColors: {
  330. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  331. },
  332. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  333. fcMapLayers: [
  334. {
  335. layerID: 0,
  336. fcPropName: 'F_System',
  337. idPropName: 'OBJECTID',
  338. outFields: ['OBJECTID', 'F_System'],
  339. roadTypeMap: {
  340. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  341. },
  342. maxRecordCount: 1000,
  343. supportsPagination: false
  344. }
  345. ],
  346. isPermitted() { return ['mapomatic', 'turbomkt', 'tonestertm', 'ottonomy', 'jemay', 'ojlaw'].includes(userNameLC); },
  347. information: { Source: 'Caltrans', Permission: 'Visible to ?', Description: '' },
  348. getWhereClause(context) {
  349. if (context.mapContext.zoom < 16) {
  350. return `${context.layer.fcPropName} <> 7`;
  351. }
  352. return null;
  353. },
  354. getFeatureRoadType(feature, layer) {
  355. const fc = parseInt(feature.attributes[layer.fcPropName], 10);
  356. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  357. }
  358. },
  359. CO: {
  360. baseUrl: 'https://dtdapps.coloradodot.info/arcgis/rest/services/CPLAN/open_data_sde/FeatureServer/',
  361. defaultColors: {
  362. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  363. },
  364. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  365. fcMapLayers: [
  366. {
  367. layerID: 14,
  368. fcPropName: 'FUNCCLASS',
  369. idPropName: 'OBJECTID',
  370. outFields: ['OBJECTID', 'FUNCCLASS', 'ROUTE', 'REFPT'],
  371. roadTypeMap: {
  372. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  373. },
  374. maxRecordCount: 1000,
  375. supportsPagination: false
  376. },
  377. {
  378. layerID: 17,
  379. fcPropName: 'FUNCCLASSID',
  380. idPropName: 'OBJECTID',
  381. outFields: ['OBJECTID', 'FUNCCLASSID', 'ROUTE', 'FIPSCOUNTY'],
  382. roadTypeMap: {
  383. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  384. },
  385. maxRecordCount: 1000,
  386. supportsPagination: false
  387. },
  388. {
  389. layerID: 21,
  390. fcPropName: 'FUNCCLASSID',
  391. idPropName: 'OBJECTID',
  392. outFields: ['OBJECTID', 'FUNCCLASSID', 'ROUTE'],
  393. roadTypeMap: {
  394. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  395. },
  396. maxRecordCount: 1000,
  397. supportsPagination: false
  398. }
  399. ],
  400. isPermitted() { return rank >= 3; },
  401. information: {
  402. Source: 'CDOT',
  403. Permission: 'Visible to R3+',
  404. Description: 'Please consult with a state manager before making any changes to road types based on the data presented.'
  405. },
  406. getWhereClause(context) {
  407. if (context.mapContext.zoom < 16) {
  408. return `${context.layer.fcPropName} <> '7'`;
  409. }
  410. return null;
  411. },
  412. getFeatureRoadType(feature, layer) {
  413. const attr = feature.attributes;
  414. let fc = parseInt(attr[layer.fcPropName], 10);
  415. const route = attr.ROUTE.replace(/ +/g, ' ');
  416. if (layer.layerID === 7) {
  417. const rtnum = parseInt(route.slice(0, 3), 10);
  418. const refpt = attr.REFPT;
  419. const hwys = [6, 24, 25, 34, 36, 40, 50, 70, 84, 85, 87, 138, 160, 285, 287, 350, 385, 400, 491, 550];
  420. // Exceptions first, then normal classification
  421. const doNothing = ['024D', '040G'];
  422. const notNothing = ['070K', '070L', '070O', '070Q', '070R'];
  423. const doMin = ['024E', '050D', '070O', '085F', '160D'];
  424. if (doNothing.includes(route) || (rtnum === 70 && route !== '070K' && !notNothing.includes(route))) {
  425. // do nothing
  426. } else if (doMin.includes(route)
  427. || (rtnum === 40 && refpt > 320 && refpt < 385)
  428. || (rtnum === 36 && refpt > 79 && refpt < 100.99)
  429. || (route === '034D' && refpt > 11)) {
  430. fc = 4;
  431. } else if (hwys.includes(rtnum)) {
  432. fc = Math.min(fc, 3);
  433. } else {
  434. fc = Math.min(fc, 4);
  435. }
  436. } else {
  437. // All exceptions
  438. const fips = parseInt(attr.FIPSCOUNTY, 10);
  439. if ((fips === 19 && route === 'COLORADO BD') || (fips === 37 && (route === 'GRAND AV' || route === 'S H6'))) {
  440. fc = 3;
  441. } else if (fips === 67 && route === 'BAYFIELDPAY') {
  442. fc = 4;
  443. }
  444. }
  445. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  446. }
  447. },
  448. CT: {
  449. baseUrl: 'https://services1.arcgis.com/FCaUeJ5SOVtImake/ArcGIS/rest/services/CTDOT_Roadway_Classification_and_Characteristic_Data/FeatureServer/',
  450. defaultColors: {
  451. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  452. },
  453. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  454. fcMapLayers: [
  455. {
  456. layerID: 3,
  457. fcPropName: 'FC_FC_CODE',
  458. idPropName: 'OBJECTID',
  459. outFields: ['OBJECTID', 'FC_FC_CODE'],
  460. roadTypeMap: {
  461. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  462. },
  463. maxRecordCount: 1000,
  464. supportsPagination: false
  465. }
  466. ],
  467. isPermitted() { return rank >= 3; },
  468. information: { Source: 'CTDOT', Permission: 'Visible to R3+', Description: 'Federal and State highways set to a minimum of mH.' },
  469. getWhereClause(context) {
  470. if (context.mapContext.zoom < 16) {
  471. return `${context.layer.fcPropName} <> 7`;
  472. }
  473. return null;
  474. },
  475. getFeatureRoadType(feature, layer) {
  476. let fc = parseInt(feature.attributes[layer.fcPropName], 10);
  477. if (fc > 4 && feature.attributes.State_Sys === 'YES') {
  478. fc = 4;
  479. }
  480. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  481. }
  482. },
  483. DE: {
  484. baseUrl: 'https://enterprise.firstmap.delaware.gov/arcgis/rest/services/Transportation/DE_Roadways_Main/FeatureServer/',
  485. defaultColors: {
  486. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  487. },
  488. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  489. fcMapLayers: [
  490. {
  491. layerID: 16,
  492. fcPropName: 'VALUE_TEXT',
  493. idPropName: 'OBJECTID',
  494. outFields: ['OBJECTID', 'VALUE_TEXT'],
  495. maxRecordCount: 1000,
  496. supportsPagination: false,
  497. roadTypeMap: {
  498. Fw: ['Interstate'], Ew: ['Other Expressways & Freeway'], MH: ['Other Principal Arterials'], mH: ['Minor Arterial'], PS: ['Major Collector', 'Minor Collector'], St: ['Local']
  499. }
  500. }
  501. ],
  502. information: { Source: 'Delaware FirstMap', Permission: 'Visible to R4+ or R3-AM', Description: 'Raw unmodified FC data.' },
  503. getWhereClause(context) {
  504. if (context.mapContext.zoom < 16) {
  505. return `${context.layer.fcPropName} <> 'Local'`;
  506. }
  507. return null;
  508. },
  509. getFeatureRoadType(feature, layer) {
  510. if (layer.getFeatureRoadType) {
  511. return layer.getFeatureRoadType(feature);
  512. }
  513. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  514. }
  515. },
  516. DC: {
  517. baseUrl: 'https://maps2.dcgis.dc.gov/dcgis/rest/services/DCGIS_DATA/Transportation_WebMercator/MapServer/',
  518. supportsPagination: false,
  519. defaultColors: {
  520. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  521. },
  522. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  523. fetchAllFC: false,
  524. fcMapLayers: [
  525. {
  526. layerID: 48,
  527. fcPropName: 'FHWAFUNCTIONALCLASS',
  528. idPropName: 'OBJECTID',
  529. outFields: ['OBJECTID', 'FHWAFUNCTIONALCLASS'],
  530. maxRecordCount: 1000,
  531. supportsPagination: false,
  532. roadTypeMap: {
  533. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  534. }
  535. }
  536. ],
  537. information: { Source: 'DDOT', Permission: 'Visible to R4+ or R3-AM' },
  538. getWhereClause() {
  539. return null;
  540. },
  541. getFeatureRoadType(feature, layer) {
  542. if (layer.getFeatureRoadType) {
  543. return layer.getFeatureRoadType(feature);
  544. }
  545. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  546. }
  547. },
  548. FL: {
  549. baseUrl: 'https://services1.arcgis.com/O1JpcwDW8sjYuddV/ArcGIS/rest/services/Functional_Classification_TDA/FeatureServer/',
  550. supportsPagination: false,
  551. defaultColors: {
  552. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  553. },
  554. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  555. fetchAllFC: false,
  556. fcMapLayers: [
  557. {
  558. layerID: 0,
  559. fcPropName: 'FUNCLASS',
  560. idPropName: 'FID',
  561. outFields: ['FID', 'FUNCLASS'],
  562. maxRecordCount: 1000,
  563. supportsPagination: false,
  564. roadTypeMap: {
  565. Fw: ['01', '11'], Ew: ['02', '12'], MH: ['04', '14'], mH: ['06', '16'], PS: ['07', '08', '17', '18']
  566. }
  567. }
  568. ],
  569. information: { Source: 'FDOT', Permission: 'Visible to R4+ or R3-AM', Description: 'Raw unmodified FC data.' },
  570. getWhereClause() {
  571. return null;
  572. },
  573. getFeatureRoadType(feature, layer) {
  574. if (layer.getFeatureRoadType) {
  575. return layer.getFeatureRoadType(feature);
  576. }
  577. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  578. }
  579. },
  580. GA: {
  581. baseUrl: 'https://maps.itos.uga.edu/arcgis/rest/services/GDOT/GDOT_FunctionalClass/mapserver/',
  582. supportsPagination: true,
  583. defaultColors: {
  584. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  585. },
  586. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  587. fetchAllFC: false,
  588. /* eslint-disable object-curly-newline */
  589. fcMapLayers: [
  590. { layerID: 0, fcPropName: 'FUNCTIONAL_CLASS', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FUNCTIONAL_CLASS', 'SYSTEM_CODE'], maxRecordCount: 1000, supportsPagination: true, roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6] } },
  591. { layerID: 1, fcPropName: 'FUNCTIONAL_CLASS', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FUNCTIONAL_CLASS', 'SYSTEM_CODE'], maxRecordCount: 1000, supportsPagination: true, roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6] } },
  592. { layerID: 2, fcPropName: 'FUNCTIONAL_CLASS', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FUNCTIONAL_CLASS', 'SYSTEM_CODE'], maxRecordCount: 1000, supportsPagination: true, roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6] } },
  593. { layerID: 3, fcPropName: 'FUNCTIONAL_CLASS', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FUNCTIONAL_CLASS', 'SYSTEM_CODE'], maxRecordCount: 1000, supportsPagination: true, roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6] } },
  594. { layerID: 4, fcPropName: 'FUNCTIONAL_CLASS', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FUNCTIONAL_CLASS', 'SYSTEM_CODE'], maxRecordCount: 1000, supportsPagination: true, roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6] } },
  595. { layerID: 5, fcPropName: 'FUNCTIONAL_CLASS', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FUNCTIONAL_CLASS', 'SYSTEM_CODE'], maxRecordCount: 1000, supportsPagination: true, roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6] } },
  596. { layerID: 6, fcPropName: 'FUNCTIONAL_CLASS', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FUNCTIONAL_CLASS', 'SYSTEM_CODE'], maxRecordCount: 1000, supportsPagination: true, roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6] } }
  597. ],
  598. /* eslint-enable object-curly-newline */
  599. information: { Source: 'GDOT', Permission: 'Visible to R4+ or R3-AM', Description: 'Federal and State highways set to a minimum of mH.' },
  600. getWhereClause() {
  601. return null;
  602. },
  603. getFeatureRoadType(feature, layer) {
  604. if (layer.getFeatureRoadType) {
  605. return layer.getFeatureRoadType(feature);
  606. }
  607. const attr = feature.attributes;
  608. const fc = attr.FUNCTIONAL_CLASS;
  609. if (attr.SYSTEM_CODE === '1' && fc > 4) {
  610. return STATE_SETTINGS.global.getRoadTypeFromFC(4, layer);
  611. }
  612. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  613. }
  614. },
  615. HI: {
  616. baseUrl: 'http://geodata.hawaii.gov/arcgis/rest/services/Transportation/MapServer/',
  617. defaultColors: {
  618. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  619. },
  620. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  621. fcMapLayers: [
  622. {
  623. layerID: 12,
  624. fcPropName: 'funsystem',
  625. idPropName: 'OBJECTID',
  626. outFields: ['OBJECTID', 'funsystem'],
  627. roadTypeMap: {
  628. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  629. },
  630. maxRecordCount: 1000,
  631. supportsPagination: false
  632. }
  633. ],
  634. information: { Source: 'HDOT', Permission: 'Visible to R4+ or R3-AM', Description: 'Raw unmodified FC data.' },
  635. getWhereClause(context) {
  636. if (context.mapContext.zoom < 16) {
  637. return `${context.layer.fcPropName} <> 7`;
  638. }
  639. return null;
  640. },
  641. getFeatureRoadType(feature, layer) {
  642. if (layer.getFeatureRoadType) {
  643. return layer.getFeatureRoadType(feature);
  644. }
  645. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  646. }
  647. },
  648. ID: {
  649. baseUrl: 'https://gisportalp.itd.idaho.gov/xserver/rest/services/RH_GeneralService/MapServer/',
  650. supportsPagination: false,
  651. defaultColors: {
  652. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  653. },
  654. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  655. fetchAllFC: true,
  656. /* eslint-disable object-curly-newline */
  657. fcMapLayers: [
  658. { layerID: 67, fcPropName: 'FunctionalClass', idPropName: 'ObjectId', outFields: ['ObjectId', 'FunctionalClass'], maxRecordCount: 1000, supportsPagination: false, roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6] } }
  659. ],
  660. /* eslint-enable object-curly-newline */
  661. information: { Source: 'ITD', Permission: 'Visible to R4+ or R3-AM', Description: 'Raw unmodified FC data.' },
  662. getWhereClause() {
  663. return null;
  664. },
  665. getFeatureRoadType(feature, layer) {
  666. if (layer.getFeatureRoadType) {
  667. return layer.getFeatureRoadType(feature);
  668. }
  669. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  670. }
  671. },
  672. IL: {
  673. baseUrl: 'https://gis1.dot.illinois.gov/arcgis/rest/services/AdministrativeData/FunctionalClass/MapServer/',
  674. supportsPagination: false,
  675. defaultColors: {
  676. Fw: '#ff00c5', Ew: '#ff00c5', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  677. },
  678. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1] },
  679. fcMapLayers: [
  680. {
  681. layerID: 0,
  682. idPropName: 'OBJECTID',
  683. fcPropName: 'FC',
  684. outFields: ['FC'],
  685. roadTypeMap: {
  686. Fw: ['1'], Ew: ['2'], MH: ['3'], mH: ['4'], PS: ['5', '6'], St: ['7']
  687. },
  688. maxRecordCount: 1000,
  689. supportsPagination: false
  690. }
  691. ],
  692. isPermitted() { return rank >= 4; },
  693. information: { Source: 'IDOT', Permission: 'Visible to R4+', Description: 'Raw unmodified FC data.' },
  694. getWhereClause(context) {
  695. return context.mapContext.zoom < 16 ? 'FC<>7' : null;
  696. },
  697. getFeatureRoadType(feature, layer) {
  698. if (layer.getFeatureRoadType) {
  699. return layer.getFeatureRoadType(feature);
  700. }
  701. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  702. }
  703. },
  704. IN: {
  705. baseUrl: 'https://gis.indot.in.gov/ro/rest/services/DOT/INDOT_LTAP/MapServer/',
  706. supportsPagination: false,
  707. overrideUrl: '1Sbwc7e6BfHpZWSTfU3_1otXGSxHrdDYcbn7fOf1VjpA',
  708. defaultColors: {
  709. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  710. },
  711. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []], hideRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  712. fcMapLayers: [
  713. {
  714. layerID: 10,
  715. idPropName: 'OBJECTID',
  716. fcPropName: 'FUNCTIONAL_CLASS',
  717. outFields: ['FUNCTIONAL_CLASS', 'OBJECTID', 'TO_DATE'],
  718. roadTypeMap: {
  719. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  720. },
  721. maxRecordCount: 100000,
  722. supportsPagination: false
  723. }
  724. ],
  725. isPermitted() { return true; },
  726. information: { Source: 'INDOT', Description: 'Raw unmodified FC data.' },
  727. getWhereClause(context) {
  728. let whereParts = ['TO_DATE IS NULL'];
  729. if (context.mapContext.zoom < 16) {
  730. whereParts += ` AND ${context.layer.fcPropName} <> 7`;
  731. }
  732. return whereParts;
  733. },
  734. getFeatureRoadType(feature, layer) {
  735. if (layer.getFeatureRoadType) {
  736. return layer.getFeatureRoadType(feature);
  737. }
  738. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  739. }
  740. },
  741. IA: {
  742. baseUrl: 'https://gis.iowadot.gov/agshost/rest/services/RAMS/Road_Network/FeatureServer/',
  743. defaultColors: {
  744. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee', PSGr: '#cc6533', StGr: '#e99cb6'
  745. },
  746. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  747. fcMapLayers: [
  748. {
  749. layerID: 0,
  750. fcPropName: 'FED_FUNCTIONAL_CLASS',
  751. idPropName: 'OBJECTID',
  752. outFields: ['OBJECTID', 'FED_FUNCTIONAL_CLASS', 'STATE_ROUTE_NAME_1', 'ACCESS_CONTROL', 'SURFACE_TYPE'],
  753. roadTypeMap: {
  754. Fw: [1], MH: [2, 3], mH: [4], PS: [5, 6], St: [7]
  755. },
  756. maxRecordCount: 1000,
  757. supportsPagination: false
  758. }
  759. ],
  760. information: { Source: 'Iowa DOT', Permission: 'Visible to R4+ or R3-AM', Description: 'Additional colors denote unpaved PS and LS segements.' },
  761. getWhereClause(context) {
  762. let theWhereClause = "FACILITY_TYPE<>'7'"; // Removed proposed roads
  763. if (context.mapContext.zoom < 16) {
  764. theWhereClause += ` AND ${context.layer.fcPropName}<>'7'`;
  765. }
  766. return theWhereClause;
  767. },
  768. getFeatureRoadType(feature, layer) {
  769. const attr = feature.attributes;
  770. let fc = parseInt(attr[layer.fcPropName], 10);
  771. const isFw = attr.ACCESS_CONTROL === 1;
  772. const isUS = /^STATE OF IOWA, US/.test(attr.STATE_ROUTE_NAME_1);
  773. const isState = /^STATE OF IOWA, IA/.test(attr.STATE_ROUTE_NAME_1);
  774. if (isFw) fc = 1;
  775. else if (fc > 3 && isUS) fc = 3;
  776. else if (fc > 4 && isState) fc = 4;
  777. if (fc > 4 && attr.SURFACE_TYPE === 20) {
  778. return fc < 7 ? 'PSGr' : 'StGr';
  779. }
  780. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  781. }
  782. },
  783. KS: {
  784. baseUrl: 'http://wfs.ksdot.org/arcgis_web_adaptor/rest/services/Transportation/',
  785. defaultColors: {
  786. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  787. },
  788. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  789. fcMapLayers: [
  790. {
  791. layerID: 3,
  792. layerPath: 'Functional_Classification/MapServer/',
  793. idPropName: 'Id',
  794. fcPropName: 'FunctionalClassification',
  795. outFields: ['FunctionalClassification', 'Id'],
  796. roadTypeMap: {
  797. Fw: [1], MH: [2, 3], mH: [4], PS: [5, 6], St: [7]
  798. },
  799. maxRecordCount: 1000,
  800. supportsPagination: false
  801. }// ,
  802.  
  803. // 2024-03-20 (mapomatic) The "non-state system" layer was removed from the KS server,
  804. // so we're forced to use the function_classification layer (above) which doesn't include
  805. // any metadata for US/state road designations. I'm leaving the old layers commented below
  806. // in case they're of use in the future.
  807.  
  808. // {
  809. // layerID: 0,
  810. // layerPath: 'Non_State_System/MapServer/',
  811. // idPropName: 'ID2',
  812. // fcPropName: 'FUNCLASS',
  813. // outFields: ['FUNCLASS', 'ID2', 'ROUTE_ID'],
  814. // roadTypeMap: {
  815. // Fw: [1], MH: [2, 3], mH: [4], PS: [5, 6], St: [7]
  816. // },
  817. // maxRecordCount: 1000,
  818. // supportsPagination: false
  819. // },
  820. // {
  821. // layerID: 0,
  822. // layerPath: 'State_System/MapServer/',
  823. // idPropName: 'OBJECTID',
  824. // fcPropName: 'FUN_CLASS_CD',
  825. // outFields: ['FUN_CLASS_CD', 'OBJECTID', 'PREFIX', 'ACCESS_CONTROL'],
  826. // roadTypeMap: {
  827. // Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  828. // },
  829. // maxRecordCount: 1000,
  830. // supportsPagination: false
  831. // }
  832. ],
  833. isPermitted() { return rank >= 3 || isAM; },
  834. information: { Source: 'KDOT', Permission: 'Visible to area managers' },
  835. getWhereClause(context) {
  836. if (context.mapContext.zoom < 16) {
  837. return `${context.layer.fcPropName}<>'7'`;
  838. }
  839. return null;
  840. },
  841. getFeatureRoadType(feature, layer) {
  842. const attr = feature.attributes;
  843. let fc = parseInt(attr[layer.fcPropName], 10);
  844. const roadPrefix = attr.PREFIX;
  845. const isUS = roadPrefix === 'U';
  846. const isState = roadPrefix === 'K';
  847. if ((fc > 3 && isUS) || (fc === 2 && parseInt(attr.ACCESS_CONTROL, 10) !== 1)) fc = 3;
  848. else if (fc > 4 && isState) fc = 4;
  849. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  850. }
  851. },
  852. KY: {
  853. baseUrl: 'https://maps.kytc.ky.gov/arcgis/rest/services/BaseMap/System/MapServer/',
  854. supportsPagination: false,
  855. defaultColors: {
  856. Fw: '#ffaac5', Ew: '#ff00c5', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  857. },
  858. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1] },
  859. fcMapLayers: [
  860. {
  861. layerID: 0,
  862. idPropName: 'OBJECTID',
  863. fcPropName: 'FC',
  864. outFields: ['FC', 'OBJECTID', 'RT_PREFIX', 'RT_SUFFIX'],
  865. roadTypeMap: {
  866. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  867. },
  868. maxRecordCount: 1000,
  869. supportsPagination: false
  870. }
  871. ],
  872. isPermitted() { return true; },
  873. information: { Source: 'KYTC' },
  874. getWhereClause(context) {
  875. if (context.mapContext.zoom < 16) {
  876. return `${context.layer.fcPropName}<>'7'`;
  877. }
  878. return null;
  879. },
  880. getFeatureRoadType(feature, layer) {
  881. const attr = feature.attributes;
  882. let fc = parseInt(attr[layer.fcPropName], 10);
  883. if (fc > 3 && attr.RT_PREFIX === 'US') {
  884. const suffix = attr.RT_SUFFIX;
  885. fc = (suffix && suffix.indexOf('X') > -1) ? 4 : 3;
  886. }
  887. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  888. }
  889. },
  890. LA: {
  891. baseUrl: 'https://giswebnew.dotd.la.gov/arcgis/rest/services/Transportation/LA_RoadwayFunctionalClassification/FeatureServer/',
  892. supportsPagination: false,
  893. defaultColors: {
  894. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  895. },
  896. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  897. /* eslint-disable object-curly-newline */
  898. fcMapLayers: [
  899. { layerID: 0, fcPropName: 'FunctionalSystem', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FunctionalSystem', 'RouteID'], roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7] }, maxRecordCount: 1000, supportsPagination: false },
  900. { layerID: 1, fcPropName: 'FunctionalSystem', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FunctionalSystem', 'RouteID'], roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7] }, maxRecordCount: 1000, supportsPagination: false },
  901. { layerID: 2, fcPropName: 'FunctionalSystem', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FunctionalSystem', 'RouteID'], roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7] }, maxRecordCount: 1000, supportsPagination: false },
  902. { layerID: 3, fcPropName: 'FunctionalSystem', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FunctionalSystem', 'RouteID'], roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7] }, maxRecordCount: 1000, supportsPagination: false },
  903. { layerID: 4, fcPropName: 'FunctionalSystem', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FunctionalSystem', 'RouteID'], roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7] }, maxRecordCount: 1000, supportsPagination: false },
  904. { layerID: 5, fcPropName: 'FunctionalSystem', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FunctionalSystem', 'RouteID'], roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7] }, maxRecordCount: 1000, supportsPagination: false },
  905. { layerID: 6, fcPropName: 'FunctionalSystem', idPropName: 'OBJECTID', outFields: ['OBJECTID', 'FunctionalSystem', 'RouteID'], roadTypeMap: { Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7] }, maxRecordCount: 1000, supportsPagination: false }
  906. ],
  907. /* eslint-enable object-curly-newline */
  908. information: { Source: 'LaDOTD', Permission: 'Visible to R4+ or R3-AM' },
  909. getWhereClause(context) {
  910. if (context.mapContext.zoom < 16) {
  911. return `${context.layer.fcPropName}<>'7'`; // OR State_Route LIKE 'US%' OR State_Route LIKE 'LA%'";
  912. }
  913. return null;
  914. },
  915. getFeatureRoadType(feature, layer) {
  916. let fc = feature.attributes[layer.fcPropName];
  917. if (fc === '2a' || fc === '2b') { fc = 2; }
  918. fc = parseInt(fc, 10);
  919. const route = feature.attributes.RouteID.split('_')[1].trim();
  920. const isUS = /^US \d/.test(route);
  921. const isState = /^LA \d/.test(route);
  922. const isBiz = / BUS$/.test(route);
  923. if (fc > 3 && isUS) fc = isBiz ? 4 : 3;
  924. else if (fc > 4 && isState) fc = isBiz ? 5 : 4;
  925. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  926. }
  927. },
  928. ME: {
  929. baseUrl: 'https://arcgisserver.maine.gov/arcgis/rest/services/mdot/MaineDOT_Dynamic/MapServer/',
  930. defaultColors: {
  931. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  932. },
  933. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  934. fcMapLayers: [
  935. {
  936. layerID: 1024,
  937. fcPropName: 'fedfunccls',
  938. idPropName: 'objectid',
  939. outFields: ['objectid', 'fedfunccls'],
  940. roadTypeMap: {
  941. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  942. },
  943. maxRecordCount: 1000,
  944. supportsPagination: false
  945. }
  946. ],
  947. information: { Source: 'MaineDOT', Permission: 'Visible to R4+ or R3-AM' },
  948. isPermitted() { return rank >= 4 || (rank === 3 && isAM); },
  949. getWhereClause(context) {
  950. if (context.mapContext.zoom < 16) {
  951. return `${context.layer.fcPropName}<>'Local'`;
  952. }
  953. return null;
  954. },
  955. getFeatureRoadType(feature, layer) {
  956. const attr = feature.attributes;
  957. let fc = attr[layer.fcPropName];
  958. switch (fc) {
  959. case 'Interstate': fc = 1; break;
  960. case 'Other Freeway or Expressway': fc = 2; break;
  961. case 'Other Principal Arterial': fc = 3; break;
  962. case 'Minor Arterial': fc = 4; break;
  963. case 'Major Collector':
  964. case 'Minor Collector': fc = 5; break;
  965. default: fc = 7;
  966. }
  967. // 2024-6-28 (mapomatic) MaineDOT removed the prirtename field so we can't "upgrade" FC anymore.
  968. // const route = attr.prirtename;
  969. // const isUS = /^US \d/.test(route);
  970. // const isState = /^ST RTE \d/.test(route);
  971. // const isBiz = (isUS && /(1B|1BS)$/.test(route)) || (isState && /(15B|24B|25B|137B)$/.test(route));
  972. // if (fc > 3 && isUS) fc = isBiz ? 4 : 3;
  973. // else if (fc > 4 && isState) fc = isBiz ? 5 : 4;
  974. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  975. }
  976. },
  977. MD: {
  978. baseUrl: 'https://services.arcgis.com/njFNhDsUCentVYJW/arcgis/rest/services/MDOT_SHA_Roadway_Functional_Classification/FeatureServer/',
  979. defaultColors: {
  980. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#ffff00', St: '#eeeeee'
  981. },
  982. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  983. fcMapLayers: [
  984. {
  985. layerID: 0,
  986. fcPropName: 'FUNCTIONAL_CLASS',
  987. idPropName: 'OBJECTID',
  988. outFields: ['OBJECTID', 'FUNCTIONAL_CLASS', 'ID_PREFIX', 'MP_SUFFIX'],
  989. roadTypeMap: {
  990. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  991. },
  992. maxRecordCount: 1000,
  993. supportsPagination: false
  994. }
  995. ],
  996. information: { Source: 'MDOT', Permission: 'Visible to R4+ or R3-AM' },
  997. getWhereClause(context) {
  998. if (context.mapContext.zoom < 16) {
  999. return "(FUNCTIONAL_CLASS < 7 OR ID_PREFIX IN('MD'))";
  1000. }
  1001. return null;
  1002. },
  1003. getFeatureRoadType(feature, layer) {
  1004. const attr = feature.attributes;
  1005. let fc = parseInt(attr.FUNCTIONAL_CLASS, 10);
  1006. const isUS = attr.ID_PREFIX === 'US';
  1007. const isState = attr.ID_PREFIX === 'MD';
  1008. const isBiz = attr.MP_SUFFIX === 'BU';
  1009. if (fc > 3 && isUS) fc = isBiz ? 4 : 3;
  1010. else if (fc > 4 && isState) fc = isBiz ? 5 : 4;
  1011. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1012. }
  1013. },
  1014. MA: {
  1015. baseUrl: 'https://gis.massdot.state.ma.us/arcgis/rest/services/Roads/RoadInventory/MapServer/',
  1016. defaultColors: {
  1017. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1018. },
  1019. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1020. fcMapLayers: [
  1021. {
  1022. layerID: 0,
  1023. fcPropName: 'F_F_Class',
  1024. idPropName: 'OBJECTID',
  1025. outFields: ['OBJECTID', 'F_F_Class', 'Route_ID'],
  1026. roadTypeMap: {
  1027. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1028. },
  1029. maxRecordCount: 1000,
  1030. supportsPagination: false
  1031. }
  1032. ],
  1033. information: { Source: 'MDOT', Permission: 'Visible to R2+' },
  1034. isPermitted() { return rank >= 2; },
  1035. getWhereClause(context) {
  1036. if (context.mapContext.zoom < 16) {
  1037. return `${context.layer.fcPropName}<>'7'`;
  1038. }
  1039. return null;
  1040. },
  1041. getFeatureRoadType(feature, layer) {
  1042. const attr = feature.attributes;
  1043. let fc = parseInt(attr[layer.fcPropName], 10);
  1044. const route = attr.Route_ID;
  1045. const isUS = /^US\d/.test(route);
  1046. const isState = /^SR\d/.test(route);
  1047. if (fc > 3 && isUS) fc = 3;
  1048. else if (fc > 4 && isState) fc = 4;
  1049. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1050. }
  1051. },
  1052. MI: {
  1053. baseUrl: 'https://gisp.mcgi.state.mi.us/arcgis/rest/services/MDOT/NFC/MapServer/',
  1054. defaultColors: {
  1055. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1056. },
  1057. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1058. fcMapLayers: [
  1059. {
  1060. layerID: 2,
  1061. idPropName: 'OBJECTID',
  1062. fcPropName: 'NFC',
  1063. outFields: ['NFC'],
  1064. roadTypeMap: {
  1065. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1066. },
  1067. maxRecordCount: 1000,
  1068. supportsPagination: false
  1069. }
  1070. ],
  1071. isPermitted() { return true; },
  1072. information: { Source: 'MDOT', Description: 'Raw unmodified FC data.' },
  1073. getWhereClause(context) {
  1074. if (context.mapContext.zoom < 16) {
  1075. return `${context.layer.fcPropName}<>7`;
  1076. }
  1077. return null;
  1078. },
  1079. getFeatureRoadType(feature, layer) {
  1080. if (layer.getFeatureRoadType) {
  1081. return layer.getFeatureRoadType(feature);
  1082. }
  1083. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  1084. }
  1085. },
  1086. MN: {
  1087. baseUrl: 'https://dotapp9.dot.state.mn.us/lrs/rest/services/emma/emma_op/MapServer/',
  1088. defaultColors: {
  1089. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1090. },
  1091. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1092. fcMapLayers: [
  1093. {
  1094. layerID: 13,
  1095. idPropName: 'OBJECTID',
  1096. fcPropName: 'FUNCTIONAL_CLASS',
  1097. outFields: ['FUNCTIONAL_CLASS', 'ROUTE_ID'],
  1098. roadTypeMap: {
  1099. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1100. },
  1101. maxRecordCount: 1000,
  1102. supportsPagination: false
  1103. }
  1104. ],
  1105. isPermitted() { return true; },
  1106. information: { Source: 'MnDOT', Description: 'Raw unmodified FC data.' },
  1107. getWhereClause(context) {
  1108. if (context.mapContext.zoom < 16) {
  1109. return `${context.layer.fcPropName}<>7`;
  1110. }
  1111. return null;
  1112. },
  1113. getFeatureRoadType(feature, layer) {
  1114. if (layer.getFeatureRoadType) {
  1115. return layer.getFeatureRoadType(feature);
  1116. }
  1117. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  1118. }
  1119. },
  1120. MO: {
  1121. baseUrl: 'https://mapping.modot.org/arcgis/rest/services/BaseMap/TmsUtility/MapServer/',
  1122. defaultColors: {
  1123. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1124. },
  1125. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1126. fcMapLayers: [
  1127. {
  1128. layerID: 5,
  1129. fcPropName: 'FUNC_CLASS_NAME',
  1130. idPropName: 'SS_PAVEMENT_ID',
  1131. outFields: ['SS_PAVEMENT_ID', 'FUNC_CLASS_NAME', 'TRAVELWAY_DESG', 'TRAVELWAY_NAME', 'ACCESS_CAT_NAME'],
  1132. roadTypeMap: {
  1133. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1134. },
  1135. maxRecordCount: 1000,
  1136. supportsPagination: false
  1137. }
  1138. ],
  1139. isPermitted() { return rank >= 3 || (rank >= 2 && isAM); },
  1140. information: { Source: 'MoDOT', Permission: 'Visible to R3+ or R2-AM' },
  1141. getWhereClause(context) {
  1142. if (context.mapContext.zoom < 13) {
  1143. return '1=0'; // WME very laggy at zoom 0
  1144. }
  1145. // Remove duplicate rows, but suss out interstate business loops
  1146. return "FUNC_CLASS_NAME <> ' ' AND (TRAVELWAY_ID = CNTL_TW_ID OR (TRAVELWAY_ID <> CNTL_TW_ID AND TRAVELWAY_DESG = 'LP'))";
  1147. },
  1148. getFeatureRoadType(feature, layer) {
  1149. const attr = feature.attributes;
  1150. let fc = attr[layer.fcPropName];
  1151. const rtType = attr.TRAVELWAY_DESG;
  1152. const route = attr.TRAVELWAY_NAME;
  1153. switch (fc) {
  1154. case 'INTERSTATE': fc = 1; break;
  1155. case 'FREEWAY': fc = 2; break;
  1156. case 'PRINCIPAL ARTERIAL': fc = 3; break;
  1157. case 'MINOR ARTERIAL': fc = 4; break;
  1158. case 'MAJOR COLLECTOR': fc = 5; break;
  1159. case 'MINOR COLLECTOR': fc = 6; break;
  1160. default: fc = 8; // not a typo
  1161. }
  1162. const usHwys = ['24', '36', '40', '50', '54', '56', '59', '60', '61', '62', '63', '65', '67', '69', '71', '136', '159', '160', '166', '169', '275', '400', '412'];
  1163. const isUS = ['US', 'LP'].includes(rtType); // is US or interstate biz
  1164. const isState = ['MO', 'AL'].includes(rtType);
  1165. const isSup = rtType === 'RT';
  1166. const isBiz = ['BU', 'SP'].includes(rtType) || /BUSINESS .+ \d/.test(route);
  1167. const isUSBiz = isBiz && usHwys.includes(route);
  1168. if ((fc === 2 && attr.ACCESS_CAT_NAME !== 'FULL') || (fc > 3 && isUS)) fc = 3;
  1169. else if (fc > 4 && (isState || isUSBiz)) fc = 4;
  1170. else if (fc > 6 && (isSup || isBiz)) fc = 6;
  1171. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1172. }
  1173. },
  1174. MT: {
  1175. baseUrl: 'https://app.mdt.mt.gov/arcgis/rest/services/Standard/FUNCTIONAL_CLASS/MapServer/',
  1176. defaultColors: {
  1177. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1178. },
  1179. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1180. fcMapLayers: [
  1181. {
  1182. layerID: 0,
  1183. fcPropName: 'FUNC_CLASS',
  1184. idPropName: 'OBJECTID',
  1185. outFields: ['OBJECTID', 'FUNC_CLASS', 'SIGN_ROUTE', 'ROUTE_NAME'],
  1186. roadTypeMap: {
  1187. Fw: ['1-Interstate']
  1188. },
  1189. maxRecordCount: 1000,
  1190. supportsPagination: false
  1191. },
  1192. {
  1193. layerID: 1,
  1194. fcPropName: 'FUNC_CLASS',
  1195. idPropName: 'OBJECTID',
  1196. outFields: ['OBJECTID', 'FUNC_CLASS', 'SIGN_ROUTE', 'ROUTE_NAME'],
  1197. roadTypeMap: {
  1198. MH: ['3-Principal Arterial - Other']
  1199. },
  1200. maxRecordCount: 1000,
  1201. supportsPagination: false
  1202. },
  1203. {
  1204. layerID: 2,
  1205. fcPropName: 'FUNC_CLASS',
  1206. idPropName: 'OBJECTID',
  1207. outFields: ['OBJECTID', 'FUNC_CLASS', 'SIGN_ROUTE', 'ROUTE_NAME'],
  1208. roadTypeMap: {
  1209. mH: ['4-Minor Arterial']
  1210. },
  1211. maxRecordCount: 1000,
  1212. supportsPagination: false
  1213. },
  1214. {
  1215. layerID: 3,
  1216. fcPropName: 'FUNC_CLASS',
  1217. idPropName: 'OBJECTID',
  1218. outFields: ['OBJECTID', 'FUNC_CLASS', 'SIGN_ROUTE', 'ROUTE_NAME'],
  1219. roadTypeMap: {
  1220. PS: ['5-Major Collector']
  1221. },
  1222. maxRecordCount: 1000,
  1223. supportsPagination: false
  1224. },
  1225. {
  1226. layerID: 4,
  1227. fcPropName: 'FUNC_CLASS',
  1228. idPropName: 'OBJECTID',
  1229. outFields: ['OBJECTID', 'FUNC_CLASS', 'SIGN_ROUTE', 'ROUTE_NAME'],
  1230. roadTypeMap: {
  1231. PS: ['6-Minor Collector']
  1232. },
  1233. maxRecordCount: 1000,
  1234. supportsPagination: false
  1235. },
  1236. {
  1237. layerID: 5,
  1238. fcPropName: 'FUNC_CLASS',
  1239. idPropName: 'OBJECTID',
  1240. outFields: ['OBJECTID', 'FUNC_CLASS', 'SIGN_ROUTE', 'ROUTE_NAME'],
  1241. roadTypeMap: {
  1242. St: ['7-Local']
  1243. },
  1244. maxRecordCount: 1000,
  1245. supportsPagination: false
  1246. }
  1247. ],
  1248. isPermitted() { /* return _r >= 3; */ return ['mapomatic', 'bobc455'].includes(userNameLC); },
  1249. information: { Source: 'MDT', Permission: 'Visible to R3+' },
  1250. getWhereClause(context) {
  1251. if (context.mapContext.zoom < 16) {
  1252. return `${context.layer.fcPropName}<>'LOCAL'`;
  1253. }
  1254. return null;
  1255. },
  1256. getFeatureRoadType(feature, layer) {
  1257. let rt = STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  1258. const roadID = feature.attributes.SIGN_ROUTE || feature.attributes.ROUTE_NAME;
  1259. const isUS = /^US[ -]?\d+/.test(roadID);
  1260. const isState = /^MONTANA \d+|ROUTE \d+|S-\d{3}\b/.test(roadID);
  1261. if (isUS && ['St', 'PS', 'mH'].includes(rt)) {
  1262. rt = 'MH';
  1263. } else if (isState && ['St', 'PS'].includes(rt)) {
  1264. rt = 'mH';
  1265. }
  1266. return rt;
  1267. }
  1268. },
  1269. NV: {
  1270. baseUrl: 'https://gis.dot.nv.gov/rhgis/rest/services/GeoHub/FSystem/MapServer/',
  1271. defaultColors: {
  1272. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1273. },
  1274. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  1275. fcMapLayers: [
  1276. {
  1277. layerID: 0,
  1278. fcPropName: 'FSystem',
  1279. idPropName: 'OBJECTID',
  1280. outFields: ['OBJECTID', 'FSystem'],
  1281. roadTypeMap: {
  1282. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1283. },
  1284. maxRecordCount: 1000,
  1285. supportsPagination: false
  1286. }
  1287. ],
  1288. isPermitted() { return ['mapomatic', 'turbomkt', 'tonestertm', 'geopgeop', 'ojlaw'].includes(userNameLC); },
  1289. information: { Source: 'NDOT', Permission: '?' },
  1290. getWhereClause(context) {
  1291. if (context.mapContext.zoom < 16) {
  1292. return `${context.layer.fcPropName}<>7`;
  1293. }
  1294. return null;
  1295. },
  1296. getFeatureRoadType(feature, layer) {
  1297. const fc = parseInt(feature.attributes[layer.fcPropName], 10);
  1298. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1299. }
  1300. },
  1301. NH: {
  1302. baseUrl: 'https://nhgeodata.unh.edu/nhgeodata/rest/services/TN/RoadsForDOTViewer/MapServer/',
  1303. defaultColors: {
  1304. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1305. },
  1306. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  1307. fcMapLayers: [
  1308. {
  1309. layerID: 0,
  1310. fcPropName: 'FUNCT_SYSTEM',
  1311. idPropName: 'OBJECTID',
  1312. outFields: ['OBJECTID', 'FUNCT_SYSTEM', 'STREET_ALIASES', 'TIER'],
  1313. roadTypeMap: {
  1314. Fw: [1], Ew: [2], MH: [2, 3], mH: [4], PS: [5, 6], St: [7, 0]
  1315. },
  1316. maxRecordCount: 1000,
  1317. supportsPagination: false
  1318. }
  1319. ],
  1320. isPermitted() { return rank >= 2; },
  1321. information: { Source: 'NH GRANIT', Permission: 'Visible to R2+' },
  1322. getWhereClause(context) {
  1323. if (context.mapContext.zoom < 16) {
  1324. return `${context.layer.fcPropName}<>7 AND ${context.layer.fcPropName}<>0`;
  1325. }
  1326. return null;
  1327. },
  1328. getFeatureRoadType(feature, layer) {
  1329. let fc = parseInt(feature.attributes[layer.fcPropName], 10);
  1330. if (!(fc > 0)) { fc = 7; }
  1331. const route = feature.attributes.STREET_ALIASES;
  1332. const isUS = /US /.test(route);
  1333. const isState = /NH /.test(route);
  1334. if (fc === 2) fc = feature.attributes.TIER === 1 ? 1 : 3;
  1335. else if (fc > 3 && isUS) fc = /US 3B/.test(route) ? 4 : 3;
  1336. else if (fc > 4 && isState) fc = 4;
  1337. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1338. }
  1339. },
  1340. NM: {
  1341. baseUrl: 'https://services.arcgis.com/hOpd7wfnKm16p9D9/ArcGIS/rest/services/NMDOT_Functional_Class/FeatureServer/',
  1342. defaultColors: {
  1343. Fw: '#ff00c5', Ew: '#ff00c5', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1344. },
  1345. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1] },
  1346. fcMapLayers: [
  1347. {
  1348. layerID: 0,
  1349. fcPropName: 'Func_Class',
  1350. idPropName: 'OBJECTID_1',
  1351. maxRecordCount: 1000,
  1352. supportsPagination: false,
  1353. outFields: ['OBJECTID_1', 'Func_Class', 'D_RT_ROUTE'],
  1354. roadTypeMap: {
  1355. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1356. }
  1357. }
  1358. ],
  1359. isPermitted() { return true; },
  1360. information: { Source: 'NMDOT' },
  1361. getWhereClause() {
  1362. return null;
  1363. },
  1364. getFeatureRoadType(feature, layer) {
  1365. let fc = parseInt(feature.attributes[layer.fcPropName], 10);
  1366. const roadType = feature.attributes.D_RT_ROUTE.split('-', 1).shift();
  1367. const isBiz = roadType === 'BL'; // Interstate Business Loop
  1368. const isUS = roadType === 'US';
  1369. const isState = roadType === 'NM';
  1370. if (roadType === 'IX') fc = 0;
  1371. else if (fc > 3 && (isBiz || isUS)) fc = 3;
  1372. else if (fc > 4 && isState) fc = 4;
  1373. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1374. }
  1375. },
  1376. NY: { // https://gis.dot.ny.gov/hostingny/rest/services/Basemap/MapServer/21
  1377. baseUrl: 'https://gis.dot.ny.gov/hostingny/rest/services',
  1378. defaultColors: {
  1379. Fw: '#ff00c5', Ew: '#5f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1380. },
  1381. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1] },
  1382. fcMapLayers: [
  1383. {
  1384. layerID: '/Geocortex/FC/MapServer/1',
  1385. fcPropName: 'FUNC_CLASS',
  1386. idPropName: 'OBJECTID',
  1387. outFields: ['OBJECTID', 'FUNC_CLASS', 'SEGMENT_NAME', 'ROUTE_NO'],
  1388. roadTypeMap: {
  1389. Fw: [1, 11], Ew: [2, 12], MH: [4, 14], mH: [6, 16], PS: [7, 8, 17, 18], St: [9, 19]
  1390. },
  1391. maxRecordCount: 1000,
  1392. supportsPagination: false
  1393. },
  1394. {
  1395. layerID: 'Basemap/MapServer/21',
  1396. idPropName: 'OBJECTID',
  1397. outFields: ['OBJECTID', 'SHIELD'],
  1398. maxRecordCount: 1000,
  1399. supportsPagination: false
  1400. }
  1401. ],
  1402. information: { Source: 'NYSDOT', Permission: 'Visible to R4+ or R3-AM' },
  1403. getWhereClause(context) {
  1404. if (context.layer.layerID === 'Basemap/MapServer/21') {
  1405. return ("SHIELD IN ('C','CT')");
  1406. }
  1407. return null;
  1408. },
  1409. getFeatureRoadType(feature, layer) {
  1410. let roadType;
  1411. if (layer.layerID === 'Basemap/MapServer/21') {
  1412. roadType = 'PS';
  1413. } else {
  1414. roadType = STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  1415. const routeNo = feature.attributes.ROUTE_NO;
  1416. if (/^NY.*/.test(routeNo)) {
  1417. if (roadType === 'PS') roadType = 'mH';
  1418. } else if (/^US.*/.test(routeNo)) {
  1419. if (roadType === 'PS' || roadType === 'mH') roadType = 'MH';
  1420. }
  1421. }
  1422. return roadType;
  1423. }
  1424. },
  1425. NC: {
  1426. baseUrl: 'https://gis11.services.ncdot.gov/arcgis/rest/services/NCDOT_FunctionalClassQtr/MapServer/',
  1427. defaultColors: {
  1428. Fw: '#ff00c5', Rmp: '#999999', Ew: '#5f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1429. },
  1430. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1431. fcMapLayers: [
  1432. {
  1433. layerID: 0,
  1434. fcPropName: 'FuncClass',
  1435. idPropName: 'OBJECTID',
  1436. outFields: ['OBJECTID', 'FuncClass', 'RouteClass', 'RouteQualifier'],
  1437. roadTypeMap: {
  1438. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1439. },
  1440. zoomLevels: [3, 4, 5, 6, 7, 8, 9, 10],
  1441. maxRecordCount: 1000,
  1442. supportsPagination: false
  1443. }
  1444. ],
  1445. isPermitted() { return rank >= 3; },
  1446. information: { Source: 'NCDOT', Permission: 'Visible to R3+' },
  1447. getWhereClause(context) {
  1448. if (context.mapContext.zoom < 16) {
  1449. const clause = `(${context.layer.fcPropName} < 7 OR RouteClass IN ('I','FED','NC','RMP','US'))`;
  1450. return clause;
  1451. }
  1452. return null;
  1453. },
  1454. getFeatureRoadType(feature, layer) {
  1455. const fc = feature.attributes[layer.fcPropName];
  1456. let roadType;
  1457. switch (this.getHwySys(feature)) {
  1458. case 'interstate':
  1459. if (fc <= 2 || !this.isBusinessRoute(feature)) roadType = 'Fw';
  1460. else roadType = 'MH';
  1461. break;
  1462. case 'us':
  1463. if (fc <= 2) roadType = 'Ew';
  1464. else if (fc === 3 || !this.isBusinessRoute(feature)) roadType = 'MH';
  1465. else roadType = 'mH';
  1466. break;
  1467. case 'state':
  1468. if (fc <= 2) roadType = 'Ew';
  1469. else if (fc === 3) roadType = 'MH';
  1470. else if (fc === 4 || !this.isBusinessRoute(feature)) roadType = 'mH';
  1471. else roadType = 'PS';
  1472. break;
  1473. case 'ramp':
  1474. roadType = 'Rmp';
  1475. break;
  1476. default:
  1477. if (fc === 2) roadType = 'Ew';
  1478. else if (fc === 3) roadType = 'MH';
  1479. else if (fc === 4) roadType = 'mH';
  1480. else if (fc <= 6) roadType = 'PS';
  1481. else roadType = 'St';
  1482. // roadType = fc === 2 ? 'Ew' : (fc === 3 ? 'MH' : (fc === 4 ? 'mH' : (fc <= 6 ? 'PS' : 'St')));
  1483. }
  1484. return roadType;
  1485. },
  1486. getHwySys(feature) {
  1487. let hwySys;
  1488. switch (feature.attributes.RouteClass.toString()) {
  1489. case '1':
  1490. hwySys = 'interstate';
  1491. break;
  1492. case '2':
  1493. hwySys = 'us';
  1494. break;
  1495. case '3':
  1496. hwySys = 'state';
  1497. break;
  1498. case '80':
  1499. hwySys = 'ramp';
  1500. break;
  1501. default:
  1502. hwySys = 'local';
  1503. }
  1504. return hwySys;
  1505. },
  1506. isBusinessRoute(feature) {
  1507. const qual = feature.attributes.RouteQualifier.toString();
  1508. return qual === '9';
  1509. }
  1510. },
  1511. ND: {
  1512. baseUrl: 'https://ndgishub.nd.gov/arcgis/rest/services/Basemap_General/MapServer/',
  1513. defaultColors: {
  1514. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1515. },
  1516. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1517. fcMapLayers: [
  1518. {
  1519. layerID: 193,
  1520. fcPropName: 'FUNCTIONAL_CLASS',
  1521. idPropName: 'OBJECTID',
  1522. outFields: ['OBJECTID', 'FUNCTIONAL_CLASS'],
  1523. roadTypeMap: {
  1524. MH: [3], mH: [4], PS: [5, 6], St: [7]
  1525. },
  1526. maxRecordCount: 1000,
  1527. supportsPagination: false
  1528. },
  1529. {
  1530. layerID: 192,
  1531. fcPropName: 'RTE_SIN',
  1532. idPropName: 'OBJECTID',
  1533. outFields: ['OBJECTID', 'RTE_SIN'],
  1534. roadTypeMap: {
  1535. Fw: ['I'], MH: ['U'], mH: ['S']
  1536. },
  1537. maxRecordCount: 1000,
  1538. supportsPagination: false
  1539. }
  1540. ],
  1541. information: { Source: 'NDDOT', Permission: 'Visible to R4+ or R3-AM' },
  1542. getWhereClause(context) {
  1543. if (context.mapContext.zoom < 16) {
  1544. return `${context.layer.fcPropName} <> 7`;
  1545. }
  1546. return null;
  1547. },
  1548. getFeatureRoadType(feature, layer) {
  1549. if (layer.getFeatureRoadType) {
  1550. return layer.getFeatureRoadType(feature);
  1551. }
  1552. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  1553. }
  1554. },
  1555. OH: {
  1556. baseUrl: 'https://gis.dot.state.oh.us/arcgis/rest/services/TIMS/Roadway_Information/MapServer/',
  1557. defaultColors: {
  1558. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1559. },
  1560. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1561.  
  1562. fcMapLayers: [
  1563. {
  1564. layerID: 8,
  1565. fcPropName: 'FUNCTION_CLASS_CD',
  1566. idPropName: 'ObjectID',
  1567. outFields: ['FUNCTION_CLASS_CD', 'ROUTE_TYPE', 'ROUTE_NBR', 'ObjectID'],
  1568. maxRecordCount: 1000,
  1569. supportsPagination: false,
  1570. roadTypeMap: {
  1571. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1572. }
  1573. }
  1574. ],
  1575. isPermitted() { return true; },
  1576. information: { Source: 'ODOT' },
  1577. getWhereClause(context) {
  1578. if (context.mapContext.zoom < 16) {
  1579. const clause = `(${context.layer.fcPropName} < 7 OR ROUTE_TYPE IN ('CR','SR','US')) AND ${context.layer.fcPropName} IS NOT NULL`;
  1580. return clause;
  1581. }
  1582. return `${context.layer.fcPropName} IS NOT NULL`;
  1583. },
  1584. getFeatureRoadType(feature, layer) {
  1585. let fc = feature.attributes[layer.fcPropName];
  1586. const prefix = feature.attributes.ROUTE_TYPE;
  1587. const isUS = prefix === 'US';
  1588. const isState = prefix === 'SR';
  1589. const isCounty = prefix === 'CR';
  1590. if (isUS && fc > 3) { fc = 3; }
  1591. if (isState && fc > 4) { fc = 4; }
  1592. if (isCounty && fc > 6) { fc = 6; }
  1593. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1594. }
  1595. },
  1596. OK: {
  1597. baseUrl: 'https://services6.arcgis.com/RBtoEUQ2lmN0K3GY/arcgis/rest/services/Roadways/FeatureServer/',
  1598. defaultColors: {
  1599. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1600. },
  1601. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1602. fcMapLayers: [
  1603. {
  1604. layerID: 0,
  1605. fcPropName: 'FUNCTIONALCLASS',
  1606. idPropName: 'OBJECTID',
  1607. outFields: ['OBJECTID', 'FUNCTIONALCLASS', 'FHWAPRIMARYROUTE', 'ODOTROUTECLASS', 'ACCESSCONTROL'],
  1608. maxRecordCount: 1000,
  1609. supportsPagination: false,
  1610. roadTypeMap: {
  1611. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1612. }
  1613. }
  1614. ],
  1615. information: { Source: 'ODOT', Permission: 'Visible to R4+ or R3-AM' },
  1616. getWhereClause(context) {
  1617. if (context.mapContext.zoom < 16) {
  1618. return `${context.layer.fcPropName} < 7 OR ODOTROUTECLASS IN ('U','S','I')`;
  1619. }
  1620. return null;
  1621. },
  1622. getFeatureRoadType(feature, layer) {
  1623. let fc = feature.attributes[layer.fcPropName];
  1624. const route = (feature.attributes.FHWAPRIMARYROUTE || '').trim();
  1625. const isBusinessOrSpur = /BUS$|SPR$/i.test(route);
  1626. const prefix = isBusinessOrSpur ? route.substring(0, 1) : feature.attributes.ODOTROUTECLASS;
  1627. const isFw = parseInt(feature.attributes.ACCESSCONTROL, 10) === 1;
  1628. const isInterstate = prefix === 'I';
  1629. const isUS = prefix === 'U';
  1630. const isState = prefix === 'S';
  1631. if (isFw) fc = 1;
  1632. else if (fc > 3 && ((isUS && !isBusinessOrSpur) || (isInterstate && isBusinessOrSpur))) fc = 3;
  1633. else if (fc > 4 && ((isUS && isBusinessOrSpur) || (isState && !isBusinessOrSpur))) fc = 4;
  1634. else if (fc > 5 && isState && isBusinessOrSpur) fc = 5;
  1635. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1636. }
  1637. },
  1638. OR: {
  1639. baseUrl: 'https://gis.odot.state.or.us/arcgis/rest/services/transgis/data_catalog_display/Mapserver/',
  1640. defaultColors: {
  1641. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1642. },
  1643. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1644. fcMapLayers: [
  1645. {
  1646. layerID: 78,
  1647. fcPropName: 'NEW_FC_CD',
  1648. idPropName: 'OBJECTID',
  1649. outFields: ['OBJECTID', 'NEW_FC_CD'],
  1650. roadTypeMap: {
  1651. Fw: ['1'], Ew: ['2'], MH: ['3'], mH: ['4'], PS: ['5', '6'], St: ['7']
  1652. },
  1653. maxRecordCount: 1000,
  1654. supportsPagination: false
  1655. },
  1656. {
  1657. layerID: 80,
  1658. fcPropName: 'NEW_FC_CD',
  1659. idPropName: 'OBJECTID',
  1660. outFields: ['OBJECTID', 'NEW_FC_CD'],
  1661. roadTypeMap: {
  1662. Fw: ['1'], Ew: ['2'], MH: ['3'], mH: ['4'], PS: ['5', '6'], St: ['7']
  1663. },
  1664. maxRecordCount: 1000,
  1665. supportsPagination: false
  1666. }
  1667. ],
  1668. information: { Source: 'ODOT', Permission: 'Visible to R4+ or R3-AM', Description: 'Raw unmodified FC data.' },
  1669. getWhereClause(context) {
  1670. if (context.mapContext.zoom < 16) {
  1671. return `${context.layer.fcPropName} <> '7'`;
  1672. }
  1673. return null;
  1674. },
  1675. getFeatureRoadType(feature, layer) {
  1676. if (layer.getFeatureRoadType) {
  1677. return layer.getFeatureRoadType(feature);
  1678. }
  1679. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  1680. }
  1681. },
  1682. PA: {
  1683. baseUrl: 'https://gis.penndot.gov/arcgis/rest/services/opendata/roadwayadmin/MapServer/',
  1684. supportsPagination: false,
  1685. defaultColors: {
  1686. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1687. },
  1688. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1689. fcMapLayers: [
  1690. {
  1691. layerID: 0,
  1692. features: new Map(),
  1693. fcPropName: 'FUNC_CLS',
  1694. idPropName: 'MSLINK',
  1695. outFields: ['MSLINK', 'FUNC_CLS'],
  1696. maxRecordCount: 1000,
  1697. supportsPagination: false,
  1698. roadTypeMap: {
  1699. Fw: ['01', '11'], Ew: ['12'], MH: ['02', '14'], mH: ['06', '16'], PS: ['07', '08', '17'], St: ['09', '19']
  1700. }
  1701. }
  1702. ],
  1703. isPermitted() { return rank >= 4; },
  1704. information: { Source: 'PennDOT', Permission: 'Visible to R4+', Description: 'Raw unmodified FC data.' },
  1705. getWhereClause(context) {
  1706. return (context.mapContext.zoom < 16) ? `${context.layer.fcPropName} NOT IN ('09','19')` : null;
  1707. },
  1708. getFeatureRoadType(feature, layer) {
  1709. if (layer.getFeatureRoadType) {
  1710. return layer.getFeatureRoadType(feature);
  1711. }
  1712. const fc = feature.attributes[layer.fcPropName];
  1713. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1714. }
  1715. },
  1716. RI: {
  1717. baseUrl: 'https://services2.arcgis.com/S8zZg9pg23JUEexQ/arcgis/rest/services/RIDOT_Roads_2016/FeatureServer/',
  1718. defaultColors: {
  1719. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1720. },
  1721. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [[], [], [], [], [], [], [], [], [], [], []] },
  1722. fcMapLayers: [
  1723. {
  1724. layerID: 0,
  1725. fcPropName: 'F_SYSTEM',
  1726. idPropName: 'OBJECTID',
  1727. outFields: ['OBJECTID', 'F_SYSTEM', 'ROADTYPE', 'RTNO'],
  1728. roadTypeMap: {
  1729. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7, 0]
  1730. },
  1731. maxRecordCount: 1000,
  1732. supportsPagination: false
  1733. }
  1734. ],
  1735. isPermitted() { return rank >= 2; },
  1736. information: { Source: 'RIDOT', Permission: 'Visible to R2+' },
  1737. getWhereClause(context) {
  1738. return (context.mapContext.zoom < 16) ? `${context.layer.fcPropName} NOT IN (7,0)` : null;
  1739. },
  1740. getFeatureRoadType(feature, layer) {
  1741. let fc = parseInt(feature.attributes[layer.fcPropName], 10);
  1742. const type = feature.attributes.ROADTYPE;
  1743. const rtnum = feature.attributes.RTNO;
  1744. if (fc === 2 && ['10', '24', '37', '78', '99', '138', '403'].includes(rtnum)) fc = 1; // isFW
  1745. else if ((fc > 3 && type === 'US') || rtnum === '1') fc = 3; // isUS
  1746. else if (fc > 4 && rtnum.trim() !== '') fc = 4; // isState
  1747. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1748. }
  1749. },
  1750. SC: {
  1751. baseUrl: 'https://services1.arcgis.com/VaY7cY9pvUYUP1Lf/arcgis/rest/services/Functional_Class/FeatureServer/',
  1752. defaultColors: {
  1753. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1754. },
  1755. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1756. fcMapLayers: [
  1757. {
  1758. layerID: 0,
  1759. fcPropName: 'FC_GIS',
  1760. idPropName: 'FID',
  1761. outFields: ['FID', 'FC_GIS', 'ROUTE_LRS'],
  1762. maxRecordCount: 1000,
  1763. supportsPagination: false,
  1764. roadTypeMap: {
  1765. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1766. }
  1767. }
  1768. ],
  1769. isPermitted() { return rank >= 4; },
  1770. information: { Source: 'SCDOT', Permission: 'Visible to R4+' },
  1771. getWhereClause() {
  1772. return null;
  1773. },
  1774. getFeatureRoadType(feature, layer) {
  1775. const roadID = feature.attributes.ROUTE_LRS;
  1776. const roadType = parseInt(roadID.slice(3, 4), 10);
  1777. const isFw = roadType === 1;
  1778. const isUS = roadType === 2;
  1779. const isState = roadType === 4;
  1780. const isBiz = parseInt(roadID.slice(-2, -1), 10) === 7;
  1781. let fc = 7;
  1782. switch (feature.attributes[layer.fcPropName]) {
  1783. case 'INT': fc = 1; break;
  1784. case 'EXP': fc = 2; break;
  1785. case 'PRA': fc = 3; break;
  1786. case 'MIA': fc = 4; break;
  1787. case 'MAC':
  1788. case 'MIC': fc = 5; break;
  1789. default: throw new Error(`FC Layer: unexpected fc value: ${fc}`);
  1790. }
  1791. if (fc > 1 && isFw) fc = 1;
  1792. else if (fc > 3 && isUS) fc = isBiz ? 4 : 3;
  1793. else if (fc > 4 && isState) fc = (isBiz ? 5 : 4);
  1794. if (layer.getFeatureRoadType) {
  1795. return layer.getFeatureRoadType(feature);
  1796. }
  1797. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1798. }
  1799. },
  1800. SD: {
  1801. baseUrl: 'https://arcgis.sd.gov/arcgis/rest/services/DOT/LocalRoads/MapServer/',
  1802. defaultColors: {
  1803. Fw: '#ff00c5', Ew: '#149ece', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee', PSGr: '#cc6533', StGr: '#e99cb6'
  1804. },
  1805. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1806. fcMapLayers: [{
  1807. layerID: 1,
  1808. fcPropName: 'FUNC_CLASS',
  1809. idPropName: 'OBJECTID',
  1810. maxRecordCount: 1000,
  1811. supportsPagination: false,
  1812. outFields: ['OBJECTID', 'FUNC_CLASS', 'SURFACE_TYPE', 'ROADNAME'],
  1813. roadTypeMap: {
  1814. Fw: [1, 11], Ew: [2, 12], MH: [4, 14], mH: [6, 16], PS: [7, 8, 17], St: [9, 19]
  1815. }
  1816. }],
  1817. information: { Source: 'SDDOT', Permission: 'Visible to R4+ or R3-AM', Description: 'Additional colors denote unpaved PS and LS segements.' },
  1818. getWhereClause(context) {
  1819. if (context.mapContext.zoom < 16) {
  1820. return `${context.layer.fcPropName} NOT IN (9,19)`;
  1821. }
  1822. return null;
  1823. },
  1824. getFeatureRoadType(feature, layer) {
  1825. const attr = feature.attributes;
  1826. let fc = parseInt(attr[layer.fcPropName], 10) % 10;
  1827. const isFw = attr.ACCESS_CONTROL === 1;
  1828. const isUS = /^US HWY /i.test(attr.ROADNAME);
  1829. const isState = /^SD HWY /i.test(attr.ROADNAME);
  1830. const isBiz = /^(US|SD) HWY .* (E|W)?(B|L)$/i.test(attr.ROADNAME);
  1831. const isPaved = parseInt(attr.SURFACE_TYPE, 10) > 5;
  1832. if (isFw) fc = 1;
  1833. else if (fc > 4 && isUS) fc = (isBiz ? 6 : 4);
  1834. else if (fc > 6 && isState) fc = (isBiz ? 7 : 6);
  1835. if (fc > 6 && !isPaved) {
  1836. return fc < 9 ? 'PSGr' : 'StGr';
  1837. }
  1838. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer.fcPropName);
  1839. }
  1840. },
  1841. TN: {
  1842. baseUrl: 'https://',
  1843. defaultColors: {
  1844. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', PS2: '#cfae0e', St: '#eeeeee'
  1845. },
  1846. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1] },
  1847. fcMapLayers: [
  1848. {
  1849. layerPath: 'services2.arcgis.com/nf3p7v7Zy4fTOh6M/ArcGIS/rest/services/Road_Segment/FeatureServer/',
  1850. maxRecordCount: 1000,
  1851. supportsPagination: false,
  1852. layerID: 0,
  1853. fcPropName: 'FUNC_CLASS',
  1854. idPropName: 'OBJECTID',
  1855. outFields: ['OBJECTID', 'FUNC_CLASS', 'NBR_RTE', 'NBR_US_RTE'],
  1856. getWhereClause(context) {
  1857. if (context.mapContext.zoom < 16) {
  1858. return `${context.layer.fcPropName} NOT LIKE '%Local'`;
  1859. }
  1860. return null;
  1861. },
  1862. roadTypeMap: {
  1863. Fw: ['Urban Interstate', 'Rural Interstate'],
  1864. Ew: ['Urban Freeway or Expressway', 'Rural Freeway or Expressway'],
  1865. MH: ['Urban Other Principal Arterial', 'Rural Other Principal Arterial'],
  1866. mH: ['Urban Minor Arterial', 'Rural Minor Arterial'],
  1867. PS: ['Urban Major Collector', 'Rural Major Collector'],
  1868. PS2: ['Urban Minor Collector', 'Rural Minor Collector'],
  1869. St: ['Urban Local', 'Rural Local']
  1870. }
  1871. }
  1872. ],
  1873. information: {
  1874. Source: 'Memphis, Nashville Area MPO',
  1875. Permission: 'Visible to R4+ or R3-AM',
  1876. Description: 'Raw unmodified FC data for the Memphis and Nashville regions only.'
  1877. },
  1878. getWhereClause(context) {
  1879. if (context.layer.getWhereClause) {
  1880. return context.layer.getWhereClause(context);
  1881. }
  1882. return null;
  1883. },
  1884. getFeatureRoadType(feature, layer) {
  1885. if (layer.getFeatureRoadType) {
  1886. return layer.getFeatureRoadType(feature);
  1887. }
  1888. let fc = STATE_SETTINGS.global.getRoadTypeFromFC(feature.attributes.FUNC_CLASS, layer);
  1889. if ((fc === 'PS' || fc === 'mH') && feature.attributes.NBR_US_RTE != null) {
  1890. fc = feature.attributes.NBR_US_RTE.endsWith('BR') ? 'mH' : 'MH';
  1891. } else if (fc === 'PS' && (feature.attributes.NBR_RTE.startsWith('SR') || feature.attributes.NBR_RTE.startsWith('TN'))) {
  1892. fc = 'mH';
  1893. }
  1894. return fc;
  1895. }
  1896. },
  1897. TX: {
  1898. baseUrl: 'https://services.arcgis.com/KTcxiTD9dsQw4r7Z/ArcGIS/rest/services/TxDOT_Functional_Classification/FeatureServer/',
  1899. defaultColors: {
  1900. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1901. },
  1902. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1] },
  1903. fcMapLayers: [
  1904. {
  1905. layerID: 0,
  1906. fcPropName: 'F_SYSTEM',
  1907. idPropName: 'OBJECTID',
  1908. outFields: ['OBJECTID', 'F_SYSTEM', 'RTE_PRFX'],
  1909. maxRecordCount: 1000,
  1910. supportsPagination: false,
  1911. roadTypeMap: {
  1912. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1913. }
  1914. }
  1915. ],
  1916. isPermitted() { return rank >= 2; },
  1917. information: { Source: 'TxDOT', Permission: 'Visible to R2+' },
  1918. getWhereClause(context) {
  1919. let where = ' F_SYSTEM IS NOT NULL AND RTE_PRFX IS NOT NULL';
  1920. if (context.mapContext.zoom < 16) {
  1921. where += ` AND ${context.layer.fcPropName} <> 7`;
  1922. }
  1923. return where;
  1924. },
  1925. getFeatureRoadType(feature, layer) {
  1926. // On-System:
  1927. // IH=Interstate BF=Business FM
  1928. // US=US Highway FM=Farm to Mkt
  1929. // UA=US Alt. RM=Ranch to Mkt
  1930. // UP=US Spur RR=Ranch Road
  1931. // SH=State Highway PR=Park Road
  1932. // SA=State Alt. RE=Rec Road
  1933. // SL=State Loop RP=Rec Rd Spur
  1934. // SS=State Spur FS=FM Spur
  1935. // BI=Business IH RS=RM Spur
  1936. // BU=Business US RU=RR Spur
  1937. // BS=Business State PA=Principal Arterial
  1938. // Off-System:
  1939. // TL=Off-System Tollroad CR=County Road
  1940. // FC=Func. Classified St. LS=Local Street
  1941. if (layer.getFeatureRoadType) {
  1942. return layer.getFeatureRoadType(feature);
  1943. }
  1944. let fc = feature.attributes[layer.fcPropName];
  1945. const type = feature.attributes.RTE_PRFX.substring(0, 2).toUpperCase();
  1946. if (type === 'IH' && fc > 1) {
  1947. fc = 1;
  1948. } else if ((type === 'US' || type === 'BI' || type === 'UA') && fc > 3) {
  1949. fc = 3;
  1950. } else if ((type === 'UP' || type === 'BU' || type === 'SH' || type === 'SA') && fc > 4) {
  1951. fc = 4;
  1952. } else if ((type === 'SL' || type === 'SS' || type === 'BS') && fc > 6) {
  1953. fc = 6;
  1954. }
  1955. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  1956. }
  1957. },
  1958. UT: {
  1959. baseUrl: 'https://roads.udot.utah.gov/server/rest/services/Public/Functional_Class/MapServer/0',
  1960. defaultColors: {
  1961. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  1962. },
  1963. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  1964. fcMapLayers: [
  1965. {
  1966. layerID: 0,
  1967. fcPropName: 'FUNCTIONAL_CLASS',
  1968. idPropName: 'OBJECTID',
  1969. outFields: ['OBJECTID', 'FUNCTIONAL_CLASS', 'route_id'],
  1970. roadTypeMap: {
  1971. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  1972. },
  1973. maxRecordCount: 1000,
  1974. supportsPagination: false
  1975. }
  1976. ],
  1977. information: { Source: 'UDOT', Permission: 'Visible to R4+ or R3-AM' },
  1978. getWhereClause(context) {
  1979. return `${context.layer.fcPropName} NOT LIKE 'Proposed%'`;
  1980. },
  1981. getFeatureRoadType(feature, layer) {
  1982. const attr = feature.attributes;
  1983. let fc = attr[layer.fcPropName];
  1984. const routeID = attr.route_id;
  1985. const roadNum = parseInt(routeID.substring(0, 4), 10);
  1986. switch (fc) {
  1987. case 'Interstate': fc = 1; break;
  1988. case 'Other Freeways and Expressways': fc = 2; break;
  1989. case 'Other Principal Arterial': fc = 3; break;
  1990. case 'Minor Arterial': fc = 4; break;
  1991. case 'Major Collector': fc = 5; break;
  1992. case 'Minor Collector': fc = 6; break;
  1993. default: fc = 7;
  1994. }
  1995. const re = /^(6|40|50|89|91|163|189|191|491)$/;
  1996. if (re.test(roadNum) && fc > 3) {
  1997. // US highway
  1998. fc = 3;
  1999. } else if (roadNum <= 491 && fc > 4) {
  2000. // State highway
  2001. fc = 4;
  2002. }
  2003. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  2004. }
  2005. },
  2006. VT: {
  2007. baseUrl: 'https://maps.vtrans.vermont.gov/arcgis/rest/services/Master/General/FeatureServer/',
  2008. defaultColors: {
  2009. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  2010. },
  2011. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  2012. fcMapLayers: [
  2013. {
  2014. layerID: 39,
  2015. fcPropName: 'FUNCL',
  2016. idPropName: 'OBJECTID',
  2017. outFields: ['OBJECTID', 'FUNCL', 'HWYSIGN'],
  2018. roadTypeMap: {
  2019. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2020. },
  2021. maxRecordCount: 1000,
  2022. supportsPagination: false
  2023. }
  2024. ],
  2025. information: { Source: 'VTrans', Permission: 'Visible to R2+' },
  2026. isPermitted() { return rank >= 2; },
  2027. getWhereClause(context) {
  2028. if (context.mapContext.zoom < 16) {
  2029. return `${context.layer.fcPropName}<>7 AND ${context.layer.fcPropName}<>0`;
  2030. }
  2031. return null;
  2032. },
  2033. getFeatureRoadType(feature, layer) {
  2034. const roadID = feature.attributes.HWYSIGN;
  2035. let fc = feature.attributes[layer.fcPropName];
  2036. if (!(fc > 0)) { fc = 7; }
  2037. const isUS = /^U/.test(roadID);
  2038. const isState = /^V/.test(roadID);
  2039. const isUSBiz = /^B/.test(roadID);
  2040. if (fc > 3 && isUS) fc = 3;
  2041. else if (fc > 4 && (isUSBiz || isState)) fc = 4;
  2042. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  2043. }
  2044. },
  2045. VA: {
  2046. baseUrl: 'https://services.arcgis.com/p5v98VHDX9Atv3l7/arcgis/rest/services/FC_2014_FHWA_Submittal1/FeatureServer/',
  2047. defaultColors: {
  2048. Fw: '#ff00c5', Ew: '#ff00c5', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  2049. },
  2050. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  2051. fcMapLayers: [
  2052. {
  2053. layerID: 0,
  2054. fcPropName: 'STATE_FUNCT_CLASS_ID',
  2055. idPropName: 'OBJECTID',
  2056. outFields: ['OBJECTID', 'STATE_FUNCT_CLASS_ID', 'RTE_NM'],
  2057. maxRecordCount: 2000,
  2058. supportsPagination: true,
  2059. roadTypeMap: {
  2060. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2061. }
  2062. }, {
  2063. layerID: 1,
  2064. fcPropName: 'STATE_FUNCT_CLASS_ID',
  2065. idPropName: 'OBJECTID',
  2066. outFields: ['OBJECTID', 'STATE_FUNCT_CLASS_ID', 'RTE_NM', 'ROUTE_NO'],
  2067. maxRecordCount: 2000,
  2068. supportsPagination: true,
  2069. roadTypeMap: {
  2070. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2071. }
  2072. }, {
  2073. layerID: 3,
  2074. fcPropName: 'TMPD_FC',
  2075. idPropName: 'OBJECTID',
  2076. outFields: ['OBJECTID', 'TMPD_FC', 'RTE_NM'],
  2077. maxRecordCount: 2000,
  2078. supportsPagination: true,
  2079. roadTypeMap: {
  2080. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2081. }
  2082. }
  2083. ],
  2084. information: { Source: 'VDOT', Permission: 'Visible to R4+ or R3-AM' },
  2085. srExceptions: [217, 302, 303, 305, 308, 310, 313, 314, 315, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328,
  2086. 329, 330, 331, 332, 333, 334, 335, 336, 339, 341, 342, 343, 344, 345, 346, 347, 348, 350, 353, 355, 357, 358, 361,
  2087. 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 382, 383, 384, 385, 386,
  2088. 387, 388, 389, 390, 391, 392, 393, 394, 396, 397, 398, 399, 785, 895],
  2089. getWhereClause(context) {
  2090. if (context.mapContext.zoom < 16) {
  2091. return `${context.layer.fcPropName}<>7`;
  2092. }
  2093. // NOTE: As of 9/14/2016 there does not appear to be any US/SR/VA labeled routes with FC = 7.
  2094. return null;
  2095. },
  2096. getFeatureRoadType(feature, layer) {
  2097. if (layer.getFeatureRoadType) {
  2098. return layer.getFeatureRoadType(feature);
  2099. }
  2100. let fc = parseInt(feature.attributes[layer.fcPropName], 10);
  2101. const rtName = feature.attributes.RTE_NM;
  2102. const match = /^R-VA\s*(US|VA|SR)(\d{5})..(BUS)?/.exec(rtName);
  2103. const isBusiness = (match && (match !== null) && (match[3] === 'BUS'));
  2104. const isState = (match && (match !== null) && (match[1] === 'VA' || match[1] === 'SR'));
  2105. let rtNumText;
  2106. if (layer.layerID === 1) {
  2107. rtNumText = feature.attributes.ROUTE_NO;
  2108. } else if (match) {
  2109. // eslint-disable-next-line prefer-destructuring
  2110. rtNumText = match[2];
  2111. } else {
  2112. rtNumText = '99999';
  2113. }
  2114. const rtNum = parseInt(rtNumText, 10);
  2115. const rtPrefix = match && match[1];
  2116. if (fc > 3 && rtPrefix === 'US') {
  2117. fc = isBusiness ? 4 : 3;
  2118. } else if (isState && fc > 4 && this.srExceptions.indexOf(rtNum) === -1 && rtNum < 600) {
  2119. fc = isBusiness ? 5 : 4;
  2120. }
  2121. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  2122. }
  2123. },
  2124. WA: {
  2125. baseUrl: 'https://data.wsdot.wa.gov/arcgis/rest/services/FunctionalClass/WSDOTFunctionalClassMap/MapServer/',
  2126. defaultColors: {
  2127. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  2128. },
  2129. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  2130. fcMapLayers: [
  2131. {
  2132. layerID: 2,
  2133. fcPropName: 'FederalFunctionalClassCode',
  2134. idPropName: 'OBJECTID',
  2135. outFields: ['OBJECTID', 'FederalFunctionalClassCode'],
  2136. roadTypeMap: {
  2137. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2138. },
  2139. maxRecordCount: 1000,
  2140. supportsPagination: false
  2141. }, {
  2142. layerID: 1,
  2143. fcPropName: 'FederalFunctionalClassCode',
  2144. idPropName: 'OBJECTID',
  2145. outFields: ['OBJECTID', 'FederalFunctionalClassCode'],
  2146. roadTypeMap: {
  2147. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2148. },
  2149. maxRecordCount: 1000,
  2150. supportsPagination: false
  2151. }, {
  2152. layerID: 4,
  2153. fcPropName: 'FederalFunctionalClassCode',
  2154. idPropName: 'OBJECTID',
  2155. outFields: ['OBJECTID', 'FederalFunctionalClassCode'],
  2156. roadTypeMap: {
  2157. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2158. },
  2159. maxRecordCount: 1000,
  2160. supportsPagination: false
  2161. }
  2162. ],
  2163. information: { Source: 'WSDOT', Permission: 'Visible to R4+ or R3-AM', Description: 'Raw unmodified FC data.' },
  2164. getWhereClause(context) {
  2165. if (context.mapContext.zoom < 16) {
  2166. return `${context.layer.fcPropName} <> 7`;
  2167. }
  2168. return null;
  2169. },
  2170. getFeatureRoadType(feature, layer) {
  2171. if (layer.getFeatureRoadType) {
  2172. return layer.getFeatureRoadType(feature);
  2173. }
  2174. return STATE_SETTINGS.global.getFeatureRoadType(feature, layer);
  2175. }
  2176. },
  2177. WV: {
  2178. baseUrl: 'https://gis.transportation.wv.gov/arcgis/rest/services/Routes/MapServer/',
  2179. defaultColors: {
  2180. Fw: '#ff00c5', Ew: '#ff00c5', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  2181. },
  2182. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  2183. fcMapLayers: [
  2184. {
  2185. layerID: 2,
  2186. fcPropName: 'NAT_FUNCTIONAL_CLASS',
  2187. idPropName: 'OBJECTID',
  2188. outFields: ['OBJECTID', 'NAT_FUNCTIONAL_CLASS', 'ROUTE_ID'],
  2189. maxRecordCount: 1000,
  2190. supportsPagination: true,
  2191. roadTypeMap: {
  2192. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2193. }
  2194. }
  2195. ],
  2196. information: { Source: 'WV DOT' },
  2197. isPermitted() { return true; },
  2198. getWhereClause(context) {
  2199. if (context.mapContext.zoom < 16) {
  2200. return `${context.layer.fcPropName} NOT IN (9,19)`;
  2201. }
  2202. return null;
  2203. },
  2204. getFeatureRoadType(feature, layer) {
  2205. if (layer.getFeatureRoadType) {
  2206. return layer.getFeatureRoadType(feature);
  2207. }
  2208. const fcCode = feature.attributes[layer.fcPropName];
  2209. let fc = fcCode;
  2210. if (fcCode === 11) fc = 1;
  2211. else if (fcCode === 4 || fcCode === 12) fc = 2;
  2212. else if (fcCode === 2 || fcCode === 14) fc = 3;
  2213. else if (fcCode === 6 || fcCode === 16) fc = 4;
  2214. else if (fcCode === 7 || fcCode === 17 || fcCode === 8 || fcCode === 18) fc = 5;
  2215. else fc = 7;
  2216. const id = feature.attributes.ROUTE_ID;
  2217. const prefix = id.substr(2, 1);
  2218. const isInterstate = prefix === '1';
  2219. const isUS = prefix === '2';
  2220. const isState = prefix === '3';
  2221. if (fc > 1 && isInterstate) fc = 1;
  2222. else if (fc > 3 && isUS) fc = 3;
  2223. else if (fc > 4 && isState) fc = 4;
  2224. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  2225. }
  2226. },
  2227. WY: {
  2228. baseUrl: 'https://gisservices.wyoroad.info/arcgis/rest/services/ITSM/LAYERS/MapServer/',
  2229. defaultColors: {
  2230. Fw: '#ff00c5', Ew: '#4f33df', MH: '#149ece', mH: '#4ce600', PS: '#cfae0e', St: '#eeeeee'
  2231. },
  2232. zoomSettings: { maxOffset: [30, 15, 8, 4, 2, 1, 1, 1, 1, 1], excludeRoadTypes: [['St'], ['St'], ['St'], ['St'], [], [], [], [], [], [], []] },
  2233. fcMapLayers: [
  2234. {
  2235. layerID: 20,
  2236. fcPropName: 'CLASSIFICATION',
  2237. idPropName: 'OBJECTID',
  2238. outFields: ['OBJECTID', 'CLASSIFICATION', 'COMMON_ROUTE_NAME'],
  2239. roadTypeMap: {
  2240. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2241. },
  2242. maxRecordCount: 1000,
  2243. supportsPagination: false
  2244. }, {
  2245. layerID: 21,
  2246. fcPropName: 'CLASSIFICATION',
  2247. idPropName: 'OBJECTID',
  2248. outFields: ['OBJECTID', 'CLASSIFICATION', 'COMMON_ROUTE_NAME'],
  2249. roadTypeMap: {
  2250. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2251. },
  2252. maxRecordCount: 1000,
  2253. supportsPagination: false
  2254. }, {
  2255. layerID: 22,
  2256. fcPropName: 'CLASSIFICATION',
  2257. idPropName: 'OBJECTID',
  2258. outFields: ['OBJECTID', 'CLASSIFICATION', 'COMMON_ROUTE_NAME'],
  2259. roadTypeMap: {
  2260. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2261. },
  2262. maxRecordCount: 1000,
  2263. supportsPagination: false
  2264. }, {
  2265. layerID: 23,
  2266. fcPropName: 'CLASSIFICATION',
  2267. idPropName: 'OBJECTID',
  2268. outFields: ['OBJECTID', 'CLASSIFICATION', 'COMMON_ROUTE_NAME'],
  2269. roadTypeMap: {
  2270. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2271. },
  2272. maxRecordCount: 1000,
  2273. supportsPagination: false
  2274. }, {
  2275. layerID: 24,
  2276. fcPropName: 'CLASSIFICATION',
  2277. idPropName: 'OBJECTID',
  2278. outFields: ['OBJECTID', 'CLASSIFICATION', 'COMMON_ROUTE_NAME'],
  2279. roadTypeMap: {
  2280. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2281. },
  2282. maxRecordCount: 1000,
  2283. supportsPagination: false
  2284. }, {
  2285. layerID: 25,
  2286. fcPropName: 'CLASSIFICATION',
  2287. idPropName: 'OBJECTID',
  2288. outFields: ['OBJECTID', 'CLASSIFICATION', 'COMMON_ROUTE_NAME'],
  2289. roadTypeMap: {
  2290. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2291. },
  2292. maxRecordCount: 1000,
  2293. supportsPagination: false
  2294. }, {
  2295. layerID: 26,
  2296. fcPropName: 'CLASSIFICATION',
  2297. idPropName: 'OBJECTID',
  2298. outFields: ['OBJECTID', 'CLASSIFICATION', 'COMMON_ROUTE_NAME'],
  2299. roadTypeMap: {
  2300. Fw: [1], Ew: [2], MH: [3], mH: [4], PS: [5, 6], St: [7]
  2301. },
  2302. maxRecordCount: 1000,
  2303. supportsPagination: false
  2304. }
  2305. ],
  2306. information: { Source: 'WYDOT', Permission: 'Visible to R4+ or R3-AM', Description: 'Minimum suggested FC.' },
  2307. getWhereClause(context) {
  2308. if (context.mapContext.zoom < 16) {
  2309. return `${context.layer.fcPropName} <> 'Local'`;
  2310. }
  2311. return null;
  2312. },
  2313. getFeatureRoadType(feature, layer) {
  2314. const attr = feature.attributes;
  2315. let fc = attr[layer.fcPropName];
  2316. const route = attr.COMMON_ROUTE_NAME;
  2317. switch (fc) {
  2318. case 'Interstate': fc = 1; break;
  2319. case 'Expressway': fc = 2; break;
  2320. case 'Principal Arterial': fc = 3; break;
  2321. case 'Minor Arterial': fc = 4; break;
  2322. case 'Major Collector': fc = 5; break;
  2323. case 'Minor Collector': fc = 6; break;
  2324. default: fc = 7;
  2325. }
  2326. const isIntBiz = /I (25|80) BUS/.test(route);
  2327. const isUS = /US \d+/.test(route);
  2328. const isUSBiz = /US \d+ BUS/.test(route);
  2329. const isState = /WY \d+/.test(route);
  2330. const isStateBiz = /WY \d+ BUS/.test(route);
  2331. if (fc > 3 && (isUS || isIntBiz)) fc = isUSBiz ? 4 : 3;
  2332. else if (fc > 4 && isState) fc = isStateBiz ? 5 : 4;
  2333. return STATE_SETTINGS.global.getRoadTypeFromFC(fc, layer);
  2334. }
  2335. }
  2336. };
  2337.  
  2338. function log(message) {
  2339. console.log('FC Layer: ', message);
  2340. }
  2341. function debugLog(message) {
  2342. console.debug('FC Layer: ', message);
  2343. }
  2344. function errorLog(message) {
  2345. console.error('FC Layer: ', message);
  2346. }
  2347.  
  2348. function loadSettingsFromStorage() {
  2349. const storedSettings = $.parseJSON(localStorage.getItem(settingsStoreName)) || {};
  2350. const defaultSettings = {
  2351. layerVisible: true,
  2352. activeStateAbbr: 'ALL',
  2353. hideStreet: false
  2354. };
  2355. settings = { ...defaultSettings, ...storedSettings };
  2356. }
  2357.  
  2358. function saveSettingsToStorage() {
  2359. if (localStorage) {
  2360. // In case the layer is turned off some other way...
  2361. settings.layerVisible = sdk.Map.isLayerVisible({ layerName });
  2362. localStorage.setItem(settingsStoreName, JSON.stringify(settings));
  2363. }
  2364. }
  2365.  
  2366. function sortArray(array) {
  2367. array.sort((a, b) => { if (a < b) return -1; if (a > b) return 1; return 0; });
  2368. }
  2369.  
  2370. function getVisibleStateAbbreviations() {
  2371. const { activeStateAbbr } = settings;
  2372. return sdk.DataModel.States.getAll()
  2373. .map(state => STATES_HASH[state.name])
  2374. .filter(stateAbbr => STATE_SETTINGS[stateAbbr]
  2375. && STATE_SETTINGS.global.isPermitted(stateAbbr)
  2376. && (!activeStateAbbr || activeStateAbbr === 'ALL' || activeStateAbbr === stateAbbr));
  2377. }
  2378.  
  2379. function getAsync(url, context) {
  2380. return new Promise((resolve, reject) => {
  2381. GM_xmlhttpRequest({
  2382. context,
  2383. method: 'GET',
  2384. url,
  2385. onload(res) {
  2386. if (res.status.toString() === '200') {
  2387. resolve({ responseText: res.responseText, context });
  2388. } else {
  2389. reject(new Error({ responseText: res.responseText, context }));
  2390. }
  2391. },
  2392. onerror() {
  2393. reject(Error('Network Error'));
  2394. }
  2395. });
  2396. });
  2397. }
  2398.  
  2399. function getUrl(context, queryType, queryParams) {
  2400. const { extent } = context.mapContext;
  2401. const { zoom } = context.mapContext;
  2402. const { layer } = context;
  2403. const { state } = context;
  2404.  
  2405. const whereParts = [];
  2406. const mercatorExtentLeftBottom = turf.toMercator([extent[0], extent[1]]);
  2407. const mercatorExtentRightTop = turf.toMercator([extent[2], extent[3]]);
  2408. const geometry = {
  2409. xmin: mercatorExtentLeftBottom[0],
  2410. ymin: mercatorExtentLeftBottom[1],
  2411. xmax: mercatorExtentRightTop[0],
  2412. ymax: mercatorExtentRightTop[1],
  2413. spatialReference: {
  2414. wkid: 102100,
  2415. latestWkid: 3857
  2416. }
  2417. };
  2418. const geometryStr = JSON.stringify(geometry);
  2419. const stateWhereClause = state.getWhereClause(context);
  2420. const layerPath = layer.layerPath || '';
  2421. let url = `${state.baseUrl + layerPath + layer.layerID}/query?geometry=${encodeURIComponent(geometryStr)}`;
  2422.  
  2423. if (queryType === 'countOnly') {
  2424. url += '&returnCountOnly=true';
  2425. } else if (queryType === 'idsOnly') {
  2426. url += '&returnIdsOnly=true';
  2427. } else if (queryType === 'paged') {
  2428. // TODO
  2429. } else {
  2430. url += `&returnGeometry=true&maxAllowableOffset=${state.zoomSettings.maxOffset[zoom - 12]}`;
  2431. url += `&outFields=${encodeURIComponent(layer.outFields.join(','))}`;
  2432. if (queryType === 'idRange') {
  2433. whereParts.push(`(${queryParams.idFieldName}>=${queryParams.range[0]} AND ${queryParams.idFieldName}<=${queryParams.range[1]})`);
  2434. }
  2435. }
  2436. if (stateWhereClause) whereParts.push(stateWhereClause);
  2437. if (whereParts.length > 0) url += `&where=${encodeURIComponent(whereParts.join(' AND '))}`;
  2438. url += '&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&inSR=102100&outSR=3857&f=json';
  2439. console.log(url);
  2440. return url;
  2441. }
  2442.  
  2443. function convertFcToRoadTypeLineStrings(feature, context) {
  2444. const { state, stateAbbr, layer } = context;
  2445. const roadType = state.getFeatureRoadType(feature, layer);
  2446. // debugLog(feature);
  2447. const attr = {
  2448. state: stateAbbr,
  2449. layerID: layer.layerID,
  2450. roadType,
  2451. color: state.defaultColors[roadType]
  2452. };
  2453.  
  2454. const lineStrings = feature.geometry.paths.map(path => {
  2455. const line = turf.toWgs84(turf.lineString(path, attr));
  2456. line.id = 0;
  2457. return line;
  2458. });
  2459.  
  2460. return lineStrings;
  2461. }
  2462.  
  2463. function fetchLayerFC(context) {
  2464. const url = getUrl(context, 'idsOnly');
  2465. debugLog(url);
  2466. if (!context.parentContext.cancel) {
  2467. return getAsync(url, context).bind(context).then(res => {
  2468. const ids = $.parseJSON(res.responseText);
  2469. if (!ids.objectIds) ids.objectIds = [];
  2470. sortArray(ids.objectIds);
  2471. // debugLog(ids);
  2472. return ids;
  2473. }).then(res => {
  2474. const idRanges = [];
  2475. if (res.objectIds) {
  2476. const len = res.objectIds ? res.objectIds.length : 0;
  2477. let currentIndex = 0;
  2478. const offset = Math.min(context.layer.maxRecordCount, 1000);
  2479. while (currentIndex < len) {
  2480. let nextIndex = currentIndex + offset;
  2481. if (nextIndex >= len) nextIndex = len - 1;
  2482. idRanges.push({ range: [res.objectIds[currentIndex], res.objectIds[nextIndex]], idFieldName: res.objectIdFieldName });
  2483. currentIndex = nextIndex + 1;
  2484. }
  2485. // debugLog(context.layer.layerID);
  2486. // debugLog(idRanges);
  2487. }
  2488. return idRanges;
  2489. }).map(idRange => {
  2490. if (!context.parentContext.cancel) {
  2491. const newUrl = getUrl(context, 'idRange', idRange);
  2492. debugLog(url);
  2493. return getAsync(newUrl, context).then(res => {
  2494. if (!context.parentContext.cancel) {
  2495. let { features } = $.parseJSON(res.responseText);
  2496. context.parentContext.callCount++;
  2497. // debugLog('Feature Count=' + (features ? features.length : 0));
  2498. features = features || [];
  2499. return features.map(feature => convertFcToRoadTypeLineStrings(feature, context))
  2500. .filter(feature => !(feature[0].properties.roadType === 'St' && settings.hideStreet));
  2501. }
  2502. return null;
  2503. });
  2504. }
  2505. // debugLog('Async call cancelled');
  2506. return null;
  2507. });
  2508. }
  2509. return null;
  2510. }
  2511.  
  2512. function fetchStateFC(context) {
  2513. const state = STATE_SETTINGS[context.stateAbbr];
  2514. const contexts = state.fcMapLayers.map(layer => ({
  2515. parentContext: context.parentContext, layer, state, stateAbbr: context.stateAbbr, mapContext: context.mapContext
  2516. }));
  2517.  
  2518. return Promise.map(contexts, ctx => fetchLayerFC(ctx));
  2519. }
  2520.  
  2521. let _lastPromise = null;
  2522. let _lastContext = null;
  2523. let _fcCallCount = 0;
  2524. function fetchAllFC() {
  2525. if (!sdk.Map.isLayerVisible({ layerName })) return;
  2526.  
  2527. if (_lastPromise) { _lastPromise.cancel(); }
  2528. $('#fc-loading-indicator').text('Loading FC...');
  2529.  
  2530. const mapContext = { zoom: sdk.Map.getZoomLevel(), extent: sdk.Map.getMapExtent() };
  2531. if (mapContext.zoom > MIN_ZOOM_LEVEL) {
  2532. const parentContext = { callCount: 0, startTime: Date.now() };
  2533.  
  2534. if (_lastContext) _lastContext.cancel = true;
  2535. _lastContext = parentContext;
  2536. const contexts = getVisibleStateAbbreviations().map(stateAbbr => ({ parentContext, stateAbbr, mapContext }));
  2537. const map = Promise.map(contexts, ctx => fetchStateFC(ctx)).then(statesLineStringArrays => {
  2538. if (!parentContext.cancel) {
  2539. sdk.Map.removeAllFeaturesFromLayer({ layerName });
  2540. // TODO: Handle all the arrays better...
  2541. statesLineStringArrays.forEach(stateLineStringsArray => {
  2542. stateLineStringsArray.forEach(lineStringsArray1 => {
  2543. lineStringsArray1.forEach(lineStringsArray2 => {
  2544. lineStringsArray2.forEach(lineStringsArray3 => {
  2545. lineStringsArray3.forEach(feature => {
  2546. sdk.Map.addFeatureToLayer({ layerName, feature });
  2547. });
  2548. });
  2549. });
  2550. });
  2551. });
  2552. }
  2553. return statesLineStringArrays;
  2554. }).catch(e => {
  2555. $('#fc-loading-indicator').text('FC Error! (check console for details)');
  2556. errorLog(e);
  2557. }).finally(() => {
  2558. _fcCallCount -= 1;
  2559. if (_fcCallCount === 0) {
  2560. $('#fc-loading-indicator').text('');
  2561. }
  2562. });
  2563.  
  2564. _fcCallCount += 1;
  2565. _lastPromise = map;
  2566. } else {
  2567. // if zoomed out too far, clear the layer
  2568. sdk.Map.removeAllFeaturesFromLayer({ layerName });
  2569. }
  2570. }
  2571.  
  2572. function onLayerCheckboxChanged(args) {
  2573. setEnabled(args.checked);
  2574. }
  2575.  
  2576. function checkLayerZIndex() {
  2577. try {
  2578. if (sdk.Map.getLayerZIndex({ layerName }) !== MAP_LAYER_Z_INDEX) {
  2579. // ("ADJUSTED FC LAYER Z-INDEX " + mapLayerZIndex + ', ' + mapLayer.getZIndex());
  2580. sdk.Map.setLayerZIndex({ layerName, zIndex: MAP_LAYER_Z_INDEX });
  2581. }
  2582. } catch {
  2583. // ignore this hack if it crashes
  2584. }
  2585. }
  2586.  
  2587. function initLayer() {
  2588. const styleRules = [
  2589. {
  2590. style: {
  2591. strokeColor: 'black',
  2592. strokeDashstyle: 'solid',
  2593. strokeOpacity: 1.0,
  2594. strokeWidth: '15'
  2595. }
  2596. }
  2597. ];
  2598. for (let zoom = 12; zoom < 22; zoom++) {
  2599. styleRules.push({
  2600. // eslint-disable-next-line no-loop-func
  2601. predicate: () => sdk.Map.getZoomLevel() === zoom,
  2602. style: {
  2603. strokeWidth: 12 * (1.15 ** (zoom - 13))
  2604. }
  2605. });
  2606. }
  2607. Object.values(STATE_SETTINGS)
  2608. .filter(state => !!state.defaultColors)
  2609. .forEach(state => Object.values(state.defaultColors)
  2610. .forEach(color => {
  2611. if (!styleRules.some(rule => rule.style.strokeColor === color)) {
  2612. styleRules.push({
  2613. predicate: props => props.color === color,
  2614. style: { strokeColor: color }
  2615. });
  2616. }
  2617. }));
  2618.  
  2619. STATE_SETTINGS.global.roadTypes.forEach((roadType, index) => {
  2620. styleRules.push({
  2621. predicate: props => props.roadType === roadType,
  2622. style: { graphicZIndex: index * 100 }
  2623. });
  2624. });
  2625. sdk.Map.addLayer({
  2626. layerName,
  2627. styleRules,
  2628. zIndexing: true
  2629. });
  2630.  
  2631. sdk.Map.setLayerOpacity({ layerName, opacity: 0.5 });
  2632. sdk.Map.setLayerVisibility({ layerName, visibility: settings.layerVisible });
  2633. MAP_LAYER_Z_INDEX = sdk.Map.getLayerZIndex({ layerName: 'roads' }) - 3;
  2634. sdk.Map.setLayerZIndex({ layerName, zIndex: MAP_LAYER_Z_INDEX });
  2635.  
  2636. window.addEventListener('beforeunload', () => saveSettingsToStorage);
  2637.  
  2638. sdk.LayerSwitcher.addLayerCheckbox({ name: 'FC Layer' });
  2639. sdk.LayerSwitcher.setLayerCheckboxChecked({ name: 'FC Layer', isChecked: settings.layerVisible });
  2640. sdk.Events.on({ eventName: 'wme-layer-checkbox-toggled', eventHandler: onLayerCheckboxChanged });
  2641.  
  2642. // Hack to fix layer zIndex. Some other code is changing it sometimes but I have not been able to figure out why.
  2643. // It may be that the FC layer is added to the map before some Waze code loads the base layers and forces other layers higher. (?)
  2644. setInterval(checkLayerZIndex, 1000);
  2645.  
  2646. sdk.Events.on({ eventName: 'wme-map-move-end', eventHandler: fetchAllFC });
  2647. }
  2648.  
  2649. function onHideStreetsClicked() {
  2650. settings.hideStreet = $(this).is(':checked');
  2651. saveSettingsToStorage();
  2652. sdk.Map.removeAllFeaturesFromLayer({ layerName });
  2653. fetchAllFC();
  2654. }
  2655.  
  2656. function onStateSelectionChanged() {
  2657. settings.activeStateAbbr = this.value;
  2658. saveSettingsToStorage();
  2659. loadStateFCInfo();
  2660. fetchAllFC();
  2661. }
  2662.  
  2663. function setEnabled(value) {
  2664. sdk.Map.setLayerVisibility({ layerName, visibility: value });
  2665. settings.layerVisible = value;
  2666. saveSettingsToStorage();
  2667.  
  2668. const color = value ? '#00bd00' : '#ccc';
  2669. $('span#fc-layer-power-btn').css({ color });
  2670. if (value) fetchAllFC();
  2671. sdk.LayerSwitcher.setLayerCheckboxChecked({ name: 'FC Layer', isChecked: value });
  2672. }
  2673.  
  2674. async function initUserPanel() {
  2675. const $panel = $('<div>');
  2676. const $stateSelect = $('<select>', { id: 'fcl-state-select', class: 'form-control disabled', style: 'disabled' }).append($('<option>', { value: 'ALL' }).text('All'));
  2677.  
  2678. Object.keys(STATE_SETTINGS).forEach(stateAbbr => {
  2679. if (stateAbbr !== 'global') {
  2680. $stateSelect.append($('<option>', { value: stateAbbr }).text(reverseStatesHash(stateAbbr)));
  2681. }
  2682. });
  2683.  
  2684. const $hideStreet = $('<div>', { id: 'fcl-hide-street-container', class: 'controls-container' })
  2685. .append($('<input>', { type: 'checkbox', name: 'fcl-hide-street', id: 'fcl-hide-street' }).prop('checked', settings.hideStreet).click(onHideStreetsClicked))
  2686. .append($('<label>', { for: 'fcl-hide-street' }).text('Hide local street highlights'));
  2687.  
  2688. $stateSelect.val(settings.activeStateAbbr ? settings.activeStateAbbr : 'ALL');
  2689.  
  2690. $panel.append(
  2691. $('<div>', { class: 'form-group' }).append(
  2692. $('<label>', { class: 'control-label' }).text('Select a state')
  2693. ).append(
  2694. $('<div>', { class: 'controls', id: 'fcl-state-select-container' }).append(
  2695. $('<div>').append($stateSelect)
  2696. )
  2697. ),
  2698. $hideStreet,
  2699. $('<div>', { id: 'fcl-table-container' })
  2700. );
  2701.  
  2702. $panel.append($('<div>', { id: 'fcl-state-info' }));
  2703.  
  2704. $panel.append(
  2705. $('<div>', { style: 'margin-top:10px;font-size:10px;color:#999999;' })
  2706. .append($('<div>').text(`version ${scriptVersion}`))
  2707. .append(
  2708. $('<div>').append(
  2709. $('<a>', { href: '#' /* , target:'__blank' */ }).text('Discussion Forum (currently n/a)')
  2710. )
  2711. )
  2712. );
  2713.  
  2714. const { tabLabel, tabPane } = await sdk.Sidebar.registerScriptTab();
  2715. $(tabLabel).text('FC');
  2716. $(tabPane).append($panel);
  2717.  
  2718. // append the power button
  2719. if (!$('#fc-layer-power-btn').length) {
  2720. const color = settings.layerVisible ? '#00bd00' : '#ccc';
  2721. $(tabLabel).prepend(
  2722. $('<span>', {
  2723. class: 'fa fa-power-off',
  2724. id: 'fc-layer-power-btn',
  2725. style: `margin-right: 5px;cursor: pointer;color: ${color};font-size: 13px;`,
  2726. title: 'Toggle FC Layer'
  2727. }).click(evt => {
  2728. evt.stopPropagation();
  2729. setEnabled(!settings.layerVisible);
  2730. })
  2731. );
  2732. }
  2733.  
  2734. $('#fcl-state-select').change(onStateSelectionChanged);
  2735. loadStateFCInfo();
  2736. }
  2737.  
  2738. function loadStateFCInfo() {
  2739. $('#fcl-state-info').empty();
  2740. if (STATE_SETTINGS[settings.activeStateAbbr]) {
  2741. const stateInfo = STATE_SETTINGS[settings.activeStateAbbr].information;
  2742. const $panelStateInfo = $('<dl>');
  2743. Object.keys(stateInfo).forEach(propertyName => {
  2744. $panelStateInfo.append($('<dt>', { style: 'margin-top:1em;color:#777777' }).text(propertyName))
  2745. .append($('<dd>').text(stateInfo[propertyName]));
  2746. });
  2747. $('#fcl-state-info').append($panelStateInfo);
  2748. }
  2749. }
  2750.  
  2751. function addLoadingIndicator() {
  2752. $('.loading-indicator').after($('<div class="loading-indicator" style="margin-right:10px" id="fc-loading-indicator">'));
  2753. }
  2754.  
  2755. async function initGui() {
  2756. addLoadingIndicator();
  2757. initLayer();
  2758. await initUserPanel();
  2759. }
  2760.  
  2761. async function init() {
  2762. if (debug && Promise.config) {
  2763. Promise.config({
  2764. warnings: true,
  2765. longStackTraces: true,
  2766. cancellation: true,
  2767. monitoring: false
  2768. });
  2769. } else {
  2770. Promise.config({
  2771. warnings: false,
  2772. longStackTraces: false,
  2773. cancellation: true,
  2774. monitoring: false
  2775. });
  2776. }
  2777.  
  2778. const u = sdk.State.getUserInfo();
  2779. rank = u.rank + 1;
  2780. isAM = u.isAreaManager;
  2781. userNameLC = u.userName.toLowerCase();
  2782.  
  2783. loadSettingsFromStorage();
  2784. await initGui();
  2785. fetchAllFC();
  2786. log('Initialized.');
  2787. }
  2788.  
  2789. init();
  2790. })();