WME GIS Layers

Adds GIS layers in WME

当前为 2025-06-15 提交的版本,查看 最新版本

  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.15.000
  7. // @description Adds GIS layers in WME
  8. // @author MapOMatic
  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. 'SDK Performance and Stability Update: We have made improvements to enhance your experience. If you encounter any issues, please report them on Discord or Discuss forums.',
  1158. ];
  1159.  
  1160. // **************************************************************************************************************
  1161. // IMPORTANT: Update this when releasing a new version of script that includes changes to the spreadsheet format
  1162. // that may cause old code to break. This # should match the version listed in the spreadsheet
  1163. // i.e. update them at the same time.
  1164.  
  1165. // const LAYER_DEF_VERSION = '2018.04.27.001'; // NOT ACTUALLY USED YET
  1166.  
  1167. // **************************************************************************************************************
  1168. // const UPDATE_MESSAGE = 'Bug fix due to WME update';
  1169. // const UPDATE_MESSAGE = `<ul>${[
  1170. // 'Added ability to shift layers. Right click a layer in the list to bring up the layer settings window.'
  1171. // ].map(item => `<li>${item}</li>`).join('')}</ul><br>`;
  1172. const GF_URL = 'https://greasyfork.org/scripts/369632-wme-gis-layers';
  1173. // Used in tooltips to tell people who to report issues to. Update if a new author takes ownership of this script.
  1174. const SCRIPT_AUTHOR = 'MapOMatic';
  1175. // const LAYER_INFO_URL = 'https://spreadsheets.google.com/feeds/list/1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw/o7gusx3/public/values?alt=json';
  1176. const LAYER_DEF_SPREADSHEET_URL = 'https://sheets.googleapis.com/v4/spreadsheets/1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw/values/layerDefs';
  1177. const API_KEY = 'YTJWNVBVRkplbUZUZVVGTlNXOWlVR1pWVjIxcE9VdHJNbVY0TTFoeWNrSlpXbFZuVmtWelRrMVVWUT09';
  1178. const REQUEST_FORM_URL = 'https://docs.google.com/forms/d/e/1FAIpQLSevPQLz2ohu_LTge9gJ9Nv6PURmCmaSSjq0ayOJpGdRr2xI0g/viewform?usp=pp_url&entry.2116052852={username}';
  1179. const DEC = (s) => atob(atob(s));
  1180. const PRIVATE_LAYERS = { 'nc-henderson-sl-signs': ['the_cre8r', 'mapomatic'] }; // case sensitive -- use all lower case
  1181. // const COUNTRIES = {
  1182. // 'United States': {
  1183. // sheetId: '1cEG3CvXSCI4TOZyMQTI50SQGbVhJ48Xip-jjWg4blWw',
  1184. // sheetLayerRange: 'layerDefs'
  1185. // }
  1186. // };
  1187. const DEFAULT_LAYER_NAME = 'GIS Layers - Default';
  1188. const ROAD_LAYER_NAME = 'GIS Layers - Roads';
  1189. const DEFAULT_STYLE = {
  1190. fillColor: '#000',
  1191. pointRadius: 4,
  1192. label: '${getLabel}',
  1193. fillOpacity: '0.95',
  1194. strokeColor: '#ffa500',
  1195. strokeOpacity: '0.95',
  1196. strokeWidth: 1.5,
  1197. fontColor: '#ffc520',
  1198. fontSize: '13',
  1199. labelOutlineColor: 'black',
  1200. labelOutlineWidth: 3,
  1201. };
  1202. const LAYER_STYLES = {
  1203. cities: {
  1204. fillOpacity: 0.3,
  1205. fillColor: '#f65',
  1206. strokeColor: '#f65',
  1207. fontColor: '#f62',
  1208. },
  1209. forests_parks: {
  1210. fillOpacity: 0.4,
  1211. fillColor: '#585',
  1212. strokeColor: '#484',
  1213. fontColor: '#8b8',
  1214. },
  1215. milemarkers: {
  1216. strokeColor: '#fff',
  1217. fontColor: '#fff',
  1218. fontWeight: 'bold',
  1219. fillOpacity: 0,
  1220. labelYOffset: 10,
  1221. pointRadius: 2,
  1222. fontSize: 12,
  1223. },
  1224. parcels: {
  1225. fillOpacity: 0,
  1226. fillColor: '#ffa500',
  1227. },
  1228. points: {
  1229. strokeColor: '#000',
  1230. fontColor: '#0ff',
  1231. fillColor: '#0ff',
  1232. labelYOffset: -10,
  1233. labelAlign: 'ct',
  1234. },
  1235. post_offices: {
  1236. strokeColor: '#000',
  1237. fontColor: '#f84',
  1238. fillColor: '#f84',
  1239. fontWeight: 'bold',
  1240. labelYOffset: -10,
  1241. labelAlign: 'ct',
  1242. },
  1243. state_parcels: {
  1244. fillOpacity: 0,
  1245. strokeColor: '#e62',
  1246. fillColor: '#e62',
  1247. fontColor: '#e73',
  1248. },
  1249. state_points: {
  1250. strokeColor: '#000',
  1251. fontColor: '#3cf',
  1252. fillColor: '#3cf',
  1253. labelYOffset: -10,
  1254. labelAlign: 'ct',
  1255. },
  1256. road_labels: {
  1257. strokeOpacity: 0,
  1258. fillOpacity: 0,
  1259. fontColor: '#faf',
  1260. },
  1261. structures: {
  1262. fillOpacity: 0,
  1263. strokeColor: '#f7f',
  1264. fontColor: '#f7f',
  1265. },
  1266. };
  1267. let ROAD_STYLE;
  1268. function initRoadStyle() {
  1269. ROAD_STYLE = {
  1270. pointRadius: 12,
  1271. fillColor: '#369',
  1272. pathLabel: '${getLabel}',
  1273. label: '',
  1274. fontColor: '#faf',
  1275. labelSelect: true,
  1276. pathLabelYOffset: '${getOffset}',
  1277. pathLabelCurve: '${getSmooth}',
  1278. pathLabelReadable: '${getReadable}',
  1279. labelAlign: '${getAlign}',
  1280. labelOutlineWidth: 3,
  1281. labelOutlineColor: '#000',
  1282. strokeWidth: 3,
  1283. stroke: true,
  1284. strokeColor: '#f0f',
  1285. strokeOpacity: 0.4,
  1286. fontWeight: 'bold',
  1287. fontSize: 11,
  1288. };
  1289. }
  1290.  
  1291. // eslint-disable-next-line no-unused-vars
  1292. const _regexReplace = {
  1293. // Strip leading zeros or blank full label for any label starting with a non-digit or
  1294. // is a Zero Address, use with '' as replace.
  1295. r0: /^(0+(\s.*)?|\D.*)/,
  1296. // Strip Everything After Street Type to end of the string by use $1 and $2 capture
  1297. // groups, use with replace '$1$2'
  1298. // eslint-disable-next-line max-len
  1299. 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,
  1300. // Strip SPACE 5 Digits from end of string, use with replace ''
  1301. r2: /\s\d{5}$/,
  1302. // Strip Everything after a "~", ",", ";" to the end of the string, use with replace ''
  1303. r3: /(~|,|;|\s?\r\n).*$/,
  1304. // Move the digits after the last space to before the rest of the string using, use with
  1305. // replace '$2 $1'
  1306. r4: /^(.*)\s(\d+).*/,
  1307. // Insert newline between digits (including "-") and everything after the digits,
  1308. // except(and before) a ",", use with replace '$1\n$2'
  1309. r5: /^([-\d]+)\s+([^,]+).*/,
  1310. // Insert newline between digits and everything after the digits, use with
  1311. // replace '$1\n$2'
  1312. r6: /^(\d+)\s+(.*)/,
  1313. };
  1314.  
  1315. let _gisLayers = [];
  1316.  
  1317. const _layerRefinements = [
  1318. {
  1319. id: 'us-post-offices',
  1320. labelHeaderFields: ['LOCALE_NAME'],
  1321. },
  1322. ];
  1323.  
  1324. const STATES = {
  1325. _states: [
  1326. ['US (Country)', 'US', -1],
  1327. ['Alabama', 'AL', 1],
  1328. ['Alaska', 'AK', 2],
  1329. ['American Samoa', 'AS', 60],
  1330. ['Arizona', 'AZ', 4],
  1331. ['Arkansas', 'AR', 5],
  1332. ['California', 'CA', 6],
  1333. ['Colorado', 'CO', 8],
  1334. ['Connecticut', 'CT', 9],
  1335. ['Delaware', 'DE', 10],
  1336. ['District of Columbia', 'DC', 11],
  1337. ['Florida', 'FL', 12],
  1338. ['Georgia', 'GA', 13],
  1339. ['Guam', 'GU', 66],
  1340. ['Hawaii', 'HI', 15],
  1341. ['Idaho', 'ID', 16],
  1342. ['Illinois', 'IL', 17],
  1343. ['Indiana', 'IN', 18],
  1344. ['Iowa', 'IA', 19],
  1345. ['Kansas', 'KS', 20],
  1346. ['Kentucky', 'KY', 21],
  1347. ['Louisiana', 'LA', 22],
  1348. ['Maine', 'ME', 23],
  1349. ['Maryland', 'MD', 24],
  1350. ['Massachusetts', 'MA', 25],
  1351. ['Michigan', 'MI', 26],
  1352. ['Minnesota', 'MN', 27],
  1353. ['Mississippi', 'MS', 28],
  1354. ['Missouri', 'MO', 29],
  1355. ['Montana', 'MT', 30],
  1356. ['Nebraska', 'NE', 31],
  1357. ['Nevada', 'NV', 32],
  1358. ['New Hampshire', 'NH', 33],
  1359. ['New Jersey', 'NJ', 34],
  1360. ['New Mexico', 'NM', 35],
  1361. ['New York', 'NY', 36],
  1362. ['North Carolina', 'NC', 37],
  1363. ['North Dakota', 'ND', 38],
  1364. ['Northern Mariana Islands', 'MP', 69],
  1365. ['Ohio', 'OH', 39],
  1366. ['Oklahoma', 'OK', 40],
  1367. ['Oregon', 'OR', 41],
  1368. ['Pennsylvania', 'PA', 42],
  1369. ['Puerto Rico', 'PR', 72],
  1370. ['Rhode Island', 'RI', 44],
  1371. ['South Carolina', 'SC', 45],
  1372. ['South Dakota', 'SD', 46],
  1373. ['Tennessee', 'TN', 47],
  1374. ['Texas', 'TX', 48],
  1375. ['Utah', 'UT', 49],
  1376. ['Vermont', 'VT', 50],
  1377. ['Virgin Islands', 'VI', 78],
  1378. ['Virginia', 'VA', 51],
  1379. ['Washington', 'WA', 53],
  1380. ['West Virginia', 'WV', 54],
  1381. ['Wisconsin', 'WI', 55],
  1382. ['Wyoming', 'WY', 56],
  1383. ],
  1384. toAbbr(fullName) {
  1385. return this._states.find((a) => a[0] === fullName)?.[1]; // Returns undefined if not found
  1386. },
  1387. toFullName(abbr) {
  1388. return this._states.find((a) => a[1] === abbr)?.[0]; // Returns undefined if not found
  1389. },
  1390. toFullNameArray() {
  1391. return this._states.map((a) => a[0]);
  1392. },
  1393. toAbbrArray() {
  1394. return this._states.map((a) => a[1]);
  1395. },
  1396. fromId(id) {
  1397. return this._states.find((a) => a[2] === id); // Returns undefined if not found
  1398. },
  1399. };
  1400. const DEFAULT_VISIBLE_AT_ZOOM = 18;
  1401. const SETTINGS_STORE_NAME = 'wme_gis_layers_fl';
  1402. const COUNTIES_URL = 'https://tigerweb.geo.census.gov/arcgis/rest/services/Census2020/State_County/MapServer/1/';
  1403. const scriptName = GM_info.script.name;
  1404. const scriptVersion = GM_info.script.version;
  1405. const downloadUrl = 'https://greasyfork.org/scripts/369632-wme-gis-layers/code/WME%20GIS%20Layers.user.js';
  1406. const sdk = await bootstrap({ scriptUpdateMonitor: { downloadUrl } });
  1407. let settings = {};
  1408. let ignoreFetch = false;
  1409. let lastToken = {};
  1410. let userInfo;
  1411.  
  1412. // Variables to store Label popup position and selected layer
  1413. const layerLabels = {};
  1414. let isPopupVisible = null;
  1415. const popupPosition = { left: '50%', top: '50%' };
  1416. let popupActiveLayer = null;
  1417. let useAcronyms = false;
  1418. let useTitleCase = false;
  1419. let useStateHwy = false;
  1420. let removeNewLines = false;
  1421.  
  1422. const DEBUG = true;
  1423. // function log(message) { console.log('GIS Layers:', message); }
  1424. function logError(message, args = []) {
  1425. console.error(`${scriptName}:`, message, ...args);
  1426. }
  1427. function logDebug(message, args = []) {
  1428. if (DEBUG) console.debug(`${scriptName}:`, message, ...args);
  1429. }
  1430. // function logWarning(message) { console.warn('GIS Layers:', message); }
  1431.  
  1432. let _layerSettingsDialog;
  1433.  
  1434. class LayerSettingsDialog {
  1435. #gisLayer;
  1436. #minVisibleAtZoom = 12;
  1437. #maxVisibleAtZoom = 22;
  1438. #titleText;
  1439. #shiftUpButton;
  1440. #visibleAtZoomInput;
  1441.  
  1442. constructor() {
  1443. this.#titleText = $('<span>');
  1444. const closeButton = $('<span>', {
  1445. style: 'cursor:pointer;padding-left:4px;font-size:17px;color:#d6e6f3;float:right;',
  1446. class: 'fa fa-window-close',
  1447. }).click(() => this.#onCloseButtonClick());
  1448. const shiftUpButton = LayerSettingsDialog.#createShiftButton('fa-angle-up').click(() => this.#onShiftButtonClick(0, 1));
  1449. const shiftLeftButton = LayerSettingsDialog.#createShiftButton('fa-angle-left').click(() => this.#onShiftButtonClick(-1, 0));
  1450. const shiftRightButton = LayerSettingsDialog.#createShiftButton('fa-angle-right').click(() => this.#onShiftButtonClick(1, 0));
  1451. const shiftDownButton = LayerSettingsDialog.#createShiftButton('fa-angle-down').click(() => this.#onShiftButtonClick(0, -1));
  1452. const resetOffsetButton = $('<button>', {
  1453. class: 'form-control',
  1454. style: 'height: 24px; width: auto; padding: 2px 6px 0px 6px; display: inline-block; float: right;',
  1455. })
  1456. .text('Reset')
  1457. .click(() => this.#onResetOffsetButtonClick());
  1458.  
  1459. this._dialogDiv = $('<div>', {
  1460. style:
  1461. 'position: fixed; top: 15%; left: 400px; width: 200px; z-index: 100; background-color: #73a9bd; border-width: 1px; border-style: solid;' +
  1462. 'border-radius: 10px; box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.7); border-color: #50667b; padding: 4px;',
  1463. }).append(
  1464. $('<div>').append(
  1465. // The extra div is needed here. When the header text wraps, the main dialog div won't expand properly without it.
  1466. // HEADER
  1467. $('<div>', { style: 'border-radius:5px 5px 0px 0px; padding: 4px; color: #fff; font-weight: bold; text-align:left; cursor: default;' }).append(closeButton, this.#titleText),
  1468. // BODY
  1469. $('<div>').append(
  1470. $('<div>', { style: 'border-radius: 5px; width: 100%; padding: 4px; background-color:#d6e6f3; display:inline-block; margin-right:5px;' }).append(
  1471. resetOffsetButton,
  1472. $('<input>', {
  1473. type: 'radio',
  1474. id: 'gisLayerShiftAmt1',
  1475. name: 'gisLayerShiftAmt',
  1476. value: '1',
  1477. checked: 'checked',
  1478. }),
  1479. $('<label>', { for: 'gisLayerShiftAmt1' }).text('1m'),
  1480. $('<input>', {
  1481. type: 'radio',
  1482. id: 'gisLayerShiftAmt10',
  1483. name: 'gisLayerShiftAmt',
  1484. value: '10',
  1485. style: 'margin-left: 6px',
  1486. }),
  1487. $('<label>', { for: 'gisLayerShiftAmt10' }).text('10m'),
  1488. $('<div>', { style: 'padding: 4px' }).append(
  1489. $('<table>', { style: 'table-layout:fixed; width:60px; height:84px; margin-left:auto;margin-right:auto;' }).append(
  1490. $('<tr>', { style: 'width: 20px; height: 28px;' }).append($('<td>', { align: 'center' }), $('<td>', { align: 'center' }).append(shiftUpButton), $('<td>', { align: 'center' })),
  1491. $('<tr>', { style: 'width: 20px; height: 28px;' }).append(
  1492. $('<td>', { align: 'center' }).append(shiftLeftButton),
  1493. $('<td>', { align: 'center' }),
  1494. $('<td>', { align: 'center' }).append(shiftRightButton)
  1495. ),
  1496. $('<tr>', { style: 'width: 20px; height: 28px;' }).append($('<td>', { align: 'center' }), $('<td>', { align: 'center' }).append(shiftDownButton), $('<td>', { align: 'center' }))
  1497. )
  1498. )
  1499. ),
  1500. $('<div>', { style: 'border-radius: 5px; width: 100%; padding: 4px; background-color: #d6e6f3; display: inline-block; margin-right: 5px; margin-top: 2px;' }).append(
  1501. $('<div>', { style: 'display: flex; justify-content: flex-end; margin-bottom: 4px;' }).append(
  1502. $('<button>', { class: 'form-control', style: 'height: 24px; width: auto; padding: 2px 6px 0px 6px;' }).text('Reset').click(this.#onResetVisibleAtZoomClick.bind(this))
  1503. ),
  1504. $('<div>').append(
  1505. $('<label>', { for: 'visible-at-zoom-input' }).text('Visible at zoom:'),
  1506. (this.#visibleAtZoomInput = $('<input>', {
  1507. type: 'number',
  1508. id: 'visible-at-zoom-input',
  1509. min: this.#minVisibleAtZoom,
  1510. max: this.#maxVisibleAtZoom,
  1511. style: 'margin-left: 4px;',
  1512. }).change((v) => this.#onVisibleAtZoomChange(v)))
  1513. ),
  1514. $('<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.')
  1515. )
  1516. )
  1517. )
  1518. );
  1519.  
  1520. this.hide();
  1521. this._dialogDiv.appendTo('body');
  1522.  
  1523. if (typeof jQuery.ui !== 'undefined') {
  1524. const that = this;
  1525. this._dialogDiv.draggable({
  1526. // Gotta nuke the height setting the dragging inserts otherwise the panel cannot dynamically resize
  1527. stop() {
  1528. that._dialogDiv.css('height', '');
  1529. },
  1530. });
  1531. }
  1532. }
  1533.  
  1534. get gisLayer() {
  1535. return this.#gisLayer;
  1536. }
  1537.  
  1538. set gisLayer(value) {
  1539. if (value !== this.#gisLayer) {
  1540. this.#gisLayer = value;
  1541. this.#titleText.text(this.#gisLayer.name);
  1542. this.#initVisibleAtZoomInput();
  1543. }
  1544. }
  1545.  
  1546. #initVisibleAtZoomInput() {
  1547. this.#visibleAtZoomInput.val(getGisLayerVisibleAtZoom(this.#gisLayer));
  1548. }
  1549.  
  1550. // eslint-disable-next-line class-methods-use-this
  1551. getShiftAmount() {
  1552. return $('input[name=gisLayerShiftAmt]:checked').val();
  1553. }
  1554.  
  1555. show() {
  1556. this._dialogDiv.show();
  1557. }
  1558.  
  1559. hide() {
  1560. this._dialogDiv.hide();
  1561. }
  1562.  
  1563. #onResetVisibleAtZoomClick() {
  1564. settings.removeLayerSetting(this.#gisLayer.id, 'visibleAtZoom');
  1565. this.#initVisibleAtZoomInput();
  1566. }
  1567.  
  1568. #onCloseButtonClick() {
  1569. this.hide();
  1570. }
  1571.  
  1572. #onVisibleAtZoomChange() {
  1573. const min = this.#minVisibleAtZoom;
  1574. const max = this.#maxVisibleAtZoom;
  1575. let value = parseInt(this.#visibleAtZoomInput.val(), 10);
  1576.  
  1577. if (value < min) {
  1578. value = min;
  1579. this.#visibleAtZoomInput.val(value);
  1580. } else if (value > max) {
  1581. value = max;
  1582. this.#visibleAtZoomInput.val(value);
  1583. }
  1584.  
  1585. settings.setLayerSetting(this.#gisLayer.id, 'visibleAtZoom', value);
  1586. saveSettingsToStorage();
  1587. }
  1588.  
  1589. #onShiftButtonClick(x, y) {
  1590. const shiftAmount = this.getShiftAmount();
  1591. x *= shiftAmount;
  1592. y *= shiftAmount;
  1593. this.#shiftLayerFeatures(x, y);
  1594. const { id } = this.gisLayer;
  1595. let offset = settings.getLayerSetting(id, 'offset');
  1596. if (!offset) {
  1597. offset = { x: 0, y: 0 };
  1598. settings.setLayerSetting(id, 'offset', offset);
  1599. }
  1600. offset.x += x;
  1601. offset.y += y;
  1602. saveSettingsToStorage();
  1603. }
  1604.  
  1605. #onResetOffsetButtonClick() {
  1606. const offset = settings.getLayerSetting(this.gisLayer.id, 'offset');
  1607. if (offset) {
  1608. this.#shiftLayerFeatures(offset.x * -1, offset.y * -1);
  1609. delete settings.layers[this.gisLayer.id].offset;
  1610. saveSettingsToStorage();
  1611. }
  1612. }
  1613.  
  1614. #shiftLayerFeatures(x, y) {
  1615. const { isRoadLayer } = this.gisLayer;
  1616. let featureCollection = isRoadLayer ? roadFeatures : defaultFeatures;
  1617. const { distance, bearing } = LayerSettingsDialog.#calculateDistanceAndBearing(x, y);
  1618. featureCollection = featureCollection.filter((f) => f.properties.layerID === this.gisLayer.id).map((f) => turf.transformTranslate(f, distance, bearing, { units: 'meters' }));
  1619. if (isRoadLayer) {
  1620. roadFeatures = featureCollection;
  1621. } else {
  1622. defaultFeatures = featureCollection;
  1623. }
  1624. const layerName = isRoadLayer ? ROAD_LAYER_NAME : DEFAULT_LAYER_NAME;
  1625. const featureIds = featureCollection.map((f) => f.id);
  1626. sdk.Map.removeFeaturesFromLayer({ layerName, featureIds });
  1627. sdk.Map.addFeaturesToLayer({ layerName, features: featureCollection });
  1628. }
  1629.  
  1630. /**
  1631. * Calculates the total distance and bearing from X and Y meter offsets.
  1632. * @param {number} dx_meters - X offset in meters (east/west).
  1633. * @param {number} dy_meters - Y offset in meters (north/south).
  1634. * @returns {{distance: number, bearing: number}}
  1635. */
  1636. static #calculateDistanceAndBearing(dx_meters, dy_meters) {
  1637. const distance = Math.sqrt(dx_meters ** 2 + dy_meters ** 2);
  1638.  
  1639. // Calculate bearing in radians
  1640. // Math.atan2(y, x) returns angle in radians between -PI and PI
  1641. // Need to adjust to be 0-360 degrees clockwise from North
  1642. const bearing_rad = Math.atan2(dx_meters, dy_meters); // dx_meters is 'x' (east), dy_meters is 'y' (north)
  1643.  
  1644. // Convert to degrees and adjust for 0-360, clockwise from North
  1645. let bearing_deg = bearing_rad * (180 / Math.PI);
  1646. bearing_deg = (bearing_deg + 360) % 360; // Ensure positive and within 0-360 range
  1647.  
  1648. return { distance, bearing: bearing_deg };
  1649. }
  1650.  
  1651. static #createShiftButton(fontAwesomeClass) {
  1652. return $('<button>', {
  1653. class: 'form-control',
  1654. style: 'cursor:pointer;font-size:14px;padding: 3px;border-radius: 5px;width: 21px;height: 21px;',
  1655. }).append($('<i>', { class: 'fa', style: 'vertical-align: super' }).addClass(fontAwesomeClass));
  1656. }
  1657. }
  1658.  
  1659. function loadSettingsFromStorage() {
  1660. const defaultSettings = {
  1661. lastVersion: null,
  1662. visibleLayers: [],
  1663. onlyShowApplicableLayers: false,
  1664. selectedStates: [],
  1665. enabled: true,
  1666. fillParcels: false,
  1667. oneTimeAlerts: {},
  1668. layers: {},
  1669. shortcuts: {},
  1670. isPopupVisible: false,
  1671. useAcronyms: false,
  1672. useTitleCase: false,
  1673. useStateHwy: false,
  1674. removeNewLines: false,
  1675. collapsedSections: {},
  1676. };
  1677.  
  1678. let loadedSettings = {}; // Initialize as an empty object
  1679. const storedSettings = localStorage.getItem(SETTINGS_STORE_NAME);
  1680.  
  1681. if (storedSettings) {
  1682. try {
  1683. const parsed = JSON.parse(storedSettings);
  1684. if (parsed && typeof parsed === 'object') {
  1685. loadedSettings = parsed;
  1686. } else {
  1687. logDebug(`Stored settings under key "${SETTINGS_STORE_NAME}" were not a valid object.`);
  1688. }
  1689. } catch (e) {
  1690. logError(`Failed to parse settings from localStorage key "${SETTINGS_STORE_NAME}":`, e);
  1691. // loadedSettings remains {}
  1692. }
  1693. }
  1694.  
  1695. // Merge defaultSettings and loadedSettings.
  1696. // If loadedSettings is empty (due to error or no storage), it effectively uses defaults.
  1697. settings = { ...defaultSettings, ...loadedSettings };
  1698.  
  1699. isPopupVisible = settings.isPopupVisible;
  1700. useAcronyms = settings.useAcronyms;
  1701. useTitleCase = settings.useTitleCase;
  1702. useStateHwy = settings.useStateHwy;
  1703. removeNewLines = settings.removeNewLines;
  1704.  
  1705. settings.getLayerSetting = function getLayerSetting(layerID, settingName) {
  1706. const layerSettings = this.layers[layerID];
  1707. if (!layerSettings) {
  1708. return undefined;
  1709. }
  1710. return layerSettings[settingName];
  1711. };
  1712. settings.setLayerSetting = function setLayerSetting(layerID, settingName, value) {
  1713. let layerSettings = this.layers[layerID];
  1714. if (!layerSettings) {
  1715. layerSettings = {};
  1716. this.layers[layerID] = layerSettings;
  1717. }
  1718. layerSettings[settingName] = value;
  1719. };
  1720. settings.removeLayerSetting = function removeLayerSetting(layerID, settingName) {
  1721. const layerSettings = this.layers[layerID];
  1722. if (layerSettings) {
  1723. delete layerSettings[settingName];
  1724. }
  1725. };
  1726.  
  1727. // Handle legacy shortcut keys settings.
  1728. // TODO: Delete this later, after most users have updated.
  1729. if (settings.toggleHnsOnlyShortcut) {
  1730. settings.shortcuts.toggleHnsOnly = settings.toggleHnsOnlyShortcut;
  1731. delete settings.toggleHnsOnlyShortcut;
  1732. }
  1733. if (settings.toggleEnabledShortcut) {
  1734. settings.shortcuts.toggleEnabled = settings.toggleEnabledShortcut;
  1735. delete settings.toggleEnabledShortcut;
  1736. }
  1737. }
  1738.  
  1739. function saveSettingsToStorage() {
  1740. settings.shortcuts = {};
  1741. sdk.Shortcuts.getAllShortcuts().forEach((shortcut) => {
  1742. settings.shortcuts[shortcut.shortcutId] = shortcut.shortcutKeys;
  1743. });
  1744. settings.lastVersion = scriptVersion;
  1745. settings.isPopupVisible = isPopupVisible;
  1746. settings.useAcronyms = useAcronyms;
  1747. settings.useTitleCase = useTitleCase;
  1748. settings.useStateHwy = useStateHwy;
  1749. settings.removeNewLines = removeNewLines;
  1750. localStorage.setItem(SETTINGS_STORE_NAME, JSON.stringify(settings));
  1751. logDebug('Settings saved');
  1752. }
  1753.  
  1754. function getUrl(extent, gisLayer) {
  1755. const layerOffset = settings.getLayerSetting(gisLayer.id, 'offset') ?? { x: 0, y: 0 };
  1756. const geometry = {
  1757. xmin: extent[0] - layerOffset.x,
  1758. ymin: extent[1] - layerOffset.y,
  1759. xmax: extent[2] - layerOffset.x,
  1760. ymax: extent[3] - layerOffset.y,
  1761. spatialReference: {
  1762. wkid: 4326,
  1763. },
  1764. };
  1765. const geometryStr = JSON.stringify(geometry);
  1766. let fields = gisLayer.labelFields;
  1767. if (gisLayer.labelHeaderFields) {
  1768. fields = fields.concat(gisLayer.labelHeaderFields);
  1769. }
  1770. if (gisLayer.distinctFields) {
  1771. fields = fields.concat(gisLayer.distinctFields);
  1772. }
  1773. let url = `${gisLayer.url}/query?geometry=${encodeURIComponent(geometryStr)}`;
  1774. url += gisLayer.token ? `&token=${gisLayer.token}` : '';
  1775. url += `&outFields=${encodeURIComponent(fields.join(','))}`;
  1776. url += '&returnGeometry=true&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope';
  1777. url += `&inSR=${/* gisLayer.spatialReference ? gisLayer.spatialReference : */ '4326'}`;
  1778. url += '&outSR=4326&f=json';
  1779. url += gisLayer.where ? `&where=${encodeURIComponent(gisLayer.where)}` : '';
  1780.  
  1781. logDebug(`Request URL: ${url}`);
  1782. return url;
  1783. }
  1784.  
  1785. function hashString(value) {
  1786. let hash = 0;
  1787. if (value.length === 0) return hash;
  1788. for (let i = 0; i < value.length; i++) {
  1789. const chr = value.charCodeAt(i);
  1790. // eslint-disable-next-line no-bitwise
  1791. hash = (hash << 5) - hash + chr;
  1792. // eslint-disable-next-line no-bitwise
  1793. hash |= 0; // Convert to 32bit integer
  1794. }
  1795. return hash;
  1796. }
  1797.  
  1798. function getMapExtent(projection = 'wgs84') {
  1799. const wgs84Extent = sdk.Map.getMapExtent(); // Assume this provides WGS84 coordinates
  1800. const wgs84LeftBottom = [wgs84Extent[0], wgs84Extent[1]];
  1801. const wgs84RightTop = [wgs84Extent[2], wgs84Extent[3]];
  1802. const wgs84Projections = ['wgs84', 'CRS84', '4326', 'EPSG:4326'];
  1803.  
  1804. if (wgs84Projections.includes(projection.toLowerCase())) {
  1805. return [wgs84LeftBottom[0], wgs84LeftBottom[1], wgs84RightTop[0], wgs84RightTop[1]];
  1806. } else {
  1807. throw new Error('Unsupported projection type');
  1808. }
  1809. }
  1810.  
  1811. function getArcGisMapExtentGeometry() {
  1812. const extent = getMapExtent('wgs84');
  1813. const geometry = {
  1814. xmin: extent[0],
  1815. ymin: extent[1],
  1816. xmax: extent[2],
  1817. ymax: extent[3],
  1818. spatialReference: {
  1819. wkid: 4326,
  1820. },
  1821. };
  1822. return geometry;
  1823. }
  1824.  
  1825. function getCountiesUrl() {
  1826. const geometry = getArcGisMapExtentGeometry();
  1827. const url = `${COUNTIES_URL}/query?geometry=${encodeURIComponent(JSON.stringify(geometry))}`;
  1828. return `${url}&outFields=BASENAME%2CSTATE&returnGeometry=false&spatialRel=esriSpatialRelIntersects` + '&geometryType=esriGeometryEnvelope&inSR=4326&outSR=4326&f=json';
  1829. }
  1830.  
  1831. let _countiesInExtent = [];
  1832.  
  1833. function getGisLayerVisibleAtZoom(gisLayer) {
  1834. const overrideVisibleAtZoom = settings.getLayerSetting(gisLayer.id, 'visibleAtZoom');
  1835. if (overrideVisibleAtZoom) return overrideVisibleAtZoom;
  1836. return gisLayer.hasOwnProperty('visibleAtZoom') ? gisLayer.visibleAtZoom : DEFAULT_VISIBLE_AT_ZOOM;
  1837. }
  1838.  
  1839. function getGisLayerLabelsVisibleAtZoom(gisLayer, layerVisibleAtZoom) {
  1840. let labelsVisibleAtZoom;
  1841. layerVisibleAtZoom = +layerVisibleAtZoom;
  1842. if (gisLayer.hasOwnProperty('labelsVisibleAtZoom')) {
  1843. labelsVisibleAtZoom = layerVisibleAtZoom + (+gisLayer.labelsVisibleAtZoom - (+gisLayer.visibleAtZoom ?? DEFAULT_VISIBLE_AT_ZOOM));
  1844. } else {
  1845. labelsVisibleAtZoom = layerVisibleAtZoom + 1;
  1846. }
  1847. if (labelsVisibleAtZoom < 1) labelsVisibleAtZoom = 1;
  1848. return labelsVisibleAtZoom;
  1849. }
  1850.  
  1851. function getFetchableLayers(getInvisible) {
  1852. const zoom = sdk.Map.getZoomLevel();
  1853. if (zoom < 12) return [];
  1854. return _gisLayers.filter((gisLayer) => {
  1855. const isValidUrl = gisLayer.url && gisLayer.url.trim().length > 0;
  1856. const isVisible = (getInvisible || settings.visibleLayers.includes(gisLayer.id)) && settings.selectedStates.includes(gisLayer.state);
  1857. const isInState = gisLayer.state === 'US' || _countiesInExtent.some((county) => county.stateInfo[1] === gisLayer.state);
  1858. // Be sure to use hasOwnProperty when checking this, since 0 is a valid value.
  1859. const isValidZoom = getInvisible || zoom >= getGisLayerVisibleAtZoom(gisLayer);
  1860. return isValidUrl && isInState && isVisible && isValidZoom;
  1861. });
  1862. }
  1863.  
  1864. function filterLayerCheckboxes() {
  1865. const applicableLayers = getFetchableLayers(true).filter((layer) => {
  1866. const hasCounties = layer.hasOwnProperty('counties');
  1867. return (hasCounties && layer.counties.some((countyName) => _countiesInExtent.some((county) => county.name === countyName.toLowerCase() && layer.state === county.stateInfo[1]))) || !hasCounties;
  1868. });
  1869. const statesToHide = STATES.toAbbrArray();
  1870.  
  1871. _gisLayers.forEach((gisLayer) => {
  1872. const id = `#gis-layer-${gisLayer.id}-container`;
  1873. if (!settings.onlyShowApplicableLayers || applicableLayers.includes(gisLayer)) {
  1874. $(id).show();
  1875. $(`#gis-layers-for-${gisLayer.state}`).show();
  1876. const idx = statesToHide.indexOf(gisLayer.state);
  1877. if (idx > -1) statesToHide.splice(idx, 1);
  1878. } else {
  1879. $(id).hide();
  1880. }
  1881. });
  1882. if (settings.onlyShowApplicableLayers) {
  1883. statesToHide.forEach((st) => $(`#gis-layers-for-${st}`).hide());
  1884. }
  1885. }
  1886.  
  1887. const ROAD_ABBR = [
  1888. [/\bAVENUE$/, 'AVE'],
  1889. [/\bCIRCLE$/, 'CIR'],
  1890. [/\bCOURT$/, 'CT'],
  1891. [/\bDRIVE$/, 'DR'],
  1892. [/\bLANE$/, 'LN'],
  1893. [/\bPARK$/, 'PK'],
  1894. [/\bPLACE$/, 'PL'],
  1895. [/\bROAD$/, 'RD'],
  1896. [/\bSTREET$/, 'ST'],
  1897. [/\bTERRACE$/, 'TER'],
  1898. ];
  1899.  
  1900. const labelProcessingGlobalVariables = {
  1901. Number,
  1902. Math,
  1903. Boolean,
  1904. parseInt,
  1905. Date,
  1906. _regexReplace: {
  1907. // Strip leading zeros or blank full label for any label starting with a non-digit or
  1908. // is a Zero Address, use with '' as replace.
  1909. r0: /^(0+(\s.*)?|\D.*)/,
  1910. // Strip Everything After Street Type to end of the string by use $1 and $2 capture
  1911. // groups, use with replace '$1$2'
  1912. // eslint-disable-next-line max-len
  1913. 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,
  1914. // Strip SPACE 5 Digits from end of string, use with replace ''
  1915. r2: /\s\d{5}$/,
  1916. // Strip Everything after a "~", ",", ";" to the end of the string, use with replace ''
  1917. r3: /(~|,|;|\s?\r\n).*$/,
  1918. // Move the digits after the last space to before the rest of the string using, use with
  1919. // replace '$2 $1'
  1920. r4: /^(.*)\s(\d+).*/,
  1921. // Insert newline between digits (including "-") and everything after the digits,
  1922. // except(and before) a ",", use with replace '$1\n$2'
  1923. r5: /^([-\d]+)\s+([^,]+).*/,
  1924. // Insert newline between digits and everything after the digits, use with
  1925. // replace '$1\n$2'
  1926. r6: /^(\d+)\s+(.*)/,
  1927. },
  1928. };
  1929.  
  1930. function processLabel(gisLayer, item, displayLabelsAtZoom, area, isPolyLine = false) {
  1931. let label = '';
  1932. if (gisLayer.labelHeaderFields) {
  1933. label = `${gisLayer.labelHeaderFields
  1934. .map((fieldName) => item.attributes[fieldName])
  1935. .join(' ')
  1936. .trim()}\n`;
  1937. }
  1938. if (sdk.Map.getZoomLevel() >= displayLabelsAtZoom || area >= 1000000) { // Raised this 1 million sq meeters
  1939. label += gisLayer.labelFields
  1940. .map((fieldName) => item.attributes[fieldName])
  1941. .join(' ')
  1942. .trim();
  1943. if (gisLayer.processLabel) {
  1944. if (gisLayer.labelProcessingError) {
  1945. label = 'ERROR';
  1946. } else {
  1947. labelProcessingGlobalVariables.label = label;
  1948. labelProcessingGlobalVariables.fieldValues = item.attributes;
  1949. const result = ESTreeProcessor.execute(gisLayer.processLabel, labelProcessingGlobalVariables);
  1950. label = result.output?.trim() ?? '';
  1951. }
  1952. }
  1953. }
  1954.  
  1955. if (!isPolyLine) {
  1956. if (label && ['points', 'parcels', 'state_points', 'state_parcels'].includes(gisLayer.style)) {
  1957. if (settings.addrLabelDisplay === 'hn') {
  1958. const m = label.match(/^\d+/);
  1959. label = m ? m[0] : '';
  1960. } else if (settings.addrLabelDisplay === 'street') {
  1961. const m = label.match(/^(?:\d+\s)?(.*)/);
  1962. label = m ? m[1].trim() : '';
  1963. } else if (settings.addrLabelDisplay === 'none') {
  1964. label = '';
  1965. }
  1966. }
  1967. }
  1968. return label;
  1969. }
  1970.  
  1971. let lastFeatureId = 0;
  1972. function generateFeatureId() {
  1973. lastFeatureId++;
  1974. return lastFeatureId;
  1975. }
  1976.  
  1977. // SDK: Remove these once Map.getFeaturesByProperty is implemented: https://issuetracker.google.com/issues/419596843
  1978. let defaultFeatures = [];
  1979. let roadFeatures = [];
  1980.  
  1981. function processFeatures(data, token, gisLayer) {
  1982. const features = [];
  1983.  
  1984. if (data.skipIt) {
  1985. // do nothing
  1986. } else if (data.error) {
  1987. logError(`Error in layer "${gisLayer.name}": ${data.error.message}`);
  1988. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  1989. } else {
  1990. const items = data.features || [];
  1991. if (!token.cancel) {
  1992. let error = false;
  1993. const distinctValues = [];
  1994. items.forEach((item) => {
  1995. const featuresToAdd = [];
  1996. let skipIt = false;
  1997. if (!token.cancel && !error) {
  1998. const layerOffset = settings.getLayerSetting(gisLayer.id, 'offset') ?? { x: 0, y: 0 };
  1999.  
  2000. if (gisLayer.distinctFields) {
  2001. if (distinctValues.some((v) => gisLayer.distinctFields.every((fld) => v[fld] === item.attributes[fld]))) {
  2002. skipIt = true;
  2003. } else {
  2004. const dist = {};
  2005. gisLayer.distinctFields.forEach((fld) => (dist[fld] = item.attributes[fld]));
  2006. distinctValues.push(dist);
  2007. }
  2008. }
  2009. if (!skipIt) {
  2010. let area = 0; // Default area is 0 for non-polygon features
  2011. const displayLabelsAtZoom = getGisLayerLabelsVisibleAtZoom(gisLayer, getGisLayerVisibleAtZoom(gisLayer));
  2012. if (item.geometry) {
  2013. if (item.geometry.x) {
  2014. const feature = turf.point([item.geometry.x + layerOffset.x, item.geometry.y + layerOffset.y]);
  2015. const label = processLabel(gisLayer, item, displayLabelsAtZoom, '', false);
  2016.  
  2017. feature.properties = {
  2018. layerID: gisLayer.id,
  2019. label,
  2020. };
  2021. feature.id = generateFeatureId();
  2022. features.push(feature);
  2023.  
  2024. if (isPopupVisible) {
  2025. addLabelToLayer(gisLayer.name, label);
  2026. }
  2027. } else if (item.geometry.points) {
  2028. const points = item.geometry.points.map((point) => turf.point([point[0] + layerOffset.x, point[1] + layerOffset.y]));
  2029. featuresToAdd.push(...points);
  2030.  
  2031. points.forEach((pointFeature) => {
  2032. const label = processLabel(gisLayer, item, displayLabelsAtZoom, '', false);
  2033. pointFeature.properties = {
  2034. layerID: gisLayer.id,
  2035. label,
  2036. };
  2037. pointFeature.id = generateFeatureId();
  2038. features.push(pointFeature);
  2039.  
  2040. if (isPopupVisible) {
  2041. addLabelToLayer(gisLayer.name, label);
  2042. }
  2043. });
  2044. } else if (item.geometry.rings) {
  2045. const separatePolygons = [];
  2046. let currentOuterRing = null;
  2047. const innerRings = [];
  2048.  
  2049. item.geometry.rings.forEach((ringIn) => {
  2050. const ring = ringIn.map((point) => [point[0] + layerOffset.x, point[1] + layerOffset.y]);
  2051.  
  2052. if (turf.booleanClockwise(ring)) {
  2053. // Store previous polygon
  2054. if (currentOuterRing) {
  2055. separatePolygons.push({
  2056. outer: currentOuterRing,
  2057. inners: [...innerRings],
  2058. });
  2059. }
  2060. // Begin new outer ring and reset inner rings
  2061. currentOuterRing = ring;
  2062. innerRings.length = 0;
  2063. } else {
  2064. // It's an inner ring, push to current inner rings list
  2065. innerRings.push(ring);
  2066. }
  2067. });
  2068.  
  2069. // Add final polygon (if any)
  2070. if (currentOuterRing) {
  2071. separatePolygons.push({
  2072. outer: currentOuterRing,
  2073. inners: [...innerRings],
  2074. });
  2075. }
  2076.  
  2077. // Create features for each polygon group
  2078. separatePolygons.forEach(({ outer, inners }) => {
  2079. const polygonRings = [outer, ...inners];
  2080. const tempPolygon = turf.polygon(polygonRings);
  2081. const ringArea = turf.area(tempPolygon);
  2082. const label = processLabel(gisLayer, item, displayLabelsAtZoom, ringArea, false);
  2083.  
  2084. tempPolygon.properties = {
  2085. layerID: gisLayer.id,
  2086. label,
  2087. };
  2088. tempPolygon.id = generateFeatureId();
  2089.  
  2090. if (isPopupVisible) {
  2091. addLabelToLayer(gisLayer.name, label);
  2092. }
  2093. features.push(tempPolygon);
  2094. });
  2095. } else if (data.geometryType === 'esriGeometryPolyline') {
  2096. const mls = turf.multiLineString(item.geometry.paths);
  2097. const e = getMapExtent('wgs84');
  2098. const bbox = [e[0], e[1], e[2], e[3]];
  2099. const clipped = turf.bboxClip(mls, bbox);
  2100.  
  2101. if (clipped.geometry.type === 'LineString') {
  2102. item.geometry.paths = [clipped.geometry.coordinates];
  2103. } else if (clipped.geometry.type === 'MultiLineString') {
  2104. item.geometry.paths = clipped.geometry.coordinates;
  2105. }
  2106.  
  2107. item.geometry.paths.forEach((path) => {
  2108. const pointList = path.map((point) => [point[0] + layerOffset.x, point[1] + layerOffset.y]);
  2109. const feature = turf.lineString(pointList);
  2110. feature.skipDupeCheck = true;
  2111. featuresToAdd.push(feature);
  2112. const label = processLabel(gisLayer, item, displayLabelsAtZoom, '', true);
  2113.  
  2114. feature.properties = {
  2115. layerID: gisLayer.id,
  2116. label,
  2117. };
  2118. feature.id = generateFeatureId();
  2119.  
  2120. if (isPopupVisible) {
  2121. addLabelToLayer(gisLayer.name, label);
  2122. }
  2123. features.push(feature);
  2124. });
  2125. } else {
  2126. logDebug(`Unexpected feature type in layer: ${JSON.stringify(item)}`);
  2127. logError(`Error: Unexpected feature type in layer "${gisLayer.name}"`);
  2128. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  2129. error = true;
  2130. }
  2131. }
  2132. }
  2133. }
  2134. });
  2135. }
  2136. }
  2137. if (!token.cancel) {
  2138. // Check for duplicate geometries.
  2139. for (let i = 0; i < features.length; i++) {
  2140. const f1 = features[i];
  2141. if (f1.geometry.type === 'Point' && !f1.skipDupeCheck && f1.properties.label) {
  2142. let labels = [f1.properties.label];
  2143. for (let j = i + 1; j < features.length; j++) {
  2144. const f2 = features[j];
  2145. if (f2.geometry.type === 'Point' && !f2.skipDupeCheck && f2.properties.label && turf.distance(f1, f2, { units: 'meters' }) < 1) {
  2146. features.splice(j, 1);
  2147. labels.push(f2.properties.label);
  2148. j--;
  2149. }
  2150. }
  2151. labels = _.uniq(labels);
  2152. if (labels.length > 1) {
  2153. labels.forEach((label, idx) => {
  2154. label = label
  2155. .replace(/\n/g, ' ')
  2156. .replace(/\s{2,}/, ' ')
  2157. .replace(/\bUNIT\s.{1,5}$/i, '')
  2158. .trim();
  2159. ROAD_ABBR.forEach((abbr) => (label = label.replace(abbr[0], abbr[1])));
  2160. labels[idx] = label;
  2161. });
  2162. labels = _.uniq(labels);
  2163. labels.sort();
  2164. if (labels.length > 12) {
  2165. const len = labels.length;
  2166. labels = labels.slice(0, 10);
  2167. labels.push(`(${len - 10} more...)`);
  2168. }
  2169. f1.properties.label = _.uniq(labels).join('\n');
  2170. } else {
  2171. let { label } = f1.properties;
  2172. ROAD_ABBR.forEach((abbr) => (label = label.replace(abbr[0], abbr[1])));
  2173. f1.properties.label = label;
  2174. }
  2175. }
  2176. }
  2177.  
  2178. // Determine layer and source collection
  2179. const isRoad = gisLayer.isRoadLayer;
  2180. const layerName = isRoad ? ROAD_LAYER_NAME : DEFAULT_LAYER_NAME;
  2181. const sourceCollection = isRoad ? roadFeatures : defaultFeatures;
  2182.  
  2183. // Process the collection in one go
  2184. const { featureIdsToRemove, remainingFeatures } = sourceCollection.reduce(
  2185. (acc, feature) => {
  2186. if (feature.properties.layerID === gisLayer.id) {
  2187. acc.featureIdsToRemove.push(feature.id); // Collect IDs to remove
  2188. } else {
  2189. acc.remainingFeatures.push(feature); // Collect features to keep
  2190. }
  2191. return acc;
  2192. },
  2193. { featureIdsToRemove: [], remainingFeatures: [] }
  2194. );
  2195.  
  2196. // Initialize counters for individual feature addition
  2197. let successCount = features.length;
  2198.  
  2199. // Track the total processing time for the layer
  2200. const layerStartTime = performance.now();
  2201.  
  2202. sdk.Map.dangerouslyAddFeaturesToLayerWithoutValidation({ features, layerName });
  2203.  
  2204. // Handle completion logging
  2205. // Calculate and log the total processing time for the layer
  2206. const layerEndTime = performance.now();
  2207. const totalLayerDuration = layerEndTime - layerStartTime;
  2208. logDebug(`layer: ${gisLayer.id} processed in ${totalLayerDuration.toFixed(2)} ms - ${successCount} features added`);
  2209.  
  2210. // Remove features from the map (only if there are any)
  2211. if (featureIdsToRemove.length > 0) {
  2212. sdk.Map.removeFeaturesFromLayer({ layerName, featureIds: featureIdsToRemove });
  2213. }
  2214.  
  2215. // Create the new collection (kept + new)
  2216. const newCollection = [...remainingFeatures, ...features];
  2217.  
  2218. // Update the original reference (if needed, or handle based on your scope)
  2219. if (isRoad) {
  2220. roadFeatures = newCollection;
  2221. } else {
  2222. defaultFeatures = newCollection;
  2223. }
  2224.  
  2225. if (features.length) {
  2226. $(`label[for="gis-layer-${gisLayer.id}"]`).css({ color: '#00a009' });
  2227. }
  2228. }
  2229. } // END processFeatures()
  2230.  
  2231. function copyTextToClipboard(text) {
  2232. try {
  2233. GM_setClipboard(text);
  2234. logDebug(`Copy Text To Clipboard: ${text}`);
  2235. } catch (err) {
  2236. logError(`Failed to Text To Clipboard: ${err}`);
  2237. }
  2238. }
  2239.  
  2240. function addLabelToLayer(layerName, label) {
  2241. if (!layerLabels[layerName]) {
  2242. layerLabels[layerName] = new Set();
  2243. }
  2244. layerLabels[layerName].add(label);
  2245. }
  2246.  
  2247. function replacePhrasesWithAcronyms(text) {
  2248. // Order phrases such that compound phrases come before individual words
  2249. const replacements = [
  2250. // compound phrases here
  2251. { phrase: 'Alternate Route', acronym: 'ALT' },
  2252. { phrase: 'Army Air Field', acronym: 'AAF' },
  2253. { phrase: 'County Highway', acronym: 'CH-' },
  2254. { phrase: 'County Road', acronym: 'CR-' },
  2255. { phrase: 'East Bound', acronym: 'EB' },
  2256. { phrase: 'North Bound', acronym: 'NB' },
  2257. { phrase: 'North East', acronym: 'NE' },
  2258. { phrase: 'North West', acronym: 'NW' },
  2259. { phrase: 'South Bound', acronym: 'SB' },
  2260. { phrase: 'South East', acronym: 'SE' },
  2261. { phrase: 'South West', acronym: 'SW' },
  2262. { phrase: 'State Highway', acronym: 'SH-' },
  2263. { phrase: 'State Route', acronym: 'SR-' },
  2264. { phrase: 'State Rte', acronym: 'SR-' },
  2265. { phrase: 'U.S. Highway', acronym: 'US-' },
  2266. { phrase: 'U.S. Route', acronym: 'US-' },
  2267. { phrase: 'U.S. Rte', acronym: 'US-' },
  2268. { phrase: 'U.S.Rte', acronym: 'US-' },
  2269. { phrase: 'US Highway', acronym: 'US-' },
  2270. { phrase: 'U S Highway', acronym: 'US-' },
  2271. { phrase: 'US Route', acronym: 'US-' },
  2272. { phrase: 'U S Route', acronym: 'US-' },
  2273. { phrase: 'US RTE', acronym: 'US-' },
  2274. { phrase: 'U S RTE', acronym: 'US-' },
  2275. { phrase: 'USRTE', acronym: 'US-' },
  2276. { phrase: 'West Bound', acronym: 'WB' },
  2277. // Start of single words list
  2278. { phrase: 'Alley', acronym: 'Aly' },
  2279. { phrase: 'Apartments', acronym: 'Apts' },
  2280. { phrase: 'Avenue', acronym: 'Ave' },
  2281. { phrase: 'Beach', acronym: 'Bch' },
  2282. { phrase: 'Boulevard', acronym: 'Blvd' },
  2283. { phrase: 'Bridge', acronym: 'Br' },
  2284. { phrase: 'Business', acronym: 'BUS' },
  2285. { phrase: 'Bypass', acronym: 'BYP' },
  2286. { phrase: 'Canyon', acronym: 'Cyn' },
  2287. { phrase: 'Captain', acronym: 'Capt' },
  2288. { phrase: 'Causeway', acronym: 'Cswy' },
  2289. { phrase: 'Center', acronym: 'Ctr' },
  2290. { phrase: 'Circle', acronym: 'Cir' },
  2291. { phrase: 'Colonel', acronym: 'Col.' },
  2292. { phrase: 'Commander', acronym: 'Cmdr.' },
  2293. { phrase: 'Connector', acronym: 'CONN' },
  2294. { phrase: 'Corners', acronym: 'Cors' },
  2295. { phrase: 'Corporal', acronym: 'Cpl.' },
  2296. { phrase: 'Court', acronym: 'Ct' },
  2297. { phrase: 'Cove', acronym: 'Cv' },
  2298. { phrase: 'Creek', acronym: 'Crk' },
  2299. { phrase: 'Crescent', acronym: 'Cres' },
  2300. { phrase: 'Crossing', acronym: 'X-ing' },
  2301. { phrase: 'Doctor', acronym: 'Dr.' },
  2302. { phrase: 'Drive', acronym: 'Dr' },
  2303. { phrase: 'East', acronym: 'E' },
  2304. { phrase: 'Eastbound', acronym: 'EB' },
  2305. { phrase: 'Eb', acronym: 'EB' },
  2306. { phrase: 'Express', acronym: 'EXP' },
  2307. { phrase: 'Expressway', acronym: 'Expwy' },
  2308. { phrase: 'Extension', acronym: 'Ext' },
  2309. { phrase: 'Fort', acronym: 'Ft.' },
  2310. { phrase: 'Freeway', acronym: 'Fwy' },
  2311. { phrase: 'General', acronym: 'Gen.' },
  2312. { phrase: 'Governor', acronym: 'Gov.' },
  2313. { phrase: 'Grove', acronym: 'Grv' },
  2314. { phrase: 'Heights', acronym: 'Hts' },
  2315. { phrase: 'Highway', acronym: 'Hwy' },
  2316. { phrase: 'Honerable', acronym: 'Hon.' },
  2317. { phrase: 'International', acronym: 'Intl' },
  2318. { phrase: 'Interstate', acronym: 'I-' },
  2319. { phrase: 'Junior', acronym: 'Jr.' },
  2320. { phrase: 'Landing', acronym: 'Lndg' },
  2321. { phrase: 'Lane', acronym: 'Ln' },
  2322. { phrase: 'Lieutenant', acronym: 'Lt.' },
  2323. { phrase: 'Loop', acronym: 'Lp' },
  2324. { phrase: 'Major', acronym: 'Maj.' },
  2325. { phrase: 'Manor', acronym: 'Mnr.' },
  2326. { phrase: 'Meadow', acronym: 'Mdw' },
  2327. { phrase: 'Mount', acronym: 'Mt' },
  2328. { phrase: 'Mountain', acronym: 'Mtn' },
  2329. { phrase: 'Mountains', acronym: 'Mtns' },
  2330. { phrase: 'National', acronym: "Nat'l" },
  2331. { phrase: 'North', acronym: 'N' },
  2332. { phrase: 'Northbound', acronym: 'NB' },
  2333. { phrase: 'Nb', acronym: 'NB' },
  2334. { phrase: 'Northeast', acronym: 'NE' },
  2335. { phrase: 'Northwest', acronym: 'NW' },
  2336. { phrase: 'Park', acronym: 'Pk' },
  2337. { phrase: 'Parkway', acronym: 'Pkwy' },
  2338. { phrase: 'Parkways', acronym: 'Pkwys' },
  2339. { phrase: 'Passage', acronym: 'Psge' },
  2340. { phrase: 'Place', acronym: 'Pl' },
  2341. { phrase: 'Plaza', acronym: 'Plz' },
  2342. { phrase: 'Point', acronym: 'Pt' },
  2343. { phrase: 'Points', acronym: 'Pts' },
  2344. { phrase: 'President', acronym: 'Pres.' },
  2345. { phrase: 'Professor', acronym: 'Prof.' },
  2346. { phrase: 'Railroad', acronym: 'R.R.' },
  2347. { phrase: 'Road', acronym: 'Rd' },
  2348. { phrase: 'Recreational', acronym: 'Rec.' },
  2349. { phrase: 'Reverend', acronym: 'Rev.' },
  2350. { phrase: 'Route', acronym: 'SR-' },
  2351. { phrase: 'Saint', acronym: 'St.' },
  2352. { phrase: 'Sainte', acronym: 'Ste.' },
  2353. { phrase: 'Senior', acronym: 'Sr.' },
  2354. { phrase: 'Sergeant', acronym: 'Sgt.' },
  2355. { phrase: 'Skyway', acronym: 'Skwy' },
  2356. { phrase: 'South', acronym: 'S' },
  2357. { phrase: 'Southbound', acronym: 'SB' },
  2358. { phrase: 'Sb', acronym: 'SB' },
  2359. { phrase: 'Southeast', acronym: 'SE' },
  2360. { phrase: 'Southwest', acronym: 'SW' },
  2361. { phrase: 'Springs', acronym: 'Spgs' },
  2362. { phrase: 'Square', acronym: 'Sq' },
  2363. { phrase: 'Station', acronym: 'Sta' },
  2364. { phrase: 'Street', acronym: 'St' },
  2365. { phrase: 'Terrace', acronym: 'Ter' },
  2366. { phrase: 'Throughway', acronym: 'Thwy' },
  2367. { phrase: 'Thruway', acronym: 'Thwy' },
  2368. { phrase: 'Tollway', acronym: 'Tlwy' },
  2369. { phrase: 'Township', acronym: 'Twp' },
  2370. { phrase: 'Trafficway', acronym: 'Trfy' },
  2371. { phrase: 'Trail', acronym: 'Trl' },
  2372. { phrase: 'Tunnel', acronym: 'Tun' },
  2373. { phrase: 'Turnpike', acronym: 'Tpk' },
  2374. { phrase: 'Upper', acronym: 'Upr' },
  2375. { phrase: 'U.S.', acronym: 'US' },
  2376. { phrase: 'Valley', acronym: 'Vly' },
  2377. { phrase: 'West', acronym: 'W' },
  2378. { phrase: 'Westbound', acronym: 'WB' },
  2379. { phrase: 'Wb', acronym: 'WB' },
  2380. { phrase: '--', acronym: '-' },
  2381. { phrase: ' -', acronym: '-' },
  2382. { phrase: '- ', acronym: '-' },
  2383. { phrase: '- -', acronym: '-' },
  2384. ];
  2385.  
  2386. let updatedText = text;
  2387.  
  2388. // Replace phrases with their acronyms, case insensitive
  2389. replacements.forEach(({ phrase, acronym }) => {
  2390. const regex = new RegExp(`\\b${phrase}\\b`, 'gi'); // Uses \\b to match words with word boundaries
  2391. updatedText = updatedText.replace(regex, acronym);
  2392. });
  2393.  
  2394. return updatedText;
  2395. }
  2396.  
  2397. function fixSateHwyRoadNames(text) {
  2398. // Regular expression to capture patterns like "XXX ###", "XXX-###", "XXX###", as well as "Us Route #", "Us Rte #", and "Route #", "Rte #"
  2399. 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;
  2400.  
  2401. // Replacement function formats matched patterns
  2402. return text.replace(regex, (match, letters, numbers, usRouteNumber, rteNumber, routeNumber) => {
  2403. if (usRouteNumber) {
  2404. return `US-${usRouteNumber}`; // for "US Route"/s
  2405. }
  2406. if (rteNumber || routeNumber) {
  2407. return `SR-${rteNumber || routeNumber}`; // Change "Rte" or "Route" to "SR"
  2408. }
  2409. if (letters && numbers) {
  2410. return `${letters.toUpperCase()}-${numbers}`; // General format for other letter-number combos
  2411. }
  2412. return match;
  2413. });
  2414. }
  2415.  
  2416. function titleCaseLabel(text) {
  2417. // Read each line separately
  2418. const lines = text.split('\n');
  2419. return lines
  2420. .map((line) => {
  2421. const trimmedLine = line.trim(); // Trim the line to remove leading/trailing spaces
  2422. const words = trimmedLine.split(' '); // Split the line into individual words
  2423. // Capitalize the first letter of each word and convert the rest to lowercase
  2424. const titleCasedLine = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' '); // Recombine the words into a title-cased line
  2425. return titleCasedLine; // Return the formatted line
  2426. })
  2427. .join('\n'); // Combine all the lines back into a single string separated by new lines
  2428. }
  2429.  
  2430. function processedLabel(label) {
  2431. if (useTitleCase) {
  2432. label = titleCaseLabel(label);
  2433. }
  2434. if (useAcronyms) {
  2435. label = replacePhrasesWithAcronyms(label);
  2436. }
  2437. if (useStateHwy) {
  2438. label = fixSateHwyRoadNames(label);
  2439. }
  2440. if (removeNewLines) {
  2441. label = label.replace(/[\r\n]+/g, ' ');
  2442. }
  2443. return label;
  2444. }
  2445.  
  2446. function updatePopup(labels) {
  2447. let popup = document.getElementById('layerLabelPopup');
  2448. if (!popup) {
  2449. popup = document.createElement('div');
  2450. popup.id = 'layerLabelPopup';
  2451. popup.style = `position: absolute; background: #f5f5f5; border: 2px solid #007bff; border-radius: 5px;
  2452. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); z-index: 1000; width: 500px; max-width: 800px;
  2453. height: 300px; resize: both; overflow: hidden; max-height: 700px; left: ${popupPosition.left}; top: ${popupPosition.top}; `;
  2454.  
  2455. const header = document.createElement('div');
  2456. 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; ';
  2457.  
  2458. const title = document.createElement('span');
  2459. title.innerText = 'GIS-L Layer Labels';
  2460. header.appendChild(title);
  2461.  
  2462. const closeButton = document.createElement('span');
  2463. closeButton.innerText = '×';
  2464. closeButton.style = 'cursor: pointer; font-size: 20px; margin-left: 10px; ';
  2465. closeButton.addEventListener('click', () => {
  2466. isPopupVisible = false;
  2467. togglePopupVisibility();
  2468. $('input[name="popupVisibility"][value="show"]').prop('checked', isPopupVisible);
  2469. $('input[name="popupVisibility"][value="hide"]').prop('checked', !isPopupVisible);
  2470. saveSettingsToStorage();
  2471. });
  2472. header.appendChild(closeButton);
  2473. popup.appendChild(header);
  2474.  
  2475. const formatOptionContainer = document.createElement('div');
  2476. formatOptionContainer.style = 'background: #72767d; color: #fff;';
  2477.  
  2478. const firstRow = document.createElement('div');
  2479. firstRow.style = 'display: flex; gap: 10px; align-items: flex-start; justify-content: flex-start;';
  2480.  
  2481. const formatCheckbox = document.createElement('input');
  2482. formatCheckbox.type = 'checkbox';
  2483. formatCheckbox.id = 'useTitleCaseCheckbox';
  2484. formatCheckbox.style = 'margin-left: 10px';
  2485. formatCheckbox.checked = useTitleCase;
  2486. formatCheckbox.addEventListener('change', () => {
  2487. useTitleCase = formatCheckbox.checked;
  2488. updatePopupContent(labels);
  2489. saveSettingsToStorage();
  2490. });
  2491. firstRow.appendChild(formatCheckbox);
  2492.  
  2493. const formatCheckboxLabel = document.createElement('label');
  2494. formatCheckboxLabel.htmlFor = 'useTitleCaseCheckbox';
  2495. formatCheckboxLabel.innerText = 'Use Title Case';
  2496. formatCheckboxLabel.style = 'font-weight: 100; width: 150px;';
  2497. firstRow.appendChild(formatCheckboxLabel);
  2498.  
  2499. const acronymCheckbox = document.createElement('input');
  2500. acronymCheckbox.type = 'checkbox';
  2501. acronymCheckbox.id = 'useacronymsCheckbox';
  2502. acronymCheckbox.checked = useAcronyms;
  2503. acronymCheckbox.addEventListener('change', () => {
  2504. useAcronyms = acronymCheckbox.checked;
  2505. updatePopupContent(labels);
  2506. saveSettingsToStorage();
  2507. });
  2508. firstRow.appendChild(acronymCheckbox);
  2509.  
  2510. const acronymCheckboxLabel = document.createElement('label');
  2511. acronymCheckboxLabel.htmlFor = 'useacronymsCheckbox';
  2512. acronymCheckboxLabel.innerText = 'Use Acronyms & Abbreviations';
  2513. acronymCheckboxLabel.style = 'font-weight: 100;';
  2514. firstRow.appendChild(acronymCheckboxLabel);
  2515. formatOptionContainer.appendChild(firstRow);
  2516.  
  2517. const secondRow = document.createElement('div');
  2518. secondRow.style = 'display: flex; gap: 10px; align-items: flex-start; justify-content: flex-start;';
  2519.  
  2520. const stateHwyCheckbox = document.createElement('input');
  2521. stateHwyCheckbox.type = 'checkbox';
  2522. stateHwyCheckbox.id = 'useStateHwyCheckbox';
  2523. stateHwyCheckbox.style = 'margin-left: 10px';
  2524. stateHwyCheckbox.checked = useStateHwy;
  2525. stateHwyCheckbox.addEventListener('change', () => {
  2526. useStateHwy = stateHwyCheckbox.checked;
  2527. updatePopupContent(labels);
  2528. saveSettingsToStorage();
  2529. });
  2530. secondRow.appendChild(stateHwyCheckbox);
  2531.  
  2532. const stateHwyCheckboxLabel = document.createElement('label');
  2533. stateHwyCheckboxLabel.htmlFor = 'useStateHwyCheckbox';
  2534. stateHwyCheckboxLabel.innerText = 'Fix Highway Labels';
  2535.  
  2536. stateHwyCheckboxLabel.style = 'font-weight: 100; width: 150px;';
  2537. secondRow.appendChild(stateHwyCheckboxLabel);
  2538.  
  2539. const removeNewLinesCheckbox = document.createElement('input');
  2540. removeNewLinesCheckbox.type = 'checkbox';
  2541. removeNewLinesCheckbox.id = 'removeNewLinesCheckbox';
  2542. removeNewLinesCheckbox.checked = removeNewLines;
  2543. removeNewLinesCheckbox.addEventListener('change', () => {
  2544. removeNewLines = removeNewLinesCheckbox.checked;
  2545. updatePopupContent(labels);
  2546. saveSettingsToStorage();
  2547. });
  2548. secondRow.appendChild(removeNewLinesCheckbox);
  2549.  
  2550. const removeNewLinesCheckboxLabel = document.createElement('label');
  2551. removeNewLinesCheckboxLabel.htmlFor = 'removeNewLinesCheckbox';
  2552. removeNewLinesCheckboxLabel.innerText = 'Remove New Lines';
  2553. removeNewLinesCheckboxLabel.style = 'font-weight: 100;';
  2554. secondRow.appendChild(removeNewLinesCheckboxLabel);
  2555.  
  2556. formatOptionContainer.appendChild(secondRow);
  2557. popup.appendChild(formatOptionContainer);
  2558.  
  2559. const dropdownContainer = document.createElement('div');
  2560. dropdownContainer.style = 'margin-bottom: 10px;';
  2561. popup.appendChild(dropdownContainer);
  2562.  
  2563. const contentContainer = document.createElement('div');
  2564. contentContainer.style = 'padding: 5px; overflow-y: auto; overflow-x: auto; height: calc(100% - 110px);';
  2565. popup.appendChild(contentContainer);
  2566.  
  2567. const mapElement = document.getElementsByTagName('wz-page-content')[0];
  2568. if (mapElement) {
  2569. mapElement.appendChild(popup);
  2570. }
  2571.  
  2572. header.onmousedown = function (event) {
  2573. event.preventDefault();
  2574. const parentRect = mapElement.getBoundingClientRect();
  2575. const initialX = event.clientX;
  2576. const initialY = event.clientY;
  2577. const offsetX = initialX - parentRect.left - popup.offsetLeft;
  2578. const offsetY = initialY - parentRect.top - popup.offsetTop;
  2579.  
  2580. document.onmousemove = function (ev) {
  2581. popup.style.left = `${ev.clientX - offsetX - parentRect.left}px`;
  2582. popup.style.top = `${ev.clientY - offsetY - parentRect.top}px`;
  2583.  
  2584. popupPosition.left = popup.style.left;
  2585. popupPosition.top = popup.style.top;
  2586. };
  2587.  
  2588. document.onmouseup = function () {
  2589. document.onmousemove = null;
  2590. document.onmouseup = null;
  2591. };
  2592. };
  2593. }
  2594.  
  2595. updatePopupContent(labels);
  2596. popup.style.display = isPopupVisible ? 'block' : 'none';
  2597. }
  2598.  
  2599. function updatePopupContent(labels) {
  2600. const dropdownContainer = document.querySelector('#layerLabelPopup div:nth-child(3)');
  2601. const contentContainer = document.querySelector('#layerLabelPopup div:nth-child(4)');
  2602.  
  2603. dropdownContainer.innerHTML = '';
  2604. contentContainer.innerHTML = '';
  2605.  
  2606. const select = document.createElement('select');
  2607. select.style = 'width: 100%; padding: 5px; border: 1px solid #ccc;';
  2608.  
  2609. const sortedLayerNames = Object.keys(labels).sort();
  2610. sortedLayerNames.forEach((layerName) => {
  2611. const option = document.createElement('option');
  2612. option.value = layerName;
  2613. option.innerText = layerName;
  2614. select.appendChild(option);
  2615.  
  2616. const uniqueLabels = Array.from(labels[layerName]).sort();
  2617. const tabContent = document.createElement('div');
  2618. tabContent.style = 'display: none; width: 100%; white-space: pre;';
  2619.  
  2620. const processedLabels = uniqueLabels
  2621. .map((label) => {
  2622. const text = processedLabel(label);
  2623. const copyIcon = '<span style="cursor: pointer; margin-left: 5px;" title="Copy to clipboard">📋</span>';
  2624. return `<li style="margin-bottom: 0.3em; color: #555;" data-label="${text}">${text}${copyIcon}</li>`;
  2625. })
  2626. .join('');
  2627.  
  2628. tabContent.innerHTML = `<ul style="padding-left: 20px; margin-top: 0;">${processedLabels}</ul>`;
  2629. contentContainer.appendChild(tabContent);
  2630.  
  2631. // Add copying functionality
  2632. tabContent.querySelectorAll('li').forEach((li) => {
  2633. const icon = li.querySelector('span');
  2634. if (icon) {
  2635. icon.addEventListener('click', () => {
  2636. const textToCopy = li.getAttribute('data-label'); // Get the text from a custom data attribute
  2637. copyTextToClipboard(textToCopy);
  2638. });
  2639. }
  2640. });
  2641. });
  2642.  
  2643. dropdownContainer.appendChild(select);
  2644.  
  2645. let selectedLayerIndex = sortedLayerNames.indexOf(popupActiveLayer);
  2646.  
  2647. if (selectedLayerIndex === -1 && select.options.length > 0) {
  2648. selectedLayerIndex = 0;
  2649. popupActiveLayer = sortedLayerNames[selectedLayerIndex];
  2650. }
  2651. select.selectedIndex = selectedLayerIndex;
  2652.  
  2653. const allContents = contentContainer.querySelectorAll('div');
  2654. allContents.forEach((content, index) => {
  2655. content.style.display = index === select.selectedIndex ? 'block' : 'none';
  2656. });
  2657.  
  2658. select.addEventListener('change', () => {
  2659. const contents = contentContainer.querySelectorAll('div');
  2660. contents.forEach((content, index) => {
  2661. content.style.display = index === select.selectedIndex ? 'block' : 'none';
  2662. });
  2663. popupActiveLayer = select.value;
  2664. });
  2665. }
  2666.  
  2667. function fetchFeatures() {
  2668. if (isPopupVisible) {
  2669. Object.keys(layerLabels).forEach((key) => delete layerLabels[key]);
  2670. }
  2671. if (ignoreFetch) return;
  2672. if (sdk.Map.getZoomLevel() < 12) {
  2673. filterLayerCheckboxes();
  2674. return;
  2675. }
  2676. lastToken.cancel = true;
  2677. lastToken = { cancel: false, features: [], layersProcessed: 0 };
  2678. $('.gis-state-layer-label').css({ color: '#777' });
  2679.  
  2680. let _layersCleared = false;
  2681.  
  2682. // if (layersToFetch.length) {
  2683. const extentWGS84 = getMapExtent('wgs84');
  2684. GM_xmlhttpRequest({
  2685. url: getCountiesUrl(extentWGS84),
  2686. method: 'GET',
  2687. onload(res) {
  2688. if (res.status < 400) {
  2689. const data = $.parseJSON(res.responseText);
  2690. if (data.error) {
  2691. logError(`Error in US Census counties data: ${data.error.message}`);
  2692. } else {
  2693. _countiesInExtent = data.features.map((feature) => {
  2694. const name = feature.attributes.BASENAME.toLowerCase();
  2695. const stateInfo = STATES.fromId(parseInt(feature.attributes.STATE, 10));
  2696. return { name, stateInfo };
  2697. });
  2698. logDebug(`US Census counties: ${_countiesInExtent.map((c) => `${c.name} ${c.stateInfo[1]}`).join(', ')}`);
  2699.  
  2700. let layersToFetch;
  2701. if (!_layersCleared) {
  2702. _layersCleared = true;
  2703. layersToFetch = getFetchableLayers();
  2704.  
  2705. // Remove features of any layers that won't be mapped.
  2706. _gisLayers.forEach((gisLayer) => {
  2707. if (!layersToFetch.includes(gisLayer)) {
  2708. let featureCollection = gisLayer.isRoadLayer ? roadFeatures : defaultFeatures;
  2709. const layerName = gisLayer.isRoadLayer ? ROAD_LAYER_NAME : DEFAULT_LAYER_NAME;
  2710. const featureIds = featureCollection.filter((f) => f.properties.layerID === gisLayer.id).map((f) => f.id);
  2711. if (featureIds.length) {
  2712. sdk.Map.removeFeaturesFromLayer({ layerName, featureIds });
  2713. featureCollection = featureCollection.filter((f) => !featureIds.includes(f.id));
  2714. if (gisLayer.isRoadLayer) {
  2715. roadFeatures = featureCollection;
  2716. } else {
  2717. defaultFeatures = featureCollection;
  2718. }
  2719. }
  2720. }
  2721. });
  2722. }
  2723.  
  2724. layersToFetch = layersToFetch.filter(
  2725. (layer) =>
  2726. !layer.hasOwnProperty('counties') ||
  2727. layer.counties.some((countyName) => _countiesInExtent.some((county) => county.name === countyName.toLowerCase() && layer.state === county.stateInfo[1]))
  2728. );
  2729. filterLayerCheckboxes();
  2730. logDebug(`Fetching ${layersToFetch.length} layers...`);
  2731. logDebug(layersToFetch);
  2732. let layersProcessedCount = 0; // Track processed layers
  2733.  
  2734. layersToFetch.forEach((gisLayer) => {
  2735. const url = getUrl(extentWGS84, gisLayer);
  2736. GM_xmlhttpRequest({
  2737. url,
  2738. context: lastToken,
  2739. method: 'GET',
  2740. onload(res2) {
  2741. if (res2.status < 400) {
  2742. // Handle successful response
  2743. try {
  2744. const parsedData = $.parseJSON(res2.responseText);
  2745. processFeatures(parsedData, res2.context, gisLayer);
  2746. } catch (parseError) {
  2747. logError(`Parsing error for layer "${gisLayer.id}": ${parseError.message}`);
  2748. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  2749. }
  2750.  
  2751. // Update popup after processing all layers
  2752. layersProcessedCount += 1;
  2753. if (layersProcessedCount === layersToFetch.length && isPopupVisible) {
  2754. updatePopup(layerLabels);
  2755. }
  2756. } else {
  2757. // Handle HTTP error response
  2758. logError(`HTTP error for layer "${gisLayer.id}": ${res2.status} ${res2.statusText}`);
  2759. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  2760. }
  2761. },
  2762. onerror(res3) {
  2763. // Handle request error, particularly timeouts or network issues
  2764. logError(`Could not fetch layer "${gisLayer.id}". Error: ${res3.statusText} (status code: ${res3.status})`);
  2765. $(`#gis-layer-${gisLayer.id}-container > label`).css('color', 'red');
  2766. },
  2767. });
  2768. });
  2769. }
  2770. } else {
  2771. logDebug(`HTTP request error: ${JSON.stringify(res)}`);
  2772. logError(`Could not fetch counties from US Census site. Request returned ${res.status}`);
  2773. }
  2774. },
  2775. onerror(res) {
  2776. logDebug(`xmlhttpRequest error:${JSON.stringify(res)}`);
  2777. logError('Could not fetch counties from US Census site. An error was thrown.');
  2778. },
  2779. });
  2780. }
  2781.  
  2782. function showScriptInfoAlert() {
  2783. /* Check version and alert on update */
  2784. if (SHOW_UPDATE_MESSAGE && scriptVersion !== settings.lastVersion) {
  2785. // alert(SCRIPT_VERSION_CHANGES);
  2786. let releaseNotes = '';
  2787. releaseNotes += "<p>What's New:</p>";
  2788. if (SCRIPT_VERSION_CHANGES.length > 0) {
  2789. releaseNotes += '<ul>';
  2790. for (let idx = 0; idx < SCRIPT_VERSION_CHANGES.length; idx++) releaseNotes += `<li>${SCRIPT_VERSION_CHANGES[idx]}`;
  2791. releaseNotes += '</ul>';
  2792. } else {
  2793. releaseNotes += '<ul><li>Nothing major.</ul>';
  2794. }
  2795. WazeWrap.Interface.ShowScriptUpdate(GM_info.script.name, scriptVersion, releaseNotes, GF_URL);
  2796. }
  2797. }
  2798.  
  2799. function setEnabled(value) {
  2800. settings.enabled = value;
  2801. saveSettingsToStorage();
  2802. sdk.Map.setLayerVisibility({ layerName: DEFAULT_LAYER_NAME, visibility: value });
  2803. sdk.Map.setLayerVisibility({ layerName: ROAD_LAYER_NAME, visibility: value });
  2804. const color = value ? '#00bd00' : '#ccc';
  2805. $('span#gis-layers-power-btn').css({ color });
  2806. if (value) fetchFeatures();
  2807. sdk.LayerSwitcher.setLayerCheckboxChecked({ name: 'GIS Layers', isChecked: value });
  2808.  
  2809. // Show/hide the popup based on the enabled state
  2810. const popup = document.getElementById('layerLabelPopup');
  2811. if (popup) {
  2812. popup.style.display = value ? 'block' : 'none';
  2813. isPopupVisible = value;
  2814. }
  2815. }
  2816.  
  2817. function onGisLayerToggleChanged() {
  2818. const checked = $(this).is(':checked');
  2819. const layerId = $(this).data('layer-id');
  2820. const idx = settings.visibleLayers.indexOf(layerId);
  2821. if (checked) {
  2822. const gisLayer = _gisLayers.find((l) => l.id === layerId);
  2823. if (gisLayer.oneTimeAlert) {
  2824. const lastAlertHash = settings.oneTimeAlerts[layerId];
  2825. const newAlertHash = hashString(gisLayer.oneTimeAlert);
  2826. if (lastAlertHash !== newAlertHash) {
  2827. // alert(`Layer: ${gisLayer.name}\n\nMessage:\n${gisLayer.oneTimeAlert}`);
  2828. WazeWrap.Alerts.info(GM_info.script.name, `Layer: ${gisLayer.name}<br><br>Message:<br>${gisLayer.oneTimeAlert}`);
  2829. settings.oneTimeAlerts[layerId] = newAlertHash;
  2830. saveSettingsToStorage();
  2831. }
  2832. }
  2833. if (idx === -1) settings.visibleLayers.push(layerId);
  2834. } else if (idx > -1) settings.visibleLayers.splice(idx, 1);
  2835. if (!ignoreFetch) {
  2836. saveSettingsToStorage();
  2837. fetchFeatures();
  2838. }
  2839. }
  2840.  
  2841. function onOnlyShowApplicableLayersChanged() {
  2842. settings.onlyShowApplicableLayers = $(this).is(':checked');
  2843. saveSettingsToStorage();
  2844. fetchFeatures();
  2845. }
  2846.  
  2847. function onStateCheckChanged(evt) {
  2848. const state = evt.data;
  2849. const idx = settings.selectedStates.indexOf(state);
  2850. if (evt.target.checked) {
  2851. if (idx === -1) settings.selectedStates.push(state);
  2852. } else if (idx > -1) settings.selectedStates.splice(idx, 1);
  2853. if (!ignoreFetch) {
  2854. saveSettingsToStorage();
  2855. initLayersTab();
  2856. fetchFeatures();
  2857. }
  2858. }
  2859.  
  2860. function onLayerCheckboxChanged(args) {
  2861. setEnabled(args.checked);
  2862. }
  2863.  
  2864. function setFillParcels(doFill) {
  2865. [LAYER_STYLES.parcels, LAYER_STYLES.state_parcels].forEach((style) => {
  2866. style.fillOpacity = doFill ? 0.2 : 0;
  2867. });
  2868. }
  2869.  
  2870. function onFillParcelsCheckedChanged(evt) {
  2871. const { checked } = evt.target;
  2872. setFillParcels(checked);
  2873. settings.fillParcels = checked;
  2874. saveSettingsToStorage();
  2875. fetchFeatures();
  2876. }
  2877.  
  2878. function onMapMove() {
  2879. if (settings.enabled) fetchFeatures();
  2880. }
  2881.  
  2882. function onRefreshLayersClick() {
  2883. const $btn = $('#gis-layers-refresh');
  2884. if (!$btn.hasClass('fa-spin')) {
  2885. $btn.css({ cursor: 'auto' });
  2886. $btn.addClass('fa-spin');
  2887. init(false);
  2888. }
  2889. }
  2890.  
  2891. function onChevronClick(evt) {
  2892. const $target = $(evt.currentTarget);
  2893. const $div = $($target.siblings()[0]);
  2894. const fieldsetId = $target.parent('fieldset').attr('id');
  2895. const sectionKey = fieldsetId ? fieldsetId.replace('gis-layers-for-', '') : null;
  2896. $($target.children()[0]).toggleClass('fa fa-fw fa-chevron-down').toggleClass('fa fa-fw fa-chevron-right');
  2897. if ($div.css('display') === 'none') {
  2898. $div.css('display', 'block');
  2899. if (sectionKey) settings.collapsedSections[sectionKey] = false;
  2900. } else {
  2901. $div.css('display', 'none');
  2902. if (sectionKey) settings.collapsedSections[sectionKey] = true;
  2903. }
  2904. if (sectionKey) saveSettingsToStorage();
  2905. }
  2906.  
  2907. function doToggleABunch(evt, checkState) {
  2908. ignoreFetch = true;
  2909. $(evt.target).closest('fieldset').find('input').prop('checked', !checkState).trigger('click');
  2910. ignoreFetch = false;
  2911. saveSettingsToStorage();
  2912. if (evt.data) initLayersTab();
  2913. fetchFeatures();
  2914. }
  2915.  
  2916. function onSelectAllClick(evt) {
  2917. doToggleABunch(evt, true);
  2918. }
  2919.  
  2920. function onSelectNoneClick(evt) {
  2921. doToggleABunch(evt, false);
  2922. }
  2923.  
  2924. function onGisAddrDisplayChange(evt) {
  2925. settings.addrLabelDisplay = evt.target.value;
  2926. saveSettingsToStorage();
  2927. fetchFeatures();
  2928. }
  2929.  
  2930. function onAddressDisplayShortcutKey() {
  2931. if (!$('#gisAddrDisplay-hn').is(':checked')) {
  2932. $('#gisAddrDisplay-hn').click();
  2933. } else {
  2934. $('#gisAddrDisplay-all').click();
  2935. }
  2936. }
  2937.  
  2938. function onToggleGisLayersShortcutKey() {
  2939. setEnabled(!settings.enabled);
  2940. }
  2941.  
  2942. function togglePopupVisibility() {
  2943. const popup = document.getElementById('layerLabelPopup');
  2944. if (popup) {
  2945. popup.style.display = isPopupVisible ? 'block' : 'none';
  2946. }
  2947. saveSettingsToStorage();
  2948. }
  2949.  
  2950. function initLayer() {
  2951. const rules = _gisLayers
  2952. .filter((gisLayer) => gisLayer.style && gisLayer.style !== 'roads')
  2953. .map((gisLayer) => {
  2954. let style;
  2955. if (LAYER_STYLES.hasOwnProperty(gisLayer.style)) {
  2956. style = LAYER_STYLES[gisLayer.style];
  2957. } else {
  2958. style = gisLayer.style;
  2959. }
  2960. return {
  2961. predicate: (featureProperties) => featureProperties.layerID === gisLayer.id,
  2962. style,
  2963. };
  2964. });
  2965.  
  2966. setFillParcels(settings.fillParcels);
  2967.  
  2968. try {
  2969. sdk.Map.removeLayer({ layerName: DEFAULT_LAYER_NAME });
  2970. } catch (e) {
  2971. // If InvalidStateError, then the layer doesn't exist yet. Ignore the error
  2972. if (!(e instanceof sdk.Errors.InvalidStateError)) {
  2973. throw e;
  2974. }
  2975. }
  2976. sdk.Map.addLayer({
  2977. layerName: DEFAULT_LAYER_NAME,
  2978. styleContext: {
  2979. getLabel: (context) => context.feature?.properties?.label,
  2980. },
  2981. styleRules: [{ style: DEFAULT_STYLE }, ...rules],
  2982. zIndexing: true,
  2983. });
  2984.  
  2985. try {
  2986. sdk.Map.removeLayer({ layerName: ROAD_LAYER_NAME });
  2987. } catch (e) {
  2988. // If InvalidStateError, then the layer doesn't exist yet. Ignore the error
  2989. if (!(e instanceof sdk.Errors.InvalidStateError)) {
  2990. throw e;
  2991. }
  2992. }
  2993. sdk.Map.addLayer({
  2994. layerName: ROAD_LAYER_NAME,
  2995. styleContext: {
  2996. getLabel: (context) => context.feature?.properties?.label,
  2997. getOffset: () => -(sdk.Map.getZoomLevel() + 5),
  2998. getSmooth: () => '',
  2999. getReadable: () => '1',
  3000. },
  3001. styleRules: [{ style: ROAD_STYLE }],
  3002. });
  3003.  
  3004. sdk.Map.setLayerVisibility({ layerName: DEFAULT_LAYER_NAME, visibility: settings.enabled });
  3005. sdk.Map.setLayerVisibility({ layerName: ROAD_LAYER_NAME, visibility: settings.enabled });
  3006. } // END InitLayer
  3007.  
  3008. function initLayersTab() {
  3009. const user = userInfo.userName.toLowerCase();
  3010. const states = _.uniq(_gisLayers.map((l) => l.state)).filter((st) => settings.selectedStates.includes(st));
  3011.  
  3012. $('#panel-gis-state-layers')
  3013. .empty()
  3014. .append(
  3015. $('<div>', { class: 'controls-container' })
  3016. .css({ 'padding-top': '0px' })
  3017. .append(
  3018. $('<input>', { type: 'checkbox', id: 'only-show-applicable-gis-layers' }).change(onOnlyShowApplicableLayersChanged).prop('checked', settings.onlyShowApplicableLayers),
  3019. $('<label>', { for: 'only-show-applicable-gis-layers' }).css({ 'white-space': 'pre-line' }).text('Only show applicable layers')
  3020. ),
  3021. $('.gis-layers-state-checkbox:checked').length === 0
  3022. ? $('<div>').text('Turn on layer categories in the Settings tab.')
  3023. : states.map((st) =>
  3024. $('<fieldset>', {
  3025. id: `gis-layers-for-${st}`,
  3026. style: 'border:1px solid silver;padding:4px;border-radius:4px;-webkit-padding-before: 0;',
  3027. }).append(
  3028. $('<legend>', { style: 'margin-bottom:0px;border-bottom-style:none;width:auto;' })
  3029. .click(onChevronClick)
  3030. .append(
  3031. $('<i>', {
  3032. class: settings.collapsedSections[st] ? 'fa fa-fw fa-chevron-right' : 'fa fa-fw fa-chevron-down',
  3033. style: 'cursor: pointer;font-size: 12px;margin-right: 4px',
  3034. }),
  3035. $('<span>', {
  3036. style: 'font-size:14px;font-weight:600;text-transform: uppercase; cursor: pointer',
  3037. }).text(STATES.toFullName(st))
  3038. ),
  3039. $('<div>', {
  3040. id: `${st}_body`,
  3041. style: settings.collapsedSections[st] ? 'display: none;' : 'display: block;',
  3042. }).append(
  3043. $('<div>')
  3044. .css({ 'font-size': '11px' })
  3045. .append($('<span>').append('Select ', $('<a>', { href: '#' }).text('All').click(onSelectAllClick), ' / ', $('<a>', { href: '#' }).text('None').click(onSelectNoneClick))),
  3046. $('<div>', { class: 'controls-container', style: 'padding-top:0px;' }).append(
  3047. _gisLayers
  3048. .filter((l) => l.state === st && (!PRIVATE_LAYERS.hasOwnProperty(l.id) || PRIVATE_LAYERS[l.id].includes(user)))
  3049. .map((gisLayer) => {
  3050. const id = `gis-layer-${gisLayer.id}`;
  3051. return $('<div>', { class: 'controls-container', id: `${id}-container` })
  3052. .css({ 'padding-top': '0px', display: 'block' })
  3053. .append(
  3054. $('<input>', { type: 'checkbox', id }).data('layer-id', gisLayer.id).change(onGisLayerToggleChanged).prop('checked', settings.visibleLayers.includes(gisLayer.id)),
  3055. $('<label>', { for: id, class: 'gis-state-layer-label' })
  3056. .css({ 'white-space': 'pre-line' })
  3057. .text(`${gisLayer.name}${gisLayer.restrictTo ? ' *' : ''}`)
  3058. .attr('title', gisLayer.restrictTo ? `Restricted to: ${gisLayer.restrictTo}` : '')
  3059. .contextmenu((evt) => {
  3060. evt.preventDefault();
  3061. // TODO - enable the layer if it isn't already.
  3062. // Tried using click function on the evt target, but that didn't work.
  3063. _layerSettingsDialog.gisLayer = gisLayer;
  3064. _layerSettingsDialog.show();
  3065. })
  3066. );
  3067. })
  3068. )
  3069. )
  3070. )
  3071. )
  3072. );
  3073. }
  3074.  
  3075. function initSettingsTab() {
  3076. const states = _.uniq(_gisLayers.map((l) => l.state));
  3077. const createRadioBtn = (name, value, text, checked) => {
  3078. const id = `${name}-${value}`;
  3079. return [
  3080. $('<input>', {
  3081. type: 'radio',
  3082. id,
  3083. name,
  3084. value,
  3085. }).prop('checked', checked),
  3086. $('<label>', { for: id }).text(text).css({
  3087. paddingLeft: '15px',
  3088. marginRight: '4px',
  3089. }),
  3090. ];
  3091. };
  3092. $('#panel-gis-layers-settings')
  3093. .empty()
  3094. .append(
  3095. $('<fieldset>', {
  3096. style: 'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;margin-top:-8px;',
  3097. }).append(
  3098. $('<legend>', {
  3099. style: 'margin-bottom:0px;border-bottom-style:none;width:auto;',
  3100. }).append(
  3101. $('<span>', {
  3102. style: 'font-size:14px;font-weight:600;text-transform: uppercase;',
  3103. }).text('Labels')
  3104. ),
  3105. $('<div>', { id: 'labelSettings' }).append(
  3106. $('<div>', { class: 'controls-container' })
  3107. .css({ 'padding-top': '2px' })
  3108. .append(
  3109. $('<label>', { style: 'font-weight:normal;' }).text('Addresses:'),
  3110. createRadioBtn('gisAddrDisplay', 'hn', 'HN', settings.addrLabelDisplay === 'hn'),
  3111. createRadioBtn('gisAddrDisplay', 'street', 'Street', settings.addrLabelDisplay === 'street'),
  3112. createRadioBtn('gisAddrDisplay', 'all', 'Both', settings.addrLabelDisplay === 'all'),
  3113. createRadioBtn('gisAddrDisplay', 'none', 'None', settings.addrLabelDisplay === 'none'),
  3114. $('<i>', {
  3115. class: 'waze-tooltip',
  3116. id: 'gisAddrDisplayInfo',
  3117. 'data-toggle': 'tooltip',
  3118. style: 'margin-left:8px; font-size:12px',
  3119. 'data-placement': 'bottom',
  3120. title: `This may not work properly for all layers. Please report issues to ${SCRIPT_AUTHOR}.`,
  3121. }).tooltip(),
  3122. $('<br>'),
  3123. $('<label>', { style: 'font-weight:normal; margin-left:8px;' }).text('Label Popup:'),
  3124. createRadioBtn('popupVisibility', 'show', 'Show', isPopupVisible),
  3125. createRadioBtn('popupVisibility', 'hide', 'Hide', !isPopupVisible)
  3126. )
  3127. )
  3128. ),
  3129. $('<fieldset>', {
  3130. style: 'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;',
  3131. }).append(
  3132. $('<legend>', {
  3133. style: 'margin-bottom:0px;border-bottom-style:none;width:auto;',
  3134. }).append(
  3135. $('<span>', {
  3136. style: 'font-size:14px;font-weight:600;text-transform: uppercase;',
  3137. }).text('Layer Categories')
  3138. ),
  3139. $('<div>', { id: 'states_body' }).append(
  3140. $('<div>')
  3141. .css({ 'font-size': '11px' })
  3142. .append($('<span>').append('Select ', $('<a>', { href: '#' }).text('All').click(true, onSelectAllClick), ' / ', $('<a>', { href: '#' }).text('None').click(true, onSelectNoneClick))),
  3143. $('<div>', { class: 'controls-container', style: 'padding-top:0px;' }).append(
  3144. states.map((st) => {
  3145. const fullName = STATES.toFullName(st);
  3146. const id = `gis-layer-enable-state-${st}`;
  3147. return $('<div>', { class: 'controls-container' })
  3148. .css({ 'padding-top': '0px', display: 'block' })
  3149. .append(
  3150. $('<input>', { type: 'checkbox', id, class: 'gis-layers-state-checkbox' }).change(st, onStateCheckChanged).prop('checked', settings.selectedStates.includes(st)),
  3151. $('<label>', { for: id }).css({ 'white-space': 'pre-line', color: '#777' }).text(fullName)
  3152. );
  3153. })
  3154. )
  3155. )
  3156. )
  3157. );
  3158. $('#panel-gis-layers-settings').append(
  3159. $('<fieldset>', { style: 'border:1px solid silver;padding:8px;border-radius:4px;-webkit-padding-before: 0;' }).append(
  3160. $('<legend>', { style: 'margin-bottom:0px;border-bottom-style:none;width:auto;' }).append(
  3161. $('<span>', { style: 'font-size:14px;font-weight:600;text-transform: uppercase;' }).text('Appearance')
  3162. ),
  3163. $('<div>', { class: 'controls-container' })
  3164. .css({ 'padding-top': '2px' })
  3165. .append(
  3166. $('<input>', { type: 'checkbox', id: 'fill-parcels' }).change(onFillParcelsCheckedChanged).prop('checked', settings.fillParcels),
  3167. $('<label>', { for: 'fill-parcels' }).css({ 'white-space': 'pre-line', color: '#777' }).text('Fill parcels')
  3168. )
  3169. )
  3170. );
  3171. $('input[name="gisAddrDisplay"]').change(onGisAddrDisplayChange);
  3172. $('input[name="popupVisibility"]').change(function () {
  3173. isPopupVisible = $(this).val() === 'show';
  3174. togglePopupVisibility();
  3175. });
  3176. }
  3177.  
  3178. async function initTab(firstCall = true) {
  3179. if (firstCall) {
  3180. const content = $('<div>')
  3181. .append(
  3182. $('<span>', { style: 'font-size:14px;font-weight:600' }).text('GIS Layers'),
  3183. $('<span>', { style: 'font-size:11px;margin-left:10px;color:#aaa;' }).text(GM_info.script.version),
  3184. // <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>
  3185. $('<a>', {
  3186. href: REQUEST_FORM_URL.replace('{username}', userInfo.userName),
  3187. target: '_blank',
  3188. style: 'color: #6290b7;font-size: 12px;margin-left: 8px;',
  3189. title: 'Report broken layers, bugs, request new layers, script features',
  3190. }).text('Submit a request'),
  3191. $('<span>', {
  3192. id: 'gis-layers-refresh',
  3193. class: 'fa fa-refresh',
  3194. style: 'float: right;',
  3195. 'data-toggle': 'tooltip',
  3196. title: 'Pull new layer info from master sheet and refresh all layers.',
  3197. }),
  3198. '<ul class="nav nav-tabs">' +
  3199. '<li class="active"><a data-toggle="tab" href="#panel-gis-state-layers" aria-expanded="true">' +
  3200. 'Layers' +
  3201. '</a></li>' +
  3202. '<li><a data-toggle="tab" href="#panel-gis-layers-settings" aria-expanded="true">' +
  3203. 'Settings' +
  3204. '</a></li> ' +
  3205. '</ul>',
  3206. $('<div>', { class: 'tab-content', style: 'padding:8px;padding-top:2px' }).append(
  3207. $('<div>', { class: 'tab-pane active', id: 'panel-gis-state-layers', style: 'padding: 4px 0px 0px 0px; width: auto' }),
  3208. $('<div>', { class: 'tab-pane', id: 'panel-gis-layers-settings', style: 'padding: 4px 0px 0px 0px; width: auto' })
  3209. )
  3210. )
  3211. .html();
  3212.  
  3213. const powerButtonColor = settings.enabled ? '#00bd00' : '#ccc';
  3214. const labelText = $('<div>')
  3215. .append(
  3216. $('<span>', {
  3217. class: 'fa fa-power-off',
  3218. id: 'gis-layers-power-btn',
  3219. style: `margin-right: 5px;cursor: pointer;color: ${powerButtonColor};font-size: 13px;`,
  3220. title: 'Toggle GIS Layers',
  3221. }),
  3222. $('<span>', { title: 'GIS Layers' }).text('GIS-L')
  3223. )
  3224. .html();
  3225.  
  3226. const { tabLabel, tabPane } = await sdk.Sidebar.registerScriptTab();
  3227. tabLabel.innerHTML = labelText;
  3228. tabPane.innerHTML = content;
  3229. // Fix tab content div spacing.
  3230. $(tabPane).parent().css({ width: 'auto', padding: '6px' });
  3231. $('#gis-layers-power-btn').click(() => {
  3232. setEnabled(!settings.enabled);
  3233.  
  3234. // return false to prevent event from bubbling up the DOM tree and causing the GIS-L tab to activate
  3235. return false;
  3236. });
  3237. $('#gis-layers-refresh').click(onRefreshLayersClick);
  3238. }
  3239.  
  3240. initSettingsTab();
  3241. initLayersTab();
  3242. }
  3243.  
  3244. function initGui(firstCall = true) {
  3245. initLayer();
  3246.  
  3247. if (firstCall) {
  3248. initTab(true);
  3249.  
  3250. sdk.LayerSwitcher.addLayerCheckbox({ name: 'GIS Layers' });
  3251. sdk.LayerSwitcher.setLayerCheckboxChecked({ name: 'GIS Layers', isChecked: settings.enabled });
  3252. sdk.Events.on({ eventName: 'wme-layer-checkbox-toggled', eventHandler: onLayerCheckboxChanged });
  3253. sdk.Events.on({ eventName: 'wme-map-move-end', eventHandler: onMapMove });
  3254. showScriptInfoAlert();
  3255. } else {
  3256. initTab(firstCall);
  3257. }
  3258. }
  3259.  
  3260. async function loadSpreadsheetAsync() {
  3261. let data;
  3262. try {
  3263. data = await $.getJSON(`${LAYER_DEF_SPREADSHEET_URL}?${DEC(API_KEY)}`);
  3264. } catch (err) {
  3265. throw new Error(`Spreadsheet call failed. (${err.status}: ${err.statusText})`);
  3266. }
  3267. const [[minVersion], fieldNames, ...layerDefRows] = data.values;
  3268. const REQUIRED_FIELD_NAMES = [
  3269. 'state',
  3270. 'name',
  3271. 'id',
  3272. 'counties',
  3273. 'url',
  3274. 'where',
  3275. 'labelFields',
  3276. 'processLabel',
  3277. 'style',
  3278. 'visibleAtZoom',
  3279. 'labelsVisibleAtZoom',
  3280. 'enabled',
  3281. 'restrictTo',
  3282. 'oneTimeAlert',
  3283. ];
  3284. const result = { error: null };
  3285. const checkFieldNames = (fldName) => fieldNames.includes(fldName);
  3286.  
  3287. if (scriptVersion < minVersion) {
  3288. result.error = `Script must be updated to at least version ${minVersion} before layer definitions can be loaded.`;
  3289. } else if (fieldNames.length < REQUIRED_FIELD_NAMES.length) {
  3290. result.error = `Expected ${REQUIRED_FIELD_NAMES.length} columns in layer definition data. Spreadsheet returned ${fieldNames.length}.`;
  3291. } else if (!REQUIRED_FIELD_NAMES.every((fldName) => checkFieldNames(fldName))) {
  3292. result.error =
  3293. 'Script expected to see the following column names in the layer ' +
  3294. `definition spreadsheet:\n${REQUIRED_FIELD_NAMES.join(', ')}\n` +
  3295. `But the spreadsheet returned these:\n${fieldNames.join(', ')}`;
  3296. }
  3297. if (!result.error) {
  3298. layerDefRows
  3299. .filter((row) => row.length)
  3300. .forEach((layerDefRow) => {
  3301. const layerDef = { enabled: '0' };
  3302. fieldNames.forEach((fldName, fldIdx) => {
  3303. let value = layerDefRow[fldIdx];
  3304. if (value !== undefined && value.trim().length > 0) {
  3305. value = value.trim();
  3306. if (fldName === 'counties' || fldName === 'labelFields') {
  3307. value = value.split(',').map((item) => item.trim());
  3308. } else if (fldName === 'processLabel') {
  3309. try {
  3310. value = ESTreeProcessor.compile(`function __$proc(){${value}} __$proc();`);
  3311. } catch (ex) {
  3312. layerDef.labelProcessingError = true;
  3313. logError(`Error loading label processing function for layer "${layerDef.id}".`);
  3314. logDebug(ex);
  3315. }
  3316. } else if (fldName === 'style') {
  3317. layerDef.isRoadLayer = value === 'roads';
  3318. if (!layerDef.isRoadLayer && !LAYER_STYLES.hasOwnProperty(value)) {
  3319. // If style is not defined, try to read in as JSON (custom style)
  3320. try {
  3321. value = JSON.parse(value);
  3322. } catch (ex) {
  3323. logError(`Invalid style definition for layer "${layerDef.id}".`);
  3324. }
  3325. }
  3326. } else if (fldName === 'state') {
  3327. value = value ? value.toUpperCase() : value;
  3328. } else if (fldName === 'restrictTo') {
  3329. try {
  3330. const values = value.split(',').map((v) => v.trim().toLowerCase());
  3331. layerDef.notAllowed = !values.some((entry) => {
  3332. const rankMatch = entry.match(/^r(\d)(\+am)?$/);
  3333. if (rankMatch) {
  3334. if (rankMatch[1] <= userInfo.rank + 1 && (!rankMatch[2] || userInfo.isAreaManager)) {
  3335. return true;
  3336. }
  3337. } else if (entry === 'am' && userInfo.isAreaManager) {
  3338. return true;
  3339. } else if (entry === userInfo.userName.toLowerCase()) {
  3340. return true;
  3341. }
  3342. return false;
  3343. });
  3344. } catch (ex) {
  3345. logError(ex);
  3346. }
  3347. }
  3348. layerDef[fldName] = value;
  3349. } else if (fldName === 'labelFields') {
  3350. layerDef[fldName] = [''];
  3351. }
  3352. });
  3353. const enabled = layerDef.enabled && !['0', 'false', 'no', 'n'].includes(layerDef.enabled.toString().trim().toLowerCase());
  3354. if (!layerDef.notAllowed && enabled) {
  3355. _gisLayers.push(layerDef);
  3356. }
  3357. });
  3358. }
  3359.  
  3360. return result;
  3361. }
  3362.  
  3363. function createShortcut(shortcutId, description, callback) {
  3364. let shortcutKeys = settings.shortcuts?.[shortcutId] ?? null;
  3365. if (shortcutKeys && sdk.Shortcuts.areShortcutKeysInUse({ shortcutKeys })) {
  3366. shortcutKeys = null;
  3367. }
  3368. sdk.Shortcuts.createShortcut({
  3369. shortcutId,
  3370. shortcutKeys,
  3371. description,
  3372. callback,
  3373. });
  3374. }
  3375.  
  3376. async function init(firstCall = true) {
  3377. _gisLayers = [];
  3378. if (firstCall) {
  3379. userInfo = sdk.State.getUserInfo();
  3380. labelProcessingGlobalVariables.sdk = sdk;
  3381. initRoadStyle();
  3382. loadSettingsFromStorage();
  3383. createShortcut('toggleHnsOnly', 'Toggle HN-only address labels (GIS Layers)', onAddressDisplayShortcutKey);
  3384. createShortcut('toggleEnabled', 'Toggle display of GIS Layers', onToggleGisLayersShortcutKey);
  3385. installPathFollowingLabels();
  3386. window.addEventListener('beforeunload', saveSettingsToStorage, false);
  3387. _layerSettingsDialog = new LayerSettingsDialog();
  3388. }
  3389. const t0 = performance.now();
  3390. try {
  3391. const result = await loadSpreadsheetAsync();
  3392. if (result.error) {
  3393. logError(result.error);
  3394. return;
  3395. }
  3396. _layerRefinements.forEach((layerRefinement) => {
  3397. const layerDef = _gisLayers.find((layerDef2) => layerDef2.id === layerRefinement.id);
  3398. if (layerDef) {
  3399. Object.keys(layerRefinement).forEach((fldName) => {
  3400. const value = layerRefinement[fldName];
  3401. if (fldName !== 'id' && layerDef.hasOwnProperty(fldName)) {
  3402. 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.');
  3403. } else if (value) layerDef[fldName] = value;
  3404. });
  3405. } else {
  3406. logDebug(`Refined layer "${layerRefinement.id}" does not have a corresponding layer defined` + ' in the spreadsheet. It can probably be removed from the script.');
  3407. }
  3408. });
  3409. logDebug(`Loaded ${_gisLayers.length} layer definitions in ${Math.round(performance.now() - t0)} ms.`);
  3410. initGui(firstCall);
  3411. fetchFeatures();
  3412. $('#gis-layers-refresh').removeClass('fa-spin').css({ cursor: 'pointer' });
  3413. logDebug('Initialized.');
  3414. } catch (err) {
  3415. logError(err);
  3416. }
  3417. }
  3418.  
  3419. init();
  3420.  
  3421. /*eslint-disable*/
  3422. function installPathFollowingLabels() {
  3423. // Copyright (c) 2015 by Jean-Marc.Viglino [at]ign.fr
  3424. // Dual-licensed under the CeCILL-B Licence (http://www.cecill.info/)
  3425. // and the Beerware license (http://en.wikipedia.org/wiki/Beerware),
  3426. // feel free to use and abuse it in your projects (the code, not the beer ;-).
  3427. //
  3428. //* Overwrite the SVG function to allow text along a path
  3429. //* setStyle function
  3430. //*
  3431. //* Add new options to the Openlayers.Style
  3432.  
  3433. // pathLabel: {String} Label to draw on the path
  3434. // pathLabelXOffset: {String} Offset along the line to start drawing text in pixel or %, default: "50%"
  3435. // pathLabelYOffset: {Number} Distance of the line to draw the text
  3436. // pathLabelCurve: {String} Smooth the line the label is drawn on (empty string for no)
  3437. // pathLabelReadable: {String} Make the label readable (empty string for no)
  3438.  
  3439. // * Extra standard values : all label and text values
  3440.  
  3441. // *
  3442. // * Method: removeChildById
  3443. // * Remove child in a node.
  3444. // *
  3445.  
  3446. function removeChildById(node, id) {
  3447. if (node.querySelector) {
  3448. var c = node.querySelector('#' + id);
  3449. if (c) node.removeChild(c);
  3450. return;
  3451. }
  3452. // For old browsers
  3453. var c = node.childNodes;
  3454. if (c)
  3455. for (var i = 0; i < c.length; i++) {
  3456. if (c[i].id === id) {
  3457. node.removeChild(c[i]);
  3458. return;
  3459. }
  3460. }
  3461. }
  3462.  
  3463. // *
  3464. // * Method: setStyle
  3465. // * Use to set all the style attributes to a SVG node.
  3466. // *
  3467. // * Takes care to adjust stroke width and point radius to be
  3468. // * resolution-relative
  3469. // *
  3470. // * Parameters:
  3471. // * node - {SVGDomElement} An SVG element to decorate
  3472. // * style - {Object}
  3473. // * options - {Object} Currently supported options include
  3474. // * 'isFilled' {Boolean} and
  3475. // * 'isStroked' {Boolean}
  3476.  
  3477. var setStyle = OpenLayers.Renderer.SVG.prototype.setStyle;
  3478. OpenLayers.Renderer.SVG.LABEL_STARTOFFSET = { l: '0%', r: '100%', m: '50%' };
  3479.  
  3480. OpenLayers.Renderer.SVG.prototype.pathText = function (node, style, suffix) {
  3481. var label = this.nodeFactory(null, 'text');
  3482. label.setAttribute('id', node._featureId + '_' + suffix);
  3483. if (style.fontColor) label.setAttributeNS(null, 'fill', style.fontColor);
  3484. if (style.fontStrokeColor) label.setAttributeNS(null, 'stroke', style.fontStrokeColor);
  3485. if (style.fontStrokeWidth) label.setAttributeNS(null, 'stroke-width', style.fontStrokeWidth);
  3486. if (style.fontOpacity) label.setAttributeNS(null, 'opacity', style.fontOpacity);
  3487. if (style.fontFamily) label.setAttributeNS(null, 'font-family', style.fontFamily);
  3488. if (style.fontSize) label.setAttributeNS(null, 'font-size', style.fontSize);
  3489. if (style.fontWeight) label.setAttributeNS(null, 'font-weight', style.fontWeight);
  3490. if (style.fontStyle) label.setAttributeNS(null, 'font-style', style.fontStyle);
  3491. if (style.labelSelect === true) {
  3492. label.setAttributeNS(null, 'pointer-events', 'visible');
  3493. label._featureId = node._featureId;
  3494. } else {
  3495. label.setAttributeNS(null, 'pointer-events', 'none');
  3496. }
  3497.  
  3498. function getpath(pathStr, readeable) {
  3499. var npath = pathStr.split(',');
  3500. var pts = [];
  3501. if (!readeable || Number(npath[0]) - Number(npath[npath.length - 2]) < 0) {
  3502. while (npath.length) pts.push({ x: Number(npath.shift()), y: Number(npath.shift()) });
  3503. } else {
  3504. while (npath.length) pts.unshift({ x: Number(npath.shift()), y: Number(npath.shift()) });
  3505. }
  3506. return pts;
  3507. }
  3508.  
  3509. var path = this.nodeFactory(null, 'path');
  3510. var tpid = node._featureId + '_t' + suffix;
  3511. var tpath = node.getAttribute('points');
  3512. if (style.pathLabelCurve) {
  3513. var pts = getpath(tpath, style.pathLabelReadable);
  3514. var p = pts[0].x + ' ' + pts[0].y;
  3515. var dx, dy, s1, s2;
  3516. dx = (pts[0].x - pts[1].x) / 4;
  3517. dy = (pts[0].y - pts[1].y) / 4;
  3518. for (var i = 1; i < pts.length - 1; i++) {
  3519. p += ' C ' + (pts[i - 1].x - dx) + ' ' + (pts[i - 1].y - dy);
  3520. dx = (pts[i - 1].x - pts[i + 1].x) / 4;
  3521. dy = (pts[i - 1].y - pts[i + 1].y) / 4;
  3522. s1 = Math.sqrt(Math.pow(pts[i - 1].x - pts[i].x, 2) + Math.pow(pts[i - 1].y - pts[i].y, 2));
  3523. s2 = Math.sqrt(Math.pow(pts[i + 1].x - pts[i].x, 2) + Math.pow(pts[i + 1].y - pts[i].y, 2));
  3524. p += ' ' + (pts[i].x + (s1 * dx) / s2) + ' ' + (pts[i].y + (s1 * dy) / s2);
  3525. dx *= s2 / s1;
  3526. dy *= s2 / s1;
  3527. p += ' ' + pts[i].x + ' ' + pts[i].y;
  3528. }
  3529. p += ' C ' + (pts[i - 1].x - dx) + ' ' + (pts[i - 1].y - dy);
  3530. dx = (pts[i - 1].x - pts[i].x) / 4;
  3531. dy = (pts[i - 1].y - pts[i].y) / 4;
  3532. p += ' ' + (pts[i].x + dx) + ' ' + (pts[i].y + dy);
  3533. p += ' ' + pts[i].x + ' ' + pts[i].y;
  3534.  
  3535. path.setAttribute('d', 'M ' + p);
  3536. } else {
  3537. if (style.pathLabelReadable) {
  3538. var pts = getpath(tpath, style.pathLabelReadable);
  3539. var p = '';
  3540. for (var i = 0; i < pts.length; i++) p += ' ' + pts[i].x + ' ' + pts[i].y;
  3541. path.setAttribute('d', 'M ' + p);
  3542. } else path.setAttribute('d', 'M ' + tpath);
  3543. }
  3544. path.setAttribute('id', tpid);
  3545.  
  3546. var defs = this.createDefs();
  3547. removeChildById(defs, tpid);
  3548. defs.appendChild(path);
  3549.  
  3550. var textPath = this.nodeFactory(null, 'textPath');
  3551. textPath.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + tpid);
  3552. var align = style.labelAlign || OpenLayers.Renderer.defaultSymbolizer.labelAlign;
  3553. label.setAttributeNS(null, 'text-anchor', OpenLayers.Renderer.SVG.LABEL_ALIGN[align[0]] || 'middle');
  3554. textPath.setAttribute('startOffset', style.pathLabelXOffset || OpenLayers.Renderer.SVG.LABEL_STARTOFFSET[align[0]] || '50%');
  3555. label.setAttributeNS(null, 'dominant-baseline', OpenLayers.Renderer.SVG.LABEL_ALIGN[align[1]] || 'central');
  3556. if (style.pathLabelYOffset) label.setAttribute('dy', style.pathLabelYOffset);
  3557. //textPath.setAttribute('method','stretch');
  3558. //textPath.setAttribute('spacing','auto');
  3559.  
  3560. textPath.textContent = style.pathLabel;
  3561. label.appendChild(textPath);
  3562.  
  3563. removeChildById(this.textRoot, node._featureId + '_' + suffix);
  3564. this.textRoot.appendChild(label);
  3565. };
  3566.  
  3567. OpenLayers.Renderer.SVG.prototype.setStyle = function (node, style, options) {
  3568. if (node._geometryClass === 'OpenLayers.Geometry.LineString' && style.pathLabel) {
  3569. if (node._geometryClass === 'OpenLayers.Geometry.LineString' && style.pathLabel) {
  3570. var drawOutline = !!style.labelOutlineWidth;
  3571. // First draw text in halo color and size and overlay the
  3572. // normal text afterwards
  3573. if (drawOutline) {
  3574. var outlineStyle = OpenLayers.Util.extend({}, style);
  3575. outlineStyle.fontColor = outlineStyle.labelOutlineColor;
  3576. outlineStyle.fontStrokeColor = outlineStyle.labelOutlineColor;
  3577. outlineStyle.fontStrokeWidth = style.labelOutlineWidth;
  3578. if (style.labelOutlineOpacity) outlineStyle.fontOpacity = style.labelOutlineOpacity;
  3579. delete outlineStyle.labelOutlineWidth;
  3580. this.pathText(node, outlineStyle, 'txtpath0');
  3581. }
  3582. this.pathText(node, style, 'txtpath');
  3583. setStyle.apply(this, arguments);
  3584. }
  3585. } else setStyle.apply(this, arguments);
  3586. return node;
  3587. };
  3588.  
  3589. // *
  3590. // * Method: drawGeometry
  3591. // * Remove the textpath if no geometry is drawn.
  3592. // *
  3593. // * Parameters:
  3594. // * geometry - {<OpenLayers.Geometry>}
  3595. // * style - {Object}
  3596. // * featureId - {String}
  3597. // *
  3598. // * Returns:
  3599. // * {Boolean} true if the geometry has been drawn completely; null if
  3600. // * incomplete; false otherwise
  3601.  
  3602. var drawGeometry = OpenLayers.Renderer.SVG.prototype.drawGeometry;
  3603. OpenLayers.Renderer.SVG.prototype.drawGeometry = function (geometry, style, id) {
  3604. var rendered = drawGeometry.apply(this, arguments);
  3605. if (rendered === false) {
  3606. removeChildById(this.textRoot, id + '_txtpath');
  3607. removeChildById(this.textRoot, id + '_txtpath0');
  3608. }
  3609. return rendered;
  3610. };
  3611.  
  3612. // *
  3613. // * Method: eraseGeometry
  3614. // * Erase a geometry from the renderer. In the case of a multi-geometry,
  3615. // * we cycle through and recurse on ourselves. Otherwise, we look for a
  3616. // * node with the geometry.id, destroy its geometry, and remove it from
  3617. // * the DOM.
  3618. // *
  3619. // * Parameters:
  3620. // * geometry - {<OpenLayers.Geometry>}
  3621. // * featureId - {String}
  3622.  
  3623. var eraseGeometry = OpenLayers.Renderer.SVG.prototype.eraseGeometry;
  3624. OpenLayers.Renderer.SVG.prototype.eraseGeometry = function (geometry, featureId) {
  3625. eraseGeometry.apply(this, arguments);
  3626. removeChildById(this.textRoot, featureId + '_txtpath');
  3627. removeChildById(this.textRoot, featureId + '_txtpath0');
  3628. };
  3629. }
  3630. })();