Greasy Fork is available in English.

WME Cities Overlay

Adds a city overlay for selected states

  1. // ==UserScript==
  2. // @name WME Cities Overlay
  3. // @namespace https://greasyfork.org/en/users/166843-wazedev
  4. // @version 2024.04.08.01
  5. // @description Adds a city overlay for selected states
  6. // @author WazeDev
  7. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  8. // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
  9. // @require https://greasyfork.org/scripts/369729-wme-cities-overlay-db/code/WME%20Cities%20Overlay%20DB.js
  10. // @license GNU GPLv3
  11. // @grant GM_xmlhttpRequest
  12. // @connect api.github.com
  13. // @connect raw.githubusercontent.com
  14. // @connect
  15. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  16. // ==/UserScript==
  17.  
  18. /* global W */
  19. /* global OpenLayers */
  20. /* ecmaVersion 2017 */
  21. /* global $ */
  22. /* global idbKeyval */
  23. /* global WazeWrap */
  24. /* global I18n */
  25. /* eslint curly: ["warn", "multi-or-nest"] */
  26.  
  27. (function() {
  28. 'use strict';
  29.  
  30. var _color = '#E6E6E6';
  31. var _settingsStoreName = '_wme_cities';
  32. var _settings;
  33. var _features;
  34. var _kml;
  35. var _layerName = 'Cities Overlay';
  36. var _layer = null;
  37. var defaultFillOpacity = 0.3;
  38. var defaultStrokeOpacity = 0.6;
  39. var noFillStrokeOpacity = 0.9;
  40. var repoOwner = 'WazeDev';
  41.  
  42. let currState = "";
  43. let currCity = "";
  44. let _US_States = {};
  45. let _MX_States = {};
  46. let kmlCache = {};
  47.  
  48. let indexedDBSupport = false;
  49. let citiesDB;
  50.  
  51. function isChecked(checkboxId) {
  52. return $('#' + checkboxId).is(':checked');
  53. }
  54.  
  55. function setChecked(checkboxId, checked) {
  56. $('#' + checkboxId).prop('checked', checked);
  57. }
  58.  
  59. function loadSettings() {
  60. _settings = $.parseJSON(localStorage.getItem(_settingsStoreName));
  61. let _defaultsettings = {
  62. layerVisible: true,
  63. ShowCityLabels: true,
  64. FillPolygons: true,
  65. HighlightFocusedCity: true,
  66. AutoUpdateKMLs: true
  67. //hiddenAreas: []
  68. };
  69. if(!_settings)
  70. _settings = _defaultsettings;
  71. for (var prop in _defaultsettings) {
  72. if (!_settings.hasOwnProperty(prop))
  73. _settings[prop] = _defaultsettings[prop];
  74. }
  75. }
  76.  
  77. function saveSettings() {
  78. if (localStorage) {
  79. var settings = {
  80. layerVisible: _layer.visibility,
  81. ShowCityLabels: _settings.ShowCityLabels,
  82. FillPolygons: _settings.FillPolygons,
  83. HighlightFocusedCity: _settings.HighlightFocusedCity,
  84. AutoUpdateKMLs: _settings.AutoUpdateKMLs
  85. };
  86. localStorage.setItem(_settingsStoreName, JSON.stringify(settings));
  87. }
  88. }
  89.  
  90. function GetFeaturesFromKMLString(strKML) {
  91. var format = new OpenLayers.Format.KML({
  92. 'internalProjection': W.map.getProjectionObject(),
  93. 'externalProjection': new OpenLayers.Projection("EPSG:4326")
  94. });
  95. return format.read(strKML);
  96. }
  97.  
  98. function findCurrCity(){
  99. let newCity = "";
  100. var mapCenter = new OpenLayers.Geometry.Point(W.map.getCenter().lon,W.map.getCenter().lat);
  101. for (var i=0;i<_layer.features.length;i++){
  102. var feature = _layer.features[i];
  103. if(pointInFeature(feature.geometry, mapCenter)){
  104. newCity = feature.attributes.name;
  105. break;
  106. }
  107. }
  108. return newCity;
  109. }
  110.  
  111. async function updateCitiesLayer(){
  112. let newCurrCity = findCurrCity();
  113. if(currCity != newCurrCity){
  114. currCity = newCurrCity;
  115. _layer.redraw();
  116. }
  117. await updateCityPolygons();
  118. updateDistrictNameDisplay();
  119. }
  120.  
  121. function updateDistrictNameDisplay(){
  122. $('.wmecitiesoverlay-region').remove();
  123. if (_layer !== null) {
  124. if(_layer.features.length > 0){
  125. if(currCity != ""){
  126. let color = '#00ffff';
  127. var $div = $('<div>', {id:'wmecitiesoverlay', class:"wmecitiesoverlay-region", style:'float:left; margin-left:10px;'})//, title:'Click to toggle color on/off for this group'})
  128. .css({color:color, cursor:"pointer"});
  129. //.click(toggleAreaFill);
  130. var $span = $('<span>').css({display:'inline-block'});
  131. $span.text(currCity).appendTo($div);
  132. $('.location-info-region').after($div);
  133. }
  134. }
  135. }
  136. else
  137. _layer.destroyFeatures();
  138. }
  139.  
  140. function pointInFeature(geometry, mapCenter){
  141. try{
  142. if(geometry.CLASS_NAME == "OpenLayers.Geometry.Collection" || geometry.CLASS_NAME == "OpenLayers.Geometry.Collection"){
  143. for(let i=0; i<geometry.components.length; i++){
  144. if(geometry.components[i].containsPoint(mapCenter))
  145. return true;
  146. }
  147. }
  148. else
  149. return geometry.containsPoint(mapCenter);
  150. }
  151. catch(err){
  152. console.log(err);
  153. }
  154. return false;
  155. }
  156.  
  157. async function fetch(url){
  158. //return await $.get(url);
  159. return new Promise((resolve, reject) => {
  160. GM_xmlhttpRequest({
  161. url: url,
  162. method: 'GET',
  163. onload(res) {
  164. if (res.status < 400) {
  165. resolve(res.responseText);
  166. } else {
  167. reject(res);
  168. }
  169. },
  170. onerror(res) {
  171. reject(res);
  172. }
  173. });
  174. });
  175. }
  176.  
  177. async function updateAllMaps(){
  178. let countryAbbr = W.model.getTopCountry().attributes.abbr;
  179. let keys = await idbKeyval.keys(`${countryAbbr}_states_cities`);
  180. let updatedCount = 0;
  181. let updatedStates = "";
  182. let countryAbbrObj;
  183.  
  184. if(countryAbbr === "US")
  185. countryAbbrObj = _US_States;
  186. else if(countryAbbr === "MX")
  187. countryAbbrObj = _MX_States;
  188.  
  189. let KMLinfoArr = await fetch(`https://api.github.com/repos/WazeDev/WME-Cities-Overlay/contents/KMLs/${countryAbbr}`);
  190. KMLinfoArr = $.parseJSON(KMLinfoArr);
  191. let state;
  192. for(let i=0; i<keys.length; i++){
  193. state = keys[i];
  194.  
  195. for(let j=0; j<KMLinfoArr.length; j++){
  196. if(KMLinfoArr[j].name === `${state}_Cities.kml`){ //check the size in db against server - if different, update db
  197. let stateObj = await idbKeyval.get(`${countryAbbr}_states_cities`, state);
  198.  
  199. if(stateObj.kmlsize !== KMLinfoArr[j].size){
  200. let kml = await fetch(`https://raw.githubusercontent.com/${repoOwner}/WME-Cities-Overlay/master/KMLs/${countryAbbr}/${state}_Cities.kml`);
  201.  
  202. if(state === countryAbbrObj.getAbbreviation(currState))
  203. _kml = kml;
  204.  
  205. await idbKeyval.set(`${countryAbbr}_states_cities`, {
  206. kml: kml,
  207. state: state,
  208. kmlsize: KMLinfoArr[j].size
  209. });
  210. if(kmlCache[state] != null)
  211. kmlCache[state] = _kml;
  212. if(updatedStates != "")
  213. updatedStates += `, ${state}`;
  214. else
  215. updatedStates += state;
  216. updatedCount+=1;
  217. }
  218. break;
  219. }
  220. }
  221. }
  222. if(updatedCount > 0)
  223. $('#WMECOupdateStatus').text(`${updatedCount} state file${updatedCount >1 ? "s" : ""} updated - ${updatedStates}`);
  224. else
  225. $('#WMECOupdateStatus').text("No updates available");
  226.  
  227. updatePolygons();
  228. }
  229.  
  230. /*function toggleAreaFill() {
  231. var text = $('#wmecitiesoverlay span').text();
  232. if (text) {
  233. var match = text.match(/WV-(\d+)/);
  234. if (match.length > 1) {
  235. var group = parseInt(match[1]);
  236. var f = _layer.features[group-1];
  237. var hide = f.attributes.fillOpacity !== 0;
  238. f.attributes.fillOpacity = hide ? 0 : defaultFillOpacity;
  239. var idx = _settings.hiddenAreas.indexOf(group);
  240. if (hide) {
  241. if (idx === -1) _settings.hiddenAreas.push(group);
  242. } else {
  243. if (idx > -1) {
  244. _settings.hiddenAreas.splice(idx,1);
  245. }
  246. }
  247. //saveSettingsToStorage();
  248. _layer.redraw();
  249. }
  250. }
  251. }*/
  252.  
  253. function init() {
  254. _US_States = {
  255. Alabama:"AL", Alaska:"AK", Arizona:"AZ", Arkansas:"AR", California:"CA", Colorado:"CO", Connecticut:"CT",
  256. "District of Columbia":"DC", Delaware:"DE", Florida:"FL", Georgia:"GA", Hawaii:"HI", Idaho:"ID", Illinois:"IL", Indiana:"IN",
  257. Iowa:"IA", Kansas:"KS", Kentucky:"KY", Louisiana:"LA", Maine:"ME", Maryland:"MD", Massachusetts:"MA",
  258. Michigan:"MI", Minnesota:"MN", Mississippi:"MS", Missouri:"MO", Montana:"MT", Nebraska:"NE", Nevada:"NV", "New Hampshire":"NH",
  259. "New Jersey":"NJ", "New Mexico":"NM", "New York":"NY", "North Carolina":"NC", "North Dakota":"ND", Ohio:"OH", Oklahoma:"OK", Oregon:"OR", Pennsylvania:"PA",
  260. "Rhode Island":"RI", "South Carolina":"SC", "South Dakota":"SD", Tennessee:"TN", Texas:"TX", Utah:"UT",
  261. Vermont:"VT", Virginia:"VA", Washington:"WA", "West Virginia":"WV", Wisconsin:"WI", Wyoming:"WY",
  262. getAbbreviation: function(state) { return this[state];},
  263. getStateFromAbbr: function(abbr) { return Object.entries(_US_States).filter(x => {if(x[1] == abbr) return x})[0][0];},
  264. getStatesArray: function() { return Object.keys(_US_States).filter(x => {if(typeof _US_States[x] !== "function") return x;});},
  265. getStateAbbrArray: function() { return Object.values(_US_States).filter(x => {if(typeof x !== "function") return x;});}
  266. };
  267.  
  268. _MX_States = {
  269. Aguascalientes:"AGS", "Baja California":"BC", "Baja California Sur":"BCS",Campeche:"CAM", "Coahuila de Zaragoza":"COAH", Colima:"COL",
  270. Chiapas:"CHIS", Durango:"DGO", "Ciudad de México":"CDMX", "Guanajuato":"GTO", Guerrero:"GRO", Hidalgo:"HGO", Jalisco:"JAL",
  271. "Estado de México":"EM", "Michoacán de Ocampo":"MICH", Morelos:"MOR", Nayarit:"NAY", "Nuevo León":"NL", Oaxaca:"OAX", Puebla:"PUE",
  272. "Quintana Roo":"QROO", "Querétaro":"QRO", "San Luis Potosí":"SLP", Sinaloa:"SIN", Sonora:"SON", Tabasco:"TAB", Tamaulipas:"TAM", Tlaxcala:"TLAX",
  273. "Veracruz Ignacio de la Llave":"VER", "Yucatán":"YUC", "Zacatecas":"ZAC",
  274. getAbbreviation: function(state) { return this[state];},
  275. getStateFromAbbr: function(abbr) { return Object.entries(_MX_States).filter(x => {if(x[1] == abbr) return x})[0][0];},
  276. getStatesArray: function() { return Object.keys(_MX_States).filter(x => {if(typeof _MX_States[x] !== "function") return x;});},
  277. getStateAbbrArray: function() { return Object.values(_MX_States).filter(x => {if(typeof x !== "function") return x;});}};
  278.  
  279. loadSettings();
  280.  
  281. var layerid = 'wme_cities_overlay';
  282. var layerStyle = new OpenLayers.StyleMap({
  283. strokeDashstyle: 'solid', strokeColor: _color,
  284. strokeOpacity: _settings.FillPolygons ? defaultStrokeOpacity : noFillStrokeOpacity,
  285. strokeWidth: 2,
  286. fillOpacity: _settings.FillPolygons ? defaultFillOpacity : 0,
  287. fillColor: _color,fontColor: '#ffffff',
  288. label : "${labelText}", labelOutlineColor: '#000000',
  289. labelOutlineWidth: 4, labelAlign: 'cm',
  290. fontSize: "16px"
  291. });
  292.  
  293. _layer = new OpenLayers.Layer.Vector("Cities Overlay", {
  294. rendererOptions: { zIndexing: true },
  295. uniqueName: layerid,
  296. shortcutKey: "S+" + 0,
  297. layerGroup: 'cities_overlay',
  298. zIndex: -9999,
  299. displayInLayerSwitcher: true,
  300. visibility: _settings.layerVisible,
  301. styleMap: layerStyle
  302. });
  303. I18n.translations[I18n.locale].layers.name[layerid] = "Cities Overlay";
  304. W.map.addLayer(_layer);
  305. if(_settings.layerVisible) //"reusing" this setting - should have set it up to enable/disable the moveend handler from the start instead of just hiding the layer. Durp
  306. W.map.events.register("moveend", null, updateCitiesLayer);
  307.  
  308. if(!_settings.ShowCityLabels)
  309. _layer.styleMap.styles.default.defaultStyle.label = "";
  310.  
  311. updateCitiesLayer();
  312. // Add the layer checkbox to the Layers menu.
  313. WazeWrap.Interface.AddLayerCheckbox("display", "Cities Overlay", _settings.layerVisible, layerToggled);
  314.  
  315. var $section = $("<div>", {style:"padding:8px 16px", id:"WMECitiesOverlaySettings"});
  316. $section.html([
  317. `<h4 style="margin-bottom:0px;"><i id="citiesPower" class="fa fa-power-off" aria-hidden="true" style="color:${_settings.layerVisible ? 'rgb(0,180,0)' : 'black'}; cursor:pointer;"></i> <b>WME Cities Overlay</b></h4>`,
  318. `<h6 style="margin-top:0px;">${GM_info.script.version}</h6>`,
  319. '<div id="divWMECOFillPolygons"><input type="checkbox" id="_cbCOFillPolygons" class="wmecoSettingsCheckbox" /><label for="_cbCOFillPolygons">Fill polygons</label></div>',
  320. '<div id="divWMECOShowCityLabels"><input type="checkbox" id="_cbCOShowCityLabels" class="wmecoSettingsCheckbox" /><label for="_cbCOShowCityLabels">Show city labels</label></div>',
  321. '<div id="divWMECOHighlightFocusedCity"><input type="checkbox" id="_cbCOHighlightFocusedCity" class="wmecoSettingsCheckbox" /><label for="_cbCOHighlightFocusedCity">Highlight focused city</label></div>',
  322. '<fieldset id="fieldUpdates" style="border: 1px solid silver; padding: 8px; border-radius: 4px;">',
  323. '<legend style="margin-bottom:0px; border-bottom-style:none;width:auto;"><h4>Update Settings</h4></legend>',
  324. '<div id="divWMECOUpdateMaps" title="Checks for new state files for the current country"><button id="WMECOupdateMaps" type="button">Update database</button></div>',
  325. '<div id="WMECOupdateStatus"></div>',
  326. '<div id="divWMECOAutoUpdateKMLs" title="Checks for updated state files for the current country when WME loads"><input type="checkbox" id="_cbCOAutoUpdateKMLs" class="wmecoSettingsCheckbox" /><label for="_cbCOAutoUpdateKMLs">Automatically update database</label></div>','</fieldset>',
  327. '</div>'
  328. ].join(' '));
  329.  
  330. WazeWrap.Interface.Tab('Cities', $section.html(), init2, 'Cities');
  331. }
  332.  
  333. function init2(){
  334. $('.wmecoSettingsCheckbox').change(function() {
  335. var settingName = $(this)[0].id.substr(5);
  336. _settings[settingName] = this.checked;
  337. saveSettings();
  338. });
  339.  
  340. setChecked('_cbCOShowCityLabels', _settings.ShowCityLabels);
  341. setChecked('_cbCOFillPolygons', _settings.FillPolygons);
  342. setChecked('_cbCOHighlightFocusedCity', _settings.HighlightFocusedCity);
  343. setChecked('_cbCOAutoUpdateKMLs', _settings.AutoUpdateKMLs);
  344.  
  345. $('#citiesPower').click(function(){
  346. _settings.layerVisible = !_settings.layerVisible;
  347. layerToggled(_settings.layerVisible);
  348. if(_settings.layerVisible)
  349. W.map.events.register("moveend", null, updateCitiesLayer);
  350. else
  351. W.map.events.unregister("moveend", null, updateCitiesLayer);
  352. });
  353.  
  354. $('#WMECOupdateMaps').click(updateAllMaps);
  355.  
  356. $('#_cbCOFillPolygons').change(function(){
  357. _layer.styleMap.styles.default.defaultStyle.fillOpacity = this.checked ? defaultFillOpacity : 0;
  358. _layer.styleMap.styles.default.defaultStyle.strokeOpacity = this.checked ? defaultStrokeOpacity : noFillStrokeOpacity;
  359. _layer.redraw();
  360. });
  361.  
  362. $('#_cbCOShowCityLabels').change(function(){
  363. _layer.styleMap.styles.default.defaultStyle.label = this.checked ? "${labelText}" : "";
  364. _layer.redraw();
  365. });
  366.  
  367. $('#_cbCOHighlightFocusedCity').change(function(){
  368. if(this.checked){
  369. insertHighlightingRules();
  370. }
  371. else{
  372. let index = _layer.styleMap.styles.default.rules.findIndex(function(e){ return e.name == "WMECOHighlightCurr";});
  373. if(index > -1)
  374. _layer.styleMap.styles.default.rules.splice(index, 1);
  375.  
  376. index = _layer.styleMap.styles.default.rules.findIndex(function(e){ return e.name == "WMECONoHighlight";});
  377. if(index > -1)
  378. _layer.styleMap.styles.default.rules.splice(index, 1);
  379. _layer.redraw();
  380. }
  381. });
  382.  
  383. currCity = findCurrCity();
  384.  
  385. if(_settings.HighlightFocusedCity)
  386. insertHighlightingRules();
  387.  
  388. if(_settings.layerVisible && _settings.AutoUpdateKMLs)
  389. updateAllMaps();
  390. }
  391.  
  392. function insertHighlightingRules(){
  393. //********** Highlighting Rules ***********
  394. let myRule = new W.Rule({
  395. filter: new OpenLayers.Filter.Comparison({
  396. type: '==',
  397. evaluate: function(cityFeature) {
  398. return cityFeature.attributes.name === currCity;
  399. }
  400. }),
  401. symbolizer: {
  402. strokeColor: '#f7ad25',
  403. fillColor: '#f7ad25'
  404. },
  405. name: "WMECOHighlightCurr"
  406. });
  407. let myRule2 = new W.Rule({
  408. filter: new OpenLayers.Filter.Comparison({
  409. type: '!=',
  410. evaluate: function(cityFeature) {
  411. return cityFeature.attributes.name != currCity;
  412. }
  413. }),
  414. symbolizer: {
  415. strokeColor: _color,
  416. fillColor: _color
  417. },
  418. name: "WMECONoHighlight"
  419. });
  420. _layer.styleMap.styles['default'].rules.push(myRule);
  421. _layer.styleMap.styles['default'].rules.push(myRule2);
  422. _layer.redraw();
  423. }
  424.  
  425. function layerToggled(visible) {
  426. _settings.layerVisible = visible;
  427. _layer.setVisibility(visible);
  428. if(visible){
  429. $('#citiesPower').css("color", "rgb(0,180,0)");
  430. W.map.events.register("moveend", null, updateCitiesLayer);
  431. }
  432. else{
  433. $('#citiesPower').css("color", "black");
  434. W.map.events.unregister("moveend", null, updateCitiesLayer);
  435. }
  436. saveSettings();
  437. }
  438.  
  439. async function updateCityPolygons(){
  440. if(currState != W.model.getTopState().attributes.name)
  441. {
  442. _layer.destroyFeatures();
  443. currState = W.model.getTopState().attributes.name;
  444. let countryAbbr = W.model.getTopCountry().attributes.abbr;
  445. let stateAbbr;
  446.  
  447. if(countryAbbr === "US")
  448. stateAbbr = _US_States.getAbbreviation(currState);
  449. else if(countryAbbr === "MX")
  450. stateAbbr = _MX_States.getAbbreviation(currState);
  451.  
  452. if(typeof stateAbbr !== "undefined"){
  453. if(typeof kmlCache[stateAbbr] == 'undefined'){
  454. //get the current state info from the store.
  455. var request = await idbKeyval.get(`${countryAbbr}_states_cities`, stateAbbr);
  456.  
  457. //if the store didn't have the state, look it up from github and enter it in the store
  458. if(!request){
  459. let kml = await fetch(`https://raw.githubusercontent.com/${repoOwner}/WME-Cities-Overlay/master/KMLs/${countryAbbr}/${stateAbbr}_Cities.kml`);
  460. _kml = kml;
  461. updatePolygons();
  462.  
  463. await idbKeyval.set(`${countryAbbr}_states_cities`, {
  464. kml: kml,
  465. state: stateAbbr,
  466. kmlsize: 0
  467. });
  468. kmlCache[stateAbbr] = _kml; //keep a local cache so we don't have to hit the indexeddb repeatedly if the user crosses state lines multiple times
  469. }
  470. else{
  471. _kml = request.kml;
  472. kmlCache[stateAbbr] = _kml;//keep a local cache so we don't have to hit the indexeddb repeatedly if the user crosses state lines multiple times
  473. updatePolygons();
  474. }
  475. }
  476. else{
  477. _kml = kmlCache[stateAbbr];
  478. updatePolygons();
  479. }
  480. }
  481. }
  482. }
  483.  
  484. function updatePolygons(){
  485. var _features = GetFeaturesFromKMLString(_kml);
  486. _layer.destroyFeatures();
  487. for(let i=0; i< _features.length; i++){
  488. _features[i].attributes.name = _features[i].attributes.name.replace('<at><openparen>', '').replace('<closeparen>','');
  489. _features[i].attributes.labelText = _features[i].attributes.name;
  490. }
  491.  
  492. _layer.addFeatures(_features);
  493. }
  494.  
  495. function bootstrap(tries = 1) {
  496. if (W && W.loginManager && W.loginManager.user && W.model.getTopState() && WazeWrap.Ready) {
  497. init();
  498. console.log('WME Cities Overlay:', 'Initialized');
  499. } else if(tries < 1000){
  500. console.log('WME Cities Overlay: ', 'Bootstrap failed. Trying again...');
  501. window.setTimeout(() => bootstrap(tries++), 100);
  502. }
  503. }
  504.  
  505. bootstrap();
  506. })();