WME GIS Layers

Adds GIS layers in WME

  1. /* eslint-disable camelcase */
  2. /* eslint-disable brace-style, curly, nonblock-statement-body-position, no-template-curly-in-string, func-names */
  3. // ==UserScript==
  4. // @name WME GIS Layers
  5. // @namespace https://greasyfork.org/users/45389
  6. // @version 2025.06.22.000
  7. // @description Adds GIS layers in WME
  8. // @author MapOMatic / JS55CT
  9. // @match *://*.waze.com/*editor*
  10. // @exclude *://*.waze.com/user/editor*
  11. // @exclude *://*.waze.com/editor/sdk/*
  12. // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
  13. // @require https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js
  14. // @require https://update.greasyfork.org/scripts/506614/1441195/ESTreeProcessor.js
  15. // @require https://update.greasyfork.org/scripts/509664/WME%20Utils%20-%20Bootstrap.js
  16. // @require https://update.greasyfork.org/scripts/516445/1480246/Make%20GM%20xhr%20more%20parallel%20again.js
  17. // @connect greasyfork.org
  18. // @grant GM_xmlhttpRequest
  19. // @grant GM_info
  20. // @grant GM_setClipboard
  21. // @license GNU GPLv3
  22. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  23. // @connect *
  24. // @connect tigerweb.geo.census.gov
  25. // @connect 136.234.13.165
  26. // @connect 216.167.160.20
  27. // @connect 35.172.145.31
  28. // @connect 52.37.30.30
  29. // @connect 54.213.14.253
  30. // @connect 72.10.206.73
  31. // @connect a2maps.a2gov.org
  32. // @connect adairgis.integritygis.com
  33. // @connect agis.charlottecountyfl.gov
  34. // @connect ago.clarkcountyohio.gov
  35. // @connect agomaps.larimer.org
  36. // @connect ags.agdmaps.com
  37. // @connect ags.bhamaps.com
  38. // @connect ags.kitsapgov.com
  39. // @connect ags.myokaloosa.com
  40. // @connect ags.roseville.ca.us
  41. // @connect ags1.wgxtreme.com
  42. // @connect ags10s1.dot.illinois.gov
  43. // @connect ags2maps.srcity.org
  44. // @connect ags3.scgov.net
  45. // @connect aldotgis.dot.state.al.us
  46. // @connect alleganygis.allconet.org
  47. // @connect alphagis.alpharetta.ga.us
  48. // @connect andrewgis.integritygis.com
  49. // @connect anrmaps.vermont.gov
  50. // @connect ansoncountygis.com
  51. // @connect apnsgis1.apsu.edu
  52. // @connect apnsgis4.apsu.edu
  53. // @connect app.mdt.mt.gov
  54. // @connect apps.alamance-nc.com
  55. // @connect apps.fs.usda.gov
  56. // @connect apps.lickingcounty.gov
  57. // @connect apps.vernoncounty.org
  58. // @connect apps.wyoroad.info
  59. // @connect arcgis-morrowarcgis-1015369042.us-east-1.elb.amazonaws.com
  60. // @connect arcgis-web.chinohills.org
  61. // @connect arcgis.atlantaregional.com
  62. // @connect arcgis.c3gov.com
  63. // @connect arcgis.cityofcapegirardeau.org
  64. // @connect arcgis.cityofwatsonville.org
  65. // @connect arcgis.co.beltrami.mn.us
  66. // @connect arcgis.co.henry.ga.us
  67. // @connect arcgis.co.lancaster.pa.us
  68. // @connect arcgis.forneytx.gov
  69. // @connect arcgis.gis.lacounty.gov
  70. // @connect arcgis.kingsporttn.gov
  71. // @connect arcgis.leaguecitytx.gov
  72. // @connect arcgis.lewiscountywa.gov
  73. // @connect arcgis.mobile311.com
  74. // @connect arcgis.racinecounty.com
  75. // @connect arcgis.tampagov.net
  76. // @connect arcgis.tuscco.com
  77. // @connect arcgis.vgsi.com
  78. // @connect arcgis.water.nv.gov
  79. // @connect arcgis.waxahachie.com
  80. // @connect arcgis.yumacountyaz.gov
  81. // @connect arcgis4.roktech.net
  82. // @connect arcgis5.roktech.net
  83. // @connect arcgisce.co.valencia.nm.us
  84. // @connect arcgisserver.digital.mass.gov
  85. // @connect arcgisserver.lincolncounty.org
  86. // @connect arcgisserver.maine.gov
  87. // @connect arcgisserver2.morpc.org
  88. // @connect arcgissrv.cityofbartlesville.org
  89. // @connect arcgiswap01.ci.temple.tx.us
  90. // @connect arcgisweb.carteretcountync.gov
  91. // @connect arcgisweb.countyofnewaygo.com
  92. // @connect arcmobile.co.albany.wy.us
  93. // @connect arcportal.florenceco.org
  94. // @connect arcserv.co.washington.ar.us
  95. // @connect arcserver.madisoncountyky.us
  96. // @connect arcserver2.oconeesc.com
  97. // @connect arcweb.hcad.org
  98. // @connect ardmoregis.ardmorecity.org
  99. // @connect arlgis.arlingtonva.us
  100. // @connect atchisongis.integritygis.com
  101. // @connect atlas.co.chelan.wa.us
  102. // @connect atlas.geoportalmaps.com
  103. // @connect audraingis.integritygis.com
  104. // @connect batesgis.integritygis.com
  105. // @connect bcgis.baltimorecountymd.gov
  106. // @connect bcgis.brunswickcountync.gov
  107. // @connect bcgishub.broward.org
  108. // @connect bcmaps.bradfordco.org
  109. // @connect bentongis.integritygis.com
  110. // @connect biamaps.geoplatform.gov
  111. // @connect bocagis.ci.boca-raton.fl.us
  112. // @connect bonneville.esriemcs.com
  113. // @connect bpagis.bossierparish.org
  114. // @connect bryangis.bryan-county.org
  115. // @connect buchanangis.integritygis.com
  116. // @connect butlergis.integritygis.com
  117. // @connect c39gisserver.co.richland.nd.us
  118. // @connect ca.dep.state.fl.us
  119. // @connect cagisonline.hamilton-co.org
  120. // @connect calmaps.co.calumet.wi.us
  121. // @connect caltrans-gis.dot.ca.gov
  122. // @connect cama.shelbycountyauditors.com
  123. // @connect camdengis.integritygis.com
  124. // @connect cassweb3.co.cass.mn.us
  125. // @connect cceo.co.comal.tx.us
  126. // @connect ccmap.cccounty.us
  127. // @connect cecilmaps.org
  128. // @connect charitongis.integritygis.com
  129. // @connect christiangis.integritygis.com
  130. // @connect clearfieldco.org
  131. // @connect cloud.longviewtexas.gov
  132. // @connect cloudgis.bonnercountyid.gov
  133. // @connect co.knox.il.us
  134. // @connect coagisweb.cabq.gov
  135. // @connect com.blountgis.org
  136. // @connect concordgis.ci.concord.ca.us
  137. // @connect conservationgis.alabama.gov
  138. // @connect coopergis.integritygis.com
  139. // @connect covgis.cityofvacaville.com
  140. // @connect coweta-gis-web.coweta.ga.us
  141. // @connect cowlitzgis.net
  142. // @connect crgis.cedar-rapids.org
  143. // @connect cteco.uconn.edu
  144. // @connect currituckncgov.com
  145. // @connect cw.townofclaytonnc.org
  146. // @connect dadegis.integritygis.com
  147. // @connect dallasgis.integritygis.com
  148. // @connect data.wsdot.wa.gov
  149. // @connect data1.digitaldataservices.com
  150. // @connect dc-web-2.co.douglas.mn.us
  151. // @connect dcgis.dekalbcountyga.gov
  152. // @connect dcimapapps.countyofdane.com
  153. // @connect dekalbgis.integritygis.com
  154. // @connect delta.co.clatsop.or.us
  155. // @connect dev.wilsonvillemaps.com
  156. // @connect doniphangis.integritygis.com
  157. // @connect dotapp9.dot.state.mn.us
  158. // @connect douglasgis.integritygis.com
  159. // @connect dtdapps.coloradodot.info
  160. // @connect dungis.dunwoodyga.gov
  161. // @connect dunklingis.integritygis.com
  162. // @connect ecgis.co.ellis.tx.us
  163. // @connect egis.baltimorecity.gov
  164. // @connect egis.pinellas.gov
  165. // @connect elb.elevatemaps.io
  166. // @connect emapsplus.com
  167. // @connect enigma.accgov.com
  168. // @connect enterprise.firstmap.delaware.gov
  169. // @connect eoc.franklin-gov.com
  170. // @connect epv.ci.juneau.ak.us
  171. // @connect eservices.co.crook.or.us
  172. // @connect essex-gis.co.essex.ny.us
  173. // @connect fcgis.franklincountypa.gov
  174. // @connect feature.geographic.texas.gov
  175. // @connect feature.tnris.org
  176. // @connect fieldstone.orangecountync.gov
  177. // @connect fremontgis.com
  178. // @connect gasconadegis.integritygis.com
  179. // @connect gateway.maps.rlid.org
  180. // @connect gcgis.guilfordcountync.gov
  181. // @connect geaugarealink.co.geauga.oh.us
  182. // @connect geo.co.butler.pa.us
  183. // @connect geo.co.harrison.ms.us
  184. // @connect geo.dentoncad.com
  185. // @connect geo.forsythco.com
  186. // @connect geo.friscotexas.gov
  187. // @connect geo.oit.ohio.gov
  188. // @connect geo.sandag.org
  189. // @connect geo.sanjoseca.gov
  190. // @connect geo.statcan.gc.ca
  191. // @connect geo.tompkins-co.org
  192. // @connect geo.vbgov.com
  193. // @connect geo1.oit.ohio.gov
  194. // @connect geo2.co.dodge.wi.us
  195. // @connect geodata.hawaii.gov
  196. // @connect geodata.md.gov
  197. // @connect geodata.sarpy.com
  198. // @connect geodataportal.net
  199. // @connect geonb.snb.ca
  200. // @connect geopower.jws.com
  201. // @connect geoweb.martin.fl.us
  202. // @connect geoweb02.ci.richmond.ca.us
  203. // @connect gis-2.warrencountyny.gov
  204. // @connect gis-server.co.becker.mn.us
  205. // @connect gis-server.co.montezuma.co.us
  206. // @connect gis.aacounty.org
  207. // @connect gis.abilenetx.com
  208. // @connect gis.adamscounty.org
  209. // @connect gis.addisontx.gov
  210. // @connect gis.aecomonline.net
  211. // @connect gis.allegancounty.org
  212. // @connect gis.allencountyohio.com
  213. // @connect gis.apachejunctionaz.gov
  214. // @connect gis.arapahoegov.com
  215. // @connect gis.arkansas.gov
  216. // @connect gis.arlingtonva.us
  217. // @connect gis.ashecountygov.com
  218. // @connect gis.ashevillenc.gov
  219. // @connect gis.atlantaga.gov
  220. // @connect gis.auburnalabama.org
  221. // @connect gis.auglaizecounty.org
  222. // @connect gis.azdot.gov
  223. // @connect gis.bakersfieldcity.us
  224. // @connect gis.baycountyfl.gov
  225. // @connect gis.beaufortcountysc.gov
  226. // @connect gis.beaumonttexas.gov
  227. // @connect gis.belmont.gov
  228. // @connect gis.bentoncountyar.gov
  229. // @connect gis.berkeleycountysc.gov
  230. // @connect gis.bigstonecounty.gov
  231. // @connect gis.bladenco.org
  232. // @connect gis.blairco.org
  233. // @connect gis.blm.gov
  234. // @connect gis.blueearthcountymn.gov
  235. // @connect gis.bransonmo.gov
  236. // @connect gis.brevardfl.gov
  237. // @connect gis.browncountywi.gov
  238. // @connect gis.buncombecounty.org
  239. // @connect gis.burkenc.org
  240. // @connect gis.burleighco.com
  241. // @connect gis.buttecounty.net
  242. // @connect gis.caldwellcountync.org
  243. // @connect gis.calhouncounty.org
  244. // @connect gis.campbellca.gov
  245. // @connect gis.campbellcountywy.gov
  246. // @connect gis.carboncounty.com
  247. // @connect gis.cayugacounty.us
  248. // @connect gis.cccounty.us
  249. // @connect gis.ccgisonline.com
  250. // @connect gis.ccpa.net
  251. // @connect gis.cedarfalls.com
  252. // @connect gis.cedarhilltx.com
  253. // @connect gis.cherokeega.com
  254. // @connect gis.chippewa.mn
  255. // @connect gis.chisagocountymn.gov
  256. // @connect gis.ci.janesville.wi.us
  257. // @connect gis.ci.mcminnville.or.us
  258. // @connect gis.ci.waco.tx.us
  259. // @connect gis.citruspa.org
  260. // @connect gis.cityofaikensc.gov
  261. // @connect gis.cityofberkeley.info
  262. // @connect gis.cityofboston.gov
  263. // @connect gis.cityofdenton.com
  264. // @connect gis.cityofirvine.org
  265. // @connect gis.cityofmiddletown.com
  266. // @connect gis.cityofmoore.com
  267. // @connect gis.cityofsanmateo.org
  268. // @connect gis.cityofwestsacramento.org
  269. // @connect gis.clevelandtn.gov
  270. // @connect gis.cmpdd.org
  271. // @connect gis.co.benton.or.us
  272. // @connect gis.co.berks.pa.us
  273. // @connect gis.co.carlton.mn.us
  274. // @connect gis.co.carver.mn.us
  275. // @connect gis.co.clarion.pa.us
  276. // @connect gis.co.cumberland.nc.us
  277. // @connect gis.co.douglas.or.us
  278. // @connect gis.co.eau-claire.wi.us
  279. // @connect gis.co.fairfield.oh.us
  280. // @connect gis.co.fillmore.mn.us
  281. // @connect gis.co.grand.co.us
  282. // @connect gis.co.grant.mn.us
  283. // @connect gis.co.grant.wi.gov
  284. // @connect gis.co.green-lake.wi.us
  285. // @connect gis.co.holmes.oh.us
  286. // @connect gis.co.hubbard.mn.us
  287. // @connect gis.co.isanti.mn.us
  288. // @connect gis.co.josephine.or.us
  289. // @connect gis.co.kittitas.wa.us
  290. // @connect gis.co.linn.or.us
  291. // @connect gis.co.mille-lacs.mn.us
  292. // @connect gis.co.nezperce.id.us
  293. // @connect gis.co.oneida.wi.us
  294. // @connect gis.co.pepin.wi.us
  295. // @connect gis.co.pierce.wi.us
  296. // @connect gis.co.polk.mn.us
  297. // @connect gis.co.richland.wi.us
  298. // @connect gis.co.roseau.mn.us
  299. // @connect gis.co.sangamon.il.us
  300. // @connect gis.co.sauk.wi.us
  301. // @connect gis.co.sherburne.mn.us
  302. // @connect gis.co.stearns.mn.us
  303. // @connect gis.co.stevens.mn.us
  304. // @connect gis.co.tuscarawas.oh.us
  305. // @connect gis.co.wadena.mn.us
  306. // @connect gis.co.waseca.mn.us
  307. // @connect gis.co.waushara.wi.us
  308. // @connect gis.co.wood.wi.us
  309. // @connect gis.co.ym.mn.gov
  310. // @connect gis.colorado.gov
  311. // @connect gis.coloradosprings.gov
  312. // @connect gis.columbiacountyga.gov
  313. // @connect gis.columbiacountymaps.com
  314. // @connect gis.columbiasc.gov
  315. // @connect gis.columbusga.org
  316. // @connect gis.concordnh.gov
  317. // @connect gis.cookeville-tn.org
  318. // @connect gis.corvallisoregon.gov
  319. // @connect gis.cosb.us
  320. // @connect gis.countyofriverside.us
  321. // @connect gis.cowleycounty.org
  322. // @connect gis.cranstonri.org
  323. // @connect gis.cravencountync.gov
  324. // @connect gis.crcog.org
  325. // @connect gis.crookcounty.wy.gov
  326. // @connect gis.crowwing.us
  327. // @connect gis.cstx.gov
  328. // @connect gis.danville-va.gov
  329. // @connect gis.dauphincounty.org
  330. // @connect gis.deerparktx.gov
  331. // @connect gis.dekalbcountyga.gov
  332. // @connect gis.delcopa.gov
  333. // @connect gis.dentoncounty.gov
  334. // @connect gis.districtiii.org
  335. // @connect gis.dogis.org
  336. // @connect gis.donaanacounty.org
  337. // @connect gis.dot.nh.gov
  338. // @connect gis.dot.nv.gov
  339. // @connect gis.dot.state.oh.us
  340. // @connect gis.douglascountyks.org
  341. // @connect gis.dubuquecounty.us
  342. // @connect gis.dupageco.org
  343. // @connect gis.duplincountync.com
  344. // @connect gis.dutchessny.gov
  345. // @connect gis.eastgreenwichri.com
  346. // @connect gis.edgecombecountync.gov
  347. // @connect gis.edmondok.gov
  348. // @connect gis.elkocountynv.net
  349. // @connect gis.elpasotexas.gov
  350. // @connect gis.emmetcounty.org
  351. // @connect gis.eriecountypa.gov
  352. // @connect gis.fortlauderdale.gov
  353. // @connect gis.franklincountyohio.gov
  354. // @connect gis.fultoncountyoh.com
  355. // @connect gis.fwb.org
  356. // @connect gis.fwp.mt.gov
  357. // @connect gis.gallatin.mt.gov
  358. // @connect gis.gallupnm.us
  359. // @connect gis.garrettcounty.org
  360. // @connect gis.gastongov.com
  361. // @connect gis.gcrc.org
  362. // @connect gis.gilacountyaz.gov
  363. // @connect gis.gocolumbiamo.com
  364. // @connect gis.goshencounty.org
  365. // @connect gis.gptx.org
  366. // @connect gis.grandcountyutah.net
  367. // @connect gis.greenecountyohio.gov
  368. // @connect gis.greenegovernment.com
  369. // @connect gis.greensboro-nc.gov
  370. // @connect gis.gscplanning.com
  371. // @connect gis.hardeecounty.net
  372. // @connect gis.harnett.org
  373. // @connect gis.hartford.gov
  374. // @connect gis.hawaiicounty.gov
  375. // @connect gis.hcpafl.org
  376. // @connect gis.hennepin.us
  377. // @connect gis.huntingtonbeachca.gov
  378. // @connect gis.iberiagov.net
  379. // @connect gis.indot.in.gov
  380. // @connect gis.interdev.com
  381. // @connect gis.iowadot.gov
  382. // @connect gis.itd.idaho.gov
  383. // @connect gis.jacksonnc.org
  384. // @connect gis.jccal.org
  385. // @connect gis.johnson-county.com
  386. // @connect gis.johnsoncitytn.org
  387. // @connect gis.kalamazoocity.org
  388. // @connect gis.kanawhacountyassessor.com
  389. // @connect gis.kaufmancounty.net
  390. // @connect gis.kcgov.us
  391. // @connect gis.kcmn.us
  392. // @connect gis.kentcountyde.gov
  393. // @connect gis.kentcountymi.gov
  394. // @connect gis.kleinfelder.com
  395. // @connect gis.lacrossecounty.org
  396. // @connect gis.lafayettecountywi.org
  397. // @connect gis.lakecountyfl.gov
  398. // @connect gis.lakecountyohio.gov
  399. // @connect gis.lapazcountyaz.org
  400. // @connect gis.laplata.co.us
  401. // @connect gis.lasallecounty.org
  402. // @connect gis.latah.id.us
  403. // @connect gis.leecountyil.com
  404. // @connect gis.lehighcounty.org
  405. // @connect gis.leoc.net
  406. // @connect gis.littleelm.org
  407. // @connect gis.livingstoncounty.us
  408. // @connect gis.lja.com
  409. // @connect gis.lojic.org
  410. // @connect gis.losalamosnm.us
  411. // @connect gis.luzernecounty.org
  412. // @connect gis.lyco.org
  413. // @connect gis.lyon-county.org
  414. // @connect gis.macombgov.org
  415. // @connect gis.maconnc.org
  416. // @connect gis.maderacounty.com
  417. // @connect gis.marinpublic.com
  418. // @connect gis.marionfl.org
  419. // @connect gis.masoncountywa.gov
  420. // @connect gis.massdot.state.ma.us
  421. // @connect gis.mbakerintl.com
  422. // @connect gis.mcgtn.org
  423. // @connect gis.mckeancountypa.gov
  424. // @connect gis.mcohio.org
  425. // @connect gis.mendocinocounty.org
  426. // @connect gis.mercercountypa.gov
  427. // @connect gis.mesaaz.gov
  428. // @connect gis.mifflincountypa.gov
  429. // @connect gis.minnehahacounty.org
  430. // @connect gis.miottawa.org
  431. // @connect gis.missoulacounty.us
  432. // @connect gis.modestogov.com
  433. // @connect gis.mono.ca.gov
  434. // @connect gis.montgomeryal.gov
  435. // @connect gis.moorecountync.gov
  436. // @connect gis.mytoddcounty.com
  437. // @connect gis.napa.ca.gov
  438. // @connect gis.nashcountync.gov
  439. // @connect gis.nassaucountyny.gov
  440. // @connect gis.nccde.org
  441. // @connect gis.ne.gov
  442. // @connect gis.neccog.org
  443. // @connect gis.newedgeservices.com
  444. // @connect gis.newhavenct.gov
  445. // @connect gis.nhcgov.com
  446. // @connect gis.niagaracounty.com
  447. // @connect gis.nola.gov
  448. // @connect gis.norrycopa.net
  449. // @connect gis.northamptoncounty.org
  450. // @connect gis.odot.state.or.us
  451. // @connect gis.ohiodnr.gov
  452. // @connect gis.okc.gov
  453. // @connect gis.orangecountygov.com
  454. // @connect gis.orangecountyva.gov
  455. // @connect gis.osceola.org
  456. // @connect gis.outagamie.org
  457. // @connect gis.owensboro.org
  458. // @connect gis.pandai.com
  459. // @connect gis.pendercountync.gov
  460. // @connect gis.pendoreilleco.org
  461. // @connect gis.penndot.gov
  462. // @connect gis.penndot.pa.gov
  463. // @connect gis.peoriacounty.gov
  464. // @connect gis.personcountync.gov
  465. // @connect gis.pgatlas.com
  466. // @connect gis.pikepa.org
  467. // @connect gis.pinal.gov
  468. // @connect gis.pittcountync.gov
  469. // @connect gis.pittsburgca.gov
  470. // @connect gis.polk-county.net
  471. // @connect gis.popecountymn.gov
  472. // @connect gis.port-orange.org
  473. // @connect gis.pottcounty-ia.gov
  474. // @connect gis.putnam-fl.com
  475. // @connect gis.qac.org
  476. // @connect gis.randolphcountync.gov
  477. // @connect gis.rapides911.org
  478. // @connect gis.rcgov.org
  479. // @connect gis.renvillecountymn.com
  480. // @connect gis.rileycountyks.gov
  481. // @connect gis.rocklin.ca.us
  482. // @connect gis.rowancountync.gov
  483. // @connect gis.rrnm.gov
  484. // @connect gis.rtcsnv.com
  485. // @connect gis.rutherfordcountync.gov
  486. // @connect gis.sanjuanco.com
  487. // @connect gis.santa-clarita.com
  488. // @connect gis.santacruzcounty.us
  489. // @connect gis.santamonica.gov
  490. // @connect gis.sawyerwi.org
  491. // @connect gis.sccwi.gov
  492. // @connect gis.shastacounty.gov
  493. // @connect gis.sheboygancounty.com
  494. // @connect gis.shelbycountytn.gov
  495. // @connect gis.showmeboone.com
  496. // @connect gis.siouxfalls.gov
  497. // @connect gis.slocounty.ca.gov
  498. // @connect gis.sncoapps.us
  499. // @connect gis.southkingstownri.com
  500. // @connect gis.steele.mn
  501. // @connect gis.stlouiscountymn.gov
  502. // @connect gis.sullivanny.us
  503. // @connect gis.sumtercountyfl.gov
  504. // @connect gis.surryinfo.net
  505. // @connect gis.talbotdes.org
  506. // @connect gis.tazewell.com
  507. // @connect gis.texoma.cog.tx.us
  508. // @connect gis.thecolonytx.gov
  509. // @connect gis.thomsonreuters.com
  510. // @connect gis.transportation.wv.gov
  511. // @connect gis.transylvaniacounty.org
  512. // @connect gis.traviscountytx.gov
  513. // @connect gis.tularecounty.ca.gov
  514. // @connect gis.ucdavis.edu
  515. // @connect gis.ulstercountyny.gov
  516. // @connect gis.unioncountync.gov
  517. // @connect gis.vernon-ct.gov
  518. // @connect gis.victorvilleca.gov
  519. // @connect gis.warrensburg-mo.com
  520. // @connect gis.washingtoncountyny.gov
  521. // @connect gis.watertownwi.gov
  522. // @connect gis.waukesha-wi.gov
  523. // @connect gis.waukeshacounty.gov
  524. // @connect gis.weatherfordtx.gov
  525. // @connect gis.westmorelandcountypa.gov
  526. // @connect gis.westplains.net
  527. // @connect gis.whatcomcounty.us
  528. // @connect gis.whitfieldcountyga.com
  529. // @connect gis.wilco.org
  530. // @connect gis.wilkescounty.net
  531. // @connect gis.willcountyillinois.com
  532. // @connect gis.wilson-co.com
  533. // @connect gis.wilsonnc.org
  534. // @connect gis.wiu.edu
  535. // @connect gis.worldviewsolutions.com
  536. // @connect gis.wyo.gov
  537. // @connect gis.yadkincountync.gov
  538. // @connect gis.yanceycountync.org
  539. // @connect gis.yavapaiaz.gov
  540. // @connect gis.yolocounty.gov
  541. // @connect gis.yolocounty.org
  542. // @connect gis.yuba.org
  543. // @connect gis1.acimap.us
  544. // @connect gis1.georgetowncountysc.org
  545. // @connect gis1.hamiltoncounty.in.gov
  546. // @connect gis11.cama.io
  547. // @connect gis11.services.ncdot.gov
  548. // @connect gis12.cookcountyil.gov
  549. // @connect gis2.arlingtontx.gov
  550. // @connect gis2.arlingtonva.us
  551. // @connect gis2.co.dakota.mn.us
  552. // @connect gis2.co.marathon.wi.us
  553. // @connect gis2.co.ozaukee.wi.us
  554. // @connect gis2.erie.gov
  555. // @connect gis2.gworks.com
  556. // @connect gis2.idaho.gov
  557. // @connect gis2.lawrenceks.org
  558. // @connect gis2.orangeburgcounty.org
  559. // @connect gis2.sandyspringsga.gov
  560. // @connect gis2.totaland.com
  561. // @connect gis21svweb.lincolnparish.org
  562. // @connect gis3.cdmsmithgis.com
  563. // @connect gis3.cmpdd.org
  564. // @connect gis3.gwinnettcounty.com
  565. // @connect gis3.gworks.com
  566. // @connect gis3.montgomerycountymd.gov
  567. // @connect gis3.richmondnc.com
  568. // @connect gis4.montgomerycountymd.gov
  569. // @connect gisago-qa.mcgi.state.mi.us
  570. // @connect gisago.mcgi.state.mi.us
  571. // @connect gisapp.adcogov.org
  572. // @connect gisapp.mahoningcountyoh.gov
  573. // @connect gisapps.cityofchicago.org
  574. // @connect gisapps.glendaleca.gov
  575. // @connect gisapps.rileycountyks.gov
  576. // @connect gisapps.wicomicocounty.org
  577. // @connect gisapps1.mapoakland.com
  578. // @connect gisarcweb.jeffersoncountywv.org
  579. // @connect gisccapps.charlestoncounty.org
  580. // @connect gisdata.alleghenycounty.us
  581. // @connect gisdata.dot.ca.gov
  582. // @connect gisdata.farrwestengineering.com
  583. // @connect gisdata.in.gov
  584. // @connect gisdata.jeffersoncountyoh.com
  585. // @connect gisdata.kingcounty.gov
  586. // @connect gisdata.pandai.com
  587. // @connect gisdata.pima.gov
  588. // @connect gisdata.seattle.gov
  589. // @connect gisdemo1.cdmsmith.com
  590. // @connect gisdemo2.cdmsmith.com
  591. // @connect gisentapp01.highpointnc.gov
  592. // @connect gisext.lincoln.ne.gov
  593. // @connect gishost.cdmsmithgis.com
  594. // @connect gisinfo.co.portage.wi.gov
  595. // @connect gisinfo.co.walworth.wi.us
  596. // @connect gisinfo.lawrencevillega.org
  597. // @connect gismap.augustaga.gov
  598. // @connect gismap.cityofboise.org
  599. // @connect gismap.co.juneau.wi.us
  600. // @connect gismap.co.marshall.mn.us
  601. // @connect gismap.co.norman.mn.us
  602. // @connect gismap.co.red-lake.mn.us
  603. // @connect gismapping.stafford.va.us
  604. // @connect gismaps.cityofboise.org
  605. // @connect gismaps.cityofgreer.org
  606. // @connect gismaps.co.cerro-gordo.ia.us
  607. // @connect gismaps.coconino.az.gov
  608. // @connect gismaps.columbiapa.org
  609. // @connect gismaps.flower-mound.com
  610. // @connect gismaps.fultoncountyga.gov
  611. // @connect gismaps.hctra.org
  612. // @connect gismaps.kingcounty.gov
  613. // @connect gismaps.redwoodcity.org
  614. // @connect gismaps.sedgwickcounty.org
  615. // @connect gismaps.wichita.gov
  616. // @connect gismapserver.leegov.com
  617. // @connect gismo.spokanecounty.org
  618. // @connect gisonline.greenvillenc.gov
  619. // @connect gisp.co.genesee.ny.us
  620. // @connect gisp.mcgi.state.mi.us
  621. // @connect gisportal.champaignil.gov
  622. // @connect gisportal.co.calaveras.ca.us
  623. // @connect gisportal.co.madison.il.us
  624. // @connect gisportal.co.warren.oh.us
  625. // @connect gisportal.dorchestercounty.net
  626. // @connect gisportal.dot.ct.gov
  627. // @connect gisportal.fnsb.gov
  628. // @connect gisportal.ircgov.com
  629. // @connect gisportal.ontarioca.gov
  630. // @connect gisportal.stocktonca.gov
  631. // @connect gisportal.stpgov.org
  632. // @connect gispro.porterco.org
  633. // @connect gisprod10.co.fresno.ca.us
  634. // @connect gisprodops.chesco.org
  635. // @connect gispub.cityofaspen.com
  636. // @connect gispub.co.washington.or.us
  637. // @connect gispublic.co.lake.ca.us
  638. // @connect gispw.coloradosprings.gov
  639. // @connect gisrevprxy.seattle.gov
  640. // @connect giss3.cmpdd.org
  641. // @connect gisserver.christiancountymo.gov
  642. // @connect gisservice.cityofmesquite.com
  643. // @connect gisservicemt.gov
  644. // @connect gisservices.chathamcountync.gov
  645. // @connect gisservices.chathamnc.org
  646. // @connect gisservices.co.anoka.mn.us
  647. // @connect gisservices.douglasnv.us
  648. // @connect gisservices.its.ny.gov
  649. // @connect gisservices.oakgov.com
  650. // @connect gisservices2.suffolkcountyny.gov
  651. // @connect gissites4.centrecountypa.gov
  652. // @connect gissvr.watgov.org
  653. // @connect gisweb-18.ci.killeen.tx.us
  654. // @connect gisweb-adapters.bcpa.net
  655. // @connect gisweb.albemarle.org
  656. // @connect gisweb.birminghamal.gov
  657. // @connect gisweb.casscountynd.gov
  658. // @connect gisweb.champaignil.gov
  659. // @connect gisweb.ci.manteca.ca.us
  660. // @connect gisweb.co.aitkin.mn.us
  661. // @connect gisweb.co.mower.mn.us
  662. // @connect gisweb.co.wilkin.mn.us
  663. // @connect gisweb.fdlco.wi.gov
  664. // @connect gisweb.fortbendcountytx.gov
  665. // @connect gisweb.jeffcowa.us
  666. // @connect gisweb.miamidade.gov
  667. // @connect gisweb.pwcva.gov
  668. // @connect gisweb.wycokck.org
  669. // @connect gisweb2014.gordoncounty.org
  670. // @connect giswww.westchestergov.com
  671. // @connect git.co.tioga.ny.us
  672. // @connect gmdnags.colliercountyfl.gov
  673. // @connect grant.co.jefferson.id.us
  674. // @connect gweb01.co.olmsted.mn.us
  675. // @connect harpergis.integritygis.com
  676. // @connect harrisonms.geopowered.com
  677. // @connect haslet.halff.com
  678. // @connect hazards.fema.gov
  679. // @connect hdgis.ingham.org
  680. // @connect heartlandmpo.com
  681. // @connect helenamontanamaps.org
  682. // @connect henrygis.integritygis.com
  683. // @connect hgis.hialeahfl.gov
  684. // @connect holtgis.integritygis.com
  685. // @connect host.cdmsmithgis.com
  686. // @connect hostingdata2.tighebond.com
  687. // @connect hostingdata3.tighebond.com
  688. // @connect huntsvillegis.com
  689. // @connect ifgis.idahofallsidaho.gov
  690. // @connect ihost.tularecounty.ca.gov
  691. // @connect imap.klickitatcounty.org
  692. // @connect ims.districtiii.org
  693. // @connect intervector.leoncountyfl.gov
  694. // @connect iowagis.integritygis.com
  695. // @connect ira.property-appraiser.org
  696. // @connect jeffarcgis.jeffersoncountywi.gov
  697. // @connect joplingis.org
  698. // @connect k3gis.com
  699. // @connect kanplan.ksdot.gov
  700. // @connect kcgis.kentoncounty.org
  701. // @connect kenhagis.kenha.co.ke
  702. // @connect kygisserver.ky.gov
  703. // @connect lacledegis.integritygis.com
  704. // @connect lafayettegis.integritygis.com
  705. // @connect landrecords.greencountywi.org
  706. // @connect lawrencegis.integritygis.com
  707. // @connect lcapps.co.lucas.oh.us
  708. // @connect lcmaps.lanecounty.org
  709. // @connect lee-arcgis.leecountync.gov
  710. // @connect lincolngis.integritygis.com
  711. // @connect linngis.integritygis.com
  712. // @connect lio.milwaukeecountywi.gov
  713. // @connect livingstongis.integritygis.com
  714. // @connect location.cabarruscounty.us
  715. // @connect logis.loudoun.gov
  716. // @connect loraincountyauditor.com
  717. // @connect lrs.co.columbia.wi.us
  718. // @connect lucity.sbpg.net
  719. // @connect macongis.integritygis.com
  720. // @connect madison.rexburg.org
  721. // @connect madisongis.cityofalbany.net
  722. // @connect manitowocmaps.info
  723. // @connect map.claycountymn.gov
  724. // @connect map.co.clear-creek.co.us
  725. // @connect map.co.clearwater.mn.us
  726. // @connect map.co.merced.ca.us
  727. // @connect map.co.thurston.wa.us
  728. // @connect map.co.trempealeau.wi.us
  729. // @connect map.coppelltx.gov
  730. // @connect map.eaglecounty.us
  731. // @connect map.newberrycounty.net
  732. // @connect map.opkansas.org
  733. // @connect map.pikepass.com
  734. // @connect map.stclairco.com
  735. // @connect map.sussexcountyde.gov
  736. // @connect map.wyoroad.info
  737. // @connect map9.incog.org
  738. // @connect mapd.kcmo.org
  739. // @connect mapdata.baytown.org
  740. // @connect mapdata.lasvegasnevada.gov
  741. // @connect mapdata.tucsonaz.gov
  742. // @connect mapit.fortworthtexas.gov
  743. // @connect mapitwest.fortworthtexas.gov
  744. // @connect mapping.adamscountypa.gov
  745. // @connect mapping.kenoshacountywi.gov
  746. // @connect mapping.mitchellcounty.org
  747. // @connect mapping.modot.org
  748. // @connect maps.adaok.com
  749. // @connect maps.alexandercountync.gov
  750. // @connect maps.alexandriava.gov
  751. // @connect maps.austintexas.gov
  752. // @connect maps.bannockcounty.us
  753. // @connect maps.bayfieldcounty.wi.gov
  754. // @connect maps.bcad.org
  755. // @connect maps.berkeleywv.org
  756. // @connect maps.boonecountyil.org
  757. // @connect maps.bossierparishgis.org
  758. // @connect maps.bouldercounty.org
  759. // @connect maps.brazoriacountytx.gov
  760. // @connect maps.brla.gov
  761. // @connect maps.brookhavenga.gov
  762. // @connect maps.bryantx.gov
  763. // @connect maps.burlesontx.com
  764. // @connect maps.butlercountyauditor.org
  765. // @connect maps.canyonco.org
  766. // @connect maps.capturecama.com
  767. // @connect maps.casperwy.gov
  768. // @connect maps.chautauquacounty.com
  769. // @connect maps.cherokeecounty-nc.gov
  770. // @connect maps.ci.longmont.co.us
  771. // @connect maps.ci.nacogdoches.tx.us
  772. // @connect maps.cityhs.net
  773. // @connect maps.cityofconroe.org
  774. // @connect maps.cityofhenderson.com
  775. // @connect maps.cityofls.net
  776. // @connect maps.cityofmadison.com
  777. // @connect maps.cityofmobile.org
  778. // @connect maps.cityofsherman.com
  779. // @connect maps.cityoftulsa.org
  780. // @connect maps.cityofwaterlooiowa.com
  781. // @connect maps.clarkcountynv.gov
  782. // @connect maps.claycountygov.com
  783. // @connect maps.clermontauditor.org
  784. // @connect maps.clintoncountypa.com
  785. // @connect maps.co.blaine.id.us
  786. // @connect maps.co.forsyth.nc.us
  787. // @connect maps.co.goodhue.mn.us
  788. // @connect maps.co.gov
  789. // @connect maps.co.grayson.tx.us
  790. // @connect maps.co.itasca.mn.us
  791. // @connect maps.co.kendall.il.us
  792. // @connect maps.co.kern.ca.us
  793. // @connect maps.co.lincoln.wi.us
  794. // @connect maps.co.palm-beach.fl.us
  795. // @connect maps.co.polk.or.us
  796. // @connect maps.co.pueblo.co.us
  797. // @connect maps.co.ramsey.mn.us
  798. // @connect maps.co.shawano.wi.us
  799. // @connect maps.co.warren.oh.us
  800. // @connect maps.co.washington.mn.us
  801. // @connect maps.co.yellowstone.mt.gov
  802. // @connect maps.coj.net
  803. // @connect maps.collincountytx.gov
  804. // @connect maps.countyofmerced.com
  805. // @connect maps.crc.ga.gov
  806. // @connect maps.ctmetro.org
  807. // @connect maps.dancgis.org
  808. // @connect maps.dcad.org
  809. // @connect maps.delco-gis.org
  810. // @connect maps.deltacountyco.gov
  811. // @connect maps.deschutes.org
  812. // @connect maps.desotocountyms.gov
  813. // @connect maps.dmgov.org
  814. // @connect maps.dotd.la.gov
  815. // @connect maps.douglascountyga.gov
  816. // @connect maps.douglascountywa.net
  817. // @connect maps.dsm.city
  818. // @connect maps.elbertcounty-co.gov
  819. // @connect maps.escpa.org
  820. // @connect maps.etcog.org
  821. // @connect maps.evansvillegis.com
  822. // @connect maps.fayetteville-ar.gov
  823. // @connect maps.fishers.in.us
  824. // @connect maps.flathead.mt.gov
  825. // @connect maps.floridadisaster.org
  826. // @connect maps.frederickcountymd.gov
  827. // @connect maps.fredericksburgva.gov
  828. // @connect maps.garfield-county.com
  829. // @connect maps.garlandtx.gov
  830. // @connect maps.gov.bc.ca
  831. // @connect maps.grcity.us
  832. // @connect maps.groton-ct.gov
  833. // @connect maps.grundyco.org
  834. // @connect maps.hayward-ca.gov
  835. // @connect maps.haywoodnc.net
  836. // @connect maps.highlandvillage.org
  837. // @connect maps.hokecounty.org
  838. // @connect maps.huerfano.us
  839. // @connect maps.huntsvilleal.gov
  840. // @connect maps.iredellcountync.gov
  841. // @connect maps.itos.uga.edu
  842. // @connect maps.jocogov.org
  843. // @connect maps.kytc.ky.gov
  844. // @connect maps.lacity.org
  845. // @connect maps.lagrange-ga.org
  846. // @connect maps.lakecountyil.gov
  847. // @connect maps.laramiecounty.com
  848. // @connect maps.lcwy.org
  849. // @connect maps.lebanontn.org
  850. // @connect maps.lex-co.com
  851. // @connect maps.lexingtonky.gov
  852. // @connect maps.libertymo.gov
  853. // @connect maps.lincolncountysd.org
  854. // @connect maps.linkgis.org
  855. // @connect maps.matsugov.us
  856. // @connect maps.mckinneytexas.org
  857. // @connect maps.meshekgis.com
  858. // @connect maps.miamigov.com
  859. // @connect maps.midlandtexas.gov
  860. // @connect maps.monroecounty.gov
  861. // @connect maps.muskegoncountygis.com
  862. // @connect maps.nashville.gov
  863. // @connect maps.ncpafl.com
  864. // @connect maps.nevadacountyca.gov
  865. // @connect maps.nj.gov
  866. // @connect maps.normanok.gov
  867. // @connect maps.northaugustasc.gov
  868. // @connect maps.ocgov.net
  869. // @connect maps.opkansas.org
  870. // @connect maps.orcity.org
  871. // @connect maps.palmcoastgov.com
  872. // @connect maps.parkco.us
  873. // @connect maps.phoenix.gov
  874. // @connect maps.pitkincounty.com
  875. // @connect maps.planogis.org
  876. // @connect maps.pottercountypa.net
  877. // @connect maps.prcity.com
  878. // @connect maps.raleighnc.gov
  879. // @connect maps.richlandcountyoh.us
  880. // @connect maps.rutherfordcountytn.gov
  881. // @connect maps.santa-clarita.com
  882. // @connect maps.santabarbaraca.gov
  883. // @connect maps.sccmo.org
  884. // @connect maps.semogis.com
  885. // @connect maps.sfdpw.org
  886. // @connect maps.sgcity.org
  887. // @connect maps.shelbyal.com
  888. // @connect maps.slocity.org
  889. // @connect maps.spartanburgcounty.org
  890. // @connect maps.springfieldmo.gov
  891. // @connect maps.steamboatsprings.net
  892. // @connect maps.stlouisco.com
  893. // @connect maps.swaincountync.gov
  894. // @connect maps.tippecanoe.in.gov
  895. // @connect maps.townofcary.org
  896. // @connect maps.udot.utah.gov
  897. // @connect maps.vcgi.vermont.gov
  898. // @connect maps.ventura.org
  899. // @connect maps.victoriatx.org
  900. // @connect maps.vilascountywi.gov
  901. // @connect maps.vtrans.vermont.gov
  902. // @connect maps.wake.gov
  903. // @connect maps.washco-md.net
  904. // @connect maps.washcowisco.gov
  905. // @connect maps1.eriecounty.oh.gov
  906. // @connect maps1.larimer.org
  907. // @connect maps11.eriecounty.oh.gov
  908. // @connect maps2.bgadd.org
  909. // @connect maps2.cattco.org
  910. // @connect maps2.ci.euless.tx.us
  911. // @connect maps2.columbus.gov
  912. // @connect maps2.dcgis.dc.gov
  913. // @connect maps2.san-marcos.net
  914. // @connect maps2.timmons.com
  915. // @connect maps2.vcgov.org
  916. // @connect maps6.stlouis-mo.gov
  917. // @connect maps7.eriecounty.oh.gov
  918. // @connect maps8.eriecounty.oh.gov
  919. // @connect mapsdev.hamiltontn.gov
  920. // @connect mapserv.cityofloveland.org
  921. // @connect mapserv.mesquitenv.gov
  922. // @connect mapservice.nmstatelands.org
  923. // @connect mapservices.gis.saccounty.net
  924. // @connect mapservices.gov.yk.ca
  925. // @connect mapservices.pasda.psu.edu
  926. // @connect mapservices.santacruzcountyaz.gov
  927. // @connect mapservices.sccgov.org
  928. // @connect mapservices.weather.noaa.gov
  929. // @connect mapservices1.jeffco.us
  930. // @connect mapservices2.jeffco.us
  931. // @connect mariesgis.integritygis.com
  932. // @connect mariongis.integritygis.com
  933. // @connect mcdonaldgis.integritygis.com
  934. // @connect mcgis.mesacounty.us
  935. // @connect mcgis.mohave.gov
  936. // @connect mcgis4.monroecounty-fl.gov
  937. // @connect mcmap.montrosecounty.net
  938. // @connect mcogis.co.marion.oh.us
  939. // @connect millergis.integritygis.com
  940. // @connect mms.hursttx.gov
  941. // @connect mndotgis.dot.state.mn.us
  942. // @connect moberlygis.integritygis.com
  943. // @connect mobile.alamedaca.gov
  944. // @connect moniteaugis.integritygis.com
  945. // @connect morgangis.integritygis.com
  946. // @connect msdisweb.missouri.edu
  947. // @connect mycity2.houstontx.gov
  948. // @connect navigator.state.or.us
  949. // @connect ndgishub.nd.gov
  950. // @connect newtongis.integritygis.com
  951. // @connect nhgeodata.unh.edu
  952. // @connect northlake.halff.com
  953. // @connect nsgiwa.novascotia.ca
  954. // @connect nspdcwebsrv.csuchico.edu
  955. // @connect oak.co.lake-of-the-woods.mn.us
  956. // @connect oc17maps.co.oconto.wi.us
  957. // @connect ocgis4.ocfl.net
  958. // @connect oncorng.co.ontario.ny.us
  959. // @connect operationserver.ci.henderson.nc.us
  960. // @connect orfmaps.norfolk.gov
  961. // @connect osagegis.integritygis.com
  962. // @connect pagis.org
  963. // @connect pamap.putnam-fl.gov
  964. // @connect parcelmap.ashtabulacounty.us
  965. // @connect parcels.rsdigital.com
  966. // @connect parcelviewer.geodecisions.com
  967. // @connect pascogis.pascocountyfl.net
  968. // @connect pgis.plantation.org
  969. // @connect phelpsgis.integritygis.com
  970. // @connect polaris2.mecklenburgcountync.gov
  971. // @connect polkgis.integritygis.com
  972. // @connect portal.carolinabeach.org
  973. // @connect portal.carson.org
  974. // @connect portal.henrico.us
  975. // @connect programs.iowadnr.gov
  976. // @connect propaccess.wadtx.com
  977. // @connect propertyviewer.andersoncountysc.org
  978. // @connect proxy2.roktech.net
  979. // @connect psportal.harrisoncountywv.com
  980. // @connect pubgis.ci.lubbock.tx.us
  981. // @connect public.co.wasco.or.us
  982. // @connect public1.co.waupaca.wi.us
  983. // @connect publicmap01.co.st-clair.il.us
  984. // @connect publicmaps.txkusa.org
  985. // @connect pulaskigis.integritygis.com
  986. // @connect putnamcountygis.com
  987. // @connect pwmaps.cityofloveland.org
  988. // @connect pwmaps.reno.gov
  989. // @connect rallsgis.integritygis.com
  990. // @connect raygis.integritygis.com
  991. // @connect rc-arcgis01.co.rice.mn.us
  992. // @connect rdsgis.nctgis.nct911.org
  993. // @connect renogis3.renogov.org
  994. // @connect roads.udot.utah.gov
  995. // @connect rockgis.co.rock.wi.us
  996. // @connect rockgis.rockfordil.gov
  997. // @connect romefloyd.agdmaps.com
  998. // @connect rptsgisweb.oswegocounty.com
  999. // @connect salinegis.integritygis.com
  1000. // @connect saludacountysc.net
  1001. // @connect scgis.summitoh.net
  1002. // @connect scgisa.starkcountyohio.gov
  1003. // @connect sdgis.sd.gov
  1004. // @connect secure.boonecountygis.com
  1005. // @connect sedaliagis.integritygis.com
  1006. // @connect see-eldorado.edcgov.us
  1007. // @connect server.boundarycountyid.org
  1008. // @connect server1.mapxpress.net
  1009. // @connect server2.mapxpress.net
  1010. // @connect services.arcgis.com
  1011. // @connect services.gis.ca.gov
  1012. // @connect services.gisqatar.org.qa
  1013. // @connect services.mh-gis.com
  1014. // @connect services.nconemap.gov
  1015. // @connect services.putnamco.org
  1016. // @connect services.sagis.org
  1017. // @connect services.wvgis.wvu.edu
  1018. // @connect services1.arcgis.com
  1019. // @connect services2.arcgis.com
  1020. // @connect services2.integritygis.com
  1021. // @connect services3.arcgis.com
  1022. // @connect services5.arcgis.com
  1023. // @connect services6.arcgis.com
  1024. // @connect services7.arcgis.com
  1025. // @connect services8.arcgis.com
  1026. // @connect services9.arcgis.com
  1027. // @connect showlowmaps.com
  1028. // @connect skyview.hornershifrin.com
  1029. // @connect slcgis.stlucieco.gov
  1030. // @connect slco.org
  1031. // @connect smgis.sanmarcostx.gov
  1032. // @connect smithvillegis.integritygis.com
  1033. // @connect smpesri.scdot.org
  1034. // @connect socogis.sonomacounty.ca.gov
  1035. // @connect spatial.gishost.com
  1036. // @connect spatial.jacksoncountyor.gov
  1037. // @connect spatialags.vhb.com
  1038. // @connect stclairgis.integritygis.com
  1039. // @connect stmgis.stmarysmd.com
  1040. // @connect stokescountygis.com
  1041. // @connect stonegis.integritygis.com
  1042. // @connect svr4.sumtercountysc.org
  1043. // @connect tcgisws.tooeleco.gov
  1044. // @connect tcweb.co.teller.co.us
  1045. // @connect tfportal.tfid.org
  1046. // @connect tharcgis2.thewoodlands-tx.gov
  1047. // @connect tigerweb.geo.census.gov
  1048. // @connect tiogagis.tiogacountypa.us
  1049. // @connect tnmap.tn.gov
  1050. // @connect tpwd.texas.gov
  1051. // @connect tsc-gis-ags101a.schneidercorp.com
  1052. // @connect twu.newedgeservices.com
  1053. // @connect utility.arcgis.com
  1054. // @connect vernongis.integritygis.com
  1055. // @connect vginmaps.vdem.virginia.gov
  1056. // @connect vtransmap01.aot.state.vt.us
  1057. // @connect wallawallagis.com
  1058. // @connect warrengis.integritygis.com
  1059. // @connect wcg-gisweb.co.worcester.md.us
  1060. // @connect wcgis3.co.winnebago.wi.us
  1061. // @connect wcgisweb.washoecounty.us
  1062. // @connect wcoh.geopowered.com
  1063. // @connect web.binghamid.gov
  1064. // @connect web2.co.ottertail.mn.us
  1065. // @connect web2.kcsgis.com
  1066. // @connect web3.kcsgis.com
  1067. // @connect web4.kcsgis.com
  1068. // @connect web5.kcsgis.com
  1069. // @connect webadaptor.glynncounty-ga.gov
  1070. // @connect webgis.bedfordcountyva.gov
  1071. // @connect webgis.co.davidson.nc.us
  1072. // @connect webgis.co.humboldt.ca.us
  1073. // @connect webgis.durhamnc.gov
  1074. // @connect webgis.lafayetteassessor.com
  1075. // @connect webgis.providenceri.gov
  1076. // @connect webgis.waterburyct.org
  1077. // @connect webgis.yorbalindaca.gov
  1078. // @connect webmap.co.jackson.ms.us
  1079. // @connect webmap.jeffparish.net
  1080. // @connect webmap.trueautomation.com
  1081. // @connect webmaps.elkgrovecity.org
  1082. // @connect webmaps.sjcounty.net
  1083. // @connect webportal.co.marquette.wi.us
  1084. // @connect websrv31.clallamcountywa.gov
  1085. // @connect webstergis.integritygis.com
  1086. // @connect wfs.ksdot.org
  1087. // @connect wfs.schneidercorp.com
  1088. // @connect wvsams.mapwv.org
  1089. // @connect ww1.bucoks.com
  1090. // @connect www.1stdistrict.org
  1091. // @connect www.adacountyassessor.org
  1092. // @connect www.adamscountyarcserver.com
  1093. // @connect www.ancgis.com
  1094. // @connect www.bartowgis.org
  1095. // @connect www.bcgis.com
  1096. // @connect www.bcpao.us
  1097. // @connect www.centralilmaps.com
  1098. // @connect www.cmbgis.com
  1099. // @connect www.colesco.illinois.gov
  1100. // @connect www.ctgismaps2.ct.gov
  1101. // @connect www.denvergov.org
  1102. // @connect www.dmcwebgis.com
  1103. // @connect www.efsedge.com
  1104. // @connect www.finneycountygis.com
  1105. // @connect www.franklinmo.net
  1106. // @connect www.gcgis.org
  1107. // @connect www.gfgis.com
  1108. // @connect www.gis.hctx.net
  1109. // @connect www.gis.sjcfl.us
  1110. // @connect www.gismidwest.com
  1111. // @connect www.gisonline.ms.gov
  1112. // @connect www.greenwoodsc.gov
  1113. // @connect www.hernandocountygis-florida.us
  1114. // @connect www.hogarcmaps.org
  1115. // @connect www.horrycountysc.gov
  1116. // @connect www.landmarkgeospatial.com
  1117. // @connect www.laurenscountygis.org
  1118. // @connect www.mcgisweb.org
  1119. // @connect www.mchenrycountygis.org
  1120. // @connect www.midmogis.org
  1121. // @connect www.monroegis.org
  1122. // @connect www.mymanatee.org
  1123. // @connect www.ocgis.com
  1124. // @connect www.portlandmaps.com
  1125. // @connect www.sciotocountyengineer.org
  1126. // @connect www.semogis.com
  1127. // @connect www.sgrcmaps.com
  1128. // @connect www.sjmap.org
  1129. // @connect www.skagitcounty.net
  1130. // @connect www.smithcountymapsite.org
  1131. // @connect www.tgisites.com
  1132. // @connect www.valorgis.com
  1133. // @connect www.waynecounty.com
  1134. // @connect www.webgis.net
  1135. // @connect www.yamhillcountygis.com
  1136. // @connect www1.cityofwebster.com
  1137. // @connect www2.ci.lancaster.oh.us
  1138. // @connect www2.pottcounty.org
  1139. // @connect www3.multco.us
  1140. // @connect www7.co.union.oh.us
  1141. // @connect xara1-4.cityofpetaluma.net
  1142. // @connect xmaps.indy.gov
  1143. // ==/UserScript==
  1144.  
  1145. /* global WazeWrap */
  1146. /* global _ */
  1147. /* global turf */
  1148. /* global ESTreeProcessor */
  1149. /* global bootstrap */
  1150. /* global OpenLayers */
  1151.  
  1152. (async function main() {
  1153. 'use strict';
  1154.  
  1155. const SHOW_UPDATE_MESSAGE = true;
  1156. const SCRIPT_VERSION_CHANGES = [
  1157. 'Minor update:',
  1158. 'Enhanced the ArcGIS platform API calls by leveraging the MaxAllowableOffset capability based on zoom level.',
  1159. 'This optimization reduces data retrieval size, ensuring faster responses and minimizing unnecessary data load at wider zoom scales. '
  1160. ];
  1161.  
  1162. // **************************************************************************************************************
  1163. // IMPORTANT: Update this when releasing a new version of script that includes changes to the spreadsheet format
  1164. // that may cause old code to break. This # should match the version listed in the spreadsheet
  1165. // i.e. update them at the same time.
  1166.  
  1167. // const LAYER_DEF_VERSION = '2018.04.27.001'; // NOT ACTUALLY USED YET
  1168.  
  1169. // **************************************************************************************************************
  1170. // const UPDATE_MESSAGE = 'Bug fix due to WME update';
  1171. // const UPDATE_MESSAGE = `<ul>${[
  1172. // 'Added ability to shift layers. Right click a layer in the list to bring up the layer settings window.'
  1173. // ].map(item => `<li>${item}</li>`).join('')}</ul><br>`;
  1174. const GF_URL = 'https://greasyfork.org/scripts/369632-wme-gis-layers';
  1175. // Used in tooltips to tell people who to report issues to. Update if a new author takes ownership of this script.
  1176. const SCRIPT_AUTHOR = 'MapOMatic';
  1177. // const LAYER_INFO_URL = 'https://spreadsheets.google.com/feeds/list/1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw/o7gusx3/public/values?alt=json';
  1178. const LAYER_DEF_SPREADSHEET_URL = 'https://sheets.googleapis.com/v4/spreadsheets/1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw/values/layerDefs';
  1179. const API_KEY = 'YTJWNVBVRkplbUZUZVVGTlNXOWlVR1pWVjIxcE9VdHJNbVY0TTFoeWNrSlpXbFZuVmtWelRrMVVWUT09';
  1180. const REQUEST_FORM_URL = 'https://docs.google.com/forms/d/e/1FAIpQLSevPQLz2ohu_LTge9gJ9Nv6PURmCmaSSjq0ayOJpGdRr2xI0g/viewform?usp=pp_url&entry.2116052852={username}';
  1181. const DEC = (s) => atob(atob(s));
  1182. const PRIVATE_LAYERS = { 'nc-henderson-sl-signs': ['the_cre8r', 'mapomatic'] }; // case sensitive -- use all lower case
  1183. // const COUNTRIES = {
  1184. // 'United States': {
  1185. // sheetId: '1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw',
  1186. // sheetLayerRange: 'layerDefs'
  1187. // }
  1188. // };
  1189. const DEFAULT_LAYER_NAME = 'GIS Layers - Default';
  1190. const ROAD_LAYER_NAME = 'GIS Layers - Roads';
  1191. const DEFAULT_STYLE = {
  1192. fillColor: '#000',
  1193. pointRadius: 4,
  1194. label: '${getLabel}',
  1195. fillOpacity: '0.95',
  1196. strokeColor: '#ffa500',
  1197. strokeOpacity: '0.95',
  1198. strokeWidth: 1.5,
  1199. fontColor: '#ffc520',
  1200. fontSize: '13',
  1201. labelOutlineColor: 'black',
  1202. labelOutlineWidth: 3,
  1203. };
  1204. const LAYER_STYLES = {
  1205. cities: {
  1206. fillOpacity: 0.3,
  1207. fillColor: '#f65',
  1208. strokeColor: '#f65',
  1209. fontColor: '#f62',
  1210. },
  1211. forests_parks: {
  1212. fillOpacity: 0.4,
  1213. fillColor: '#585',
  1214. strokeColor: '#484',
  1215. fontColor: '#8b8',
  1216. },
  1217. milemarkers: {
  1218. strokeColor: '#fff',
  1219. fontColor: '#fff',
  1220. fontWeight: 'bold',
  1221. fillOpacity: 0,
  1222. labelYOffset: 10,
  1223. pointRadius: 2,
  1224. fontSize: 12,
  1225. },
  1226. parcels: {
  1227. fillOpacity: 0,
  1228. fillColor: '#ffa500',
  1229. },
  1230. points: {
  1231. strokeColor: '#000',
  1232. fontColor: '#0ff',
  1233. fillColor: '#0ff',
  1234. labelYOffset: -10,
  1235. labelAlign: 'ct',
  1236. },
  1237. post_offices: {
  1238. strokeColor: '#000',
  1239. fontColor: '#f84',
  1240. fillColor: '#f84',
  1241. fontWeight: 'bold',
  1242. labelYOffset: -10,
  1243. labelAlign: 'ct',
  1244. },
  1245. state_parcels: {
  1246. fillOpacity: 0,
  1247. strokeColor: '#e62',
  1248. fillColor: '#e62',
  1249. fontColor: '#e73',
  1250. },
  1251. state_points: {
  1252. strokeColor: '#000',
  1253. fontColor: '#3cf',
  1254. fillColor: '#3cf',
  1255. labelYOffset: -10,
  1256. labelAlign: 'ct',
  1257. },
  1258. road_labels: {
  1259. strokeOpacity: 0,
  1260. fillOpacity: 0,
  1261. fontColor: '#faf',
  1262. },
  1263. structures: {
  1264. fillOpacity: 0,
  1265. strokeColor: '#f7f',
  1266. fontColor: '#f7f',
  1267. },
  1268. };
  1269. let ROAD_STYLE;
  1270. function initRoadStyle() {
  1271. ROAD_STYLE = {
  1272. pointRadius: 12,
  1273. fillColor: '#369',
  1274. pathLabel: '${getLabel}',
  1275. label: '',
  1276. fontColor: '#faf',
  1277. labelSelect: true,
  1278. pathLabelYOffset: '${getOffset}',
  1279. pathLabelCurve: '${getSmooth}',
  1280. pathLabelReadable: '${getReadable}',
  1281. labelAlign: '${getAlign}',
  1282. labelOutlineWidth: 3,
  1283. labelOutlineColor: '#000',
  1284. strokeWidth: 3,
  1285. stroke: true,
  1286. strokeColor: '#f0f',
  1287. strokeOpacity: 0.4,
  1288. fontWeight: 'bold',
  1289. fontSize: 11,
  1290. };
  1291. }
  1292.  
  1293. // eslint-disable-next-line no-unused-vars
  1294. const _regexReplace = {
  1295. // Strip leading zeros or blank full label for any label starting with a non-digit or
  1296. // is a Zero Address, use with '' as replace.
  1297. r0: /^(0+(\s.*)?|\D.*)/,
  1298. // Strip Everything After Street Type to end of the string by use $1 and $2 capture
  1299. // groups, use with replace '$1$2'
  1300. // eslint-disable-next-line max-len
  1301. r1: /^(.* )(Ave(nue)?|Dr(ive)?|St(reet)?|C(our)?t|Cir(cle)?|Blvd|Boulevard|Pl(ace)?|Ln|Lane|Fwy|Freeway|R(oa)?d|Ter(r|race)?|Tr(ai)?l|Way|Rte \d+|Route \d+)\b.*/gi,
  1302. // Strip SPACE 5 Digits from end of string, use with replace ''
  1303. r2: /\s\d{5}$/,
  1304. // Strip Everything after a "~", ",", ";" to the end of the string, use with replace ''
  1305. r3: /(~|,|;|\s?\r\n).*$/,
  1306. // Move the digits after the last space to before the rest of the string using, use with
  1307. // replace '$2 $1'
  1308. r4: /^(.*)\s(\d+).*/,
  1309. // Insert newline between digits (including "-") and everything after the digits,
  1310. // except(and before) a ",", use with replace '$1\n$2'
  1311. r5: /^([-\d]+)\s+([^,]+).*/,
  1312. // Insert newline between digits and everything after the digits, use with
  1313. // replace '$1\n$2'
  1314. r6: /^(\d+)\s+(.*)/,
  1315. };
  1316.  
  1317. let _gisLayers = [];
  1318.  
  1319. const _layerRefinements = [
  1320. {
  1321. id: 'us-post-offices',
  1322. labelHeaderFields: ['LOCALE_NAME'],
  1323. },
  1324. ];
  1325.  
  1326. const STATES = {
  1327. _states: [
  1328. ['US (Country)', 'US', -1],
  1329. ['Alabama', 'AL', 1],
  1330. ['Alaska', 'AK', 2],
  1331. ['American Samoa', 'AS', 60],
  1332. ['Arizona', 'AZ', 4],
  1333. ['Arkansas', 'AR', 5],
  1334. ['California', 'CA', 6],
  1335. ['Colorado', 'CO', 8],
  1336. ['Connecticut', 'CT', 9],
  1337. ['Delaware', 'DE', 10],
  1338. ['District of Columbia', 'DC', 11],
  1339. ['Florida', 'FL', 12],
  1340. ['Georgia', 'GA', 13],
  1341. ['Guam', 'GU', 66],
  1342. ['Hawaii', 'HI', 15],
  1343. ['Idaho', 'ID', 16],
  1344. ['Illinois', 'IL', 17],
  1345. ['Indiana', 'IN', 18],
  1346. ['Iowa', 'IA', 19],
  1347. ['Kansas', 'KS', 20],
  1348. ['Kentucky', 'KY', 21],
  1349. ['Louisiana', 'LA', 22],
  1350. ['Maine', 'ME', 23],
  1351. ['Maryland', 'MD', 24],
  1352. ['Massachusetts', 'MA', 25],
  1353. ['Michigan', 'MI', 26],
  1354. ['Minnesota', 'MN', 27],
  1355. ['Mississippi', 'MS', 28],
  1356. ['Missouri', 'MO', 29],
  1357. ['Montana', 'MT', 30],
  1358. ['Nebraska', 'NE', 31],
  1359. ['Nevada', 'NV', 32],
  1360. ['New Hampshire', 'NH', 33],
  1361. ['New Jersey', 'NJ', 34],
  1362. ['New Mexico', 'NM', 35],
  1363. ['New York', 'NY', 36],
  1364. ['North Carolina', 'NC', 37],
  1365. ['North Dakota', 'ND', 38],
  1366. ['Northern Mariana Islands', 'MP', 69],
  1367. ['Ohio', 'OH', 39],
  1368. ['Oklahoma', 'OK', 40],
  1369. ['Oregon', 'OR', 41],
  1370. ['Pennsylvania', 'PA', 42],
  1371. ['Puerto Rico', 'PR', 72],
  1372. ['Rhode Island', 'RI', 44],
  1373. ['South Carolina', 'SC', 45],
  1374. ['South Dakota', 'SD', 46],
  1375. ['Tennessee', 'TN', 47],
  1376. ['Texas', 'TX', 48],
  1377. ['Utah', 'UT', 49],
  1378. ['Vermont', 'VT', 50],
  1379. ['Virgin Islands', 'VI', 78],
  1380. ['Virginia', 'VA', 51],
  1381. ['Washington', 'WA', 53],
  1382. ['West Virginia', 'WV', 54],
  1383. ['Wisconsin', 'WI', 55],
  1384. ['Wyoming', 'WY', 56],
  1385. ],
  1386. toAbbr(fullName) {
  1387. return this._states.find((a) => a[0] === fullName)?.[1]; // Returns undefined if not found
  1388. },
  1389. toFullName(abbr) {
  1390. return this._states.find((a) => a[1] === abbr)?.[0]; // Returns undefined if not found
  1391. },
  1392. toFullNameArray() {
  1393. return this._states.map((a) => a[0]);
  1394. },
  1395. toAbbrArray() {
  1396. return this._states.map((a) => a[1]);
  1397. },
  1398. fromId(id) {
  1399. return this._states.find((a) => a[2] === id); // Returns undefined if not found
  1400. },
  1401. };
  1402. const DEFAULT_VISIBLE_AT_ZOOM = 18;
  1403. const SETTINGS_STORE_NAME = 'wme_gis_layers_fl';
  1404. const COUNTIES_URL = 'https://tigerweb.geo.census.gov/arcgis/rest/services/Census2020/State_County/MapServer/1/';
  1405. const scriptName = GM_info.script.name;
  1406. const scriptVersion = GM_info.script.version;
  1407. const downloadUrl = 'https://greasyfork.org/scripts/369632-wme-gis-layers/code/WME%20GIS%20Layers.user.js';
  1408. const sdk = await bootstrap({ scriptUpdateMonitor: { downloadUrl } });
  1409. let settings = {};
  1410. let ignoreFetch = false;
  1411. let lastToken = {};
  1412. let userInfo;
  1413.  
  1414. // Variables to store Label popup position and selected layer
  1415. const layerLabels = {};
  1416. let isPopupVisible = null;
  1417. const popupPosition = { left: '50%', top: '50%' };
  1418. let popupActiveLayer = null;
  1419. let useAcronyms = false;
  1420. let useTitleCase = false;
  1421. let useStateHwy = false;
  1422. let removeNewLines = false;
  1423.  
  1424. const DEBUG = true;
  1425. // function log(message) { console.log('GIS Layers:', message); }
  1426. function logError(message, args = []) {
  1427. console.error(`${scriptName}:`, message, ...args);
  1428. }
  1429. function logDebug(message, args = []) {
  1430. if (DEBUG) console.debug(`${scriptName}:`, message, ...args);
  1431. }
  1432. // function logWarning(message) { console.warn('GIS Layers:', message); }
  1433.  
  1434. let _layerSettingsDialog;
  1435.  
  1436. class LayerSettingsDialog {
  1437. #gisLayer;
  1438. #minVisibleAtZoom = 12;
  1439. #maxVisibleAtZoom = 22;
  1440. #titleText;
  1441. #shiftUpButton;
  1442. #visibleAtZoomInput;
  1443.  
  1444. constructor() {
  1445. this.#titleText = $('<span>');
  1446. const closeButton = $('<span>', {
  1447. style: 'cursor:pointer;padding-left:4px;font-size:17px;color:#d6e6f3;float:right;',
  1448. class: 'fa fa-window-close',
  1449. }).click(() => this.#onCloseButtonClick());
  1450. const shiftUpButton = LayerSettingsDialog.#createShiftButton('fa-angle-up').click(() => this.#onShiftButtonClick(0, 1));
  1451. const shiftLeftButton = LayerSettingsDialog.#createShiftButton('fa-angle-left').click(() => this.#onShiftButtonClick(-1, 0));
  1452. const shiftRightButton = LayerSettingsDialog.#createShiftButton('fa-angle-right').click(() => this.#onShiftButtonClick(1, 0));
  1453. const shiftDownButton = LayerSettingsDialog.#createShiftButton('fa-angle-down').click(() => this.#onShiftButtonClick(0, -1));
  1454. const resetOffsetButton = $('<button>', {
  1455. class: 'form-control',
  1456. style: 'height: 24px; width: auto; padding: 2px 6px 0px 6px; display: inline-block; float: right;',
  1457. })
  1458. .text('Reset')
  1459. .click(() => this.#onResetOffsetButtonClick());
  1460.  
  1461. this._dialogDiv = $('<div>', {
  1462. style:
  1463. 'position: fixed; top: 15%; left: 400px; width: 200px; z-index: 100; background-color: #73a9bd; border-width: 1px; border-style: solid;' +
  1464. 'border-radius: 10px; box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.7); border-color: #50667b; padding: 4px;',
  1465. }).append(
  1466. $('<div>').append(
  1467. // The extra div is needed here. When the header text wraps, the main dialog div won't expand properly without it.
  1468. // HEADER
  1469. $('<div>', { style: 'border-radius:5px 5px 0px 0px; padding: 4px; color: #fff; font-weight: bold; text-align:left; cursor: default;' }).append(closeButton, this.#titleText),
  1470. // BODY
  1471. $('<div>').append(
  1472. $('<div>', { style: 'border-radius: 5px; width: 100%; padding: 4px; background-color:#d6e6f3; display:inline-block; margin-right:5px;' }).append(
  1473. resetOffsetButton,
  1474. $('<input>', {
  1475. type: 'radio',
  1476. id: 'gisLayerShiftAmt1',
  1477. name: 'gisLayerShiftAmt',
  1478. value: '1',
  1479. checked: 'checked',
  1480. }),
  1481. $('<label>', { for: 'gisLayerShiftAmt1' }).text('1m'),
  1482. $('<input>', {
  1483. type: 'radio',
  1484. id: 'gisLayerShiftAmt10',
  1485. name: 'gisLayerShiftAmt',
  1486. value: '10',
  1487. style: 'margin-left: 6px',
  1488. }),
  1489. $('<label>', { for: 'gisLayerShiftAmt10' }).text('10m'),
  1490. $('<div>', { style: 'padding: 4px' }).append(
  1491. $('<table>', { style: 'table-layout:fixed; width:60px; height:84px; margin-left:auto;margin-right:auto;' }).append(
  1492. $('<tr>', { style: 'width: 20px; height: 28px;' }).append($('<td>', { align: 'center' }), $('<td>', { align: 'center' }).append(shiftUpButton), $('<td>', { align: 'center' })),
  1493. $('<tr>', { style: 'width: 20px; height: 28px;' }).append(
  1494. $('<td>', { align: 'center' }).append(shiftLeftButton),
  1495. $('<td>', { align: 'center' }),
  1496. $('<td>', { align: 'center' }).append(shiftRightButton)
  1497. ),
  1498. $('<tr>', { style: 'width: 20px; height: 28px;' }).append($('<td>', { align: 'center' }), $('<td>', { align: 'center' }).append(shiftDownButton), $('<td>', { align: 'center' }))
  1499. )
  1500. )
  1501. ),
  1502. $('<div>', { style: 'border-radius: 5px; width: 100%; padding: 4px; background-color: #d6e6f3; display: inline-block; margin-right: 5px; margin-top: 2px;' }).append(
  1503. $('<div>', { style: 'display: flex; justify-content: flex-end; margin-bottom: 4px;' }).append(
  1504. $('<button>', { class: 'form-control', style: 'height: 24px; width: auto; padding: 2px 6px 0px 6px;' }).text('Reset').click(this.#onResetVisibleAtZoomClick.bind(this))
  1505. ),
  1506. $('<div>').append(
  1507. $('<label>', { for: 'visible-at-zoom-input' }).text('Visible at zoom:'),
  1508. (this.#visibleAtZoomInput = $('<input>', {
  1509. type: 'number',
  1510. id: 'visible-at-zoom-input',
  1511. min: this.#minVisibleAtZoom,
  1512. max: this.#maxVisibleAtZoom,
  1513. style: 'margin-left: 4px;',
  1514. }).change((v) => this.#onVisibleAtZoomChange(v)))
  1515. ),
  1516. $('<div>', { style: 'font-size: 13px; color: gray' }).text('Pan or zoom the map to refresh after changing.\n\nSetting this value too low may cause performance issues.')
  1517. )
  1518. )
  1519. )
  1520. );
  1521.  
  1522. this.hide();
  1523. this._dialogDiv.appendTo('body');
  1524.  
  1525. if (typeof jQuery.ui !== 'undefined') {
  1526. const that = this;
  1527. this._dialogDiv.draggable({
  1528. // Gotta nuke the height setting the dragging inserts otherwise the panel cannot dynamically resize
  1529. stop() {
  1530. that._dialogDiv.css('height', '');
  1531. },
  1532. });
  1533. }
  1534. }
  1535.  
  1536. get gisLayer() {
  1537. return this.#gisLayer;
  1538. }
  1539.  
  1540. set gisLayer(value) {
  1541. if (value !== this.#gisLayer) {
  1542. this.#gisLayer = value;
  1543. this.#titleText.text(this.#gisLayer.name);
  1544. this.#initVisibleAtZoomInput();
  1545. }
  1546. }
  1547.  
  1548. #initVisibleAtZoomInput() {
  1549. this.#visibleAtZoomInput.val(getGisLayerVisibleAtZoom(this.#gisLayer));
  1550. }
  1551.  
  1552. // eslint-disable-next-line class-methods-use-this
  1553. getShiftAmount() {
  1554. return $('input[name=gisLayerShiftAmt]:checked').val();
  1555. }
  1556.  
  1557. show() {
  1558. this._dialogDiv.show();
  1559. }
  1560.  
  1561. hide() {
  1562. this._dialogDiv.hide();
  1563. }
  1564.  
  1565. #onResetVisibleAtZoomClick() {
  1566. settings.removeLayerSetting(this.#gisLayer.id, 'visibleAtZoom');
  1567. this.#initVisibleAtZoomInput();
  1568. }
  1569.  
  1570. #onCloseButtonClick() {
  1571. this.hide();
  1572. }
  1573.  
  1574. #onVisibleAtZoomChange() {
  1575. const min = this.#minVisibleAtZoom;
  1576. const max = this.#maxVisibleAtZoom;
  1577. let value = parseInt(this.#visibleAtZoomInput.val(), 10);
  1578.  
  1579. if (value < min) {
  1580. value = min;
  1581. this.#visibleAtZoomInput.val(value);
  1582. } else if (value > max) {
  1583. value = max;
  1584. this.#visibleAtZoomInput.val(value);
  1585. }
  1586.  
  1587. settings.setLayerSetting(this.#gisLayer.id, 'visibleAtZoom', value);
  1588. saveSettingsToStorage();
  1589. }
  1590.  
  1591. #onShiftButtonClick(x, y) {
  1592. const shiftAmount = this.getShiftAmount();
  1593. x *= shiftAmount;
  1594. y *= shiftAmount;
  1595. this.#shiftLayerFeatures(x, y);
  1596. const { id } = this.gisLayer;
  1597. let offset = settings.getLayerSetting(id, 'offset');
  1598. if (!offset) {
  1599. offset = { x: 0, y: 0 };
  1600. settings.setLayerSetting(id, 'offset', offset);
  1601. }
  1602. offset.x += x;
  1603. offset.y += y;
  1604. saveSettingsToStorage();
  1605. }
  1606.  
  1607. #onResetOffsetButtonClick() {
  1608. const offset = settings.getLayerSetting(this.gisLayer.id, 'offset');
  1609. if (offset) {
  1610. this.#shiftLayerFeatures(offset.x * -1, offset.y * -1);
  1611. delete settings.layers[this.gisLayer.id].offset;
  1612. saveSettingsToStorage();
  1613. }
  1614. }
  1615.  
  1616. #shiftLayerFeatures(x, y) {
  1617. const { isRoadLayer } = this.gisLayer;
  1618. let featureCollection = isRoadLayer ? roadFeatures : defaultFeatures;
  1619. const { distance, bearing } = LayerSettingsDialog.#calculateDistanceAndBearing(x, y);
  1620. featureCollection = featureCollection.filter((f) => f.properties.layerID === this.gisLayer.id).map((f) => turf.transformTranslate(f, distance, bearing, { units: 'meters' }));
  1621. if (isRoadLayer) {
  1622. roadFeatures = featureCollection;
  1623. } else {
  1624. defaultFeatures = featureCollection;
  1625. }
  1626. const layerName = isRoadLayer ? ROAD_LAYER_NAME : DEFAULT_LAYER_NAME;
  1627. const featureIds = featureCollection.map((f) => f.id);
  1628. sdk.Map.removeFeaturesFromLayer({ layerName, featureIds });
  1629. sdk.Map.addFeaturesToLayer({ layerName, features: featureCollection });
  1630. }
  1631.  
  1632. /**
  1633. * Calculates the total distance and bearing from X and Y meter offsets.
  1634. * @param {number} dx_meters - X offset in meters (east/west).
  1635. * @param {number} dy_meters - Y offset in meters (north/south).
  1636. * @returns {{distance: number, bearing: number}}
  1637. */
  1638. static #calculateDistanceAndBearing(dx_meters, dy_meters) {
  1639. const distance = Math.sqrt(dx_meters ** 2 + dy_meters ** 2);
  1640.  
  1641. // Calculate bearing in radians
  1642. // Math.atan2(y, x) returns angle in radians between -PI and PI
  1643. // Need to adjust to be 0-360 degrees clockwise from North
  1644. const bearing_rad = Math.atan2(dx_meters, dy_meters); // dx_meters is 'x' (east), dy_meters is 'y' (north)
  1645.  
  1646. // Convert to degrees and adjust for 0-360, clockwise from North
  1647. let bearing_deg = bearing_rad * (180 / Math.PI);
  1648. bearing_deg = (bearing_deg + 360) % 360; // Ensure positive and within 0-360 range
  1649.  
  1650. return { distance, bearing: bearing_deg };
  1651. }
  1652.  
  1653. static #createShiftButton(fontAwesomeClass) {
  1654. return $('<button>', {
  1655. class: 'form-control',
  1656. style: 'cursor:pointer;font-size:14px;padding: 3px;border-radius: 5px;width: 21px;height: 21px;',
  1657. }).append($('<i>', { class: 'fa', style: 'vertical-align: super' }).addClass(fontAwesomeClass));
  1658. }
  1659. }
  1660.  
  1661. function loadSettingsFromStorage() {
  1662. const defaultSettings = {
  1663. lastVersion: null,
  1664. visibleLayers: [],
  1665. onlyShowApplicableLayers: false,
  1666. selectedStates: [],
  1667. enabled: true,
  1668. fillParcels: false,
  1669. oneTimeAlerts: {},
  1670. layers: {},
  1671. shortcuts: {},
  1672. isPopupVisible: false,
  1673. useAcronyms: false,
  1674. useTitleCase: false,
  1675. useStateHwy: false,
  1676. removeNewLines: false,
  1677. collapsedSections: {},
  1678. };
  1679.  
  1680. let loadedSettings = {}; // Initialize as an empty object
  1681. const storedSettings = localStorage.getItem(SETTINGS_STORE_NAME);
  1682.  
  1683. if (storedSettings) {
  1684. try {
  1685. const parsed = JSON.parse(storedSettings);
  1686. if (parsed && typeof parsed === 'object') {
  1687. loadedSettings = parsed;
  1688. } else {
  1689. logDebug(`Stored settings under key "${SETTINGS_STORE_NAME}" were not a valid object.`);
  1690. }
  1691. } catch (e) {
  1692. logError(`Failed to parse settings from localStorage key "${SETTINGS_STORE_NAME}":`, e);
  1693. // loadedSettings remains {}
  1694. }
  1695. }
  1696.  
  1697. // Merge defaultSettings and loadedSettings.
  1698. // If loadedSettings is empty (due to error or no storage), it effectively uses defaults.
  1699. settings = { ...defaultSettings, ...loadedSettings };
  1700.  
  1701. isPopupVisible = settings.isPopupVisible;
  1702. useAcronyms = settings.useAcronyms;
  1703. useTitleCase = settings.useTitleCase;
  1704. useStateHwy = settings.useStateHwy;
  1705. removeNewLines = settings.removeNewLines;
  1706.  
  1707. settings.getLayerSetting = function getLayerSetting(layerID, settingName) {
  1708. const layerSettings = this.layers[layerID];
  1709. if (!layerSettings) {
  1710. return undefined;
  1711. }
  1712. return layerSettings[settingName];
  1713. };
  1714. settings.setLayerSetting = function setLayerSetting(layerID, settingName, value) {
  1715. let layerSettings = this.layers[layerID];
  1716. if (!layerSettings) {
  1717. layerSettings = {};
  1718. this.layers[layerID] = layerSettings;
  1719. }
  1720. layerSettings[settingName] = value;
  1721. };
  1722. settings.removeLayerSetting = function removeLayerSetting(layerID, settingName) {
  1723. const layerSettings = this.layers[layerID];
  1724. if (layerSettings) {
  1725. delete layerSettings[settingName];
  1726. }
  1727. };
  1728.  
  1729. // Handle legacy shortcut keys settings.
  1730. // TODO: Delete this later, after most users have updated.
  1731. if (settings.toggleHnsOnlyShortcut) {
  1732. settings.shortcuts.toggleHnsOnly = settings.toggleHnsOnlyShortcut;
  1733. delete settings.toggleHnsOnlyShortcut;
  1734. }
  1735. if (settings.toggleEnabledShortcut) {
  1736. settings.shortcuts.toggleEnabled = settings.toggleEnabledShortcut;
  1737. delete settings.toggleEnabledShortcut;
  1738. }
  1739. }
  1740.  
  1741. function saveSettingsToStorage() {
  1742. settings.shortcuts = {};
  1743. sdk.Shortcuts.getAllShortcuts().forEach((shortcut) => {
  1744. settings.shortcuts[shortcut.shortcutId] = shortcut.shortcutKeys;
  1745. });
  1746. settings.lastVersion = scriptVersion;
  1747. settings.isPopupVisible = isPopupVisible;
  1748. settings.useAcronyms = useAcronyms;
  1749. settings.useTitleCase = useTitleCase;
  1750. settings.useStateHwy = useStateHwy;
  1751. settings.removeNewLines = removeNewLines;
  1752. localStorage.setItem(SETTINGS_STORE_NAME, JSON.stringify(settings));
  1753. logDebug('Settings saved');
  1754. }
  1755.  
  1756. function getMaxAllowableOffsetForZoom(zoomLevel) {
  1757. const zoomToOffsetMap = {
  1758. 12: 0.0009, // ~100 meters
  1759. 13: 0.00045, // ~50 meters
  1760. 14: 0.000225, // ~25 meters
  1761. 15: 0.0001125, // ~12.0 meters
  1762. 16: 0.000056, // ~6.0 meters
  1763. 17: 0.000028, // ~3.0 meters
  1764. 18: 0.000014, // ~1.5 meters
  1765. 19: 0.000007, // ~1.0 meters
  1766. 20: 0.000007, // ~1.0 meters
  1767. 21: 0.000007, // ~1.0 meters
  1768. 22: 0.000007, // ~1.0 meters
  1769. };
  1770. // Return the offset corresponding to the provided zoom level, or default to highest detail if not found
  1771. return zoomToOffsetMap[zoomLevel] || zoomToOffsetMap[22];
  1772. }
  1773.  
  1774. function getUrl(extent, gisLayer, zoom) {
  1775. const layerOffset = settings.getLayerSetting(gisLayer.id, 'offset') ?? { x: 0, y: 0 };
  1776. const geometry = {
  1777. xmin: extent[0] - layerOffset.x,
  1778. ymin: extent[1] - layerOffset.y,
  1779. xmax: extent[2] - layerOffset.x,
  1780. ymax: extent[3] - layerOffset.y,
  1781. spatialReference: {
  1782. wkid: 4326,
  1783. },
  1784. };
  1785.  
  1786. const maxAllowableOffset = getMaxAllowableOffsetForZoom(zoom);
  1787. const geometryStr = JSON.stringify(geometry);
  1788.  
  1789. let fields = gisLayer.labelFields;
  1790. if (gisLayer.labelHeaderFields) {
  1791. fields = fields.concat(gisLayer.labelHeaderFields);
  1792. }
  1793. if (gisLayer.distinctFields) {
  1794. fields = fields.concat(gisLayer.distinctFields);
  1795. }
  1796. let url = `${gisLayer.url}/query?geometry=${encodeURIComponent(geometryStr)}`;
  1797. url += gisLayer.token ? `&token=${gisLayer.token}` : '';
  1798. url += `&outFields=${encodeURIComponent(fields.join(','))}`;
  1799. url += '&returnGeometry=true&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope';
  1800. url += `&inSR=${'4326'}`;
  1801. url += '&outSR=4326&f=json';
  1802. url += `&maxAllowableOffset=${maxAllowableOffset}`;
  1803. url += gisLayer.where ? `&where=${encodeURIComponent(gisLayer.where)}` : '';
  1804.  
  1805. logDebug(`Request URL: ${url}`);
  1806. return url;
  1807. }
  1808.  
  1809. function hashString(value) {
  1810. let hash = 0;
  1811. if (value.length === 0) return hash;
  1812. for (let i = 0; i < value.length; i++) {
  1813. const chr = value.charCodeAt(i);
  1814. // eslint-disable-next-line no-bitwise
  1815. hash = (hash << 5) - hash + chr;
  1816. // eslint-disable-next-line no-bitwise
  1817. hash |= 0; // Convert to 32bit integer
  1818. }
  1819. return hash;
  1820. }
  1821.  
  1822. function getMapExtent(projection = 'wgs84') {
  1823. const wgs84Extent = sdk.Map.getMapExtent(); // Assume this provides WGS84 coordinates
  1824. //const wgs84LeftBottom = [wgs84Extent[0], wgs84Extent[1]]; //JS55CT
  1825. //const wgs84RightTop = [wgs84Extent[2], wgs84Extent[3]];
  1826. const wgs84Projections = ['wgs84', 'CRS84', '4326', 'EPSG:4326'];
  1827.  
  1828. if (wgs84Projections.includes(projection.toLowerCase())) {
  1829. return [wgs84Extent[0], wgs84Extent[1], wgs84Extent[2], wgs84Extent[3]];
  1830. } else {
  1831. throw new Error('Unsupported projection type');
  1832. }
  1833. }
  1834.  
  1835. function getArcGisMapExtentGeometry() {
  1836. const extent = getMapExtent('wgs84');
  1837. const geometry = {
  1838. xmin: extent[0],
  1839. ymin: extent[1],
  1840. xmax: extent[2],
  1841. ymax: extent[3],
  1842. spatialReference: {
  1843. wkid: 4326,
  1844. },
  1845. };
  1846. return geometry;
  1847. }
  1848.  
  1849. function getCountiesUrl() {
  1850. const geometry = getArcGisMapExtentGeometry();
  1851. const url = `${COUNTIES_URL}/query?geometry=${encodeURIComponent(JSON.stringify(geometry))}`;
  1852. return `${url}&outFields=BASENAME%2CSTATE&returnGeometry=false&spatialRel=esriSpatialRelIntersects` + '&geometryType=esriGeometryEnvelope&inSR=4326&outSR=4326&f=json';
  1853. }
  1854.  
  1855. let _countiesInExtent = [];
  1856.  
  1857. function getGisLayerVisibleAtZoom(gisLayer) {
  1858. const overrideVisibleAtZoom = settings.getLayerSetting(gisLayer.id, 'visibleAtZoom');
  1859. if (overrideVisibleAtZoom) return overrideVisibleAtZoom;
  1860. return gisLayer.hasOwnProperty('visibleAtZoom') ? gisLayer.visibleAtZoom : DEFAULT_VISIBLE_AT_ZOOM;
  1861. }
  1862.  
  1863. function getGisLayerLabelsVisibleAtZoom(gisLayer, layerVisibleAtZoom) {
  1864. let labelsVisibleAtZoom;
  1865. layerVisibleAtZoom = +layerVisibleAtZoom;
  1866. if (gisLayer.hasOwnProperty('labelsVisibleAtZoom')) {
  1867. labelsVisibleAtZoom = layerVisibleAtZoom + (+gisLayer.labelsVisibleAtZoom - (+gisLayer.visibleAtZoom ?? DEFAULT_VISIBLE_AT_ZOOM));
  1868. } else {
  1869. labelsVisibleAtZoom = layerVisibleAtZoom + 1;
  1870. }
  1871. if (labelsVisibleAtZoom < 1) labelsVisibleAtZoom = 1;
  1872. return labelsVisibleAtZoom;
  1873. }
  1874.  
  1875. function getFetchableLayers(getInvisible) {
  1876. const zoom = sdk.Map.getZoomLevel();
  1877. if (zoom < 12) return [];
  1878. return _gisLayers.filter((gisLayer) => {
  1879. const isValidUrl = gisLayer.url && gisLayer.url.trim().length > 0;
  1880. const isVisible = (getInvisible || settings.visibleLayers.includes(gisLayer.id)) && settings.selectedStates.includes(gisLayer.state);
  1881. const isInState = gisLayer.state === 'US' || _countiesInExtent.some((county) => county.stateInfo[1] === gisLayer.state);
  1882. // Be sure to use hasOwnProperty when checking this, since 0 is a valid value.
  1883. const isValidZoom = getInvisible || zoom >= getGisLayerVisibleAtZoom(gisLayer);
  1884. return isValidUrl && isInState && isVisible && isValidZoom;
  1885. });
  1886. }
  1887.  
  1888. function filterLayerCheckboxes() {
  1889. const applicableLayers = getFetchableLayers(true).filter((layer) => {
  1890. const hasCounties = layer.hasOwnProperty('counties');
  1891. return (hasCounties && layer.counties.some((countyName) => _countiesInExtent.some((county) => county.name === countyName.toLowerCase() && layer.state === county.stateInfo[1]))) || !hasCounties;
  1892. });
  1893. const statesToHide = STATES.toAbbrArray();
  1894.  
  1895. _gisLayers.forEach((gisLayer) => {
  1896. const id = `#gis-layer-${gisLayer.id}-container`;
  1897. if (!settings.onlyShowApplicableLayers || applicableLayers.includes(gisLayer)) {
  1898. $(id).show();
  1899. $(`#gis-layers-for-${gisLayer.state}`).show();
  1900. const idx = statesToHide.indexOf(gisLayer.state);
  1901. if (idx > -1) statesToHide.splice(idx, 1);
  1902. } else {
  1903. $(id).hide();
  1904. }
  1905. });
  1906. if (settings.onlyShowApplicableLayers) {
  1907. statesToHide.forEach((st) => $(`#gis-layers-for-${st}`).hide());
  1908. }
  1909. }
  1910.  
  1911. const ROAD_ABBR = [
  1912. [/\bAVENUE$/, 'AVE'],
  1913. [/\bCIRCLE$/, 'CIR'],
  1914. [/\bCOURT$/, 'CT'],
  1915. [/\bDRIVE$/, 'DR'],
  1916. [/\bLANE$/, 'LN'],
  1917. [/\bPARK$/, 'PK'],
  1918. [/\bPLACE$/, 'PL'],
  1919. [/\bROAD$/, 'RD'],
  1920. [/\bSTREET$/, 'ST'],
  1921. [/\bTERRACE$/, 'TER'],
  1922. ];
  1923.  
  1924. const labelProcessingGlobalVariables = {
  1925. Number,
  1926. Math,
  1927. Boolean,
  1928. parseInt,
  1929. Date,
  1930. _regexReplace: {
  1931. // Strip leading zeros or blank full label for any label starting with a non-digit or
  1932. // is a Zero Address, use with '' as replace.
  1933. r0: /^(0+(\s.*)?|\D.*)/,
  1934. // Strip Everything After Street Type to end of the string by use $1 and $2 capture
  1935. // groups, use with replace '$1$2'
  1936. // eslint-disable-next-line max-len
  1937. r1: /^(.* )(Ave(nue)?|Dr(ive)?|St(reet)?|C(our)?t|Cir(cle)?|Blvd|Boulevard|Pl(ace)?|Ln|Lane|Fwy|Freeway|R(oa)?d|Ter(r|race)?|Tr(ai)?l|Way|Rte \d+|Route \d+)\b.*/gi,
  1938. // Strip SPACE 5 Digits from end of string, use with replace ''
  1939. r2: /\s\d{5}$/,
  1940. // Strip Everything after a "~", ",", ";" to the end of the string, use with replace ''
  1941. r3: /(~|,|;|\s?\r\n).*$/,
  1942. // Move the digits after the last space to before the rest of the string using, use with
  1943. // replace '$2 $1'
  1944. r4: /^(.*)\s(\d+).*/,
  1945. // Insert newline between digits (including "-") and everything after the digits,
  1946. // except(and before) a ",", use with replace '$1\n$2'
  1947. r5: /^([-\d]+)\s+([^,]+).*/,
  1948. // Insert newline between digits and everything after the digits, use with
  1949. // replace '$1\n$2'
  1950. r6: /^(\d+)\s+(.*)/,
  1951. },
  1952. };
  1953.  
  1954. function processLabel(gisLayer, item, displayLabelsAtZoom, area, isPolyLine = false) {
  1955. let label = '';
  1956. if (gisLayer.labelHeaderFields) {
  1957. label = `${gisLayer.labelHeaderFields
  1958. .map((fieldName) => item.attributes[fieldName])
  1959. .join(' ')
  1960. .trim()}\n`;
  1961. }
  1962. if (sdk.Map.getZoomLevel() >= displayLabelsAtZoom || area >= 1000000) { // Raised this 1 million sq meeters
  1963. label += gisLayer.labelFields
  1964. .map((fieldName) => item.attributes[fieldName])
  1965. .join(' ')
  1966. .trim();
  1967. if (gisLayer.processLabel) {
  1968. if (gisLayer.labelProcessingError) {
  1969. label = 'ERROR';
  1970. } else {
  1971. labelProcessingGlobalVariables.label = label;
  1972. labelProcessingGlobalVariables.fieldValues = item.attributes;
  1973. const result = ESTreeProcessor.execute(gisLayer.processLabel, labelProcessingGlobalVariables);
  1974. label = result.output?.trim() ?? '';
  1975. }
  1976. }
  1977. }
  1978.  
  1979. if (!isPolyLine) {
  1980. if (label && ['points', 'parcels', 'state_points', 'state_parcels'].includes(gisLayer.style)) {
  1981. if (settings.addrLabelDisplay === 'hn') {
  1982. const m = label.match(/^\d+/);
  1983. label = m ? m[0] : '';
  1984. } else if (settings.addrLabelDisplay === 'street') {
  1985. const m = label.match(/^(?:\d+\s)?(.*)/);
  1986. label = m ? m[1].trim() : '';
  1987. } else if (settings.addrLabelDisplay === 'none') {
  1988. label = '';
  1989. }
  1990. }
  1991. }
  1992. return label;
  1993. }
  1994.  
  1995. let lastFeatureId = 0;
  1996. function generateFeatureId() {
  1997. lastFeatureId++;
  1998. return lastFeatureId;
  1999. }
  2000.  
  2001. // SDK: Remove these once Map.getFeaturesByProperty is implemented: https://issuetracker.google.com/issues/419596843
  2002. let defaultFeatures = [];
  2003. let roadFeatures = [];
  2004.  
  2005. function processFeatures(data, token, gisLayer) {
  2006. const features = [];
  2007.  
  2008. if (data.skipIt) {
  2009. // do nothing
  2010. } else if (data.error) {
  2011. logError(`Error in layer "${gisLayer.name}": ${data.error.message}`);
  2012. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  2013. } else {
  2014. const items = data.features || [];
  2015. if (!token.cancel) {
  2016. let error = false;
  2017. const distinctValues = [];
  2018. items.forEach((item) => {
  2019. const featuresToAdd = [];
  2020. let skipIt = false;
  2021. if (!token.cancel && !error) {
  2022. const layerOffset = settings.getLayerSetting(gisLayer.id, 'offset') ?? { x: 0, y: 0 };
  2023.  
  2024. if (gisLayer.distinctFields) {
  2025. if (distinctValues.some((v) => gisLayer.distinctFields.every((fld) => v[fld] === item.attributes[fld]))) {
  2026. skipIt = true;
  2027. } else {
  2028. const dist = {};
  2029. gisLayer.distinctFields.forEach((fld) => (dist[fld] = item.attributes[fld]));
  2030. distinctValues.push(dist);
  2031. }
  2032. }
  2033. if (!skipIt) {
  2034. let area = 0; // Default area is 0 for non-polygon features
  2035. const displayLabelsAtZoom = getGisLayerLabelsVisibleAtZoom(gisLayer, getGisLayerVisibleAtZoom(gisLayer));
  2036. if (item.geometry) {
  2037. if (item.geometry.x) {
  2038. const feature = turf.point([item.geometry.x + layerOffset.x, item.geometry.y + layerOffset.y]);
  2039. const label = processLabel(gisLayer, item, displayLabelsAtZoom, '', false);
  2040.  
  2041. feature.properties = {
  2042. layerID: gisLayer.id,
  2043. label,
  2044. };
  2045. feature.id = generateFeatureId();
  2046. features.push(feature);
  2047.  
  2048. if (isPopupVisible) {
  2049. addLabelToLayer(gisLayer.name, label);
  2050. }
  2051. } else if (item.geometry.points) {
  2052. const points = item.geometry.points.map((point) => turf.point([point[0] + layerOffset.x, point[1] + layerOffset.y]));
  2053. featuresToAdd.push(...points);
  2054.  
  2055. points.forEach((pointFeature) => {
  2056. const label = processLabel(gisLayer, item, displayLabelsAtZoom, '', false);
  2057. pointFeature.properties = {
  2058. layerID: gisLayer.id,
  2059. label,
  2060. };
  2061. pointFeature.id = generateFeatureId();
  2062. features.push(pointFeature);
  2063.  
  2064. if (isPopupVisible) {
  2065. addLabelToLayer(gisLayer.name, label);
  2066. }
  2067. });
  2068. } else if (item.geometry.rings) {
  2069. const separatePolygons = [];
  2070. let currentOuterRing = null;
  2071. const innerRings = [];
  2072.  
  2073. item.geometry.rings.forEach((ringIn) => {
  2074. const ring = ringIn.map((point) => [point[0] + layerOffset.x, point[1] + layerOffset.y]);
  2075.  
  2076. if (turf.booleanClockwise(ring)) {
  2077. // Store previous polygon
  2078. if (currentOuterRing) {
  2079. separatePolygons.push({
  2080. outer: currentOuterRing,
  2081. inners: [...innerRings],
  2082. });
  2083. }
  2084. // Begin new outer ring and reset inner rings
  2085. currentOuterRing = ring;
  2086. innerRings.length = 0;
  2087. } else {
  2088. // It's an inner ring, push to current inner rings list
  2089. innerRings.push(ring);
  2090. }
  2091. });
  2092.  
  2093. // Add final polygon (if any)
  2094. if (currentOuterRing) {
  2095. separatePolygons.push({
  2096. outer: currentOuterRing,
  2097. inners: [...innerRings],
  2098. });
  2099. }
  2100.  
  2101. // Create features for each polygon group
  2102. separatePolygons.forEach(({ outer, inners }) => {
  2103. const polygonRings = [outer, ...inners];
  2104. const tempPolygon = turf.polygon(polygonRings);
  2105. const ringArea = turf.area(tempPolygon);
  2106. const label = processLabel(gisLayer, item, displayLabelsAtZoom, ringArea, false);
  2107.  
  2108. tempPolygon.properties = {
  2109. layerID: gisLayer.id,
  2110. label,
  2111. };
  2112. tempPolygon.id = generateFeatureId();
  2113.  
  2114. if (isPopupVisible) {
  2115. addLabelToLayer(gisLayer.name, label);
  2116. }
  2117. features.push(tempPolygon);
  2118. });
  2119. } else if (data.geometryType === 'esriGeometryPolyline') {
  2120. const mls = turf.multiLineString(item.geometry.paths);
  2121. const e = getMapExtent('wgs84');
  2122. const bbox = [e[0], e[1], e[2], e[3]];
  2123. const clipped = turf.bboxClip(mls, bbox);
  2124.  
  2125. if (clipped.geometry.type === 'LineString') {
  2126. item.geometry.paths = [clipped.geometry.coordinates];
  2127. } else if (clipped.geometry.type === 'MultiLineString') {
  2128. item.geometry.paths = clipped.geometry.coordinates;
  2129. }
  2130.  
  2131. item.geometry.paths.forEach((path) => {
  2132. const pointList = path.map((point) => [point[0] + layerOffset.x, point[1] + layerOffset.y]);
  2133. const feature = turf.lineString(pointList);
  2134. feature.skipDupeCheck = true;
  2135. featuresToAdd.push(feature);
  2136. const label = processLabel(gisLayer, item, displayLabelsAtZoom, '', true);
  2137.  
  2138. feature.properties = {
  2139. layerID: gisLayer.id,
  2140. label,
  2141. };
  2142. feature.id = generateFeatureId();
  2143.  
  2144. if (isPopupVisible) {
  2145. addLabelToLayer(gisLayer.name, label);
  2146. }
  2147. features.push(feature);
  2148. });
  2149. } else {
  2150. logDebug(`Unexpected feature type in layer: ${JSON.stringify(item)}`);
  2151. logError(`Error: Unexpected feature type in layer "${gisLayer.name}"`);
  2152. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  2153. error = true;
  2154. }
  2155. }
  2156. }
  2157. }
  2158. });
  2159. }
  2160. }
  2161. if (!token.cancel) {
  2162. // Check for duplicate geometries.
  2163. for (let i = 0; i < features.length; i++) {
  2164. const f1 = features[i];
  2165. if (f1.geometry.type === 'Point' && !f1.skipDupeCheck && f1.properties.label) {
  2166. let labels = [f1.properties.label];
  2167. for (let j = i + 1; j < features.length; j++) {
  2168. const f2 = features[j];
  2169. if (f2.geometry.type === 'Point' && !f2.skipDupeCheck && f2.properties.label && turf.distance(f1, f2, { units: 'meters' }) < 1) {
  2170. features.splice(j, 1);
  2171. labels.push(f2.properties.label);
  2172. j--;
  2173. }
  2174. }
  2175. labels = _.uniq(labels);
  2176. if (labels.length > 1) {
  2177. labels.forEach((label, idx) => {
  2178. label = label
  2179. .replace(/\n/g, ' ')
  2180. .replace(/\s{2,}/, ' ')
  2181. .replace(/\bUNIT\s.{1,5}$/i, '')
  2182. .trim();
  2183. ROAD_ABBR.forEach((abbr) => (label = label.replace(abbr[0], abbr[1])));
  2184. labels[idx] = label;
  2185. });
  2186. labels = _.uniq(labels);
  2187. labels.sort();
  2188. if (labels.length > 12) {
  2189. const len = labels.length;
  2190. labels = labels.slice(0, 10);
  2191. labels.push(`(${len - 10} more...)`);
  2192. }
  2193. f1.properties.label = _.uniq(labels).join('\n');
  2194. } else {
  2195. let { label } = f1.properties;
  2196. ROAD_ABBR.forEach((abbr) => (label = label.replace(abbr[0], abbr[1])));
  2197. f1.properties.label = label;
  2198. }
  2199. }
  2200. }
  2201.  
  2202. // Determine layer and source collection
  2203. const isRoad = gisLayer.isRoadLayer;
  2204. const layerName = isRoad ? ROAD_LAYER_NAME : DEFAULT_LAYER_NAME;
  2205. const sourceCollection = isRoad ? roadFeatures : defaultFeatures;
  2206.  
  2207. // Process the collection in one go
  2208. const { featureIdsToRemove, remainingFeatures } = sourceCollection.reduce(
  2209. (acc, feature) => {
  2210. if (feature.properties.layerID === gisLayer.id) {
  2211. acc.featureIdsToRemove.push(feature.id); // Collect IDs to remove
  2212. } else {
  2213. acc.remainingFeatures.push(feature); // Collect features to keep
  2214. }
  2215. return acc;
  2216. },
  2217. { featureIdsToRemove: [], remainingFeatures: [] }
  2218. );
  2219.  
  2220. // Initialize counters for individual feature addition
  2221. let successCount = features.length;
  2222.  
  2223. // Track the total processing time for the layer
  2224. const layerStartTime = performance.now();
  2225.  
  2226. sdk.Map.dangerouslyAddFeaturesToLayerWithoutValidation({ features, layerName });
  2227.  
  2228. // Handle completion logging
  2229. // Calculate and log the total processing time for the layer
  2230. const layerEndTime = performance.now();
  2231. const totalLayerDuration = layerEndTime - layerStartTime;
  2232. logDebug(`layer: ${gisLayer.id} processed in ${totalLayerDuration.toFixed(2)} ms - ${successCount} features added`);
  2233.  
  2234. // Remove features from the map (only if there are any)
  2235. if (featureIdsToRemove.length > 0) {
  2236. sdk.Map.removeFeaturesFromLayer({ layerName, featureIds: featureIdsToRemove });
  2237. }
  2238.  
  2239. // Create the new collection (kept + new)
  2240. const newCollection = [...remainingFeatures, ...features];
  2241.  
  2242. // Update the original reference (if needed, or handle based on your scope)
  2243. if (isRoad) {
  2244. roadFeatures = newCollection;
  2245. } else {
  2246. defaultFeatures = newCollection;
  2247. }
  2248.  
  2249. if (features.length) {
  2250. $(`label[for="gis-layer-${gisLayer.id}"]`).css({ color: '#00a009' });
  2251. }
  2252. }
  2253. } // END processFeatures()
  2254.  
  2255. function copyTextToClipboard(text) {
  2256. try {
  2257. GM_setClipboard(text);
  2258. logDebug(`Copy Text To Clipboard: ${text}`);
  2259. } catch (err) {
  2260. logError(`Failed to Text To Clipboard: ${err}`);
  2261. }
  2262. }
  2263.  
  2264. function addLabelToLayer(layerName, label) {
  2265. if (!layerLabels[layerName]) {
  2266. layerLabels[layerName] = new Set();
  2267. }
  2268. layerLabels[layerName].add(label);
  2269. }
  2270.  
  2271. function replacePhrasesWithAcronyms(text) {
  2272. // Order phrases such that compound phrases come before individual words
  2273. const replacements = [
  2274. // compound phrases here
  2275. { phrase: 'Alternate Route', acronym: 'ALT' },
  2276. { phrase: 'Army Air Field', acronym: 'AAF' },
  2277. { phrase: 'County Highway', acronym: 'CH-' },
  2278. { phrase: 'County Road', acronym: 'CR-' },
  2279. { phrase: 'East Bound', acronym: 'EB' },
  2280. { phrase: 'North Bound', acronym: 'NB' },
  2281. { phrase: 'North East', acronym: 'NE' },
  2282. { phrase: 'North West', acronym: 'NW' },
  2283. { phrase: 'South Bound', acronym: 'SB' },
  2284. { phrase: 'South East', acronym: 'SE' },
  2285. { phrase: 'South West', acronym: 'SW' },
  2286. { phrase: 'State Highway', acronym: 'SH-' },
  2287. { phrase: 'State Route', acronym: 'SR-' },
  2288. { phrase: 'State Rte', acronym: 'SR-' },
  2289. { phrase: 'U.S. Highway', acronym: 'US-' },
  2290. { phrase: 'U.S. Route', acronym: 'US-' },
  2291. { phrase: 'U.S. Rte', acronym: 'US-' },
  2292. { phrase: 'U.S.Rte', acronym: 'US-' },
  2293. { phrase: 'US Highway', acronym: 'US-' },
  2294. { phrase: 'U S Highway', acronym: 'US-' },
  2295. { phrase: 'US Route', acronym: 'US-' },
  2296. { phrase: 'U S Route', acronym: 'US-' },
  2297. { phrase: 'US RTE', acronym: 'US-' },
  2298. { phrase: 'U S RTE', acronym: 'US-' },
  2299. { phrase: 'USRTE', acronym: 'US-' },
  2300. { phrase: 'West Bound', acronym: 'WB' },
  2301. // Start of single words list
  2302. { phrase: 'Alley', acronym: 'Aly' },
  2303. { phrase: 'Apartments', acronym: 'Apts' },
  2304. { phrase: 'Avenue', acronym: 'Ave' },
  2305. { phrase: 'Beach', acronym: 'Bch' },
  2306. { phrase: 'Boulevard', acronym: 'Blvd' },
  2307. { phrase: 'Bridge', acronym: 'Br' },
  2308. { phrase: 'Business', acronym: 'BUS' },
  2309. { phrase: 'Bypass', acronym: 'BYP' },
  2310. { phrase: 'Canyon', acronym: 'Cyn' },
  2311. { phrase: 'Captain', acronym: 'Capt' },
  2312. { phrase: 'Causeway', acronym: 'Cswy' },
  2313. { phrase: 'Center', acronym: 'Ctr' },
  2314. { phrase: 'Circle', acronym: 'Cir' },
  2315. { phrase: 'Colonel', acronym: 'Col.' },
  2316. { phrase: 'Commander', acronym: 'Cmdr.' },
  2317. { phrase: 'Connector', acronym: 'CONN' },
  2318. { phrase: 'Corners', acronym: 'Cors' },
  2319. { phrase: 'Corporal', acronym: 'Cpl.' },
  2320. { phrase: 'Court', acronym: 'Ct' },
  2321. { phrase: 'Cove', acronym: 'Cv' },
  2322. { phrase: 'Creek', acronym: 'Crk' },
  2323. { phrase: 'Crescent', acronym: 'Cres' },
  2324. { phrase: 'Crossing', acronym: 'X-ing' },
  2325. { phrase: 'Doctor', acronym: 'Dr.' },
  2326. { phrase: 'Drive', acronym: 'Dr' },
  2327. { phrase: 'East', acronym: 'E' },
  2328. { phrase: 'Eastbound', acronym: 'EB' },
  2329. { phrase: 'Eb', acronym: 'EB' },
  2330. { phrase: 'Express', acronym: 'EXP' },
  2331. { phrase: 'Expressway', acronym: 'Expwy' },
  2332. { phrase: 'Extension', acronym: 'Ext' },
  2333. { phrase: 'Fort', acronym: 'Ft.' },
  2334. { phrase: 'Freeway', acronym: 'Fwy' },
  2335. { phrase: 'General', acronym: 'Gen.' },
  2336. { phrase: 'Governor', acronym: 'Gov.' },
  2337. { phrase: 'Grove', acronym: 'Grv' },
  2338. { phrase: 'Heights', acronym: 'Hts' },
  2339. { phrase: 'Highway', acronym: 'Hwy' },
  2340. { phrase: 'Honerable', acronym: 'Hon.' },
  2341. { phrase: 'International', acronym: 'Intl' },
  2342. { phrase: 'Interstate', acronym: 'I-' },
  2343. { phrase: 'Junior', acronym: 'Jr.' },
  2344. { phrase: 'Landing', acronym: 'Lndg' },
  2345. { phrase: 'Lane', acronym: 'Ln' },
  2346. { phrase: 'Lieutenant', acronym: 'Lt.' },
  2347. { phrase: 'Loop', acronym: 'Lp' },
  2348. { phrase: 'Major', acronym: 'Maj.' },
  2349. { phrase: 'Manor', acronym: 'Mnr.' },
  2350. { phrase: 'Meadow', acronym: 'Mdw' },
  2351. { phrase: 'Mount', acronym: 'Mt' },
  2352. { phrase: 'Mountain', acronym: 'Mtn' },
  2353. { phrase: 'Mountains', acronym: 'Mtns' },
  2354. { phrase: 'National', acronym: "Nat'l" },
  2355. { phrase: 'North', acronym: 'N' },
  2356. { phrase: 'Northbound', acronym: 'NB' },
  2357. { phrase: 'Nb', acronym: 'NB' },
  2358. { phrase: 'Northeast', acronym: 'NE' },
  2359. { phrase: 'Northwest', acronym: 'NW' },
  2360. { phrase: 'Park', acronym: 'Pk' },
  2361. { phrase: 'Parkway', acronym: 'Pkwy' },
  2362. { phrase: 'Parkways', acronym: 'Pkwys' },
  2363. { phrase: 'Passage', acronym: 'Psge' },
  2364. { phrase: 'Place', acronym: 'Pl' },
  2365. { phrase: 'Plaza', acronym: 'Plz' },
  2366. { phrase: 'Point', acronym: 'Pt' },
  2367. { phrase: 'Points', acronym: 'Pts' },
  2368. { phrase: 'President', acronym: 'Pres.' },
  2369. { phrase: 'Professor', acronym: 'Prof.' },
  2370. { phrase: 'Railroad', acronym: 'R.R.' },
  2371. { phrase: 'Road', acronym: 'Rd' },
  2372. { phrase: 'Recreational', acronym: 'Rec.' },
  2373. { phrase: 'Reverend', acronym: 'Rev.' },
  2374. { phrase: 'Route', acronym: 'SR-' },
  2375. { phrase: 'Saint', acronym: 'St.' },
  2376. { phrase: 'Sainte', acronym: 'Ste.' },
  2377. { phrase: 'Senior', acronym: 'Sr.' },
  2378. { phrase: 'Sergeant', acronym: 'Sgt.' },
  2379. { phrase: 'Skyway', acronym: 'Skwy' },
  2380. { phrase: 'South', acronym: 'S' },
  2381. { phrase: 'Southbound', acronym: 'SB' },
  2382. { phrase: 'Sb', acronym: 'SB' },
  2383. { phrase: 'Southeast', acronym: 'SE' },
  2384. { phrase: 'Southwest', acronym: 'SW' },
  2385. { phrase: 'Springs', acronym: 'Spgs' },
  2386. { phrase: 'Square', acronym: 'Sq' },
  2387. { phrase: 'Station', acronym: 'Sta' },
  2388. { phrase: 'Street', acronym: 'St' },
  2389. { phrase: 'Terrace', acronym: 'Ter' },
  2390. { phrase: 'Throughway', acronym: 'Thwy' },
  2391. { phrase: 'Thruway', acronym: 'Thwy' },
  2392. { phrase: 'Tollway', acronym: 'Tlwy' },
  2393. { phrase: 'Township', acronym: 'Twp' },
  2394. { phrase: 'Trafficway', acronym: 'Trfy' },
  2395. { phrase: 'Trail', acronym: 'Trl' },
  2396. { phrase: 'Tunnel', acronym: 'Tun' },
  2397. { phrase: 'Turnpike', acronym: 'Tpk' },
  2398. { phrase: 'Upper', acronym: 'Upr' },
  2399. { phrase: 'U.S.', acronym: 'US' },
  2400. { phrase: 'Valley', acronym: 'Vly' },
  2401. { phrase: 'West', acronym: 'W' },
  2402. { phrase: 'Westbound', acronym: 'WB' },
  2403. { phrase: 'Wb', acronym: 'WB' },
  2404. { phrase: '--', acronym: '-' },
  2405. { phrase: ' -', acronym: '-' },
  2406. { phrase: '- ', acronym: '-' },
  2407. { phrase: '- -', acronym: '-' },
  2408. ];
  2409.  
  2410. let updatedText = text;
  2411.  
  2412. // Replace phrases with their acronyms, case insensitive
  2413. replacements.forEach(({ phrase, acronym }) => {
  2414. const regex = new RegExp(`\\b${phrase}\\b`, 'gi'); // Uses \\b to match words with word boundaries
  2415. updatedText = updatedText.replace(regex, acronym);
  2416. });
  2417.  
  2418. return updatedText;
  2419. }
  2420.  
  2421. function fixSateHwyRoadNames(text) {
  2422. // Regular expression to capture patterns like "XXX ###", "XXX-###", "XXX###", as well as "Us Route #", "Us Rte #", and "Route #", "Rte #"
  2423. const regex = /(?:([A-Z]{1,3})[-\s]?(\d{1,3})|(?:Us\s+(?:Rte|Route)\s+(\d{1,3}))|(?:Rte[-\s]?(\d{1,3})|Route\s+(\d{1,3})))\b/gi;
  2424.  
  2425. // Replacement function formats matched patterns
  2426. return text.replace(regex, (match, letters, numbers, usRouteNumber, rteNumber, routeNumber) => {
  2427. if (usRouteNumber) {
  2428. return `US-${usRouteNumber}`; // for "US Route"/s
  2429. }
  2430. if (rteNumber || routeNumber) {
  2431. return `SR-${rteNumber || routeNumber}`; // Change "Rte" or "Route" to "SR"
  2432. }
  2433. if (letters && numbers) {
  2434. return `${letters.toUpperCase()}-${numbers}`; // General format for other letter-number combos
  2435. }
  2436. return match;
  2437. });
  2438. }
  2439.  
  2440. function titleCaseLabel(text) {
  2441. // Read each line separately
  2442. const lines = text.split('\n');
  2443. return lines
  2444. .map((line) => {
  2445. const trimmedLine = line.trim(); // Trim the line to remove leading/trailing spaces
  2446. const words = trimmedLine.split(' '); // Split the line into individual words
  2447. // Capitalize the first letter of each word and convert the rest to lowercase
  2448. const titleCasedLine = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' '); // Recombine the words into a title-cased line
  2449. return titleCasedLine; // Return the formatted line
  2450. })
  2451. .join('\n'); // Combine all the lines back into a single string separated by new lines
  2452. }
  2453.  
  2454. function processedLabel(label) {
  2455. if (useTitleCase) {
  2456. label = titleCaseLabel(label);
  2457. }
  2458. if (useAcronyms) {
  2459. label = replacePhrasesWithAcronyms(label);
  2460. }
  2461. if (useStateHwy) {
  2462. label = fixSateHwyRoadNames(label);
  2463. }
  2464. if (removeNewLines) {
  2465. label = label.replace(/[\r\n]+/g, ' ');
  2466. }
  2467. return label;
  2468. }
  2469.  
  2470. function updatePopup(labels) {
  2471. let popup = document.getElementById('layerLabelPopup');
  2472. if (!popup) {
  2473. popup = document.createElement('div');
  2474. popup.id = 'layerLabelPopup';
  2475. popup.style = `position: absolute; background: #f5f5f5; border: 2px solid #007bff; border-radius: 5px;
  2476. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); z-index: 1000; width: 500px; max-width: 800px;
  2477. height: 300px; resize: both; overflow: hidden; max-height: 700px; left: ${popupPosition.left}; top: ${popupPosition.top}; `;
  2478.  
  2479. const header = document.createElement('div');
  2480. header.style = 'background: #007bff; color: #fff; padding: 5px; cursor: move; border-radius: 3px 3px 0 0; display: flex; justify-content: space-between; align-items: center; height: 30px; ';
  2481.  
  2482. const title = document.createElement('span');
  2483. title.innerText = 'GIS-L Layer Labels';
  2484. header.appendChild(title);
  2485.  
  2486. const closeButton = document.createElement('span');
  2487. closeButton.innerText = '×';
  2488. closeButton.style = 'cursor: pointer; font-size: 20px; margin-left: 10px; ';
  2489. closeButton.addEventListener('click', () => {
  2490. isPopupVisible = false;
  2491. togglePopupVisibility();
  2492. $('input[name="popupVisibility"][value="show"]').prop('checked', isPopupVisible);
  2493. $('input[name="popupVisibility"][value="hide"]').prop('checked', !isPopupVisible);
  2494. saveSettingsToStorage();
  2495. });
  2496. header.appendChild(closeButton);
  2497. popup.appendChild(header);
  2498.  
  2499. const formatOptionContainer = document.createElement('div');
  2500. formatOptionContainer.style = 'background: #72767d; color: #fff;';
  2501.  
  2502. const firstRow = document.createElement('div');
  2503. firstRow.style = 'display: flex; gap: 10px; align-items: flex-start; justify-content: flex-start;';
  2504.  
  2505. const formatCheckbox = document.createElement('input');
  2506. formatCheckbox.type = 'checkbox';
  2507. formatCheckbox.id = 'useTitleCaseCheckbox';
  2508. formatCheckbox.style = 'margin-left: 10px';
  2509. formatCheckbox.checked = useTitleCase;
  2510. formatCheckbox.addEventListener('change', () => {
  2511. useTitleCase = formatCheckbox.checked;
  2512. updatePopupContent(labels);
  2513. saveSettingsToStorage();
  2514. });
  2515. firstRow.appendChild(formatCheckbox);
  2516.  
  2517. const formatCheckboxLabel = document.createElement('label');
  2518. formatCheckboxLabel.htmlFor = 'useTitleCaseCheckbox';
  2519. formatCheckboxLabel.innerText = 'Use Title Case';
  2520. formatCheckboxLabel.style = 'font-weight: 100; width: 150px;';
  2521. firstRow.appendChild(formatCheckboxLabel);
  2522.  
  2523. const acronymCheckbox = document.createElement('input');
  2524. acronymCheckbox.type = 'checkbox';
  2525. acronymCheckbox.id = 'useacronymsCheckbox';
  2526. acronymCheckbox.checked = useAcronyms;
  2527. acronymCheckbox.addEventListener('change', () => {
  2528. useAcronyms = acronymCheckbox.checked;
  2529. updatePopupContent(labels);
  2530. saveSettingsToStorage();
  2531. });
  2532. firstRow.appendChild(acronymCheckbox);
  2533.  
  2534. const acronymCheckboxLabel = document.createElement('label');
  2535. acronymCheckboxLabel.htmlFor = 'useacronymsCheckbox';
  2536. acronymCheckboxLabel.innerText = 'Use Acronyms & Abbreviations';
  2537. acronymCheckboxLabel.style = 'font-weight: 100;';
  2538. firstRow.appendChild(acronymCheckboxLabel);
  2539. formatOptionContainer.appendChild(firstRow);
  2540.  
  2541. const secondRow = document.createElement('div');
  2542. secondRow.style = 'display: flex; gap: 10px; align-items: flex-start; justify-content: flex-start;';
  2543.  
  2544. const stateHwyCheckbox = document.createElement('input');
  2545. stateHwyCheckbox.type = 'checkbox';
  2546. stateHwyCheckbox.id = 'useStateHwyCheckbox';
  2547. stateHwyCheckbox.style = 'margin-left: 10px';
  2548. stateHwyCheckbox.checked = useStateHwy;
  2549. stateHwyCheckbox.addEventListener('change', () => {
  2550. useStateHwy = stateHwyCheckbox.checked;
  2551. updatePopupContent(labels);
  2552. saveSettingsToStorage();
  2553. });
  2554. secondRow.appendChild(stateHwyCheckbox);
  2555.  
  2556. const stateHwyCheckboxLabel = document.createElement('label');
  2557. stateHwyCheckboxLabel.htmlFor = 'useStateHwyCheckbox';
  2558. stateHwyCheckboxLabel.innerText = 'Fix Highway Labels';
  2559.  
  2560. stateHwyCheckboxLabel.style = 'font-weight: 100; width: 150px;';
  2561. secondRow.appendChild(stateHwyCheckboxLabel);
  2562.  
  2563. const removeNewLinesCheckbox = document.createElement('input');
  2564. removeNewLinesCheckbox.type = 'checkbox';
  2565. removeNewLinesCheckbox.id = 'removeNewLinesCheckbox';
  2566. removeNewLinesCheckbox.checked = removeNewLines;
  2567. removeNewLinesCheckbox.addEventListener('change', () => {
  2568. removeNewLines = removeNewLinesCheckbox.checked;
  2569. updatePopupContent(labels);
  2570. saveSettingsToStorage();
  2571. });
  2572. secondRow.appendChild(removeNewLinesCheckbox);
  2573.  
  2574. const removeNewLinesCheckboxLabel = document.createElement('label');
  2575. removeNewLinesCheckboxLabel.htmlFor = 'removeNewLinesCheckbox';
  2576. removeNewLinesCheckboxLabel.innerText = 'Remove New Lines';
  2577. removeNewLinesCheckboxLabel.style = 'font-weight: 100;';
  2578. secondRow.appendChild(removeNewLinesCheckboxLabel);
  2579.  
  2580. formatOptionContainer.appendChild(secondRow);
  2581. popup.appendChild(formatOptionContainer);
  2582.  
  2583. const dropdownContainer = document.createElement('div');
  2584. dropdownContainer.style = 'margin-bottom: 10px;';
  2585. popup.appendChild(dropdownContainer);
  2586.  
  2587. const contentContainer = document.createElement('div');
  2588. contentContainer.style = 'padding: 5px; overflow-y: auto; overflow-x: auto; height: calc(100% - 110px);';
  2589. popup.appendChild(contentContainer);
  2590.  
  2591. const mapElement = document.getElementsByTagName('wz-page-content')[0];
  2592. if (mapElement) {
  2593. mapElement.appendChild(popup);
  2594. }
  2595.  
  2596. header.onmousedown = function (event) {
  2597. event.preventDefault();
  2598. const parentRect = mapElement.getBoundingClientRect();
  2599. const initialX = event.clientX;
  2600. const initialY = event.clientY;
  2601. const offsetX = initialX - parentRect.left - popup.offsetLeft;
  2602. const offsetY = initialY - parentRect.top - popup.offsetTop;
  2603.  
  2604. document.onmousemove = function (ev) {
  2605. popup.style.left = `${ev.clientX - offsetX - parentRect.left}px`;
  2606. popup.style.top = `${ev.clientY - offsetY - parentRect.top}px`;
  2607.  
  2608. popupPosition.left = popup.style.left;
  2609. popupPosition.top = popup.style.top;
  2610. };
  2611.  
  2612. document.onmouseup = function () {
  2613. document.onmousemove = null;
  2614. document.onmouseup = null;
  2615. };
  2616. };
  2617. }
  2618.  
  2619. updatePopupContent(labels);
  2620. popup.style.display = isPopupVisible ? 'block' : 'none';
  2621. }
  2622.  
  2623. function updatePopupContent(labels) {
  2624. const dropdownContainer = document.querySelector('#layerLabelPopup div:nth-child(3)');
  2625. const contentContainer = document.querySelector('#layerLabelPopup div:nth-child(4)');
  2626.  
  2627. dropdownContainer.innerHTML = '';
  2628. contentContainer.innerHTML = '';
  2629.  
  2630. const select = document.createElement('select');
  2631. select.style = 'width: 100%; padding: 5px; border: 1px solid #ccc;';
  2632.  
  2633. const sortedLayerNames = Object.keys(labels).sort();
  2634. sortedLayerNames.forEach((layerName) => {
  2635. const option = document.createElement('option');
  2636. option.value = layerName;
  2637. option.innerText = layerName;
  2638. select.appendChild(option);
  2639.  
  2640. const uniqueLabels = Array.from(labels[layerName]).sort();
  2641. const tabContent = document.createElement('div');
  2642. tabContent.style = 'display: none; width: 100%; white-space: pre;';
  2643.  
  2644. const processedLabels = uniqueLabels
  2645. .map((label) => {
  2646. const text = processedLabel(label);
  2647. const copyIcon = '<span style="cursor: pointer; margin-left: 5px;" title="Copy to clipboard">📋</span>';
  2648. return `<li style="margin-bottom: 0.3em; color: #555;" data-label="${text}">${text}${copyIcon}</li>`;
  2649. })
  2650. .join('');
  2651.  
  2652. tabContent.innerHTML = `<ul style="padding-left: 20px; margin-top: 0;">${processedLabels}</ul>`;
  2653. contentContainer.appendChild(tabContent);
  2654.  
  2655. // Add copying functionality
  2656. tabContent.querySelectorAll('li').forEach((li) => {
  2657. const icon = li.querySelector('span');
  2658. if (icon) {
  2659. icon.addEventListener('click', () => {
  2660. const textToCopy = li.getAttribute('data-label'); // Get the text from a custom data attribute
  2661. copyTextToClipboard(textToCopy);
  2662. });
  2663. }
  2664. });
  2665. });
  2666.  
  2667. dropdownContainer.appendChild(select);
  2668.  
  2669. let selectedLayerIndex = sortedLayerNames.indexOf(popupActiveLayer);
  2670.  
  2671. if (selectedLayerIndex === -1 && select.options.length > 0) {
  2672. selectedLayerIndex = 0;
  2673. popupActiveLayer = sortedLayerNames[selectedLayerIndex];
  2674. }
  2675. select.selectedIndex = selectedLayerIndex;
  2676.  
  2677. const allContents = contentContainer.querySelectorAll('div');
  2678. allContents.forEach((content, index) => {
  2679. content.style.display = index === select.selectedIndex ? 'block' : 'none';
  2680. });
  2681.  
  2682. select.addEventListener('change', () => {
  2683. const contents = contentContainer.querySelectorAll('div');
  2684. contents.forEach((content, index) => {
  2685. content.style.display = index === select.selectedIndex ? 'block' : 'none';
  2686. });
  2687. popupActiveLayer = select.value;
  2688. });
  2689. }
  2690.  
  2691. function fetchFeatures() {
  2692. if (isPopupVisible) {
  2693. Object.keys(layerLabels).forEach((key) => delete layerLabels[key]);
  2694. }
  2695. if (ignoreFetch) return;
  2696. if (sdk.Map.getZoomLevel() < 12) {
  2697. filterLayerCheckboxes();
  2698. return;
  2699. }
  2700. lastToken.cancel = true;
  2701. lastToken = { cancel: false, features: [], layersProcessed: 0 };
  2702. $('.gis-state-layer-label').css({ color: '#777' });
  2703.  
  2704. let _layersCleared = false;
  2705.  
  2706. // if (layersToFetch.length) {
  2707. const extentWGS84 = getMapExtent('wgs84');
  2708. GM_xmlhttpRequest({
  2709. url: getCountiesUrl(extentWGS84),
  2710. method: 'GET',
  2711. onload(res) {
  2712. if (res.status < 400) {
  2713. const data = $.parseJSON(res.responseText);
  2714. if (data.error) {
  2715. logError(`Error in US Census counties data: ${data.error.message}`);
  2716. } else {
  2717. _countiesInExtent = data.features.map((feature) => {
  2718. const name = feature.attributes.BASENAME.toLowerCase();
  2719. const stateInfo = STATES.fromId(parseInt(feature.attributes.STATE, 10));
  2720. return { name, stateInfo };
  2721. });
  2722. logDebug(`US Census counties: ${_countiesInExtent.map((c) => `${c.name} ${c.stateInfo[1]}`).join(', ')}`);
  2723.  
  2724. let layersToFetch;
  2725. if (!_layersCleared) {
  2726. _layersCleared = true;
  2727. layersToFetch = getFetchableLayers();
  2728.  
  2729. // Remove features of any layers that won't be mapped.
  2730. _gisLayers.forEach((gisLayer) => {
  2731. if (!layersToFetch.includes(gisLayer)) {
  2732. let featureCollection = gisLayer.isRoadLayer ? roadFeatures : defaultFeatures;
  2733. const layerName = gisLayer.isRoadLayer ? ROAD_LAYER_NAME : DEFAULT_LAYER_NAME;
  2734. const featureIds = featureCollection.filter((f) => f.properties.layerID === gisLayer.id).map((f) => f.id);
  2735. if (featureIds.length) {
  2736. sdk.Map.removeFeaturesFromLayer({ layerName, featureIds });
  2737. featureCollection = featureCollection.filter((f) => !featureIds.includes(f.id));
  2738. if (gisLayer.isRoadLayer) {
  2739. roadFeatures = featureCollection;
  2740. } else {
  2741. defaultFeatures = featureCollection;
  2742. }
  2743. }
  2744. }
  2745. });
  2746. }
  2747.  
  2748. layersToFetch = layersToFetch.filter(
  2749. (layer) =>
  2750. !layer.hasOwnProperty('counties') ||
  2751. layer.counties.some((countyName) => _countiesInExtent.some((county) => county.name === countyName.toLowerCase() && layer.state === county.stateInfo[1]))
  2752. );
  2753. filterLayerCheckboxes();
  2754. logDebug(`Fetching ${layersToFetch.length} layers...`);
  2755. logDebug(layersToFetch);
  2756. let layersProcessedCount = 0; // Track processed layers
  2757.  
  2758. layersToFetch.forEach((gisLayer) => {
  2759. const zoom = sdk.Map.getZoomLevel();
  2760. const url = getUrl(extentWGS84, gisLayer, zoom);
  2761. GM_xmlhttpRequest({
  2762. url,
  2763. context: lastToken,
  2764. method: 'GET',
  2765. onload(res2) {
  2766. if (res2.status < 400) {
  2767. // Handle successful response
  2768. try {
  2769. const parsedData = $.parseJSON(res2.responseText);
  2770. processFeatures(parsedData, res2.context, gisLayer);
  2771. } catch (parseError) {
  2772. logError(`Parsing error for layer "${gisLayer.id}": ${parseError.message}`);
  2773. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  2774. }
  2775.  
  2776. // Update popup after processing all layers
  2777. layersProcessedCount += 1;
  2778. if (layersProcessedCount === layersToFetch.length && isPopupVisible) {
  2779. updatePopup(layerLabels);
  2780. }
  2781. } else {
  2782. // Handle HTTP error response
  2783. logError(`HTTP error for layer "${gisLayer.id}": ${res2.status} ${res2.statusText}`);
  2784. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  2785. }
  2786. },
  2787. onerror(res3) {
  2788. // Handle request error, particularly timeouts or network issues
  2789. logError(`Could not fetch layer "${gisLayer.id}". Error: ${res3.statusText} (status code: ${res3.status})`);
  2790. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  2791. },
  2792. });
  2793. });
  2794. }
  2795. } else {
  2796. logDebug(`HTTP request error: ${JSON.stringify(res)}`);
  2797. logError(`Could not fetch counties from US Census site. Request returned ${res.status}`);
  2798. }
  2799. },
  2800. onerror(res) {
  2801. logDebug(`xmlhttpRequest error:${JSON.stringify(res)}`);
  2802. logError('Could not fetch counties from US Census site. An error was thrown.');
  2803. },
  2804. });
  2805. }
  2806.  
  2807. function showScriptInfoAlert() {
  2808. /* Check version and alert on update */
  2809. if (SHOW_UPDATE_MESSAGE && scriptVersion !== settings.lastVersion) {
  2810. // alert(SCRIPT_VERSION_CHANGES);
  2811. let releaseNotes = '';
  2812. releaseNotes += "<p>What's New:</p>";
  2813. if (SCRIPT_VERSION_CHANGES.length > 0) {
  2814. releaseNotes += '<ul>';
  2815. for (let idx = 0; idx < SCRIPT_VERSION_CHANGES.length; idx++) releaseNotes += `<li>${SCRIPT_VERSION_CHANGES[idx]}`;
  2816. releaseNotes += '</ul>';
  2817. } else {
  2818. releaseNotes += '<ul><li>Nothing major.</ul>';
  2819. }
  2820. WazeWrap.Interface.ShowScriptUpdate(GM_info.script.name, scriptVersion, releaseNotes, GF_URL);
  2821. }
  2822. }
  2823.  
  2824. function setEnabled(value) {
  2825. settings.enabled = value;
  2826. saveSettingsToStorage();
  2827. sdk.Map.setLayerVisibility({ layerName: DEFAULT_LAYER_NAME, visibility: value });
  2828. sdk.Map.setLayerVisibility({ layerName: ROAD_LAYER_NAME, visibility: value });
  2829. const color = value ? '#00bd00' : '#ccc';
  2830. $('span#gis-layers-power-btn').css({ color });
  2831. if (value) fetchFeatures();
  2832. sdk.LayerSwitcher.setLayerCheckboxChecked({ name: 'GIS Layers', isChecked: value });
  2833.  
  2834. // Show/hide the popup based on the enabled state
  2835. const popup = document.getElementById('layerLabelPopup');
  2836. if (popup) {
  2837. popup.style.display = value ? 'block' : 'none';
  2838. isPopupVisible = value;
  2839. }
  2840. }
  2841.  
  2842. function onGisLayerToggleChanged() {
  2843. const checked = $(this).is(':checked');
  2844. const layerId = $(this).data('layer-id');
  2845. const idx = settings.visibleLayers.indexOf(layerId);
  2846. if (checked) {
  2847. const gisLayer = _gisLayers.find((l) => l.id === layerId);
  2848. if (gisLayer.oneTimeAlert) {
  2849. const lastAlertHash = settings.oneTimeAlerts[layerId];
  2850. const newAlertHash = hashString(gisLayer.oneTimeAlert);
  2851. if (lastAlertHash !== newAlertHash) {
  2852. // alert(`Layer: ${gisLayer.name}\n\nMessage:\n${gisLayer.oneTimeAlert}`);
  2853. WazeWrap.Alerts.info(GM_info.script.name, `Layer: ${gisLayer.name}<br><br>Message:<br>${gisLayer.oneTimeAlert}`);
  2854. settings.oneTimeAlerts[layerId] = newAlertHash;
  2855. saveSettingsToStorage();
  2856. }
  2857. }
  2858. if (idx === -1) settings.visibleLayers.push(layerId);
  2859. } else if (idx > -1) settings.visibleLayers.splice(idx, 1);
  2860. if (!ignoreFetch) {
  2861. saveSettingsToStorage();
  2862. fetchFeatures();
  2863. }
  2864. }
  2865.  
  2866. function onOnlyShowApplicableLayersChanged() {
  2867. settings.onlyShowApplicableLayers = $(this).is(':checked');
  2868. saveSettingsToStorage();
  2869. fetchFeatures();
  2870. }
  2871.  
  2872. function onStateCheckChanged(evt) {
  2873. const state = evt.data;
  2874. const idx = settings.selectedStates.indexOf(state);
  2875. if (evt.target.checked) {
  2876. if (idx === -1) settings.selectedStates.push(state);
  2877. } else if (idx > -1) settings.selectedStates.splice(idx, 1);
  2878. if (!ignoreFetch) {
  2879. saveSettingsToStorage();
  2880. initLayersTab();
  2881. fetchFeatures();
  2882. }
  2883. }
  2884.  
  2885. function onLayerCheckboxChanged(args) {
  2886. setEnabled(args.checked);
  2887. }
  2888.  
  2889. function setFillParcels(doFill) {
  2890. [LAYER_STYLES.parcels, LAYER_STYLES.state_parcels].forEach((style) => {
  2891. style.fillOpacity = doFill ? 0.2 : 0;
  2892. });
  2893. }
  2894.  
  2895. function onFillParcelsCheckedChanged(evt) {
  2896. const { checked } = evt.target;
  2897. setFillParcels(checked);
  2898. settings.fillParcels = checked;
  2899. saveSettingsToStorage();
  2900. fetchFeatures();
  2901. }
  2902.  
  2903. function onMapMove() {
  2904. if (settings.enabled) fetchFeatures();
  2905. }
  2906.  
  2907. function onRefreshLayersClick() {
  2908. const $btn = $('#gis-layers-refresh');
  2909. if (!$btn.hasClass('fa-spin')) {
  2910. $btn.css({ cursor: 'auto' });
  2911. $btn.addClass('fa-spin');
  2912. init(false);
  2913. }
  2914. }
  2915.  
  2916. function onChevronClick(evt) {
  2917. const $target = $(evt.currentTarget);
  2918. const $div = $($target.siblings()[0]);
  2919. const fieldsetId = $target.parent('fieldset').attr('id');
  2920. const sectionKey = fieldsetId ? fieldsetId.replace('gis-layers-for-', '') : null;
  2921. $($target.children()[0]).toggleClass('fa fa-fw fa-chevron-down').toggleClass('fa fa-fw fa-chevron-right');
  2922. if ($div.css('display') === 'none') {
  2923. $div.css('display', 'block');
  2924. if (sectionKey) settings.collapsedSections[sectionKey] = false;
  2925. } else {
  2926. $div.css('display', 'none');
  2927. if (sectionKey) settings.collapsedSections[sectionKey] = true;
  2928. }
  2929. if (sectionKey) saveSettingsToStorage();
  2930. }
  2931.  
  2932. function doToggleABunch(evt, checkState) {
  2933. ignoreFetch = true;
  2934. $(evt.target).closest('fieldset').find('input').prop('checked', !checkState).trigger('click');
  2935. ignoreFetch = false;
  2936. saveSettingsToStorage();
  2937. if (evt.data) initLayersTab();
  2938. fetchFeatures();
  2939. }
  2940.  
  2941. function onSelectAllClick(evt) {
  2942. doToggleABunch(evt, true);
  2943. }
  2944.  
  2945. function onSelectNoneClick(evt) {
  2946. doToggleABunch(evt, false);
  2947. }
  2948.  
  2949. function onGisAddrDisplayChange(evt) {
  2950. settings.addrLabelDisplay = evt.target.value;
  2951. saveSettingsToStorage();
  2952. fetchFeatures();
  2953. }
  2954.  
  2955. function onAddressDisplayShortcutKey() {
  2956. if (!$('#gisAddrDisplay-hn').is(':checked')) {
  2957. $('#gisAddrDisplay-hn').click();
  2958. } else {
  2959. $('#gisAddrDisplay-all').click();
  2960. }
  2961. }
  2962.  
  2963. function onToggleGisLayersShortcutKey() {
  2964. setEnabled(!settings.enabled);
  2965. }
  2966.  
  2967. function togglePopupVisibility() {
  2968. const popup = document.getElementById('layerLabelPopup');
  2969. if (popup) {
  2970. popup.style.display = isPopupVisible ? 'block' : 'none';
  2971. }
  2972. saveSettingsToStorage();
  2973. }
  2974.  
  2975. function initLayer() {
  2976. const rules = _gisLayers
  2977. .filter((gisLayer) => gisLayer.style && gisLayer.style !== 'roads')
  2978. .map((gisLayer) => {
  2979. let style;
  2980. if (LAYER_STYLES.hasOwnProperty(gisLayer.style)) {
  2981. style = LAYER_STYLES[gisLayer.style];
  2982. } else {
  2983. style = gisLayer.style;
  2984. }
  2985. return {
  2986. predicate: (featureProperties) => featureProperties.layerID === gisLayer.id,
  2987. style,
  2988. };
  2989. });
  2990.  
  2991. setFillParcels(settings.fillParcels);
  2992.  
  2993. try {
  2994. sdk.Map.removeLayer({ layerName: DEFAULT_LAYER_NAME });
  2995. } catch (e) {
  2996. // If InvalidStateError, then the layer doesn't exist yet. Ignore the error
  2997. if (!(e instanceof sdk.Errors.InvalidStateError)) {
  2998. throw e;
  2999. }
  3000. }
  3001. sdk.Map.addLayer({
  3002. layerName: DEFAULT_LAYER_NAME,
  3003. styleContext: {
  3004. getLabel: (context) => context.feature?.properties?.label,
  3005. },
  3006. styleRules: [{ style: DEFAULT_STYLE }, ...rules],
  3007. zIndexing: true,
  3008. });
  3009.  
  3010. try {
  3011. sdk.Map.removeLayer({ layerName: ROAD_LAYER_NAME });
  3012. } catch (e) {
  3013. // If InvalidStateError, then the layer doesn't exist yet. Ignore the error
  3014. if (!(e instanceof sdk.Errors.InvalidStateError)) {
  3015. throw e;
  3016. }
  3017. }
  3018. sdk.Map.addLayer({
  3019. layerName: ROAD_LAYER_NAME,
  3020. styleContext: {
  3021. getLabel: (context) => context.feature?.properties?.label,
  3022. getOffset: () => -(sdk.Map.getZoomLevel() + 5),
  3023. getSmooth: () => '',
  3024. getReadable: () => '1',
  3025. },
  3026. styleRules: [{ style: ROAD_STYLE }],
  3027. });
  3028.  
  3029. sdk.Map.setLayerVisibility({ layerName: DEFAULT_LAYER_NAME, visibility: settings.enabled });
  3030. sdk.Map.setLayerVisibility({ layerName: ROAD_LAYER_NAME, visibility: settings.enabled });
  3031. } // END InitLayer
  3032.  
  3033. function initLayersTab() {
  3034. const user = userInfo.userName.toLowerCase();
  3035. const states = _.uniq(_gisLayers.map((l) => l.state)).filter((st) => settings.selectedStates.includes(st));
  3036.  
  3037. $('#panel-gis-state-layers')
  3038. .empty()
  3039. .append(
  3040. $('<div>', { class: 'controls-container' })
  3041. .css({ 'padding-top': '0px' })
  3042. .append(
  3043. $('<input>', { type: 'checkbox', id: 'only-show-applicable-gis-layers' }).change(onOnlyShowApplicableLayersChanged).prop('checked', settings.onlyShowApplicableLayers),
  3044. $('<label>', { for: 'only-show-applicable-gis-layers' }).css({ 'white-space': 'pre-line' }).text('Only show applicable layers')
  3045. ),
  3046. $('.gis-layers-state-checkbox:checked').length === 0
  3047. ? $('<div>').text('Turn on layer categories in the Settings tab.')
  3048. : states.map((st) =>
  3049. $('<fieldset>', {
  3050. id: `gis-layers-for-${st}`,
  3051. style: 'border:1px solid silver;padding:4px;border-radius:4px;-webkit-padding-before: 0;',
  3052. }).append(
  3053. $('<legend>', { style: 'margin-bottom:0px;border-bottom-style:none;width:auto;' })
  3054. .click(onChevronClick)
  3055. .append(
  3056. $('<i>', {
  3057. class: settings.collapsedSections[st] ? 'fa fa-fw fa-chevron-right' : 'fa fa-fw fa-chevron-down',
  3058. style: 'cursor: pointer;font-size: 12px;margin-right: 4px',
  3059. }),
  3060. $('<span>', {
  3061. style: 'font-size:14px;font-weight:600;text-transform: uppercase; cursor: pointer',
  3062. }).text(STATES.toFullName(st))
  3063. ),
  3064. $('<div>', {
  3065. id: `${st}_body`,
  3066. style: settings.collapsedSections[st] ? 'display: none;' : 'display: block;',
  3067. }).append(
  3068. $('<div>')
  3069. .css({ 'font-size': '11px' })
  3070. .append($('<span>').append('Select ', $('<a>', { href: '#' }).text('All').click(onSelectAllClick), ' / ', $('<a>', { href: '#' }).text('None').click(onSelectNoneClick))),
  3071. $('<div>', { class: 'controls-container', style: 'padding-top:0px;' }).append(
  3072. _gisLayers
  3073. .filter((l) => l.state === st && (!PRIVATE_LAYERS.hasOwnProperty(l.id) || PRIVATE_LAYERS[l.id].includes(user)))
  3074. .map((gisLayer) => {
  3075. const id = `gis-layer-${gisLayer.id}`;
  3076. return $('<div>', { class: 'controls-container', id: `${id}-container` })
  3077. .css({ 'padding-top': '0px', display: 'block' })
  3078. .append(
  3079. $('<input>', { type: 'checkbox', id }).data('layer-id', gisLayer.id).change(onGisLayerToggleChanged).prop('checked', settings.visibleLayers.includes(gisLayer.id)),
  3080. $('<label>', { for: id, class: 'gis-state-layer-label' })
  3081. .css({ 'white-space': 'pre-line' })
  3082. .text(`${gisLayer.name}${gisLayer.restrictTo ? ' *' : ''}`)
  3083. .attr('title', gisLayer.restrictTo ? `Restricted to: ${gisLayer.restrictTo}` : '')
  3084. .contextmenu((evt) => {
  3085. evt.preventDefault();
  3086. // TODO - enable the layer if it isn't already.
  3087. // Tried using click function on the evt target, but that didn't work.
  3088. _layerSettingsDialog.gisLayer = gisLayer;
  3089. _layerSettingsDialog.show();
  3090. })
  3091. );
  3092. })
  3093. )
  3094. )
  3095. )
  3096. )
  3097. );
  3098. }
  3099.  
  3100. function initSettingsTab() {
  3101. const states = _.uniq(_gisLayers.map((l) => l.state));
  3102. const createRadioBtn = (name, value, text, checked) => {
  3103. const id = `${name}-${value}`;
  3104. return [
  3105. $('<input>', {
  3106. type: 'radio',
  3107. id,
  3108. name,
  3109. value,
  3110. }).prop('checked', checked),
  3111. $('<label>', { for: id }).text(text).css({
  3112. paddingLeft: '15px',
  3113. marginRight: '4px',
  3114. }),
  3115. ];
  3116. };
  3117. $('#panel-gis-layers-settings')
  3118. .empty()
  3119. .append(
  3120. $('<fieldset>', {
  3121. style: 'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;margin-top:-8px;',
  3122. }).append(
  3123. $('<legend>', {
  3124. style: 'margin-bottom:0px;border-bottom-style:none;width:auto;',
  3125. }).append(
  3126. $('<span>', {
  3127. style: 'font-size:14px;font-weight:600;text-transform: uppercase;',
  3128. }).text('Labels')
  3129. ),
  3130. $('<div>', { id: 'labelSettings' }).append(
  3131. $('<div>', { class: 'controls-container' })
  3132. .css({ 'padding-top': '2px' })
  3133. .append(
  3134. $('<label>', { style: 'font-weight:normal;' }).text('Addresses:'),
  3135. createRadioBtn('gisAddrDisplay', 'hn', 'HN', settings.addrLabelDisplay === 'hn'),
  3136. createRadioBtn('gisAddrDisplay', 'street', 'Street', settings.addrLabelDisplay === 'street'),
  3137. createRadioBtn('gisAddrDisplay', 'all', 'Both', settings.addrLabelDisplay === 'all'),
  3138. createRadioBtn('gisAddrDisplay', 'none', 'None', settings.addrLabelDisplay === 'none'),
  3139. $('<i>', {
  3140. class: 'waze-tooltip',
  3141. id: 'gisAddrDisplayInfo',
  3142. 'data-toggle': 'tooltip',
  3143. style: 'margin-left:8px; font-size:12px',
  3144. 'data-placement': 'bottom',
  3145. title: `This may not work properly for all layers. Please report issues to ${SCRIPT_AUTHOR}.`,
  3146. }).tooltip(),
  3147. $('<br>'),
  3148. $('<label>', { style: 'font-weight:normal; margin-left:8px;' }).text('Label Popup:'),
  3149. createRadioBtn('popupVisibility', 'show', 'Show', isPopupVisible),
  3150. createRadioBtn('popupVisibility', 'hide', 'Hide', !isPopupVisible)
  3151. )
  3152. )
  3153. ),
  3154. $('<fieldset>', {
  3155. style: 'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;',
  3156. }).append(
  3157. $('<legend>', {
  3158. style: 'margin-bottom:0px;border-bottom-style:none;width:auto;',
  3159. }).append(
  3160. $('<span>', {
  3161. style: 'font-size:14px;font-weight:600;text-transform: uppercase;',
  3162. }).text('Layer Categories')
  3163. ),
  3164. $('<div>', { id: 'states_body' }).append(
  3165. $('<div>')
  3166. .css({ 'font-size': '11px' })
  3167. .append($('<span>').append('Select ', $('<a>', { href: '#' }).text('All').click(true, onSelectAllClick), ' / ', $('<a>', { href: '#' }).text('None').click(true, onSelectNoneClick))),
  3168. $('<div>', { class: 'controls-container', style: 'padding-top:0px;' }).append(
  3169. states.map((st) => {
  3170. const fullName = STATES.toFullName(st);
  3171. const id = `gis-layer-enable-state-${st}`;
  3172. return $('<div>', { class: 'controls-container' })
  3173. .css({ 'padding-top': '0px', display: 'block' })
  3174. .append(
  3175. $('<input>', { type: 'checkbox', id, class: 'gis-layers-state-checkbox' }).change(st, onStateCheckChanged).prop('checked', settings.selectedStates.includes(st)),
  3176. $('<label>', { for: id }).css({ 'white-space': 'pre-line', color: '#777' }).text(fullName)
  3177. );
  3178. })
  3179. )
  3180. )
  3181. )
  3182. );
  3183. $('#panel-gis-layers-settings').append(
  3184. $('<fieldset>', { style: 'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;' }).append(
  3185. $('<legend>', { style: 'margin-bottom:0px;border-bottom-style:none;width:auto;' }).append(
  3186. $('<span>', { style: 'font-size:14px;font-weight:600;text-transform: uppercase;' }).text('Appearance')
  3187. ),
  3188. $('<div>', { class: 'controls-container' })
  3189. .css({ 'padding-top': '2px' })
  3190. .append(
  3191. $('<input>', { type: 'checkbox', id: 'fill-parcels' }).change(onFillParcelsCheckedChanged).prop('checked', settings.fillParcels),
  3192. $('<label>', { for: 'fill-parcels' }).css({ 'white-space': 'pre-line', color: '#777' }).text('Fill parcels')
  3193. )
  3194. )
  3195. );
  3196. $('input[name="gisAddrDisplay"]').change(onGisAddrDisplayChange);
  3197. $('input[name="popupVisibility"]').change(function () {
  3198. isPopupVisible = $(this).val() === 'show';
  3199. togglePopupVisibility();
  3200. });
  3201. }
  3202.  
  3203. async function initTab(firstCall = true) {
  3204. if (firstCall) {
  3205. const content = $('<div>')
  3206. .append(
  3207. $('<span>', { style: 'font-size:14px;font-weight:600' }).text('GIS Layers'),
  3208. $('<span>', { style: 'font-size:11px;margin-left:10px;color:#aaa;' }).text(GM_info.script.version),
  3209. // <a href="https://docs.google.com/forms/d/e/1FAIpQLSevPQLz2ohu_LTge9gJ9Nv6PURmCmaSSjq0ayOJpGdRr2xI0g/viewform?usp=pp_url&entry.2116052852=test" target="_blank" style="color: #6290b7;font-size: 12px;margin-left: 8px;" title="Report broken layers, bugs, request new layers, script features">Report an issue</a>
  3210. $('<a>', {
  3211. href: REQUEST_FORM_URL.replace('{username}', userInfo.userName),
  3212. target: '_blank',
  3213. style: 'color: #6290b7;font-size: 12px;margin-left: 8px;',
  3214. title: 'Report broken layers, bugs, request new layers, script features',
  3215. }).text('Submit a request'),
  3216. $('<span>', {
  3217. id: 'gis-layers-refresh',
  3218. class: 'fa fa-refresh',
  3219. style: 'float: right;',
  3220. 'data-toggle': 'tooltip',
  3221. title: 'Pull new layer info from master sheet and refresh all layers.',
  3222. }),
  3223. '<ul class="nav nav-tabs">' +
  3224. '<li class="active"><a data-toggle="tab" href="#panel-gis-state-layers" aria-expanded="true">' +
  3225. 'Layers' +
  3226. '</a></li>' +
  3227. '<li><a data-toggle="tab" href="#panel-gis-layers-settings" aria-expanded="true">' +
  3228. 'Settings' +
  3229. '</a></li> ' +
  3230. '</ul>',
  3231. $('<div>', { class: 'tab-content', style: 'padding:8px;padding-top:2px' }).append(
  3232. $('<div>', { class: 'tab-pane active', id: 'panel-gis-state-layers', style: 'padding: 4px 0px 0px 0px; width: auto' }),
  3233. $('<div>', { class: 'tab-pane', id: 'panel-gis-layers-settings', style: 'padding: 4px 0px 0px 0px; width: auto' })
  3234. )
  3235. )
  3236. .html();
  3237.  
  3238. const powerButtonColor = settings.enabled ? '#00bd00' : '#ccc';
  3239. const labelText = $('<div>')
  3240. .append(
  3241. $('<span>', {
  3242. class: 'fa fa-power-off',
  3243. id: 'gis-layers-power-btn',
  3244. style: `margin-right: 5px;cursor: pointer;color: ${powerButtonColor};font-size: 13px;`,
  3245. title: 'Toggle GIS Layers',
  3246. }),
  3247. $('<span>', { title: 'GIS Layers' }).text('GIS-L')
  3248. )
  3249. .html();
  3250.  
  3251. const { tabLabel, tabPane } = await sdk.Sidebar.registerScriptTab();
  3252. tabLabel.innerHTML = labelText;
  3253. tabPane.innerHTML = content;
  3254. // Fix tab content div spacing.
  3255. $(tabPane).parent().css({ width: 'auto', padding: '6px' });
  3256. $('#gis-layers-power-btn').click(() => {
  3257. setEnabled(!settings.enabled);
  3258.  
  3259. // return false to prevent event from bubbling up the DOM tree and causing the GIS-L tab to activate
  3260. return false;
  3261. });
  3262. $('#gis-layers-refresh').click(onRefreshLayersClick);
  3263. }
  3264.  
  3265. initSettingsTab();
  3266. initLayersTab();
  3267. }
  3268.  
  3269. function initGui(firstCall = true) {
  3270. initLayer();
  3271.  
  3272. if (firstCall) {
  3273. initTab(true);
  3274.  
  3275. sdk.LayerSwitcher.addLayerCheckbox({ name: 'GIS Layers' });
  3276. sdk.LayerSwitcher.setLayerCheckboxChecked({ name: 'GIS Layers', isChecked: settings.enabled });
  3277. sdk.Events.on({ eventName: 'wme-layer-checkbox-toggled', eventHandler: onLayerCheckboxChanged });
  3278. sdk.Events.on({ eventName: 'wme-map-move-end', eventHandler: onMapMove });
  3279. showScriptInfoAlert();
  3280. } else {
  3281. initTab(firstCall);
  3282. }
  3283. }
  3284.  
  3285. async function loadSpreadsheetAsync() {
  3286. let data;
  3287. try {
  3288. data = await $.getJSON(`${LAYER_DEF_SPREADSHEET_URL}?${DEC(API_KEY)}`);
  3289. } catch (err) {
  3290. throw new Error(`Spreadsheet call failed. (${err.status}: ${err.statusText})`);
  3291. }
  3292. const [[minVersion], fieldNames, ...layerDefRows] = data.values;
  3293. const REQUIRED_FIELD_NAMES = [
  3294. 'state',
  3295. 'name',
  3296. 'id',
  3297. 'counties',
  3298. 'url',
  3299. 'where',
  3300. 'labelFields',
  3301. 'processLabel',
  3302. 'style',
  3303. 'visibleAtZoom',
  3304. 'labelsVisibleAtZoom',
  3305. 'enabled',
  3306. 'restrictTo',
  3307. 'oneTimeAlert',
  3308. ];
  3309. const result = { error: null };
  3310. const checkFieldNames = (fldName) => fieldNames.includes(fldName);
  3311.  
  3312. if (scriptVersion < minVersion) {
  3313. result.error = `Script must be updated to at least version ${minVersion} before layer definitions can be loaded.`;
  3314. } else if (fieldNames.length < REQUIRED_FIELD_NAMES.length) {
  3315. result.error = `Expected ${REQUIRED_FIELD_NAMES.length} columns in layer definition data. Spreadsheet returned ${fieldNames.length}.`;
  3316. } else if (!REQUIRED_FIELD_NAMES.every((fldName) => checkFieldNames(fldName))) {
  3317. result.error =
  3318. 'Script expected to see the following column names in the layer ' +
  3319. `definition spreadsheet:\n${REQUIRED_FIELD_NAMES.join(', ')}\n` +
  3320. `But the spreadsheet returned these:\n${fieldNames.join(', ')}`;
  3321. }
  3322. if (!result.error) {
  3323. layerDefRows
  3324. .filter((row) => row.length)
  3325. .forEach((layerDefRow) => {
  3326. const layerDef = { enabled: '0' };
  3327. fieldNames.forEach((fldName, fldIdx) => {
  3328. let value = layerDefRow[fldIdx];
  3329. if (value !== undefined && value.trim().length > 0) {
  3330. value = value.trim();
  3331. if (fldName === 'counties' || fldName === 'labelFields') {
  3332. value = value.split(',').map((item) => item.trim());
  3333. } else if (fldName === 'processLabel') {
  3334. try {
  3335. value = ESTreeProcessor.compile(`function __$proc(){${value}} __$proc();`);
  3336. } catch (ex) {
  3337. layerDef.labelProcessingError = true;
  3338. logError(`Error loading label processing function for layer "${layerDef.id}".`);
  3339. logDebug(ex);
  3340. }
  3341. } else if (fldName === 'style') {
  3342. layerDef.isRoadLayer = value === 'roads';
  3343. if (!layerDef.isRoadLayer && !LAYER_STYLES.hasOwnProperty(value)) {
  3344. // If style is not defined, try to read in as JSON (custom style)
  3345. try {
  3346. value = JSON.parse(value);
  3347. } catch (ex) {
  3348. logError(`Invalid style definition for layer "${layerDef.id}".`);
  3349. }
  3350. }
  3351. } else if (fldName === 'state') {
  3352. value = value ? value.toUpperCase() : value;
  3353. } else if (fldName === 'restrictTo') {
  3354. try {
  3355. const values = value.split(',').map((v) => v.trim().toLowerCase());
  3356. layerDef.notAllowed = !values.some((entry) => {
  3357. const rankMatch = entry.match(/^r(\d)(\+am)?$/);
  3358. if (rankMatch) {
  3359. if (rankMatch[1] <= userInfo.rank + 1 && (!rankMatch[2] || userInfo.isAreaManager)) {
  3360. return true;
  3361. }
  3362. } else if (entry === 'am' && userInfo.isAreaManager) {
  3363. return true;
  3364. } else if (entry === userInfo.userName.toLowerCase()) {
  3365. return true;
  3366. }
  3367. return false;
  3368. });
  3369. } catch (ex) {
  3370. logError(ex);
  3371. }
  3372. }
  3373. layerDef[fldName] = value;
  3374. } else if (fldName === 'labelFields') {
  3375. layerDef[fldName] = [''];
  3376. }
  3377. });
  3378. const enabled = layerDef.enabled && !['0', 'false', 'no', 'n'].includes(layerDef.enabled.toString().trim().toLowerCase());
  3379. if (!layerDef.notAllowed && enabled) {
  3380. _gisLayers.push(layerDef);
  3381. }
  3382. });
  3383. }
  3384.  
  3385. return result;
  3386. }
  3387.  
  3388. function createShortcut(shortcutId, description, callback) {
  3389. let shortcutKeys = settings.shortcuts?.[shortcutId] ?? null;
  3390. if (shortcutKeys && sdk.Shortcuts.areShortcutKeysInUse({ shortcutKeys })) {
  3391. shortcutKeys = null;
  3392. }
  3393. sdk.Shortcuts.createShortcut({
  3394. shortcutId,
  3395. shortcutKeys,
  3396. description,
  3397. callback,
  3398. });
  3399. }
  3400.  
  3401. async function init(firstCall = true) {
  3402. _gisLayers = [];
  3403. if (firstCall) {
  3404. userInfo = sdk.State.getUserInfo();
  3405. labelProcessingGlobalVariables.sdk = sdk;
  3406. initRoadStyle();
  3407. loadSettingsFromStorage();
  3408. createShortcut('toggleHnsOnly', 'Toggle HN-only address labels (GIS Layers)', onAddressDisplayShortcutKey);
  3409. createShortcut('toggleEnabled', 'Toggle display of GIS Layers', onToggleGisLayersShortcutKey);
  3410. installPathFollowingLabels();
  3411. window.addEventListener('beforeunload', saveSettingsToStorage, false);
  3412. _layerSettingsDialog = new LayerSettingsDialog();
  3413. }
  3414. const t0 = performance.now();
  3415. try {
  3416. const result = await loadSpreadsheetAsync();
  3417. if (result.error) {
  3418. logError(result.error);
  3419. return;
  3420. }
  3421. _layerRefinements.forEach((layerRefinement) => {
  3422. const layerDef = _gisLayers.find((layerDef2) => layerDef2.id === layerRefinement.id);
  3423. if (layerDef) {
  3424. Object.keys(layerRefinement).forEach((fldName) => {
  3425. const value = layerRefinement[fldName];
  3426. if (fldName !== 'id' && layerDef.hasOwnProperty(fldName)) {
  3427. logDebug(`The "${fldName}" property of layer "${layerDef.id}" has a value hardcoded in the script, and also defined in the spreadsheet.` + ' The spreadsheet value takes precedence.');
  3428. } else if (value) layerDef[fldName] = value;
  3429. });
  3430. } else {
  3431. logDebug(`Refined layer "${layerRefinement.id}" does not have a corresponding layer defined` + ' in the spreadsheet. It can probably be removed from the script.');
  3432. }
  3433. });
  3434. logDebug(`Loaded ${_gisLayers.length} layer definitions in ${Math.round(performance.now() - t0)} ms.`);
  3435. initGui(firstCall);
  3436. fetchFeatures();
  3437. $('#gis-layers-refresh').removeClass('fa-spin').css({ cursor: 'pointer' });
  3438. logDebug('Initialized.');
  3439. } catch (err) {
  3440. logError(err);
  3441. }
  3442. }
  3443.  
  3444. init();
  3445.  
  3446. /*eslint-disable*/
  3447. function installPathFollowingLabels() {
  3448. // Copyright (c) 2015 by Jean-Marc.Viglino [at]ign.fr
  3449. // Dual-licensed under the CeCILL-B Licence (http://www.cecill.info/)
  3450. // and the Beerware license (http://en.wikipedia.org/wiki/Beerware),
  3451. // feel free to use and abuse it in your projects (the code, not the beer ;-).
  3452. //
  3453. //* Overwrite the SVG function to allow text along a path
  3454. //* setStyle function
  3455. //*
  3456. //* Add new options to the Openlayers.Style
  3457.  
  3458. // pathLabel: {String} Label to draw on the path
  3459. // pathLabelXOffset: {String} Offset along the line to start drawing text in pixel or %, default: "50%"
  3460. // pathLabelYOffset: {Number} Distance of the line to draw the text
  3461. // pathLabelCurve: {String} Smooth the line the label is drawn on (empty string for no)
  3462. // pathLabelReadable: {String} Make the label readable (empty string for no)
  3463.  
  3464. // * Extra standard values : all label and text values
  3465.  
  3466. // *
  3467. // * Method: removeChildById
  3468. // * Remove child in a node.
  3469. // *
  3470.  
  3471. function removeChildById(node, id) {
  3472. if (node.querySelector) {
  3473. var c = node.querySelector('#' + id);
  3474. if (c) node.removeChild(c);
  3475. return;
  3476. }
  3477. // For old browsers
  3478. var c = node.childNodes;
  3479. if (c)
  3480. for (var i = 0; i < c.length; i++) {
  3481. if (c[i].id === id) {
  3482. node.removeChild(c[i]);
  3483. return;
  3484. }
  3485. }
  3486. }
  3487.  
  3488. // *
  3489. // * Method: setStyle
  3490. // * Use to set all the style attributes to a SVG node.
  3491. // *
  3492. // * Takes care to adjust stroke width and point radius to be
  3493. // * resolution-relative
  3494. // *
  3495. // * Parameters:
  3496. // * node - {SVGDomElement} An SVG element to decorate
  3497. // * style - {Object}
  3498. // * options - {Object} Currently supported options include
  3499. // * 'isFilled' {Boolean} and
  3500. // * 'isStroked' {Boolean}
  3501.  
  3502. var setStyle = OpenLayers.Renderer.SVG.prototype.setStyle;
  3503. OpenLayers.Renderer.SVG.LABEL_STARTOFFSET = { l: '0%', r: '100%', m: '50%' };
  3504.  
  3505. OpenLayers.Renderer.SVG.prototype.pathText = function (node, style, suffix) {
  3506. var label = this.nodeFactory(null, 'text');
  3507. label.setAttribute('id', node._featureId + '_' + suffix);
  3508. if (style.fontColor) label.setAttributeNS(null, 'fill', style.fontColor);
  3509. if (style.fontStrokeColor) label.setAttributeNS(null, 'stroke', style.fontStrokeColor);
  3510. if (style.fontStrokeWidth) label.setAttributeNS(null, 'stroke-width', style.fontStrokeWidth);
  3511. if (style.fontOpacity) label.setAttributeNS(null, 'opacity', style.fontOpacity);
  3512. if (style.fontFamily) label.setAttributeNS(null, 'font-family', style.fontFamily);
  3513. if (style.fontSize) label.setAttributeNS(null, 'font-size', style.fontSize);
  3514. if (style.fontWeight) label.setAttributeNS(null, 'font-weight', style.fontWeight);
  3515. if (style.fontStyle) label.setAttributeNS(null, 'font-style', style.fontStyle);
  3516. if (style.labelSelect === true) {
  3517. label.setAttributeNS(null, 'pointer-events', 'visible');
  3518. label._featureId = node._featureId;
  3519. } else {
  3520. label.setAttributeNS(null, 'pointer-events', 'none');
  3521. }
  3522.  
  3523. function getpath(pathStr, readeable) {
  3524. var npath = pathStr.split(',');
  3525. var pts = [];
  3526. if (!readeable || Number(npath[0]) - Number(npath[npath.length - 2]) < 0) {
  3527. while (npath.length) pts.push({ x: Number(npath.shift()), y: Number(npath.shift()) });
  3528. } else {
  3529. while (npath.length) pts.unshift({ x: Number(npath.shift()), y: Number(npath.shift()) });
  3530. }
  3531. return pts;
  3532. }
  3533.  
  3534. var path = this.nodeFactory(null, 'path');
  3535. var tpid = node._featureId + '_t' + suffix;
  3536. var tpath = node.getAttribute('points');
  3537. if (style.pathLabelCurve) {
  3538. var pts = getpath(tpath, style.pathLabelReadable);
  3539. var p = pts[0].x + ' ' + pts[0].y;
  3540. var dx, dy, s1, s2;
  3541. dx = (pts[0].x - pts[1].x) / 4;
  3542. dy = (pts[0].y - pts[1].y) / 4;
  3543. for (var i = 1; i < pts.length - 1; i++) {
  3544. p += ' C ' + (pts[i - 1].x - dx) + ' ' + (pts[i - 1].y - dy);
  3545. dx = (pts[i - 1].x - pts[i + 1].x) / 4;
  3546. dy = (pts[i - 1].y - pts[i + 1].y) / 4;
  3547. s1 = Math.sqrt(Math.pow(pts[i - 1].x - pts[i].x, 2) + Math.pow(pts[i - 1].y - pts[i].y, 2));
  3548. s2 = Math.sqrt(Math.pow(pts[i + 1].x - pts[i].x, 2) + Math.pow(pts[i + 1].y - pts[i].y, 2));
  3549. p += ' ' + (pts[i].x + (s1 * dx) / s2) + ' ' + (pts[i].y + (s1 * dy) / s2);
  3550. dx *= s2 / s1;
  3551. dy *= s2 / s1;
  3552. p += ' ' + pts[i].x + ' ' + pts[i].y;
  3553. }
  3554. p += ' C ' + (pts[i - 1].x - dx) + ' ' + (pts[i - 1].y - dy);
  3555. dx = (pts[i - 1].x - pts[i].x) / 4;
  3556. dy = (pts[i - 1].y - pts[i].y) / 4;
  3557. p += ' ' + (pts[i].x + dx) + ' ' + (pts[i].y + dy);
  3558. p += ' ' + pts[i].x + ' ' + pts[i].y;
  3559.  
  3560. path.setAttribute('d', 'M ' + p);
  3561. } else {
  3562. if (style.pathLabelReadable) {
  3563. var pts = getpath(tpath, style.pathLabelReadable);
  3564. var p = '';
  3565. for (var i = 0; i < pts.length; i++) p += ' ' + pts[i].x + ' ' + pts[i].y;
  3566. path.setAttribute('d', 'M ' + p);
  3567. } else path.setAttribute('d', 'M ' + tpath);
  3568. }
  3569. path.setAttribute('id', tpid);
  3570.  
  3571. var defs = this.createDefs();
  3572. removeChildById(defs, tpid);
  3573. defs.appendChild(path);
  3574.  
  3575. var textPath = this.nodeFactory(null, 'textPath');
  3576. textPath.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + tpid);
  3577. var align = style.labelAlign || OpenLayers.Renderer.defaultSymbolizer.labelAlign;
  3578. label.setAttributeNS(null, 'text-anchor', OpenLayers.Renderer.SVG.LABEL_ALIGN[align[0]] || 'middle');
  3579. textPath.setAttribute('startOffset', style.pathLabelXOffset || OpenLayers.Renderer.SVG.LABEL_STARTOFFSET[align[0]] || '50%');
  3580. label.setAttributeNS(null, 'dominant-baseline', OpenLayers.Renderer.SVG.LABEL_ALIGN[align[1]] || 'central');
  3581. if (style.pathLabelYOffset) label.setAttribute('dy', style.pathLabelYOffset);
  3582. //textPath.setAttribute('method','stretch');
  3583. //textPath.setAttribute('spacing','auto');
  3584.  
  3585. textPath.textContent = style.pathLabel;
  3586. label.appendChild(textPath);
  3587.  
  3588. removeChildById(this.textRoot, node._featureId + '_' + suffix);
  3589. this.textRoot.appendChild(label);
  3590. };
  3591.  
  3592. OpenLayers.Renderer.SVG.prototype.setStyle = function (node, style, options) {
  3593. if (node._geometryClass === 'OpenLayers.Geometry.LineString' && style.pathLabel) {
  3594. if (node._geometryClass === 'OpenLayers.Geometry.LineString' && style.pathLabel) {
  3595. var drawOutline = !!style.labelOutlineWidth;
  3596. // First draw text in halo color and size and overlay the
  3597. // normal text afterwards
  3598. if (drawOutline) {
  3599. var outlineStyle = OpenLayers.Util.extend({}, style);
  3600. outlineStyle.fontColor = outlineStyle.labelOutlineColor;
  3601. outlineStyle.fontStrokeColor = outlineStyle.labelOutlineColor;
  3602. outlineStyle.fontStrokeWidth = style.labelOutlineWidth;
  3603. if (style.labelOutlineOpacity) outlineStyle.fontOpacity = style.labelOutlineOpacity;
  3604. delete outlineStyle.labelOutlineWidth;
  3605. this.pathText(node, outlineStyle, 'txtpath0');
  3606. }
  3607. this.pathText(node, style, 'txtpath');
  3608. setStyle.apply(this, arguments);
  3609. }
  3610. } else setStyle.apply(this, arguments);
  3611. return node;
  3612. };
  3613.  
  3614. // *
  3615. // * Method: drawGeometry
  3616. // * Remove the textpath if no geometry is drawn.
  3617. // *
  3618. // * Parameters:
  3619. // * geometry - {<OpenLayers.Geometry>}
  3620. // * style - {Object}
  3621. // * featureId - {String}
  3622. // *
  3623. // * Returns:
  3624. // * {Boolean} true if the geometry has been drawn completely; null if
  3625. // * incomplete; false otherwise
  3626.  
  3627. var drawGeometry = OpenLayers.Renderer.SVG.prototype.drawGeometry;
  3628. OpenLayers.Renderer.SVG.prototype.drawGeometry = function (geometry, style, id) {
  3629. var rendered = drawGeometry.apply(this, arguments);
  3630. if (rendered === false) {
  3631. removeChildById(this.textRoot, id + '_txtpath');
  3632. removeChildById(this.textRoot, id + '_txtpath0');
  3633. }
  3634. return rendered;
  3635. };
  3636.  
  3637. // *
  3638. // * Method: eraseGeometry
  3639. // * Erase a geometry from the renderer. In the case of a multi-geometry,
  3640. // * we cycle through and recurse on ourselves. Otherwise, we look for a
  3641. // * node with the geometry.id, destroy its geometry, and remove it from
  3642. // * the DOM.
  3643. // *
  3644. // * Parameters:
  3645. // * geometry - {<OpenLayers.Geometry>}
  3646. // * featureId - {String}
  3647.  
  3648. var eraseGeometry = OpenLayers.Renderer.SVG.prototype.eraseGeometry;
  3649. OpenLayers.Renderer.SVG.prototype.eraseGeometry = function (geometry, featureId) {
  3650. eraseGeometry.apply(this, arguments);
  3651. removeChildById(this.textRoot, featureId + '_txtpath');
  3652. removeChildById(this.textRoot, featureId + '_txtpath0');
  3653. };
  3654. }
  3655. })();