Greasy Fork is available in English.

WME OpenData

Provides access to certain OS OpenData products within the WME environment

// ==UserScript==
// @name                WME OpenData
// @namespace           http://greasemonkey.chizzum.com
// @description         Provides access to certain OS OpenData products within the WME environment
// @include             https://*.waze.com/*editor*
// @exclude             https://editor-beta.waze.com/*
// @exclude             https://beta.waze.com/*
// @include             https://one.network/*
// @include             https://public.londonworks.gov.uk/roadworks/*
// @include             http://public.londonworks.gov.uk/roadworks/*
// @include             https://labs.os.uk/*
// @grant               GM_setValue
// @grant               GM_getValue
// @grant               GM_addValueChangeListener
// @grant               unsafeWindow
// @version             3.32
// ==/UserScript==

// Contains Ordnance Survey data Crown copyright and database right 2023
//
// Contents of the locatorData_*.js files are derived under the
// Open Government Licence from the OS Open Names and Open Roads datasets
//
// Contents of the gazetteer.js file are derived under the
// Open Government Licence from the OS Open Names dataset

/*
=======================================================================================================================
DONE FOR THIS RELEASE
=======================================================================================================================

=======================================================================================================================
Bug fixes - MUST BE CLEARED BEFORE RELEASE
=======================================================================================================================

=======================================================================================================================
Things to be checked
=======================================================================================================================

*/

/* JSHint Directives */
/* globals W: true */
/* globals OpenLayers: true */
/* globals Elgin: true */
/* globals google: true */
/* globals gazetteerData: true */
/* globals oslRoadNameMatches: true */
/* globals map: true */
/* globals GM_setValue: true */
/* globals GM_getValue: true */
/* globals GM_addValueChangeListener: true */
/* globals unsafeWindow: true */
/* globals trustedTypes: */
/* jshint bitwise: false */
/* jshint evil: true */
/* jshint esversion: 11 */

const oslVersion = '3.32';
const oslUpdateURL = 'https://greasyfork.org/scripts/1941-wme-to-os-link';
const oslBlockPath = 'https://greasemonkey.chizzum.com/osl_v3.0/';
const oslGazetteerURL = 'https://chizzum.com/greasemonkey/gaz_v5/_gazetteer.js';
const oslPi = 3.14159265358979;
const oslPiDiv180 = (oslPi / 180);
const osl180DivPi = (180 / oslPi);
const oslLocatorBlockSize = 1000;
const oslCacheDecayPeriod = 60;

const oslNameTextPts = '12pt';
const oslNameHalfPix = 12;


const GAZ_ELM =
{
   Name: 0,
   CEast: 1,
   CNorth: 2,
   Type: 3,
   Area: 4,
   LEast: 5,
   LNorth: 6,
   REast: 7,
   RNorth: 8
};
const OSL_ELM =
{
   RoadName: 0,
   RoadNumber: 1,
   BoundW: 2,
   BoundE: 3,
   BoundS: 4,
   BoundN: 5,
   Geometry: 6,
   AreaName: 7,
   AltName: 8,
   Classification: 9,
   Function: 10,
   Form: 11,
   Structure: 12,
   IsPrimary: 13,
   IsTrunk: 14,
   MAX: 15
};
const OSL_MODE =
{
   Conversion: 0,
   OpenNames: 1,
   NameCheck: 2,
   OpenRoads: 3
};
const OSL_BBMODE = 
{
   Init: 0,
   Match: 1,
   Other: 2,
   Finalise: 3
};
const OSL_ROADRENDERER =
{
   Init: 0,
   Render: 1,
   Finalise: 2,
   Erase: 3
};

const oslRoadClassifications = new Array
(
   'Undefined',
   'Motorway',
   'A Road',
   'B Road',
   'Classified Unnumbered',
   'Unclassified',
   'Not Classified',
   'Unknown'
);

const oslRoadFunctions = new Array
(
   'Undefined',
   'Motorway',
   'A Road',
   'B Road',
   'Minor Road',
   'Local Road',
   'Local Access Road',
   'Restricted Local Access Road',
   'Secondary Access Road'
);
const oslStrokeColoursByFunction = new Array
(
   "red",
   "deepskyblue",
   "limegreen",
   "darkorange",
   "yellow",
   "white",
   "grey",
   "tan",
   "grey"
);
const oslRoadStructures = new Array
(
   '',
   '[Tunnel]',
   '[Bridge]'
);
const oslFormsOfWay = new Array
(
   'Undefined',
   'Single Carriageway',
   'Dual Carriageway',
   'Slip Road',
   'Roundabout',
   'Collapsed Dual Carriageway',
   'Guided Busway',
   'Shared Use Carriageway'
);

// List of all road name suffixes, giving both their full length and abbreviated forms,
// for use when automatically translating the standard OS names into the abbreviated
// forms we prefer to use in WME.
//
// Note that some suffixes are included where the abbreviated form is identical to the
// full length form - these are present to act as guidance to the translation code so
// that it recognises which part of the OS name is the suffix - remember that the suffix
// isn't always the last word in the name, as we also need to consider names that have 
// things after the suffix - e.g. Breakspear Road South, High Street Eastcote etc.  If
// these weren't present then we could end up incorrectly translating earlier parts of
// the name which match one of the other suffixes - e.g. West Green Way...
const oslNameAbbreviations = new Array
(
   'Avenue','Ave',
   'Boulevard','Blvd',
   'Broadway','Bdwy',
   'Circus','Cir',
   'Close','Cl',
   'Court','Ct',
   'Crescent','Cr',
   'Drive','Dr',
   'Garden','Gdn',
   'Gardens','Gdns',
   'Green','Gn',
   'Grove','Gr',
   'Lane','Ln',
   'Mews','Mews',
   'Mount','Mt',
   'Place','Pl',
   'Park','Pk',
   'Ridge','Rdg',
   'Road','Rd',
   'Square','Sq',
   'Street','St',
   'Terrace','Ter',
   'Valley','Val',
   'By-pass','Bypass',
   'Way','Way',
   'Hill','Hill'
);


let oslUserPrefs = {};

let oslGazetteerData = [];

let oslAdvancedMode = false;
let oslEvalString = '';
let oslLoadingMsg = false;
let oslMLCDiv = null;
let oslOSLDiv = null;
let oslBBDiv = null;
let oslNamesDiv = null;
let oslGazTagsDiv = null;
let oslPrevHighlighted = null;
let oslSegmentHighlighted = false;
let oslPrevMouseX = null;
let oslPrevMouseY = null;
let oslDivDragging = false;
let oslPrevSelected = null;
let oslDoOSLUpdate = false;
let oslMousepos = null;
let oslMousePixelpos = null;
let oslLastViewportWidth = null;
let oslDoneOnload = false;
let oslPrevStreetName = '';
let oslMergeGazData = false;
let oslOSLMaskLayer = null;
let oslOSLNameCheckTimer = 0;
let oslOSLNCSegments = [];
let oslInUK = false;
let oslInLondon = false;

let oslNorthings = null;
let oslEastings = null;
let oslLatitude = null;
let oslLongitude = null;
let oslHelmX = null;
let oslHelmY = null;
let oslHelmZ = null;

let oslBlocksToLoad = [];
let oslBlocksToTest = [];

let oslVPLeft = 0;
let oslVPRight = 0;
let oslVPBottom = 0;
let oslVPTop = 0;
let oslBBDivInnerHTML = '';

let oslEvalEBlock = 0;
let oslEvalNBlock = 0;
let oslBlockData = null;
let oslBlockCacheList = [];
let oslBlockCacheTestTimer = (oslCacheDecayPeriod * 10);
let oslSegGeoDivInnerHTML = '';
let oslNamesDivInnerHTML = '';

let oslONC_E = null;
let oslONC_N = null;
let oslEBlock_min = null;
let oslEBlock_max = null;
let oslNBlock_min = null;
let oslNBlock_max = null;

let oslOSLDivLeft;
let oslOSLDivTop;
let oslSegGeoDiv;
let oslWazeMapElement;
let oslDragBar;
let oslWindow;
let oslOSLDivTopMinimised;
let oslNCDiv;
let oslSegGeoUIDiv;

let oslOffsetToolbar = false;
let oslMOAdded = false;

let oslLocatorElements = null;
let oslUsingNewName = false;
let oslUseName = false;
let oslCityName = '';
let oslCountyName = '';
let oslUseAlt = false;

let oslRORCenter = null;
let oslRORZoom = null;

function oslBootstrap()
{
   if(document.location.host == 'one.network')
   {
      hlp_ONE.init();
   }
   else if(document.location.host == 'labs.os.uk')
   {
      hlp_ODM.init();
   }
   else if(document.location.host == 'public.londonworks.gov.uk')
   {
      hlp_LRR.init();
   }
   else
   {
      oslInitialise();
   }
}
function oslAddLog(logtext)
{
   console.log('WMEOpenData: '+logtext);
}
function oslModifySrc(srcIn)
{
	if(typeof trustedTypes === "undefined")
	{
		return srcIn;
	}
	else
	{
		const escapeSrcPolicy = trustedTypes.createPolicy("name", {createScriptURL: (to_escape) => to_escape});
		return escapeSrcPolicy.createScriptURL(srcIn);
	}
}
function oslModifyHTML(htmlIn)
{
	if(typeof trustedTypes === "undefined")
	{
		return htmlIn;
	}
	else
	{
		const escapeHTMLPolicy = trustedTypes.createPolicy("forceInner", {createHTML: (to_escape) => to_escape});
		return escapeHTMLPolicy.createHTML(htmlIn);
	}
}


//-----------------------------------------------------------------------------------------------------------------------------------------
// all code between here and the next ------------- marker line is a stripped down version of the original from Paul Dixon
//
// * GeoTools javascript coordinate transformations
// * http://www.nearby.org.uk/tests/geotools2.js
// *
// * This file copyright (c)2005 Paul Dixon (paul@elphin.com)
// *
// * This program is free software; you can redistribute it and/or
// * modify it under the terms of the GNU General Public License
// * as published by the Free Software Foundation; either version 2
// * of the License, or (at your option) any later version.
// *
// * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// * GNU General Public License for more details.
// *
// * You should have received a copy of the GNU General Public License
// * along with this program; if not, write to the Free Software
// * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
// *
// * ---------------------------------------------------------------------------
// *
// * Credits
// *
// * The algorithm used by the script for WGS84-OSGB36 conversions is derived
// * from an OSGB spreadsheet (www.gps.gov.uk) with permission. This has been
// * adapted into Perl by Ian Harris, and into PHP by Barry Hunter. Conversion
// * accuracy is in the order of 7m for 90% of Great Britain, and should be
// * be similar to the conversion made by a typical GPSr
// *
// * See accompanying documentation for more information
// * http://www.nearby.org.uk/tests/GeoTools2.html
function oslOSGBtoWGS(oseast, osnorth)
{
   const a = 6377563.396;
   const b = 6356256.910;
   const e0 = 400000;
   const n0 = -100000;
   const f0 = 0.999601272;
   const PHI0 = 49.00000;
   const LAM0 = -2.00000;
   const RadPHI0 = PHI0 * oslPiDiv180;
   const RadLAM0 = LAM0 * oslPiDiv180;

   //Compute af0, bf0, e squared (e2), n and Et
   const af0 = a * f0;
   const bf0 = b * f0;
   const e2 = (Math.pow(af0,2) - Math.pow(bf0,2)) / Math.pow(af0,2);
   const n = (af0 - bf0) / (af0 + bf0);
   let Et = oseast - e0;

   //Compute initial value for oslLatitude (PHId) in radians
   let PHI1 = ((osnorth - n0) / af0) + RadPHI0;
   let M = oslMarc(bf0, n, RadPHI0, PHI1);
   let PHId = ((osnorth - n0 - M) / af0) + PHI1;
   while (Math.abs(osnorth - n0 - M) > 0.00001)
   {
      PHId = ((osnorth - n0 - M) / af0) + PHI1;
      M = oslMarc(bf0, n, RadPHI0, PHId);
      PHI1 = PHId;
   }

   //Compute nu, rho and eta2 using value for PHId
   let nu = af0 / (Math.sqrt(1 - (e2 * ( Math.pow(Math.sin(PHId),2)))));
   let rho = (nu * (1 - e2)) / (1 - (e2 * Math.pow(Math.sin(PHId),2)));
   let eta2 = (nu / rho) - 1;

   //Compute Latitude/Longitude
   let tanPHId = Math.tan(PHId);
   let tanPHIdSquared = Math.pow(tanPHId, 2);
   let tanPHIdPowFour = Math.pow(tanPHId, 4);
   let cosPHId = Math.cos(PHId);
   let cosPHIdPowNegOne = Math.pow(cosPHId, -1);
   let nuPowThree = Math.pow(nu, 3);
   let nuPowFive = Math.pow(nu, 5);

   let VII = tanPHId / (2 * rho * nu);
   let VIII = (tanPHId / (24 * rho * nuPowThree)) * (5 + (3 * tanPHIdSquared) + eta2 - (9 * eta2 * tanPHIdSquared));
   let IX = (tanPHId / (720 * rho * nuPowFive)) * (61 + (90 * tanPHIdSquared) + (45 * tanPHIdPowFour));
   oslLatitude = osl180DivPi * (PHId - (Math.pow(Et,2) * VII) + (Math.pow(Et,4) * VIII) - (Math.pow(Et, 6) * IX));

   let X = cosPHIdPowNegOne / nu;
   let XI = (cosPHIdPowNegOne / (6 * nuPowThree)) * ((nu / rho) + (2 * tanPHIdSquared));
   let XII = (cosPHIdPowNegOne / (120 * nuPowFive)) * (5 + (28 * tanPHIdSquared) + (24 * tanPHIdPowFour));
   let XIIA = (cosPHIdPowNegOne / (5040 * Math.pow(nu,7))) * (61 + (662 * tanPHIdSquared) + (1320 * tanPHIdPowFour) + (720 * (Math.pow(tanPHId,6))));
   oslLongitude = osl180DivPi * (RadLAM0 + (Et * X) - (Math.pow(Et,3) * XI) + (Math.pow(Et,5) * XII) - (Math.pow(Et,7) * XIIA));



   let RadPHI = oslLatitude * oslPiDiv180;
   let RadLAM = oslLongitude * oslPiDiv180;

   const ee2 = (Math.pow(6377563.396,2) - Math.pow(6356256.910,2)) / Math.pow(6377563.396,2);
   let V = a / (Math.sqrt(1 - (ee2 * (  Math.pow(Math.sin(RadPHI),2)))));
   let cosRadPHI = Math.cos(RadPHI);

   X = V * cosRadPHI * Math.cos(RadLAM);
   let Y = V * cosRadPHI * Math.sin(RadLAM);
   let Z = (V * (1 - ee2)) * (Math.sin(RadPHI));

   // do Helmert transforms

   const sfactor = -20.4894 * 0.000001;
   const RadX_Rot = (0.1502 / 3600) * oslPiDiv180;
   const RadY_Rot = (0.2470 / 3600) * oslPiDiv180;
   const RadZ_Rot = (0.8421 / 3600) * oslPiDiv180;

   let X2 = (X + (X * sfactor) - (Y * RadZ_Rot) + (Z * RadY_Rot) + 446.448);
   let Y2 = (X * RadZ_Rot) + Y + (Y * sfactor) - (Z * RadX_Rot) -125.157;
   let Z2 = (-1 * X * RadY_Rot) + (Y * RadX_Rot) + Z + (Z * sfactor) + 542.060;

   let RootXYSqr = Math.sqrt(Math.pow(X2,2) + Math.pow(Y2,2));
   const eee2 = (Math.pow(6378137.000,2) - Math.pow(6356752.313,2)) / Math.pow(6378137.000,2);
   PHI1 = Math.atan2(Z2 , (RootXYSqr * (1 - eee2)) );
   let sinPHI1 = Math.sin(PHI1);
   let sinPHI1Squared = Math.pow(sinPHI1, 2);

   V = 6378137.000 / (Math.sqrt(1 - (eee2 * sinPHI1Squared)));
   let PHI2 = Math.atan2((Z + (eee2 * V * sinPHI1)) , RootXYSqr);
   while (Math.abs(PHI1 - PHI2) > 0.000000001)
   {
      PHI1 = PHI2;
      let innerSinPHI1 = Math.sin(PHI1);
      V = 6378137.000 / (Math.sqrt(1 - (eee2 * Math.pow(innerSinPHI1,2))));
      PHI2 = Math.atan2((Z2 + (eee2 * V * innerSinPHI1)) , RootXYSqr);
   }
   oslLatitude = PHI2 * osl180DivPi;
   oslLongitude = Math.atan2(Y2 , X2) * osl180DivPi;
}
function oslWGStoOSGB()
{
   oslLatLontoHelmXYZ();
   let oslLatitude2  = oslXYZtoLat();
   let oslLongitude2 = Math.atan2(oslHelmY , oslHelmX) * osl180DivPi;
   oslLatLonoslToOSGrid(oslLatitude2,oslLongitude2);
}
function oslLatLontoHelmXYZ()
{
   const a = 6378137.0;
   const b = 6356752.313;
   const DX = -446.448;
   const DY = 125.157;
   const DZ = -542.060;
   const rotX = -0.1502;
   const rotY = -0.2470;
   const rotZ = -0.8421;
   const sfactor = 20.4894 * 0.000001;
   const e2 = (Math.pow(a,2) - Math.pow(b,2)) / Math.pow(a,2);

   // perform initial lat-lon to cartesian coordinate translation
   let RadPHI = oslLatitude * oslPiDiv180;
   let RadLAM = oslLongitude * oslPiDiv180;
   let V = a / (Math.sqrt(1 - (e2 * (  Math.pow(Math.sin(RadPHI),2)))));
   let cartX = V * (Math.cos(RadPHI)) * (Math.cos(RadLAM));
   let cartY = V * (Math.cos(RadPHI)) * (Math.sin(RadLAM));
   let cartZ = (V * (1 - e2)) * (Math.sin(RadPHI));

   // Compute Helmert transformed coordinates
   let RadX_Rot = (rotX / 3600) * oslPiDiv180;
   let RadY_Rot = (rotY / 3600) * oslPiDiv180;
   let RadZ_Rot = (rotZ / 3600) * oslPiDiv180;
   oslHelmX = (cartX + (cartX * sfactor) - (cartY * RadZ_Rot) + (cartZ * RadY_Rot) + DX);
   oslHelmY = (cartX * RadZ_Rot) + cartY + (cartY * sfactor) - (cartZ * RadX_Rot) + DY;
   oslHelmZ = (-1 * cartX * RadY_Rot) + (cartY * RadX_Rot) + cartZ + (cartZ * sfactor) + DZ;
}
function oslXYZtoLat()
{
   const a = 6377563.396;
   const b = 6356256.910;
   const e2 = (Math.pow(a,2) - Math.pow(b,2)) / Math.pow(a,2);

   let RootXYSqr = Math.sqrt(Math.pow(oslHelmX,2) + Math.pow(oslHelmY,2));
   let PHI1 = Math.atan2(oslHelmZ , (RootXYSqr * (1 - e2)) );
   let PHI = oslIterateOSLXYZtoLat(a, e2, PHI1, oslHelmZ, RootXYSqr);
   return PHI * osl180DivPi;
}
function oslIterateOSLXYZtoLat(a, e2, PHI1, Z, RootXYSqr)
{
   let V = a / (Math.sqrt(1 - (e2 * Math.pow(Math.sin(PHI1),2))));
   let PHI2 = Math.atan2((Z + (e2 * V * (Math.sin(PHI1)))) , RootXYSqr);
   while (Math.abs(PHI1 - PHI2) > 0.000000001)
   {
      PHI1 = PHI2;
      let sinPHI1 = Math.sin(PHI1);
      V = a / (Math.sqrt(1 - (e2 * Math.pow(sinPHI1,2))));
      PHI2 = Math.atan2((Z + (e2 * V * (sinPHI1))) , RootXYSqr);
   }
   return PHI2;
}
function oslMarc(bf0, n, PHI0, PHI)
{
   let c1 = PHI - PHI0;
   let c2 = PHI + PHI0;
   let c3 = Math.pow(n, 2);
   let c4 = Math.pow(n, 3);
   return bf0 * 
            (
               (
                  (1 + n + ((5 / 4) * c3) + ((5 / 4) * c4)) * c1
               ) - 
               (
                  ((3 * n) + (3 * c3) + ((21 / 8) * c4)) * 
                  Math.sin(c1) * 
                  Math.cos(c2)
               ) + 
               (
                  (((15 / 8) * c3) + ((15 / 8) * c4)) * 
                  Math.sin(2 * c1) * 
                  Math.cos(2 * c2)
               ) - 
               (
                  ((35 / 24) * c4) *
                  (Math.sin(3 * c1)) * (Math.cos(3 * c2))
               )
            );
}
function oslLatLonoslToOSGrid(PHI, LAM)
{
   const a = 6377563.396;
   const b = 6356256.910;
   const e0 = 400000;
   const n0 = -100000;
   const f0 = 0.999601272;
   const PHI0 = 49.00000;
   const LAM0 = -2.00000;
   const RadPHI0 = PHI0 * oslPiDiv180;
   const RadLAM0 = LAM0 * oslPiDiv180;
   const af0 = a * f0;
   const bf0 = b * f0;
   const n = (af0 - bf0) / (af0 + bf0);
   const e2 = (Math.pow(af0,2) - Math.pow(bf0,2)) / Math.pow(af0,2);

   let RadPHI = PHI * oslPiDiv180;
   let RadLAM = LAM * oslPiDiv180;
   let sinRadPHI = Math.sin(RadPHI);
   let sinRadPHISquared = Math.pow(sinRadPHI, 2);
   let cosRadPHI = Math.cos(RadPHI);
   let cosRadPHIPowThree = Math.pow(cosRadPHI, 3);
   let cosRadPHIPowFive = Math.pow(cosRadPHI, 5);
   let tanRadPHI = Math.tan(RadPHI);
   let tanRadPHISquared = Math.pow(tanRadPHI, 2);
   let tanRadPHIPowFour = Math.pow(tanRadPHI, 4);

   let nu = af0 / (Math.sqrt(1 - (e2 * sinRadPHISquared)));
   let rho = (nu * (1 - e2)) / (1 - (e2 * sinRadPHISquared));
   let eta2 = (nu / rho) - 1;
   let p = RadLAM - RadLAM0;
   let M = oslMarc(bf0, n, RadPHI0, RadPHI);
   let I = M + n0;
   let II = (nu / 2) * sinRadPHI * cosRadPHI;
   let III = ((nu / 24) * sinRadPHI * cosRadPHIPowThree) * (5 - tanRadPHISquared + (9 * eta2));
   let IIIA = ((nu / 720) * sinRadPHI * cosRadPHIPowFive) * (61 - (58 * tanRadPHISquared) + tanRadPHIPowFour);
   let IV = nu * cosRadPHI;
   let V = (nu / 6) * cosRadPHIPowThree * ((nu / rho) - tanRadPHISquared);
   let VI = (nu / 120) * cosRadPHIPowFive * ((5 - (18 * tanRadPHISquared)) + tanRadPHIPowFour + (14 * eta2) - (58 * tanRadPHISquared * eta2));
   oslEastings = Math.round(e0 + (p * IV) + (Math.pow(p,3) * V) + (Math.pow(p,5) * VI));
   oslNorthings = Math.round(I + (Math.pow(p,2) * II) + (Math.pow(p,4) * III) + (Math.pow(p,6) * IIIA));

   // Conversion errors

   // 50.06574112187924 -5.699894626953322
   // 135261,25033
   // 135256,25014
   // +6, +19

   // 51.35363338966115 1.4443961072966522
   // 639795,167306
   // 639800,167284
   // -5, +22

   // 60.15795987870581 -1.1466271562283679
   // 447367,1141743
   // 447362,1141733
   // +5, +10

   let nCorrect = Math.round(70 - PHI);
   oslNorthings -= nCorrect;
}
//-----------------------------------------------------------------------------------------------------------------------------------------
function oslCaseCorrect(wrongcase)
{
   let correctedCase = '';
   for(let loop=0;loop<wrongcase.length;loop++)
   {
      // capitalise first letter following one of these substrings
      if
      (
         (loop === 0)||
         (wrongcase[loop-1] == ' ')||
         (wrongcase[loop-1] == '(')||
         (wrongcase.substr(loop-3,3) == '-Y-')||
         (wrongcase.substr(loop-4,4) == '-YR-')
      ) correctedCase += wrongcase[loop].toUpperCase();
      else correctedCase += wrongcase[loop].toLowerCase();
   }
   // recapitalise any roman numerals
   correctedCase = correctedCase.replace(' Ii ',' II ');
   correctedCase = correctedCase.replace(' Iii ',' III ');
   correctedCase = correctedCase.replace(' Iv ',' IV ');
   correctedCase = correctedCase.replace(' Vi ',' VI ');
   correctedCase = correctedCase.replace(' Vii ',' VII ');
   return correctedCase;
}
function oslSaintsPreserveUs(oslName)
{
   let nameBits = [];
   if(oslName.indexOf('St ') != -1)
   {
      nameBits = oslName.split('St ');
      oslName = nameBits[0] + 'St. ' + nameBits[1];
   }
   else if(oslName.indexOf('Saint ') != -1)
   {
      nameBits = oslName.split('Saint ');
      oslName = nameBits[0] + 'St. ' + nameBits[1];
   }
   return oslName;
}
function oslWazeifyStreetName(oslName, debugOutput)
{
   let wazeName = '';

   // strip out any HTML encoding added by the server when returning the street name data...
   let textArea = document.createElement('textarea');
   textArea.innerHTML = oslModifyHTML(oslName);
   oslName = textArea.value;

   wazeName = oslCaseCorrect(oslName);
   wazeName = oslSaintsPreserveUs(wazeName);

   let nameoslPieces = wazeName.split(' ');
   if(nameoslPieces.length > 1)
   {
      let dirSuffix = '';
      let namePrefix = '';
      if((nameoslPieces[nameoslPieces.length-1] == 'North')||(nameoslPieces[nameoslPieces.length-1] == 'South')||(nameoslPieces[nameoslPieces.length-1] == 'East')||(nameoslPieces[nameoslPieces.length-1] == 'West'))
      {
         dirSuffix = ' ' + nameoslPieces[nameoslPieces.length-1][0];
         for(let loop=0;loop<nameoslPieces.length-1;loop++) 
         {
            namePrefix += (nameoslPieces[loop] + ' ');
         }
      }
      else
      {
         for(let loop=0;loop<nameoslPieces.length;loop++) 
         {
            namePrefix += (nameoslPieces[loop] + ' ');
         }
      }
      namePrefix = namePrefix.trimRight(1);

      if(debugOutput === true) console.log(oslName);
      // replace road type with abbreviated form
      for(let pass=0;pass<2;pass++)
      {
         for(let loop=0;loop<oslNameAbbreviations.length;loop+=2)
         {
            let abbrPos = namePrefix.lastIndexOf(oslNameAbbreviations[loop]);
            let abbrLen = oslNameAbbreviations[loop].length;
            let npLength = namePrefix.length;
            let npRemaining = npLength - abbrPos;
            if(debugOutput === true) console.log(pass,' ',oslNameAbbreviations[loop],' ',abbrPos,' ',abbrLen,' ',npLength,' ',npRemaining);
            if(abbrPos != -1)
            {
               // make sure the road type we've found comes firstly at the end of the name string, or is suffixed with a space
               // if there's a non-road type at the end of the string (e.g. High Road Eastcote)
               // isn't, then we've actually found a type match within a longer string segment (e.g. The Parkside) and so we
               // should leave it alone...
               if
               (
                  ((pass === 0) && (npRemaining == abbrLen)) ||
                  ((pass == 1) && (namePrefix[abbrPos+abbrLen] == ' '))
               )
               {
                  let preName = namePrefix.substr(0,abbrPos);
                  if((preName.length >= 4) && (preName.lastIndexOf("The") != (preName.length - 4)))
                  {
                     let theName = namePrefix.substr(abbrPos);
                     theName = theName.replace(oslNameAbbreviations[loop],oslNameAbbreviations[loop+1]);
                     wazeName = preName + theName + dirSuffix;
                     return wazeName;
                  }
               }
            }
         }
      }
      wazeName = namePrefix + dirSuffix;
   }
   return wazeName;
}
function oslCPDistance(cpE, cpN, posE, posN)
{
   return Math.round(Math.sqrt(((posE - cpE) * (posE - cpE)) + ((posN - cpN) * (posN - cpN))));
}
function oslGetBBCornerPixels(boxW, boxE, boxS, boxN)
{
   let lonlat_sw = new OpenLayers.LonLat(boxW,boxS);
   let lonlat_se = new OpenLayers.LonLat(boxE,boxS);
   let lonlat_nw = new OpenLayers.LonLat(boxW,boxN);
   let lonlat_ne = new OpenLayers.LonLat(boxE,boxN);

   let pix_sw = W.map.getPixelFromLonLat(lonlat_sw);
   let pix_se = W.map.getPixelFromLonLat(lonlat_se);
   let pix_ne = W.map.getPixelFromLonLat(lonlat_ne);
   let pix_nw = W.map.getPixelFromLonLat(lonlat_nw);

   boxE = (pix_ne.x + pix_se.x) / 2;
   boxW = (pix_nw.x + pix_sw.x) / 2;
   boxN = (pix_ne.y + pix_nw.y) / 2;
   boxS = (pix_se.y + pix_sw.y) / 2;

   let boxToleranceWidth = ((boxE - boxW) * 0.05);
   let boxToleranceHeight = ((boxS - boxN) * 0.05);

   boxW -= boxToleranceWidth;
   boxE += boxToleranceWidth;
   boxS += boxToleranceHeight;
   boxN -= boxToleranceHeight;

   boxE = Math.round(boxE);
   boxW = Math.round(boxW);
   boxS = Math.round(boxS);
   boxN = Math.round(boxN);

   // extend width/height of box if the calculated dimension is too small for the box to be readily visible
   if(boxE-boxW < 20)
   {
      boxE += 10;
      boxW -= 10;
   }
   if(boxS-boxN < 20)
   {
      boxS += 10;
      boxN -= 10;
   }

   return [boxW, boxE, boxS, boxN];
}
function oslVisualiseBoundingBox(boxW, boxE, boxS, boxN, mode)
{
   if(oslOSLDiv.style.height == '0px')
   {
      oslBBDiv.innerHTML = '';
      return;
   }

   let boxPos = [boxW, boxE, boxS, boxN];

   if((mode == OSL_BBMODE.Match) || (mode == OSL_BBMODE.Other))
   {
      boxPos = oslGetBBCornerPixels(boxW, boxE, boxS, boxN);
   }

   if(mode === OSL_BBMODE.Init)
   {
      oslBBDivInnerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="'+document.getElementById('WazeMap').offsetWidth+'px" height="'+document.getElementById('WazeMap').offsetHeight+'px" version="1.1">';
   }
   else if(mode == OSL_BBMODE.Match)
   {
      oslBBDivInnerHTML += '<rect x="'+boxPos[0]+'" y="'+boxPos[3]+'" width="'+(boxPos[1]-boxPos[0])+'" height="'+(boxPos[2]-boxPos[3])+'" style="fill:yellow;stroke:pink;stroke-width:4;fill-opacity:0.25;stroke-opacity:0.25"/>';
   }
   else if(mode == OSL_BBMODE.Other)
   {
      oslBBDivInnerHTML += '<rect x="'+boxPos[0]+'" y="'+boxPos[3]+'" width="'+(boxPos[1]-boxPos[0])+'" height="'+(boxPos[2]-boxPos[3])+'" style="fill:lightgrey;stroke:grey;stroke-width:4;fill-opacity:0.25;stroke-opacity:0.25"/>';
   }
   else if(mode == OSL_BBMODE.Finalise)
   {
      oslBBDivInnerHTML += '</svg>';
      oslBBDiv.innerHTML = oslBBDivInnerHTML;
   }
}
function oslMergeGazetteerData()
{
   if(typeof(gazetteerData) == "undefined") return false;
   if(oslMergeGazData)
   {
      // We no longer need to inject the gazetteer data as two seperate arrays and then merge them here, 
      // but we do still need to create a local reference to the injected array data, as trying to access
      // it directly as gazetteerData[] throws errors...
      oslGazetteerData = gazetteerData;
      oslMergeGazData = false;
      for(let idx=0;idx<oslGazetteerData.length;idx++)
      {
         oslGazetteerData[idx] = oslSaintsPreserveUs(oslGazetteerData[idx]);
      }
      oslAddLog('gazetteer data loaded, '+oslGazetteerData.length+' entries');
   }
   return true;
}
function oslGetTextWidth(text)
{
   // from https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
   // re-use canvas object for better performance
   let canvas = oslGetTextWidth.canvas || (oslGetTextWidth.canvas = document.createElement("canvas"));
   let context = canvas.getContext("2d");
   context.font = 'bold '+oslNameTextPts+' arial';
   let metrics = context.measureText(text);
   return metrics.width;
}
function oslGetVisibleCityNames()
{
   if(oslMergeGazetteerData() === false) return;

   oslNamesDivInnerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="'+document.getElementById('WazeMap').offsetWidth+'px" height="'+document.getElementById('WazeMap').offsetHeight+'px" version="1.1">';
   if(document.getElementById('_cbGazTagsEnabled').checked === true)
   {
      let showCities = document.getElementById('_cbGazTagsCity').checked;
      let showTowns = document.getElementById('_cbGazTagsTown').checked;
      let showVillages = document.getElementById('_cbGazTagsVillage').checked;
      let showHamlets = document.getElementById('_cbGazTagsHamlet').checked;
      let showOthers = document.getElementById('_cbGazTagsOther').checked;

      for(let idx=0;idx<oslGazetteerData.length;idx++)
      {
         let gazElements = oslGazetteerData[idx].split(':');
         let cnType = gazElements[GAZ_ELM.Type];

         let showTag;
         let tagColour;
         if(cnType == 'C') 
         {
            showTag = showCities;
            tagColour = "#FFFF00";
         }
         else if(cnType == 'T') 
         {
            showTag = showTowns;
            tagColour = "#FF00FF";
         }
         else if(cnType == 'V') 
         {
            showTag = showVillages;
            tagColour = "#FF8080";
         }
         else if(cnType == 'H') 
         {
            showTag = showHamlets;
            tagColour = "#A0A0FF";
         }
         else 
         {
            showTag = showOthers;
            tagColour = "#C0C0C0";
         }

         if(showTag === true)
         {
            let cnEastings = gazElements[GAZ_ELM.CEast];
            let cnNorthings = gazElements[GAZ_ELM.CNorth];
            if
            (
               ((oslVPLeft < cnEastings) && (oslVPRight > cnEastings)) &&
               ((oslVPTop > cnNorthings) && (oslVPBottom < cnNorthings))
            )
            {
               let pix = oslOSGridRefToPixel(cnEastings,cnNorthings);
               let nameLength = oslGetTextWidth(gazElements[GAZ_ELM.Name]);

               oslNamesDivInnerHTML += '<polygon points="';
               oslNamesDivInnerHTML += pix.x+','+pix.y+' ';
               oslNamesDivInnerHTML += (pix.x+oslNameHalfPix)+','+(pix.y-oslNameHalfPix)+' ';
               oslNamesDivInnerHTML += (pix.x+nameLength+oslNameHalfPix+8)+','+(pix.y-oslNameHalfPix)+' ';
               oslNamesDivInnerHTML += (pix.x+nameLength+oslNameHalfPix+8)+','+(pix.y+oslNameHalfPix)+' ';
               oslNamesDivInnerHTML += (pix.x+oslNameHalfPix)+','+(pix.y+oslNameHalfPix)+'" ';
               oslNamesDivInnerHTML += 'fill="'+tagColour+'" fill-opacity="0.75" stroke="black" stroke-opacity="0.75" />';
               
               oslNamesDivInnerHTML += '<text x="'+(pix.x+oslNameHalfPix)+'" y="'+(pix.y+(oslNameHalfPix/2))+'" ';
               oslNamesDivInnerHTML += 'style="font-family:sans-serif;font-size:'+oslNameTextPts+';font-weight:600;fill:black;';
               oslNamesDivInnerHTML += 'fill-opacity:1">'+gazElements[GAZ_ELM.Name]+'</text>';         
            }
         }
      }
   }
   oslNamesDivInnerHTML += '</svg>';
   oslNamesDiv.innerHTML = oslNamesDivInnerHTML;
}
function oslGetNearbyCityNames()
{
   if(oslMergeGazetteerData() === false) return;

   let names = [];
   for(var idx=0;idx<oslGazetteerData.length;idx++)
   {
      let gazElements = oslGazetteerData[idx].split(':');
      let cnEastings = gazElements[GAZ_ELM.CEast];
      let cnNorthings = gazElements[GAZ_ELM.CNorth];
      if((Math.abs(cnNorthings-oslNorthings) <= 5000)&&(Math.abs(cnEastings-oslEastings) <= 5000))
      {
         let dist = oslCPDistance(cnEastings,cnNorthings,oslEastings,oslNorthings);
         if(dist <= 5000)
         {
            names.push((dist * 1000000) + idx);
         }
      }
   }
   if(names.length > 1) names.sort(function(a,b){return a-b;});

   let cityInTopTen = false;
   let matchedOSName = false;
   let matchedIdx = -1;
   let listLength = names.length;
   if(listLength > 10) listLength = 10;

   let listOpt;
   let gElements;
   let gDist;

   let cityName;

   let oOCN = document.getElementById('oslOSCityNames');
   for(idx=0;idx<listLength;idx++)
   {
      gElements = oslGazetteerData[names[idx] % 1000000].split(':');
      gDist = (Math.round(names[idx] / 100000000)/10);

      // Build namestring for entry in the drop-down list - start with the placename :-)
      listOpt = document.createElement('option');
      cityName = gElements[GAZ_ELM.Name];
      listOpt.text = cityName;

      // if the name is neither a city nor unique, append a (county) suffix
      if(gElements[GAZ_ELM.Type] == 'C') cityInTopTen = true;
      else
      {
         if(oslCheckCityNameDuplicates(gElements[GAZ_ELM.Name],1) > 1)
         {
            listOpt.text += ', '+gElements[GAZ_ELM.Area];
         }
      }

      if(sessionStorage.cityNameRB == 'optUseOS')
      {
         if(cityName == sessionStorage.myCity)
         {
            matchedOSName = true;
            matchedIdx = idx;
         }
      }

      // Add place type and distance in [] brackets to allow easy removal later...
      if(gElements[GAZ_ELM.Type] == 'C') listOpt.text += ' [City, ';
      else if(gElements[GAZ_ELM.Type] == 'T') listOpt.text += ' [Town, ';
      else if(gElements[GAZ_ELM.Type] == 'V') listOpt.text += ' [Village, ';
      else if(gElements[GAZ_ELM.Type] == 'H') listOpt.text += ' [Hamlet, ';
      else listOpt.text += ' [Other, ';
      listOpt.text += gDist + 'km]';
      oOCN.add(listOpt,null);oslOSCityNames
   }

   if((!cityInTopTen) && (names.length > 10))
   {
      idx = 10;
      while((idx < names.length) && (!cityInTopTen))
      {
         gElements = oslGazetteerData[names[idx] % 1000000].split(':');
         if(gElements[GAZ_ELM.Type] == 'C')
         {
            cityInTopTen = true;
            gDist = ' [City, '+(Math.round(names[idx] / 100000000)/10)+'km]';
            listOpt = document.createElement('option');
            listOpt.text = gElements[GAZ_ELM.Name]+gDist;
            oOCN.add(listOpt,null);
            if(sessionStorage.cityNameRB == 'optUseOS')
            {
               if(gElements[GAZ_ELM.Name] == sessionStorage.myCity)
               {
                  matchedOSName = true;
                  matchedIdx = 10;
                  break;
               }
            }
         }
         idx++;
      }
   }

   if(matchedOSName === true) oOCN.options.selectedIndex = matchedIdx;

   if((sessionStorage.cityNameRB == 'optUseOS') && (matchedOSName === false))
   {
      oslAddLog('Selected city name no longer in nearby OS list...');
      alert('City name no longer present in nearby OS data, please reselect');
      sessionStorage.cityNameRB = 'optUseExisting';
      document.getElementById('optUseExisting').checked = true;
   }
}
function oslCheckCityNameDuplicates(cityName, mode)
{
   if(oslMergeGazetteerData() === false) return;

   let cnCount = 0;
   let searchDist = Math.round(oslGazetteerData.length/2);
   let searchIdx = searchDist;
   let hasCounty = false;

   let debugOutput = false;


   // remove county suffix from actual city name string if present
   if(cityName.indexOf('(') != -1)
   {
      cityName = cityName.substr(0,cityName.indexOf('('));
      cityName = cityName.replace(/^\s+|\s+$/g, "");
      hasCounty = true;
   }
   // remove script-appended county suffix from city name held in drop down if present
   if(cityName.indexOf(',') != -1)
   {
      cityName = cityName.substr(0,cityName.indexOf(','));
      cityName = cityName.replace(/^\s+|\s+$/g, "");
   }

   cityName = cityName.toLowerCase();
   cityName = cityName.replace(/-/g, ' ');
   let gazName = '';

   if(debugOutput === true) console.log('scan for duplicates of '+cityName);

   var gazElements = [];
   while((searchDist > 1) && (cityName.localeCompare(gazName) !== 0))
   {
      searchDist = Math.round(searchDist/2);
      gazElements = oslGazetteerData[searchIdx].split(':');
      gazName = gazElements[GAZ_ELM.Name].toLowerCase();
      gazName = gazName.replace(/-/g, ' ');
      if(debugOutput === true) console.log('a: '+searchDist+' '+searchIdx+' '+gazName);
      if(cityName.localeCompare(gazName) > 0) searchIdx += searchDist;
      else if(cityName.localeCompare(gazName) < 0) searchIdx -= searchDist;
      if(searchIdx >= oslGazetteerData.length) searchIdx = oslGazetteerData.length-1;
      if(searchIdx < 0) searchIdx = 0;
   }
   gazElements = oslGazetteerData[searchIdx].split(':');
   gazName = gazElements[GAZ_ELM.Name].toLowerCase();
   gazName = gazName.replace(/-/g, ' ');
   while((searchIdx > 0) && (cityName.localeCompare(gazName) == 0))
   {
      gazElements = oslGazetteerData[--searchIdx].split(':');
      gazName = gazElements[GAZ_ELM.Name].toLowerCase();
      gazName = gazName.replace(/-/g, ' ');
      if(debugOutput === true) console.log('b: '+(searchIdx)+' '+gazName);
   }
   ++searchIdx;
   gazElements = oslGazetteerData[searchIdx].split(':');
   gazName = gazElements[GAZ_ELM.Name].toLowerCase();
   gazName = gazName.replace(/-/g, ' ');
   while((searchIdx < (oslGazetteerData.length - 1)) && (cityName.localeCompare(gazName) > 0))
   {
      ++searchIdx;
      try
      {
         gazElements = oslGazetteerData[searchIdx].split(':');
         gazName = gazElements[GAZ_ELM.Name].toLowerCase();
         gazName = gazName.replace(/-/g, ' ');
         if(debugOutput === true) console.log('c: '+(searchIdx)+' '+gazName);
      }
      catch
      {
         console.debug("ERROR THROWN - "+oslGazetteerData[searchIdx]);
      }
   }
   while((cityName.localeCompare(gazName) === 0) && (searchIdx < oslGazetteerData.length))
   {
      cnCount++;
      gazElements = oslGazetteerData[++searchIdx].split(':');
      gazName = gazElements[GAZ_ELM.Name].toLowerCase();
      gazName = gazName.replace(/-/g, ' ');
      if(debugOutput === true) console.log('d: '+(searchIdx)+' '+gazName+' '+cnCount);
   }

   if(mode === 0)
   {
      let newHTML = '';
      if(cnCount === 0) newHTML = '&nbsp;&nbsp;Place name is not in OS data';
      else if(cnCount == 1)
      {
         newHTML = '&nbsp;&nbsp;Place name is unique';
         if(hasCounty) newHTML += '<br>&nbsp;&nbsp;<i>(County) suffix not required</i>';
      }
      else
      {
         newHTML = '&nbsp;&nbsp;Place name is not unique';
      }
      document.getElementById('oslCNInfo').innerHTML = newHTML;
   }
   else return cnCount;
}
function oslHighlightAdjacentSameNameSegments(ldEastings, ldNorthings, ldIgnoreIdx, srcElements)
{
   ldNorthings -= oslLocatorBlockSize;
   ldEastings -= oslLocatorBlockSize;
   for(let x = 0; x < 3; ++x)
   {
      for(let y = 0; y < 3; ++y)
      {
         let arrayName = 'locatorData_'+(ldEastings + (x * oslLocatorBlockSize))+'_'+(ldNorthings + (y * oslLocatorBlockSize));
         oslEvalString = arrayName;
         if(typeof unsafeWindow[arrayName] != "undefined")
         {
            oslBlockData = unsafeWindow[arrayName];
            for(let loop = 0; loop < oslBlockData.length; ++loop)
            {
               if(loop != ldIgnoreIdx)
               {
                  var locatorElements = oslSplitEntry(oslBlockData[loop]);
                  if
                  (
                     (locatorElements[OSL_ELM.RoadName] == srcElements[OSL_ELM.RoadName]) &&
                     (locatorElements[OSL_ELM.RoadNumber] == srcElements[OSL_ELM.RoadNumber]) &&
                     (locatorElements[OSL_ELM.AreaName] == srcElements[OSL_ELM.AreaName])
                  )
                  {
                     oslVisualiseBoundingBox(locatorElements[OSL_ELM.BoundW],locatorElements[OSL_ELM.BoundE],locatorElements[OSL_ELM.BoundS],locatorElements[OSL_ELM.BoundN],OSL_BBMODE.Other);
                  }
               }
            }
         }
      }
   }
}
function oslRadioClick()
{
   let oslElements = document.getElementById('oslRoadNameMatches');
   let selectedName = '';
   let arrayName;

   for(let loop=0;loop<oslElements.childNodes.length;loop++)
   {
      if(oslElements.childNodes[loop].nodeType == 1)
      {
         let tagname = oslElements.childNodes[loop].tagName;
         if(tagname !== null)
         {
            if(tagname == "LABEL")
            {
               if(oslElements.childNodes[loop].childNodes[0].checked)
               {
                  let attr = oslElements.childNodes[loop].childNodes[0].attributes.getNamedItem("id").value;
                  if((attr.indexOf('oslID_') === 0) || (attr.indexOf('alt-oslID_') === 0))
                  {
                     let roadData = '';
                     let oslID = attr.split('_');
                     if(oslID[1] != 'null')
                     {
                        arrayName = 'locatorData_'+oslID[1]+'_'+oslID[2];
                        if(typeof unsafeWindow[arrayName] != "undefined")
                        {                        
                           roadData = unsafeWindow[arrayName][oslID[3]];
                        }
                     }
                     if(roadData == '')
                     {
                        roadData = "null:null";
                     }

                     let locatorElements = oslSplitEntry(roadData);

                     if(locatorElements[OSL_ELM.RoadName] != 'null')
                     {
                        selectedName = locatorElements[OSL_ELM.RoadName]+locatorElements[OSL_ELM.RoadNumber];
                        oslVisualiseBoundingBox(0,0,0,0,OSL_BBMODE.Init);
                        oslVisualiseBoundingBox(locatorElements[OSL_ELM.BoundW],locatorElements[OSL_ELM.BoundE],locatorElements[OSL_ELM.BoundS],locatorElements[OSL_ELM.BoundN],OSL_BBMODE.Match);

                        oslHighlightAdjacentSameNameSegments(oslID[1], oslID[2], oslID[3], locatorElements);
                     }
                     else
                     {
                        oslBBDiv.innerHTML = '';
                     }
                  }
               }
            }
         }
      }
   }

   if(selectedName === '')
   {
      oslBBDiv.innerHTML = '';
      return;
   }
   oslVisualiseBoundingBox(0,0,0,0,OSL_BBMODE.Finalise);
}
function oslClick()
{
   oslCityName = '';
   oslCountyName = '';
   oslUsingNewName = false;
   if(document.getElementById('optUseNewManual').checked)
   {
      oslCityName = oslCaseCorrect(document.getElementById('myCityName').value);
      oslUsingNewName = true;
   }
   else if(document.getElementById('optUseExistingWME').checked)
   {
      let oWCN = document.getElementById('oslWMECityNames');
      oslCityName = oWCN.options[oWCN.options.selectedIndex].text;
      if(oslCityName.indexOf(', ') !== -1)
      {
         oslCountyName = oslCityName.split(', ')[1];
         oslCityName = oslCityName.split(', ')[0];
      }
      oslUsingNewName = true;
   }
   else if(document.getElementById('optUseOS').checked)
   {
      let oOCN = document.getElementById('oslOSCityNames');
      oslCityName = oOCN.options[oOCN.options.selectedIndex].text;
      oslCityName = oslCityName.substring(0,oslCityName.indexOf('[')-1);
      if(oslCityName.indexOf(', ') !== -1)
      {
         oslCountyName = oslCityName.split(', ')[1];
         oslCityName = oslCityName.split(', ')[0];
      }
      oslUsingNewName = true;
   }
   if(sessionStorage.myCity === '')
   {
      oslAddLog('Update city name position at '+oslEastings+'x'+oslNorthings);
      sessionStorage.cityChangeEastings = oslEastings;
      sessionStorage.cityChangeNorthings = oslNorthings;
   }

   if(oslCountyName !== '')
   {
      sessionStorage.myCity = oslCityName+', '+oslCountyName;
   }
   else
   {
      sessionStorage.myCity = oslCityName;
   }

   oslUseName = false;
   if((oslCityName.length > 0) && oslUsingNewName)
   {
      oslCheckCityNameDuplicates(oslCityName,0);

      if(oslCityName != sessionStorage.prevCityName)
      {
         oslAddLog('Change of city name at '+oslEastings+'x'+oslNorthings);
         sessionStorage.cityChangeEastings = oslEastings;
         sessionStorage.cityChangeNorthings = oslNorthings;
         sessionStorage.prevCityName = oslCityName;
         oslUseName = true;
      }
      else
      {
         let nameChangeDist = oslCPDistance(oslEastings,oslNorthings,sessionStorage.cityChangeEastings,sessionStorage.cityChangeNorthings);
         oslAddLog('Current name was set '+nameChangeDist+'m away from segment location');
         if(nameChangeDist > 1000)
         {
            oslAddLog('Distance exceeds 1km threshold, name verification required...');
            if(confirm('Confirm continued use of this city name'))
            {
               oslAddLog('Confirm city name at '+oslEastings+'x'+oslNorthings);
               sessionStorage.cityChangeEastings = oslEastings;
               sessionStorage.cityChangeNorthings = oslNorthings;
               oslUseName = true;
            }
         }
         else
         {
            oslUseName = true;
         }
      }
   }

   let oslElements = document.getElementById('oslRoadNameMatches');
   let arrayName;
   for(let loop=0;loop<oslElements.childNodes.length;loop++)
   {
      if(oslElements.childNodes[loop].nodeType == 1)
      {
         let tagname = oslElements.childNodes[loop].tagName;
         if(tagname !== null)
         {
            if(tagname == "LABEL")
            {
               let rbElement = oslElements.childNodes[loop].childNodes[0];
               if((rbElement.name == "oslChoice") && (rbElement.checked))
               {
                  let attr = rbElement.attributes.getNamedItem("id").value;
                  let useMain = (attr.indexOf('oslID_') === 0);
                  oslUseAlt = (attr.indexOf('alt-oslID_') === 0);
                  if((useMain === true) || (oslUseAlt === true))
                  {
                     let roadData = '';
                     let oslID = attr.split('_');
                     if(oslID[1] != 'null')
                     {
                        arrayName = 'locatorData_'+oslID[1]+'_'+oslID[2];
                        if(typeof unsafeWindow[arrayName] != "undefined")
                        {
                           roadData = unsafeWindow[arrayName][oslID[3]];
                        }
                     }
                     
                     if(roadData == '')
                     {
                        console.debug(arrayName + "not found");
                        for(let i = 0; i < OSL_ELM.MAX; ++i)
                        {
                           roadData += ':';
                        }
                     }
                     oslLocatorElements = oslSplitEntry(roadData);

                     oslClearAltNames();
                     oslApplyPrimaryStreetName();
                     oslApplyAltStreetName();
                  }
               }
            }
         }
      }
   }
}
function oslApplyAltStreetName()
{
   if(oslUseAlt === true)
   {
      let tName = oslLocatorElements[OSL_ELM.RoadName];
      oslLocatorElements[OSL_ELM.RoadName] = oslLocatorElements[OSL_ELM.AltName];
      oslLocatorElements[OSL_ELM.AltName] = tName;
   }
   if(oslLocatorElements[OSL_ELM.AltName] === '')
   {
      // No alt name to apply, so exit...
      return;
   }

   // Open the altname edit UI
   if(document.querySelector('.alt-city-name') === null)
   {
      document.querySelector('.add-alt-street-btn').click();
      window.setTimeout(oslApplyAltStreetName, 10);
      return;
   }

   let altName = oslLocatorElements[OSL_ELM.RoadNumber];
   if((oslLocatorElements[OSL_ELM.RoadName].length > 0)&&(oslLocatorElements[OSL_ELM.RoadNumber].length > 0)) altName += ' - ';   
   altName += oslWazeifyStreetName(oslLocatorElements[OSL_ELM.AltName], false);

   oslInitReactUpdates();
   oslUpdateReactTextInput('.alt-street-name', altName);
   if(oslUsingNewName)
   {
      if(oslUseName)
      {
         oslUpdateReactTextInput('.alt-city-name', oslCityName);
      }
   }
   else if(document.getElementById('optClearExisting').checked === true)
   {
      oslUpdateReactTextInput('.alt-city-name', '');
   }
   document.querySelector('.alt-streets-form').querySelector('.save-button').click();
}

let oslPElm;
let oslIElm;
let oslReqValue;
function oslInitReactUpdates()
{
   oslPElm = [];
   oslIElm = [];
   oslReqValue = [];
}
function oslChangeReactTextInput()
{
   let allDone = true;
   for(let i = 0; i < oslPElm.length; ++i)
   {
      if(oslIElm[i].value !== oslReqValue[i])
      {
         allDone = false;
         oslPElm[i].dispatchEvent(new Event('focus'));
         oslPElm[i].value = "";
         oslPElm[i].dispatchEvent(new Event('focus'));
         oslPElm[i].dispatchEvent(new Event('input'));
         oslIElm[i].dispatchEvent(new Event('focus'));
         oslIElm[i].value = oslReqValue[i];
         oslIElm[i].dispatchEvent(new Event('focus'));
         oslIElm[i].dispatchEvent(new Event('input'));
         oslIElm[i].dispatchEvent(new Event('blur'));
         oslIElm[i].dispatchEvent(new Event('blur'));
      }
   }
   if(allDone === false)
   {
      window.setTimeout(oslChangeReactTextInput, 100);
   }
}
function oslUpdateReactTextInput(parent, value)
{
   let pElm = document.querySelector(parent);
   if(pElm !== null)
   {
      let iElm = pElm.shadowRoot.querySelector('#text-input');
      if(iElm !== null)
      {
         oslPElm.push(pElm);
         oslIElm.push(iElm);
         oslReqValue.push(value);
         oslChangeReactTextInput();
      }
   }
}
function oslCheckForAddressEditUI()
{
   let retval = false;
   let ui = document.querySelector('#segment-edit-general .address-form');
   if(ui !== null)
   {
      retval = true;
      retval &&= (ui.querySelector('.street-name-row') !== null);
      retval &&= (ui.querySelector('.city-name') !== null);
      retval &&= (ui.querySelector('.state-id') !== null);
      retval &&= (ui.querySelector('.country-id') !== null);
      retval &&= (ui.querySelector('.action-buttons') !== null);
      retval &&= (ui.getBoundingClientRect().height != 0);
   }
   return retval;
}
function oslClearAltNames()
{
   // Remove any alternate names on a segment
   let n = document.querySelectorAll('.alt-street-delete').length;
   while(n)
   {
      --n;
      document.querySelectorAll('.alt-street-delete')[n].click();
   }
}
function oslApplyPrimaryStreetName()
{
   if(document.querySelector('#segment-edit-general .full-address') !== null)
   {
      document.querySelector('#segment-edit-general .full-address').click();
   }
   if(oslCheckForAddressEditUI() === false)
   {
      window.setTimeout(oslApplyPrimaryStreetName, 10);
      return;
   }


   // Update the Street field
   let oslName = oslLocatorElements[OSL_ELM.RoadNumber];
   if((oslLocatorElements[OSL_ELM.RoadName].length > 0)&&(oslLocatorElements[OSL_ELM.RoadNumber].length > 0)) 
   {
      oslName += ' - ';
   }
   if((oslName.length > 0)||(oslLocatorElements[OSL_ELM.RoadName].length > 0))
   {
      oslName += oslWazeifyStreetName(oslLocatorElements[OSL_ELM.RoadName], false);
      oslPrevStreetName = oslName;
   }
   else
   {
      oslName = "";
   }
   oslInitReactUpdates();
   oslUpdateReactTextInput('#segment-edit-general .street-name', oslName);

   // Update the City field
   if(oslUsingNewName)
   {
      if(document.getElementById('optUseNewManual').checked === true)
      {
         sessionStorage.cityNameRB = 'optUseNewManual';
      }
      else if(document.getElementById('optUseExistingWME').checked === true)
      {
         sessionStorage.cityNameRB = 'optUseExistingWME';
      }
      else if(document.getElementById('optUseOS').checked === true)
      {
         sessionStorage.cityNameRB = 'optUseOS';
      }
      if(oslUseName)
      {
         oslUpdateReactTextInput('#segment-edit-general .city-name', oslCityName);
      }
   }
   else if(document.getElementById('optClearExisting').checked === true)
   {
      sessionStorage.cityNameRB = 'optClearExisting';
      oslUpdateReactTextInput('#segment-edit-general .city-name', "");
   }
   else
   {
      sessionStorage.cityNameRB = 'optUseExisting';
   }

   if(oslAdvancedMode)
   {
      // Auto-click the Apply button if the user has a suitably high editing rank
      document.querySelector('.address-form').querySelector('.save-button').click();
   }
}
function oslMatch(oslLink, oslArea, oslRadioID, oslAltRadioID)
{
   this.oslLink = oslLink;
   this.oslArea = oslArea;
   this.oslRadioID = oslRadioID;
   this.oslAltRadioID = oslAltRadioID;
}
function oslSortCandidates(a,b)
{
   let x = a.oslArea;
   let y = b.oslArea;
   return((x<y) ? -1 : ((x>y) ? 1 : 0));
}
function oslCityNameKeyup()
{
   oslCheckCityNameDuplicates(oslCaseCorrect(document.getElementById('myCityName').value),0);
   document.getElementById('optUseNewManual').checked = true;
}
function oslSelectWMEName()
{
   let oWCN = document.getElementById('oslWMECityNames');
   let cityName = oWCN.options[oWCN.options.selectedIndex].text;
   oslCheckCityNameDuplicates(cityName,0);
   document.getElementById('optUseExistingWME').checked = true;
}
function oslSelectOSName()
{
   let oOCN = document.getElementById('oslOSCityNames');
   let cityName = oOCN.options[oOCN.options.selectedIndex].text;
   cityName = cityName.substring(0,cityName.indexOf('[')-1);
   document.getElementById('optUseOS').checked = true;
   document.getElementById('oslCNInfo').innerHTML = '';
}
function oslBlockCacheObj(blockName)
{
   this.blockName = blockName;
   this.lastAccessed = Math.floor(new Date().getTime() / 1000);
}
function oslLoadBlocks()
{
   for(let i = 0; i < oslBlocksToLoad.length;)
   {
      // inject block data
      let script = document.createElement("script");
      script.setAttribute('type','text/javascript');
      script.setAttribute('charset','UTF-8');
      script.src = oslBlocksToLoad[i];
      document.head.appendChild(script);
      oslLoadingMsg = true;
      oslBlockCacheList.push(new oslBlockCacheObj(oslBlocksToLoad[i+1]));
      i += 2;
   }
}
function oslSplitEntry(entryString)
{
   let elements = entryString.split(":");
   if(entryString != "null:null")
   {
      let ts=elements[OSL_ELM.Geometry].split(/[ ]+/).map(Number);
      let tsLon=[];
      let tsLat=[];
      ts.forEach((a,i) => {(i % 2 === 0) ? tsLon.push(a) : tsLat.push(a);});
      elements[OSL_ELM.BoundE] = Math.max(...tsLon);
      elements[OSL_ELM.BoundW] = Math.min(...tsLon);
      elements[OSL_ELM.BoundN] = Math.max(...tsLat);
      elements[OSL_ELM.BoundS] = Math.min(...tsLat);
   }

   return elements;
}
function oslToOSGrid(lat, lon, mode)
{
   if(oslInUK === false) return;

   if(oslMousePixelpos == null) return;

   // Correct mouse X pos if map viewport width has changed (e.g. when clicking on segment causing sidepanel to open...)
   let vpWidthDelta = (oslLastViewportWidth - W.map.getViewport().getBoundingClientRect().width);
   oslLastViewportWidth = W.map.getViewport().getBoundingClientRect().width;
   oslMousePixelpos.x -= vpWidthDelta;

   if((lat !== 0) && (mode != OSL_MODE.OpenRoads))
   {
      if(mode == OSL_MODE.NameCheck)
      {
         oslEastings = lat;
         oslNorthings = lon;
      }
      else
      {
         oslLatitude = lat;
         oslLongitude = lon;
         oslWGStoOSGB();
      }
   }

   if((mode == OSL_MODE.OpenNames) || (mode == OSL_MODE.NameCheck))
   {
      let eBlock;
      let nBlock;

      // determine which grid block contains the current mouse position
      let eBlock_point = (Math.floor(oslEastings/oslLocatorBlockSize)) * oslLocatorBlockSize;
      let nBlock_point = (Math.floor(oslNorthings/oslLocatorBlockSize)) * oslLocatorBlockSize;
      let candidates = [];
      oslBlocksToLoad = [];
      let arrayName;
      let oslEvalString;

      for(let x = -1; x < 2; x++)
      {
         for(let y = -1; y < 2; y++)
         {
            eBlock = (eBlock_point + (oslLocatorBlockSize * x));
            nBlock = (nBlock_point + (oslLocatorBlockSize * y));
            // check we're within the outer bounds of the current OS dataset...
            if
            (
               (eBlock >= 64000) &&
               (eBlock <= 655999) &&
               (nBlock >= 8000) &&
               (nBlock <= 1214999)
            )
            {
               // check to see if there's a corresponding array already loaded...
               arrayName = 'locatorData_'+eBlock+'_'+nBlock;
               if(typeof unsafeWindow[arrayName] == "undefined")
               {
                  // create a blank placeholder which will get replaced by the actual data if the array is present on the server...
                  unsafeWindow[arrayName] = [];
                  
                  oslBlocksToLoad.push(oslBlockPath+Math.floor(eBlock / 100000)+'/'+Math.floor(nBlock / 100000)+'/'+arrayName+'.user.js');
                  oslBlocksToLoad.push(arrayName);
               }
            }
         }
      }

      if(oslBlocksToLoad.length > 0)
      {
         oslLoadBlocks();
      }

      candidates = [];
      if(mode == OSL_MODE.OpenNames) candidates[candidates.length++] = new oslMatch('<label style="display:inline;"><input type="radio" name="oslChoice" id="oslID_null_null_null" />Un-named segment</label><br>',1000000000000,'oslID_null_null_null',null);

      for(let x = -1; x < 2; x++)
      {
         for(let y = -1; y < 2; y++)
         {
            eBlock = (eBlock_point + (oslLocatorBlockSize * x));
            nBlock = (nBlock_point + (oslLocatorBlockSize * y));

            // check we're within the outer bounds of the current OS dataset...
            if ((eBlock >= 64000) && (eBlock <= 655999) && (nBlock >= 8000) && (nBlock <= 1214999))
            {
               arrayName = 'locatorData_'+eBlock+'_'+nBlock;
               oslEvalString = arrayName;
               if(typeof unsafeWindow[arrayName] != "undefined")
               {
                  oslLoadingMsg = false;
                  // yes...
                  if((eBlock != oslEvalEBlock) || (nBlock != oslEvalNBlock))
                  {
                     oslEvalEBlock = eBlock;
                     oslEvalNBlock = nBlock;
                     oslBlockData = unsafeWindow['locatorData_'+eBlock+'_'+nBlock];
                  }

                  for (let bcObj in oslBlockCacheList)
                  {
                     if(oslBlockCacheList[bcObj].blockName == arrayName)
                     {
                        oslBlockCacheList[bcObj].lastAccessed = Math.floor(new Date().getTime() / 1000);
                     }
                  }

                  let preselect = false;

                  for(let loop = 0;loop < oslBlockData.length; loop++)
                  {
                     let locatorElements = [];
                     // for each entry in the array, test the centrepoint position to see if it lies within the bounding box for that entry
                     // note that we allow a 30m tolerance on all sides of the box to allow for inaccuracies in the latlon->gridref conversion,
                     // and to increase the chance of a successful match when the road runs E-W or N-S and thus has a long but narrow bounding box

                     locatorElements = oslSplitEntry(oslBlockData[loop]);

                     if((locatorElements[OSL_ELM.RoadName].length > 0) || (locatorElements[OSL_ELM.RoadNumber].length > 0))
                     {
                        let tolE;
                        let tolN;
                        if(mode == OSL_MODE.NameCheck)
                        {
                           // wider tolerance when doing a namecheck lookup, to reduce falsely flagging roads as being mis-named
                           tolE = 20;
                           tolN = 30;
                        }
                        else
                        {
                           // tighter tolerance when doing other lookups
                           tolE = 10;
                           tolN = 10;
                        }

                        let streetName = '';
                        if(locatorElements[OSL_ELM.RoadNumber].length > 0)
                        {
                           streetName += locatorElements[OSL_ELM.RoadNumber];
                           if(locatorElements[OSL_ELM.RoadName].length > 0)
                           {
                              streetName += ' - ';
                           }
                        }
                        streetName += oslWazeifyStreetName(locatorElements[OSL_ELM.RoadName], false);

                        let altName = '';
                        if(locatorElements[OSL_ELM.AltName] != '')
                        {
                           if(locatorElements[OSL_ELM.RoadNumber].length > 0)
                           {
                              altName += locatorElements[OSL_ELM.RoadNumber];
                              if(locatorElements[OSL_ELM.AltName].length > 0)
                              {
                                 altName += ' - ';
                              }
                           }
                           altName += oslWazeifyStreetName(locatorElements[OSL_ELM.AltName], false);
                        }

                        if(mode == OSL_MODE.OpenNames)
                        {
                           // Name search from mouse position...
                           let bbPix = oslGetBBCornerPixels(parseFloat(locatorElements[OSL_ELM.BoundW]), parseFloat(locatorElements[OSL_ELM.BoundE]), parseFloat(locatorElements[OSL_ELM.BoundS]), parseFloat(locatorElements[OSL_ELM.BoundN]));
                           bbPix[0] -= tolE;
                           bbPix[1] += tolE;
                           bbPix[2] += tolN;
                           bbPix[3] -= tolN;

                           if((oslMousePixelpos.x >= bbPix[0])&&(oslMousePixelpos.x <= bbPix[1])&&(oslMousePixelpos.y <= bbPix[2])&&(oslMousePixelpos.y >= bbPix[3]))
                           {
                              let area = ((bbPix[1] - bbPix[0]) * (bbPix[2] - bbPix[3]));

                              let radioID = 'oslID_'+eBlock+'_'+nBlock+'_'+loop;
                              let altRadioID = null;
                              let oslLink = '<label style="display:inline;"><input type="radio" name="oslChoice" id="'+radioID+'"';
                              if((streetName == oslPrevStreetName)&&(preselect === false))
                              {
                                 oslLink += 'checked="true"';
                                 preselect = true;
                              }
                              oslLink += '/>';
                              oslLink += streetName+'&nbsp;&nbsp;[<i>'+locatorElements[OSL_ELM.AreaName]+'</i>]</label>';

                              if(altName != '')
                              {
                                 altRadioID = 'alt-'+radioID;
                                 oslLink += '<br>&nbsp;<label style="display:inline;"><input type="radio" name="oslChoice" id="'+altRadioID+'"';
                                 if((altName == oslPrevStreetName)&&(preselect === false))
                                 {
                                    oslLink += 'checked="true"';
                                    preselect = true;
                                 }
                                 oslLink += '/>';
                                 oslLink += '<i>Alt name: '+altName+'</i></label>';
                              }

                              if(locatorElements[OSL_ELM.Form] == 'Collapsed Dual Carriageway')
                              {
                                 locatorElements[OSL_ELM.Form] = 'Dual Carriageway';
                              }

                              oslLink += '<br>&nbsp;<i>'+oslRoadClassifications[locatorElements[OSL_ELM.Classification]] + ' - ';
                              oslLink += oslRoadFunctions[locatorElements[OSL_ELM.Function]] + ' - ' + oslFormsOfWay[locatorElements[OSL_ELM.Form]];
                              if(locatorElements[OSL_ELM.Structure] !== '')
                              {
                                 oslLink += ' ' + oslRoadStructures[locatorElements[OSL_ELM.Structure]];
                              }
                              if(locatorElements[OSL_ELM.IsPrimary] == 'Y')
                              {
                                 oslLink += ' (PRN)';
                              }
                              if(locatorElements[OSL_ELM.IsTrunk] == 'Y')
                              {
                                 oslLink += ' (TRN)';
                              }
                              oslLink += '</i><br>';
                              candidates[candidates.length++] = new oslMatch(oslLink,area,radioID,altRadioID);
                           }
                        }
                        else if(mode == OSL_MODE.NameCheck)
                        {
                           // NameCheck comparisons...
                           let bbW = parseFloat(locatorElements[OSL_ELM.BoundW]) - tolE;
                           let bbE = parseFloat(locatorElements[OSL_ELM.BoundE]) + tolE;
                           let bbS = parseFloat(locatorElements[OSL_ELM.BoundS]) - tolN;
                           let bbN = parseFloat(locatorElements[OSL_ELM.BoundN]) + tolN;

                           for(let i=0;i<oslOSLNCSegments.length;i++)
                           {
                              if(oslOSLNCSegments[i].match === false)
                              {
                                 if
                                    (
                                       ((oslOSLNCSegments[i].lonA >= bbW) && (oslOSLNCSegments[i].lonA <= bbE)) &&
                                       ((oslOSLNCSegments[i].lonB >= bbW) && (oslOSLNCSegments[i].lonB <= bbE)) &&
                                       ((oslOSLNCSegments[i].latA >= bbS) && (oslOSLNCSegments[i].latA <= bbN)) &&
                                       ((oslOSLNCSegments[i].latB >= bbS) && (oslOSLNCSegments[i].latB <= bbN))
                                    )
                                 {
                                    if((oslOSLNCSegments[i].streetname == streetName) || (oslOSLNCSegments[i].streetname == altName)) oslOSLNCSegments[i].match = true;
                                 }
                              }
                           }
                        }
                     }
                  }
               }
            }
         }
      }

      if(mode == OSL_MODE.OpenNames)
      {
         let newHTML = '<b>Matches at '+oslEastings+', '+oslNorthings+'</b>';

         if(candidates.length > 0)
         {
            let btnStyle = "cursor:pointer;";
            btnStyle += "font-size:14px;";
            btnStyle += "border: thin outset black;";
            btnStyle += "padding:2px 10px 2px 10px;";
            btnStyle += "background: #ccccff;";
            newHTML += '<div style="margin:10px;"><span id="oslSelect" style="'+btnStyle+'">';
            if(oslAdvancedMode) newHTML += 'Apply to Properties';
            else newHTML += 'Copy to Properties';
            newHTML += '</span></div>';
            if(candidates.length > 1) candidates.sort(oslSortCandidates);
            for(let loop=0;loop<candidates.length;loop++)
            {
               newHTML += candidates[loop].oslLink;
            }
            newHTML += '<br>City name:<br>';
            newHTML += '<label style="display:inline;"><input type="radio" name="oslCityNameOpt" id="optUseExisting"/>Use existing segment name(s)</label><br>';
            newHTML += '<label style="display:inline;"><input type="radio" name="oslCityNameOpt" id="optClearExisting" />Clear existing segment name(s)</label><br>';
            newHTML += '<label style="display:inline;"><input type="radio" name="oslCityNameOpt" id="optUseNewManual" />Use new name:</label><br>';
            newHTML += '&nbsp;&nbsp;<input id="myCityName" style="font-size:14px; line-height:16px; height:22px; margin-bottom:4px; transition:none; focus:none; box-shadow:none" type="text"';
            if(sessionStorage.cityNameRB == 'optUseNewManual') newHTML += 'value="'+sessionStorage.myCity+'"/><br>';
            else newHTML += 'value=""/><br>';
            newHTML += '<label style="display:inline;"><input type="radio" name="oslCityNameOpt" id="optUseExistingWME" />Use name from map:</label><br>';
            newHTML += '&nbsp;&nbsp;<select id="oslWMECityNames"></select><br>';
            newHTML += '<label style="display:inline;"><input type="radio" name="oslCityNameOpt" id="optUseOS" />Use name from OS Gazetteer:</label><br>';
            newHTML += '&nbsp;&nbsp;<select id="oslOSCityNames"></select><br>';
            newHTML += '<div id="oslCNInfo"></div><br>';
            oslRoadNameMatches.innerHTML = newHTML;
            oslGetNearbyCityNames();
            let oWCN = document.getElementById('oslWMECityNames');
            var nameList = [];
            let mc = W.map.getCenter();
            for(let cityObj in W.model.cities.objects)
            {
               if(W.model.cities.objects.hasOwnProperty(cityObj))
               {
                  let cityAttrs = W.model.cities.objects[cityObj].attributes;
                  if(cityAttrs.countryID === 234)
                  {
                     let cityname = cityAttrs.name;
                     if(cityname !== '')
                     {
                        // WME now pollutes W.model.cities.objects with a myriad of places entirely unconnected to
                        // anything in the current WME view, issue tracker list etc.  The countryID check above
                        // manages to at least filter out the obviously useless entries, however that still leaves
                        // all the "local" ones which may well be at t'other end of the country and thus local only
                        // insofar as they're within the UK somewhere...
                        //
                        // To therefore deal with this, and continue presenting a genuinely useful list of potential
                        // city names to the user, we now perform a further distance-based filtering step to cull
                        // any entries for places more than 10km away from the present centrepoint of the WME view.
                        let cLL = new OpenLayers.LonLat(cityAttrs.geometry.x, cityAttrs.geometry.y);
                        let diffX = mc.lon - cLL.lon;
                        let diffY = mc.lat - cLL.lat;
                        let distToSquared = (diffX * diffX) + (diffY * diffY);
                        if(distToSquared < (10000 * 10000))
                        {
                           let countyID = [];
                           countyID[0] = cityAttrs.stateID;
                           let countyName = W.model.states.getByIds(countyID)[0].attributes.name;
                           if(countyName !== '')
                           {
                              cityname += ', '+countyName;
                           }
                           nameList.push(cityname);
                        }
                     }
                  }
               }
            }
            nameList.sort();
            let matchedWMEName = false;
            for(let i=0; i<nameList.length; i++)
            {
               let listOpt = document.createElement('option');
               listOpt.text = nameList[i];
               oWCN.add(listOpt,null);
               if(sessionStorage.cityNameRB == 'optUseExistingWME')
               {
                  if(nameList[i] == sessionStorage.myCity)
                  {
                     oWCN.options.selectedIndex = i;
                     matchedWMEName = true;
                  }
               }
            }
            if((!matchedWMEName) && (sessionStorage.cityNameRB == 'optUseExistingWME'))
            {
               oWCN.options.selectedIndex = 0;
            }
            document.getElementById('oslSelect').addEventListener("click", oslClick, true);
            document.getElementById('oslWMECityNames').addEventListener("click", oslSelectWMEName, true);
            document.getElementById('optUseExistingWME').addEventListener("click", oslSelectWMEName, true);
            document.getElementById('oslOSCityNames').addEventListener("click", oslSelectOSName, true);
            document.getElementById('optUseOS').addEventListener("click", oslSelectOSName, true);
            document.getElementById('myCityName').addEventListener("keyup", oslCityNameKeyup, true);
            document.getElementById('optUseNewManual').addEventListener("click", oslCityNameKeyup, true);
            for(let loop=0;loop<candidates.length;loop++)
            {
               document.getElementById(candidates[loop].oslRadioID).addEventListener("click", oslRadioClick, true);
               if(candidates[loop].oslAltRadioID != null)
               {
                  document.getElementById(candidates[loop].oslAltRadioID).addEventListener("click", oslRadioClick, true);
               }
            }

            document.getElementById(sessionStorage.cityNameRB).checked = true;
            oslKeepUIVisible();
         }
         else oslRoadNameMatches.innerHTML = newHTML;
      }
   }
   if(mode == OSL_MODE.OpenRoads)
   {
      // OpenRoads check
      let eastingsLeft = (Math.floor(oslVPLeft/oslLocatorBlockSize) - 1) * oslLocatorBlockSize;
      let eastingsRight = (Math.ceil(oslVPRight/oslLocatorBlockSize) + 1) * oslLocatorBlockSize;
      let northingsTop = (Math.ceil(oslVPTop/oslLocatorBlockSize) + 1) * oslLocatorBlockSize;
      let northingsBottom = (Math.floor(oslVPBottom/oslLocatorBlockSize) - 1) * oslLocatorBlockSize;

      let highlightMode = OSL_ROADRENDERER.Init;

      for(let eLoop = eastingsLeft; eLoop <= eastingsRight; eLoop += oslLocatorBlockSize)
      {
         for(let nLoop = northingsBottom; nLoop <= northingsTop; nLoop += oslLocatorBlockSize)
         {
            oslHighlightOpenRoads(eLoop,nLoop,highlightMode);
            highlightMode = OSL_ROADRENDERER.Render;
         }
      }
      oslHighlightOpenRoads(0,0,OSL_ROADRENDERER.Finalise);
   }

   else return '?e='+oslEastings+'&n='+oslNorthings;
}
function oslRemoveDirSuffix(currentName, dirSuffix)
{
   let dPos = currentName.indexOf(dirSuffix);
   if(dPos != -1)
   {
      let dLength = dirSuffix.length;
      currentName = currentName.substr(0,dPos) + currentName.substr(dPos+dLength);
   }
   return currentName;
}
function oslNameCheckTrigger()
{
   oslOSLNameCheckTimer = 2;
}
function oslNameComparison()
{
   for(;oslONC_E<=oslEBlock_max;)
   {
      for(;oslONC_N<=oslNBlock_max;)
      {
         oslToOSGrid(oslONC_E*oslLocatorBlockSize, oslONC_N*oslLocatorBlockSize, OSL_MODE.NameCheck);
         if(oslLoadingMsg === true)
         {
            window.setTimeout(oslNameComparison,500);
            return;
         }
         oslONC_N++;
      }
      oslONC_N = oslNBlock_min;
      oslONC_E++;
   }
   for(let i=0;i<oslOSLNCSegments.length;i++)
   {
      if(oslOSLNCSegments[i].match === false)
      {
         let pline = oslOSLNCSegments[i].pline;
         pline.setAttribute("stroke","#000000");
         pline.setAttribute("stroke-opacity","0.5");
         pline.setAttribute("stroke-width","10");
         pline.setAttribute("stroke-dasharray","none");
      }
   }
}
function oslNCCandidateNew(pline, lonA, latA, lonB, latB, streetname)
{
   this.pline = pline;
   this.lonA = lonA;
   this.latA = latA;
   this.lonB = lonB;
   this.latB = latB;
   this.streetname = streetname;
   this.match = false;
}
function oslNCStateChange()
{
   if(document.getElementById('_cbNCEnabled').checked === false)
   {
      for(let segObj in W.model.segments.objects)
      {
         if(W.model.segments.objects.hasOwnProperty(segObj))
         {
            let seg = W.model.segments.objects[segObj];
            let pline = W.userscripts.getFeatureElementByDataModel(seg);
            if(pline !== null)
            {
               pline.setAttribute("stroke-width","5");
               pline.setAttribute("stroke","#dd7700");
               pline.setAttribute("stroke-opacity","0.001");
               pline.setAttribute("stroke-dasharray","none");
            }
         }
      }
   }
   else
   {
      if(W.map.getZoom() >= 16) oslNameCheck();
   }
}
function oslOpenRoadsStateChange(e)
{
   oslMapIsStaticProcessing();
   oslUpdateHighlightCBs(e.srcElement.id);
}
function oslNameCheck()
{
   if((document.getElementById('_cbNCEnabled').checked === false) || (W.map.getZoom() < 16)) return;

   let geoCenter=new OpenLayers.LonLat(W.map.getCenter().lon,W.map.getCenter().lat);
   oslToOSGrid(geoCenter.lat, geoCenter.lon, OSL_MODE.Conversion);
   if(oslLoadingMsg === true)
   {
      window.setTimeout(oslNameCheck,500);
      return;
   }

   oslEBlock_min = 99999;
   oslEBlock_max = -1;
   oslNBlock_min = 99999;
   oslNBlock_max = -1;

   let mapExtents = W.map.getExtent();
   let ignoreSegment;
   oslOSLNCSegments = [];

   for(let segObj in W.model.segments.objects)
   {
      if(W.model.segments.objects.hasOwnProperty(segObj))
      {
         ignoreSegment = false;
         let seg = W.model.segments.objects[segObj];
         let segRT = seg.attributes.roadType;
         let segBounds = seg.getOLGeometry().getBounds();

         if
         (
            (segBounds.left > mapExtents.right) ||
            (segBounds.right < mapExtents.left) ||
            (segBounds.top < mapExtents.bottom) ||
            (segBounds.bottom > mapExtents.top)
         )
         {
            // ignore segment as it's not visible...
            ignoreSegment = true;
         }
         if
         (
            (segRT < 1) ||
            ((segRT > 3) && (segRT < 6)) ||
            ((segRT > 8) && (segRT < 17)) ||
            ((segRT > 17) && (segRT < 20)) ||
            (segRT > 21)
         )
         {
            // ignore segment as it's non-driveable...
            ignoreSegment = true;
         }

         if(ignoreSegment === false)
         {
            let streetObj = W.model.streets.objects[seg.attributes.primaryStreetID];
            if(streetObj !== undefined)
            {
               let currentName = streetObj.attributes.name;
               let pline = W.userscripts.getFeatureElementByDataModel(seg);

               if((currentName !== null) && (pline !== null))
               {
                  currentName = oslRemoveDirSuffix(currentName,' (N)');
                  currentName = oslRemoveDirSuffix(currentName,' (S)');
                  currentName = oslRemoveDirSuffix(currentName,' (E)');
                  currentName = oslRemoveDirSuffix(currentName,' (W)');
                  currentName = oslRemoveDirSuffix(currentName,' (CW)');
                  currentName = oslRemoveDirSuffix(currentName,' (ACW)');

                  let endPointA = new OpenLayers.LonLat(seg.getOLGeometry().components[0].x,seg.getOLGeometry().components[0].y);
                  endPointA.transform(new OpenLayers.Projection("EPSG:900913"),new OpenLayers.Projection("EPSG:4326"));
                  oslToOSGrid(endPointA.lat, endPointA.lon, OSL_MODE.Conversion);
                  let eBlock = Math.floor(oslEastings/oslLocatorBlockSize);
                  let nBlock = Math.floor(oslNorthings/oslLocatorBlockSize);
                  if(eBlock < oslEBlock_min) oslEBlock_min = eBlock;
                  if(eBlock > oslEBlock_max) oslEBlock_max = eBlock;
                  if(nBlock < oslNBlock_min) oslNBlock_min = nBlock;
                  if(nBlock > oslNBlock_max) oslNBlock_max = nBlock;
                  let endPointB = new OpenLayers.LonLat(seg.getOLGeometry().components[seg.getOLGeometry().components.length-1].x,seg.getOLGeometry().components[seg.getOLGeometry().components.length-1].y);
                  endPointB.transform(new OpenLayers.Projection("EPSG:900913"),new OpenLayers.Projection("EPSG:4326"));
                  oslToOSGrid(endPointB.lat, endPointB.lon, OSL_MODE.Conversion);
                  oslOSLNCSegments.push(new oslNCCandidateNew(pline, endPointA.lon, endPointA.lat, endPointB.lon, endPointB.lat, currentName));
                  eBlock = Math.floor(oslEastings/oslLocatorBlockSize);
                  nBlock = Math.floor(oslNorthings/oslLocatorBlockSize);
                  if(eBlock < oslEBlock_min) oslEBlock_min = eBlock;
                  if(eBlock > oslEBlock_max) oslEBlock_max = eBlock;
                  if(nBlock < oslNBlock_min) oslNBlock_min = nBlock;
                  if(nBlock > oslNBlock_max) oslNBlock_max = nBlock;
               }
            }
         }
      }
   }

   if(oslOSLNCSegments.length > 0)
   {
      oslONC_E = oslEBlock_min;
      oslONC_N = oslNBlock_min;
      oslNameComparison();
   }
}
function oslTenthSecondTick()
{
   if(oslMOAdded === false)
   {
		if(document.getElementById('edit-panel') !== null)
		{
			oslAddLog('edit-panel mutation observer added...');
			let editPanelMO = new MutationObserver(oslEditPanelCheck);
			editPanelMO.observe(document.getElementById('edit-panel'),{childList:true,subtree:true});
			oslMOAdded = true;
		}
   }

   let hideUI = Boolean
   (
      (document.getElementsByClassName('menu').length > 0) &&
      (document.getElementsByClassName('menu')[0].className.indexOf('not-visible') === -1) &&
      (document.getElementsByClassName('menu')[0].className.indexOf('hide') === -1)
   );
   hideUI = Boolean(hideUI || (document.getElementsByClassName('toolbar-group open').length > 0));
   if(hideUI === false)
   {
      oslWindow.style.zIndex = 2000;
   }
   else
   {
      oslWindow.style.zIndex = -2000;
   }

   if(!oslAdvancedMode) oslEnableAdvancedOptions();

   let oslSelectedItems = W.selectionManager.getSelectedDataModelObjects();
   if(oslSelectedItems.length == 1)
   {
      if(oslPrevSelected === null) 
      {
         oslDoOSLUpdate = true;
      }
      else if(oslSelectedItems[0].attributes.id != oslPrevSelected) 
      {
         oslDoOSLUpdate = true;
      }
      oslPrevSelected = oslSelectedItems[0].attributes.id;
   }
   else
   {
      oslPrevSelected = null;
   }

   if(document.getElementById('oslSelect') !== null)
   {
      let editDisabled = ((document.getElementsByClassName('full-address disabled').length > 0) || (document.getElementsByClassName('full-address-container').length === 0));
      if(editDisabled === true)
      {
         document.getElementById('oslSelect').style.background = "rgb(160, 160, 160)";
      }
      else
      {
         document.getElementById('oslSelect').style.background = "rgb(204, 204, 255)";
      }
      document.getElementById('oslSelect').disabled = editDisabled;
   }

   if(oslOSLNameCheckTimer > 0)
   {
      if(--oslOSLNameCheckTimer === 0) oslNameCheck();
   }

   if(--oslBlockCacheTestTimer === 0)
   {
      oslBlockCacheTestTimer = (oslCacheDecayPeriod * 10);
      let timeNow = Math.floor(new Date().getTime() / 1000);
      for(let bcIdx = oslBlockCacheList.length-1; bcIdx >= 0; bcIdx--)
      {
         if((timeNow - oslBlockCacheList[bcIdx].lastAccessed) > oslCacheDecayPeriod)
         {
            delete unsafeWindow[oslBlockCacheList[bcIdx].blockName];
            oslBlockCacheList.splice(bcIdx,1);
         }
      }
   }

   if((oslDoOSLUpdate === true) && (oslMousepos !== null))
   {
      // update the OS Locator matches
      oslToOSGrid(oslMousepos.lat,oslMousepos.lon,OSL_MODE.OpenNames);
      oslDoOSLUpdate = oslLoadingMsg;
      if(!oslDoOSLUpdate) oslRadioClick();
   }

   if(oslBlocksToTest.length > 0)
   {
      for(let i=0; i<oslBlocksToTest.length; i++)
      {
         if(typeof unsafeWindow[oslBlocksToTest[i]] != undefined)
         {
            oslBlocksToTest.splice(i, 1);
         }
      }

      if(oslBlocksToTest.length === 0)
      {
         oslMapIsStaticProcessing();
      }
   }   
}
function oslEnableAdvancedOptions()
{
   if (oslAdvancedMode) return;
   if(W.loginManager === null) return;
   if(W.loginManager.isLoggedIn() === true)
   {
      let thisUser = W.loginManager.user;
      if (thisUser !== null && thisUser.getRank() >= 2)
      {
         oslAdvancedMode = true;
         oslAddLog('advanced mode enabled');
      }
   }
}
function oslUpdateLiveMapLink()
{
   let lmLink = document.getElementById('livemap-link');
   if(lmLink === null)
   {
      window.setTimeout(oslUpdateLiveMapLink,100);
      return;
   }

   // translate the zoom level between WME and live map.
   let livemap_zoom = parseInt(sessionStorage.zoom);
   if (livemap_zoom > 17) livemap_zoom = 17;
   let livemap_url = 'https://www.waze.com/livemap/?';
   livemap_url += 'lon='+sessionStorage.lon;
   livemap_url += '&lat='+sessionStorage.lat;
   livemap_url += '&zoom='+livemap_zoom;

   // Modify existing livemap link to reference current position in WME
   lmLink.href = livemap_url;
   lmLink.target = '_blank';
}
function oslLonLatToPixel(lon, lat)
{
   let lonlat = new OpenLayers.LonLat();
   lonlat.lon = parseFloat(lon);
   lonlat.lat = parseFloat(lat);
   return W.map.getPixelFromLonLat(lonlat);
}
function oslOSGridRefToPixel(osEast, osNorth)
{
   oslOSGBtoWGS(osEast,osNorth);
   let lonlat = new OpenLayers.LonLat(oslLongitude,oslLatitude);
   return W.map.getPixelFromLonLat(lonlat);
}  
function oslBoundsCheck(pix1, pix2, width, height)
{
   let xmin = Math.min(pix1.x,pix2.x);
   let xmax = Math.max(pix1.x,pix2.x);
   let ymin = Math.min(pix1.y,pix2.y);
   let ymax = Math.max(pix1.y,pix2.y);
   let retval = 
   (
      (xmin <= width) &&
      (xmax >= 0) &&
      (ymin <= height) &&
      (ymax >= 0)
   );
   return retval;
}
function oslHighlightOpenRoads(oslEastings, oslNorthings, mode)
{
   oslBlocksToLoad = [];
   if((mode === OSL_ROADRENDERER.Init) || (mode == OSL_ROADRENDERER.Render))
   {
      let arrayName = 'locatorData_'+oslEastings+'_'+oslNorthings;
      // check to see if there's a corresponding array loaded already
      if(typeof unsafeWindow[arrayName] == "undefined")
      {
         // create a blank placeholder which will get replaced by the actual data if the array is present on the server...
         unsafeWindow[arrayName] = [];
         oslBlocksToLoad.push(oslBlockPath+Math.floor(oslEastings / 100000)+'/'+Math.floor(oslNorthings / 100000)+'/'+arrayName+'.user.js');
         oslBlocksToLoad.push(arrayName);
         oslBlocksToTest.push(arrayName);
         oslLoadBlocks();
      }

      let divWidth = document.getElementById('WazeMap').offsetWidth;
      let divHeight = document.getElementById('WazeMap').offsetHeight;
      let displayPoints = false;
      if(mode === OSL_ROADRENDERER.Init)
      {         
         // initialise SVG container
         oslSegGeoDivInnerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="'+divWidth+'px" height="'+divHeight+'px" version="1.1">';
      }

      if(typeof unsafeWindow[arrayName] == "undefined") return;
      let oslOpenRoadsData;
      oslOpenRoadsData = unsafeWindow[arrayName];

      // calculate an appropriate stroke width for the current zoom level
      let strokeWidth = W.map.getZoom() - 12;
      if(strokeWidth < 2) strokeWidth = 2;
      if(strokeWidth > 9) strokeWidth = 9;

      let renderByFunction = [];
      renderByFunction.push(true);  // Undefined roads always get rendered when the OpenRoads layer is enabled
      renderByFunction.push(document.getElementById('_cbSegGeoMotorway').checked);
      renderByFunction.push(document.getElementById('_cbSegGeoARoad').checked);
      renderByFunction.push(document.getElementById('_cbSegGeoBRoad').checked);
      renderByFunction.push(document.getElementById('_cbSegGeoMinor').checked);
      renderByFunction.push(document.getElementById('_cbSegGeoLocal').checked);
      if(oslAdvancedMode == true)
      {
         renderByFunction.push(document.getElementById('_cbSegGeoLocalAccess').checked);
         renderByFunction.push(document.getElementById('_cbSegGeoRestricted').checked);
         renderByFunction.push(document.getElementById('_cbSegGeoSecondary').checked);
      }
      else
      {
         renderByFunction.push(false);
         renderByFunction.push(false);
         renderByFunction.push(false);
      }
      let useFullGeo = document.getElementById('_cbOpenRoadsUseGeo').checked;
      let enhancePolylines = document.getElementById('_cbOpenRoadsEnhanceGeoVis').checked;
      let highlightPRN = document.getElementById('_cbOpenRoadsHighlightPRN').checked;

      let pix1;
      let pix2;
      let nameColour;
      for(let roadIdx = 0; roadIdx < oslOpenRoadsData.length; roadIdx++)
      {
         let roadEntry = oslOpenRoadsData[roadIdx];
         let elements = oslSplitEntry(roadEntry);
         let roadFunction = parseInt(elements[OSL_ELM.Function]);

         if ((elements[OSL_ELM.RoadName] == '') && (elements[OSL_ELM.RoadNumber] == ''))
         {
            nameColour = 'cyan';
         }
         else
         {
            nameColour = 'magenta';
         }

         let renderPrimary = ((highlightPRN == true) && (elements[OSL_ELM.IsPrimary] == 'Y'));

         if((renderPrimary == true) || (renderByFunction[roadFunction] == true))
         {
            pix1 = oslLonLatToPixel(elements[OSL_ELM.BoundW], elements[OSL_ELM.BoundS]);
            pix2 = oslLonLatToPixel(elements[OSL_ELM.BoundE], elements[OSL_ELM.BoundN]);
            if(oslBoundsCheck(pix1, pix2, divWidth, divHeight) == true)
            {
               if(useFullGeo == true)
               {
                  let geoPairs = elements[OSL_ELM.Geometry].split('  ');
                  let geoPoints = geoPairs[0].split(' ');
                  let isTunnel = (elements[OSL_ELM.Structure] == 1);
                  pix1 = oslLonLatToPixel(geoPoints[0], geoPoints[1]);
                  let pline = '';
                  for(let pts=1; pts < geoPairs.length; pts++)
                  {
                     geoPoints = geoPairs[pts].split(' ');
                     pix2 = oslLonLatToPixel(geoPoints[0],geoPoints[1]);
                     if(oslBoundsCheck(pix1, pix2, divWidth, divHeight) == true)
                     {
                        if(displayPoints === false)
                        {
                           pline += '<polyline points="';
                           displayPoints = true;
                        }
                        pline += pix1.x+','+pix1.y+' '+pix2.x+','+pix2.y+' ';
                     }
                     pix1 = pix2;
                  }

                  if(displayPoints === true)
                  {
                     let strokeColour = oslStrokeColoursByFunction[roadFunction];
                     let strokeArray = '';
                     if(isTunnel == true)
                     {
                        strokeArray = 'stroke-dasharray:10,10;';
                     }
                    
                     if(renderPrimary == true)
                     {
                        oslSegGeoDivInnerHTML += pline + '" style="stroke:black';
                        oslSegGeoDivInnerHTML += ';'+strokeArray+'stroke-width:'+(strokeWidth+25)+';stroke-linecap:round;fill:none"/>';                     
                        oslSegGeoDivInnerHTML += pline + '" style="stroke:green';
                        oslSegGeoDivInnerHTML += ';'+strokeArray+'stroke-width:'+(strokeWidth+21)+';stroke-linecap:round;fill:none"/>';                     
                     }
                     if(enhancePolylines == true)
                     {
                        oslSegGeoDivInnerHTML += pline + '" style="stroke:black';
                        oslSegGeoDivInnerHTML += ';'+strokeArray+'stroke-width:'+(strokeWidth+13)+';stroke-linecap:round;fill:none"/>';                     
                        oslSegGeoDivInnerHTML += pline + '" style="stroke:';
                        oslSegGeoDivInnerHTML += nameColour;
                        oslSegGeoDivInnerHTML += ';'+strokeArray+'stroke-width:'+(strokeWidth+11)+';stroke-linecap:round;fill:none"/>';
                     }
                     oslSegGeoDivInnerHTML += pline + '" style="stroke:black;'+strokeArray+'stroke-width:'+(strokeWidth+2)+';stroke-linecap:round;fill:none"/>';
                     oslSegGeoDivInnerHTML += pline + '" style="stroke:'+strokeColour+';'+strokeArray+'stroke-width:'+strokeWidth+';stroke-linecap:round;fill:none"/>';
                     displayPoints = false;
                  }
               }
               else
               {
                  const minDim = 20;
                  let bWidth = Math.abs(pix2.x-pix1.x);
                  if(bWidth < minDim) bWidth = minDim;
                  let bHeight = Math.abs(pix2.y-pix1.y);
                  if(bHeight < minDim) bHeight = minDim;

                  oslSegGeoDivInnerHTML += '<rect x="'+pix1.x+'" y="'+pix2.y+'" width="'+bWidth+'" height="'+bHeight+'" style="fill:';
                  oslSegGeoDivInnerHTML += nameColour;
                  oslSegGeoDivInnerHTML += ';stroke:black;stroke-width:4;fill-opacity:0.25;stroke-opacity:0.25"/>';
               }
            }
         }
      }
   }
   else if(mode == OSL_ROADRENDERER.Finalise)
   {
      // finalise SVG
      oslSegGeoDivInnerHTML += '</svg>';
      oslSegGeoDiv.innerHTML = oslSegGeoDivInnerHTML;
   }
   else if(mode == OSL_ROADRENDERER.Erase)
   {
      // erase SVG
      oslSegGeoDiv.innerHTML = '';
   }
}
function oslRepositionOverlays()
{
   let divTop = getComputedStyle(W.map.getLayerByUniqueName('segments').div).top;
   let divLeft = getComputedStyle(W.map.getLayerByUniqueName('segments').div).left;
   
   oslBBDiv.style.top = divTop;
   oslBBDiv.style.left = divLeft;   
   oslSegGeoDiv.style.top = divTop;
   oslSegGeoDiv.style.left = divLeft;
   oslNamesDiv.style.top = divTop;
   oslNamesDiv.style.left = divLeft;

}
function oslGetCorrectedLonLatFromPixelPos(px, py, toolbarCompensation)
{
   if((toolbarCompensation) && (oslOffsetToolbar)) py -= document.getElementById('toolbar').clientHeight;
   py -= document.getElementById('topbar-container').clientHeight;
   let pixelPos = new OpenLayers.Pixel(px, py);
   let llfp = W.map.getLonLatFromPixel(pixelPos);
   return new OpenLayers.LonLat(llfp.lon, llfp.lat);
}
function oslGetOffsetMapCentre()
{
   // get lon/lat of viewport centrepoint for modifying the livemap link and for passing to the external mapping sites.

   // shift the longitude pixel offset by half the width of the WME sidebar to account for the lateral offset that would
   // otherwise occur when switching between the WME tab and the other map tabs - all of those use a full-width map view,
   // so their map centre is further to the left within the browser window than the WME centrepoint...
   let mapVPX;
   let mapVPY;

   if(W.map.olMap == undefined)
   {
      mapVPX = (W.map.getOLMap().getViewport().clientWidth / 2) - (document.getElementById('sidebar').clientWidth / 2);
      mapVPY = W.map.getOLMap().getViewport().clientHeight / 2;
   }
   else
   {
      mapVPX = (W.map.olMap.getViewport().clientWidth / 2) - (document.getElementById('sidebar').clientWidth / 2);
      mapVPY = W.map.olMap.getViewport().clientHeight / 2;
   }
   return oslGetCorrectedLonLatFromPixelPos(mapVPX, mapVPY, false);
}
function oslMouseMoveAndUp(e)
{
   if(oslUserPrefs.ui.state == 'minimised') 
   {
      return;
   }

   if((e.pageX !== undefined) && (e.pageY !== undefined)) 
   {
      // Only valid when a mousemove event occurs...
      let mapBCR = document.getElementById('map').getBoundingClientRect();
      let mouseX = e.pageX - mapBCR.left;
      let mouseY = e.pageY - mapBCR.top;
   
      oslMousePixelpos = new OpenLayers.Pixel(mouseX, mouseY);
      oslLastViewportWidth = W.map.getViewport().getBoundingClientRect().width;
      let mouseLonLat = oslGetCorrectedLonLatFromPixelPos(mouseX, mouseY, true);
      oslMousepos = mouseLonLat;
      if
      (
         (oslMousepos != sessionStorage.oslMousepos) || 
         (
            (oslLoadingMsg == true) && 
            (typeof unsafeWindow[oslEvalString] != undefined)
         )
      )
      {
         oslLoadingMsg = false;
         sessionStorage.oslMousepos = oslMousepos;

         oslDoOSLUpdate = false;
         // update the OSL results if there are no selected segments, but there is a highlighted segment
         // which we haven't already done an update for

         oslSegmentHighlighted = false;
         let noneSelected = (W.selectionManager.getSelectedDataModelObjects().length === 0);
         if(noneSelected === true)
         {
            for(let slIdx=0; slIdx < W.map.getLayerByUniqueName('segments').features.length; slIdx++)
            {
               if(W.map.getLayerByUniqueName('segments').features[slIdx].renderIntent == 'highlight')
               {
                  if(slIdx != oslPrevHighlighted)
                  {
                     oslPrevHighlighted = slIdx;
                  }
                  oslDoOSLUpdate = true;
                  oslSegmentHighlighted = true;
               }
            }
         }
      }
   }

   // Valid for both mousemove and zoomend events...
   let gomc = oslGetOffsetMapCentre();
   let lat = gomc.lat;
   let lon = gomc.lon;
   let zoom = W.map.getZoom();
   // compare the new parameters against the persistent copies, and update the external links
   // only if there's a change required - the newly-inserted <a> element can't be clicked
   // on until the insertion process is complete, and if we were to re-insert it every timeout
   // then it'd spend a lot of its time giving the appearance of being clickable but without
   // actually doing anything...
   if((zoom != parseInt(sessionStorage.zoom))||(lat != parseFloat(sessionStorage.lat))||(lon != parseFloat(sessionStorage.lon)))
   {
      let country = W.model.getTopCountry()?.attributes?.name;

      if(country == "United Kingdom")
      {
         if (oslInUK === false)
         {
            oslAddLog('location is the UK, enabling full UI...');
            oslInUK = true;
            document.getElementById('oslOSLDiv').style.display = "block";
            document.getElementById('oslNCDiv').style.display = "block";
            document.getElementById('oslSegGeoUIDiv').style.display = "block";
            document.getElementById('oslGazTagsDiv').style.display = "block";
            document.getElementById('_extlinksUK').style.display = "block";
         }
      }
      else
      {
         // ...somewhere not yet supported, or WME isn't telling us just yet...
         oslAddLog('location not recognised, disabling UK-specific parts of UI...');
         oslInUK = false;
         oslInLondon = false;
         document.getElementById('oslOSLDiv').style.display = "none";
         document.getElementById('oslNCDiv').style.display = "none";
         document.getElementById('oslSegGeoUIDiv').style.display = "none";
         document.getElementById('oslGazTagsDiv').style.display = "none";
         document.getElementById('_extlinksUK').style.display = "none";
      }

      if(oslInUK === true)
      {
         // we're in the UK, so test to see if we're within the approximate Greater London bounding box
         if((lon >= -0.55) && (lon <= 0.30) && (lat >= 51.285) && (lat <= 51.695))
         {
            oslInLondon = true;
            document.getElementById('lrrCtrls').style.display='inline';
         }
         else
         {
            oslInLondon = false;
            document.getElementById('lrrCtrls').style.display='none';
         }
      }

      if(zoom != sessionStorage.zoom)
      {
         if(zoom < 16) document.getElementById('_cbNCEnabled').disabled = true;
         else document.getElementById('_cbNCEnabled').disabled = false;
      }
      // update the persistent vars with the new position
      sessionStorage.zoom = zoom;
      sessionStorage.lat = lat;
      sessionStorage.lon = lon;

      if(oslInUK === true)
      {
         // update the external site datalinks with the new coords/zoom
         if(document.getElementById('_cbAutoTrackOSOD').checked == 1)
         {
            oslOSODClick();
         }
         if(document.getElementById('_cbAutoTrackRWO').checked == 1)
         {
            oslUpdateOneKey();
         }
         if(document.getElementById('_cbAutoTrackLRR').checked == 1)
         {
            oslUpdateLRRKey();
         }
      }

      // wait to update the livemap link, as WME now does its own update after this point so any changes we make here
      // end up being wiped out...
      window.setTimeout(oslUpdateLiveMapLink,100);

      document.getElementById('_linkPermalink').href = document.getElementsByClassName('WazeControlPermalink')[0].getElementsByClassName('permalink')[0].href;
   }
   
   if((e.type == "mouseup") || (e.type == "zoomend"))
   {
      oslMapIsStaticProcessing();
   }
}
function oslSetAccessKey(keyName, requiresTransform, requiresOSCoords)
{
   let lat = "0.0";
   let lon = "0.0";
   let zoom = W.map.getZoom();

   if(requiresOSCoords === true)
   {
      lat = oslNorthings;
      lon = oslEastings;
   }
   else
   {
      let gomc = oslGetOffsetMapCentre();
      if(requiresTransform === true)
      {
         gomc = gomc.transform(new OpenLayers.Projection("EPSG:4326"),new OpenLayers.Projection("EPSG:900913"));
      }
      lat = gomc.lat.toFixed(4);
      lon = gomc.lon.toFixed(4);
   }
   
   GM_setValue(keyName,Date.now()+','+lat+','+lon+','+zoom);
}
function oslOSODClick()
{
   oslSetAccessKey('_osodAccessKey', true, false);
}
function oslUpdateOneKey()
{
   oslSetAccessKey('_oneAccessKey', false, false);
}
function oslUpdateLRRKey()
{
   oslSetAccessKey('_lrrAccessKey', false, true);
}
function oslMapIsStaticProcessing()
{
   if(oslUserPrefs.ui.state == 'maximised')
   {
      let tCenter = W.map.getCenter();
      let tZoom = W.map.getZoom();
      if
      (
         (oslRORCenter == null) || (tCenter.lat != oslRORCenter.lat) || (tCenter.lon != oslRORCenter.lon) ||
         (oslRORZoom == null) || (tZoom != oslRORZoom)
      )
      {
         oslRORCenter = tCenter;
         oslRORZoom = tZoom;
         window.setTimeout(oslMapIsStaticProcessing, 50);
         return;
      }

      let geoCenter=new OpenLayers.LonLat(W.map.getCenter().lon,W.map.getCenter().lat);
      geoCenter.transform(new OpenLayers.Projection("EPSG:900913"),new OpenLayers.Projection("EPSG:4326"));
      oslToOSGrid(geoCenter.lat, geoCenter.lon, OSL_MODE.Conversion);

      oslNameCheck();
      if(oslInUK === true)
      {
         // recalculate the map viewport extents in terms of oslEastings/oslNorthings
         let vpHalfWidth = (W.map.getExtent().right-W.map.getExtent().left) / (2 * 1.61);
         let vpHalfHeight = (W.map.getExtent().top-W.map.getExtent().bottom) / (2 * 1.61);
         oslVPLeft = oslEastings - vpHalfWidth;
         oslVPRight = oslEastings + vpHalfWidth;
         oslVPBottom = oslNorthings - vpHalfHeight;
         oslVPTop = oslNorthings + vpHalfHeight;

         if(document.getElementById('_cbOpenRoadsEnabled').checked === true)
         {
            oslToOSGrid(oslEastings,oslNorthings,OSL_MODE.OpenRoads);
         }
         else
         {
            oslHighlightOpenRoads(0,0,OSL_ROADRENDERER.Erase);
         }
         
         oslGetVisibleCityNames();
      }
      else
      {
         oslHighlightOpenRoads(0,0,OSL_ROADRENDERER.Erase);
      }
      oslRepositionOverlays();
      oslRadioClick();  // this regenerates the bounding boxes on the repositioned overlay...
   }
}
function oslTestPointerOutsideMap(mX, mY)
{
   let mapElm = document.getElementById("map");
   if(mapElm === undefined) return false;

   let bLeft = mapElm.parentElement.offsetLeft;
   let bRight = (bLeft + mapElm.offsetWidth);
   let bTop = (mapElm.parentElement.offsetTop + document.getElementById("topbar-container").clientHeight);
   let bBottom = (mapElm.parentElement.offsetTop + mapElm.offsetHeight + document.getElementById("topbar-container").clientHeight - document.getElementsByClassName("wz-map-ol-footer")[0].clientHeight);

   if((mX < bLeft) || (mX > bRight) || (mY < bTop) || (mY > bBottom)) return true;
   else return false;
}
function oslMouseOut(e)
{
   if(oslTestPointerOutsideMap(e.clientX, e.clientY))
   {
      // when the mouse pointer leaves the map area, WME treats it similarly to a mouseup
      // event without generating an actual mouseup event, so we need to do the same...
      oslMapIsStaticProcessing();
   }
}
function oslCancelEvent(e)
{
  e = e ? e : window.event;
  if(e.stopPropagation)
    e.stopPropagation();
  if(e.preventDefault)
    e.preventDefault();
  e.cancelBubble = true;
  e.cancel = true;
  e.returnValue = false;
  return false;
}
function oslOSLDivMouseDown(e)
{
   oslPrevMouseX = e.pageX;
   oslPrevMouseY = e.pageY;
   oslDivDragging = true;
   oslDragBar.style.cursor = 'move';
   document.body.addEventListener('mousemove', oslOSLDivMouseMove, false);
   document.body.addEventListener('mouseup', oslOSLDivMouseUp, false);
   // lock the UI width during a drag so we can correctly detect for falling off the window edge
   oslWindow.style.width = oslWindow.getBoundingClientRect().width+'px';
   return true;
}
function oslOSLDivMouseUp()
{
   if(oslDivDragging)
   {
      oslDivDragging = false;
      oslUserPrefs.ui.x = oslOSLDivLeft;
      oslUserPrefs.ui.y = oslOSLDivTop;
      oslWriteUserPrefs();
      // unlock the UI width again so we can expand/contract as required based on other script behaviour
      oslWindow.style.width = "auto";
   }
   oslDragBar.style.cursor = 'auto';
   document.body.removeEventListener('mousemove', oslOSLDivMouseMove, false);
   document.body.removeEventListener('mouseup', oslOSLDivMouseUp, false);
   return true;
}
function oslOSLDivMouseMove(e)
{
   let vpHeight = window.innerHeight;
   let vpWidth = window.innerWidth;

   oslOSLDivTop = parseInt(oslOSLDivTop) + parseInt((e.pageY - oslPrevMouseY));
   oslOSLDivLeft = parseInt(oslOSLDivLeft) + parseInt((e.pageX - oslPrevMouseX));
   oslPrevMouseX = e.pageX;
   oslPrevMouseY = e.pageY;

   if(oslOSLDivTop < 0) oslOSLDivTop = 0;
   if(oslOSLDivTop + 16 >= vpHeight) oslOSLDivTop = vpHeight-16;
   if(oslOSLDivLeft < 0) oslOSLDivLeft = 0;
   if(oslOSLDivLeft + 32 >= vpWidth) oslOSLDivLeft = vpWidth-32;

   oslWindow.style.top = oslOSLDivTop+'px';
   oslWindow.style.left = oslOSLDivLeft+'px';

   return oslCancelEvent(e);
}
function oslKeepUIVisible()
{
   let vpHeight = window.innerHeight;
   let topbarHeight = document.getElementById('toolbar').offsetHeight;
   let maxUIHeight = (vpHeight - topbarHeight);
   // Restore auto-sizing so we can see the effect of whatever UI change caused us to get here...
   oslWindow.style.height = "auto";
   oslWindow.style.overflow = "hidden";

   // If the bottom edge of the ui would fall off the bottom edge of the map viewport, nudge
   // the UI up as far as required to keep the whole UI visible.  If this would however require
   // the top edge of the UI to be pushed off the top of the map viewport, constrain the UI
   // height to the viewport height and enable a scrollbar...
   if(oslWindow.getBoundingClientRect().bottom >= maxUIHeight)
   {
      let newTop = (vpHeight-oslWindow.getBoundingClientRect().height);
      if(newTop < topbarHeight)
      {
         newTop = topbarHeight;
         oslWindow.style.height = maxUIHeight+'px';
         oslWindow.style.overflow = "scroll";
      }
      oslOSLDivTop = newTop;
      oslUserPrefs.ui.y = oslOSLDivTop;
      oslWindow.style.top = oslOSLDivTop+'px';
   }
}
function oslMinimiseDiv(divID)
{
   let tDiv = document.getElementById(divID);
   if(tDiv != null)
   {
      tDiv.style.height = '0px';
      tDiv.style.padding = '0px';
      tDiv.style.overflow = 'hidden';
   }
}
function oslMaximiseDiv(divID)
{
   let tDiv = document.getElementById(divID);
   if(tDiv != null)
   {
      tDiv.style.height = 'auto';
      tDiv.style.padding = '2px';
      tDiv.style.overflow = 'auto';
   }
}
function oslWindowMaximise()
{
   let tHTML = '';
   tHTML += '<span style="float:left"><a href="'+oslUpdateURL+'" target="_blank"><b>WMEOpenData</a> v'+oslVersion+'</b></span>';
   tHTML += '<span id="_minimax" style="float:right"><i class="fa fa-chevron-circle-up"></i></span>';
   tHTML += '<br>';
   oslDragBar.innerHTML = tHTML;
   document.getElementById('_minimax').addEventListener('click', oslWindowMinimise, false);

   oslMaximiseDiv('oslOSLDiv');
   oslMaximiseDiv('oslNCDiv');
   oslMaximiseDiv('oslGazTagsDiv');
   oslMaximiseDiv('oslSegGeoUIDiv');
   oslMaximiseDiv('oslMLCDiv');
   oslKeepUIVisible();
   oslUserPrefs.ui.state = 'maximised';
   oslWriteUserPrefs();
   oslRepositionOverlays();
   oslMapIsStaticProcessing();
}
function oslWindowMinimise()
{
   let tHTML = '';
   tHTML += '<span style="float:left"><b>WMEOpenData v'+oslVersion+'</b></span>';
   tHTML += '<span id="_minimax" style="float:right"><i class="fa fa-chevron-circle-down"></i></span>';
   tHTML += '<br>';
   oslDragBar.innerHTML = tHTML;
   document.getElementById('_minimax').addEventListener('click', oslWindowMaximise, false);

   oslMinimiseDiv('oslOSLDiv');
   oslMinimiseDiv('oslNCDiv');
   oslMinimiseDiv('oslGazTagsDiv');
   oslMinimiseDiv('oslSegGeoUIDiv');
   oslMinimiseDiv('oslMLCDiv');
   oslKeepUIVisible();
   oslBBDiv.innerHTML = '';
   oslNamesDiv.innerHTML = '';
   oslSegGeoDiv.innerHTML = '';
   oslUserPrefs.ui.state = 'minimised';
   oslWriteUserPrefs();
}
function oslSubDivSetState(divID, state)
{
   let tSubDiv = document.getElementById(divID).children[1];
   let tToggle = document.getElementById(divID).children[0].children[1].children[0];
   if(state == 'minimised')
   {
      tSubDiv.style.height = "0px";
      tSubDiv.style.padding = "0px";
      tSubDiv.style.overflow = "hidden";
      tToggle.className = "fa fa-chevron-circle-down";
   }
   else
   {
      tSubDiv.style.height = "auto";
      tSubDiv.style.width = "100%";
      tSubDiv.style.padding = "2px";
      tSubDiv.style.overflow = "auto";
      tToggle.className = "fa fa-chevron-circle-up";
   }
}
function oslUIMinMax(e)
{
   let headerDivID = e.srcElement.parentElement.parentElement.parentElement.id;
   let tSubDiv = document.getElementById(headerDivID).children[1];
   if(tSubDiv != null)
   {
      let newState;
      if(tSubDiv.style.height == "auto")
      {
         newState = 'minimised';
      }
      else
      {
         newState = 'maximised';
      }
      oslSubDivSetState(headerDivID, newState);
      oslUpdateMinMax(headerDivID, newState);
   }
   oslKeepUIVisible();
}
function oslIsLanesTabActive()
{
   let retval = false;
   if(document.querySelector('wz-tab.lanes-tab') !== null)
   {
      if(document.querySelector('wz-tab.lanes-tab').getAttribute("is-active") !== null)
      {
         retval = true;
      }
   }
   return retval;
}
function oslEditPanelCheck()
{
   if
   (
      (oslIsLanesTabActive() == true) ||
      (document.getElementById('edit-panel')?.getElementsByClassName('map-comment-feature-editor').length)
   )
   {
      document.body.style.overflow = "auto";
   }
   else
   {
      document.body.style.overflow = "hidden";
   }
}
function oslSetupBBDiv()
{
   // add a new div to the map viewport, to hold the bounding box SVG
   oslAddLog('create bounding box DIV');
   oslBBDiv = document.createElement('div');
   oslBBDiv.id = "oslBBDiv";
   oslBBDiv.style.position = 'absolute';
   oslBBDiv.style.top = getComputedStyle(W.map.getLayerByUniqueName('segments').div).top;
   oslBBDiv.style.left = getComputedStyle(W.map.getLayerByUniqueName('segments').div).left;
   oslBBDiv.style.overflow = 'hidden';
   oslBBDiv.style.width = window.innerWidth;
   oslBBDiv.style.height = window.innerHeight;
   oslWazeMapElement.appendChild(oslBBDiv);
}
function oslSetupORDiv()
{
   // add a new div to the map viewport, to hold the OpenRoads SVG
   oslAddLog('create OpenRoads DIV');
   oslSegGeoDiv = document.createElement('div');
   oslSegGeoDiv.id = "oslSegGeoDiv";
   oslSegGeoDiv.style.position = 'absolute';
   oslSegGeoDiv.style.top = getComputedStyle(W.map.getLayerByUniqueName('segments').div).top;
   oslSegGeoDiv.style.left = getComputedStyle(W.map.getLayerByUniqueName('segments').div).left;
   oslSegGeoDiv.style.overflow = 'hidden';
   oslSegGeoDiv.style.width = window.innerWidth;
   oslSegGeoDiv.style.height = window.innerHeight;
   oslWazeMapElement.appendChild(oslSegGeoDiv);
}
function oslSetupNamesDiv()
{
   // add a new div to the map viewport, to hold the place names SVG
   oslAddLog('create place names DIV');
   oslNamesDiv = document.createElement('div');
   oslNamesDiv.id = "oslNamesDiv";
   oslNamesDiv.style.position = 'absolute';
   oslNamesDiv.style.pointerEvents = 'none';
   oslNamesDiv.style.top = getComputedStyle(W.map.getLayerByUniqueName('segments').div).top;
   oslNamesDiv.style.left = getComputedStyle(W.map.getLayerByUniqueName('segments').div).left;
   oslNamesDiv.style.overflow = 'hidden';
   oslNamesDiv.style.width = window.innerWidth;
   oslNamesDiv.style.height = window.innerHeight;
   oslWazeMapElement.appendChild(oslNamesDiv);
}
function oslGenerateOpenRoadsCBHTML(isStd, ID, label, indentLevel)
{
   let tHTML = '';
   let indent = 'padding-left: '+(indentLevel * 0.5)+'em;';
   
   tHTML += '<label style="display: inline; '+indent+'">';
   tHTML += '<input type="checkbox" name="oslOpenRoads_';
   if(isStd == true) tHTML += 'std';
   else tHTML += 'adv';
   tHTML += '" id="' + ID + '" />';
   tHTML += label + '</label>';
   return tHTML;
}
function oslGenerateCollapsibleSubDiv(headerText, subDivHTML)
{
   let tHTML;
   tHTML = '<div><span style="float:left"><b>'+headerText+'</b></span>';
   tHTML += '<span name="oslMinMaxToggle" style="float:right"><i class="fa fa-chevron-circle-down"></i></span></div>';
   tHTML += '<div style="height: 0px; padding: 0px; overflow: hidden;">';
   tHTML += subDivHTML;
   tHTML += '</div>';
   return tHTML;
}
function oslSetupUI()
{
   let subDivHTML;

   // add a new div to hold the OS Locator results, in the form of a draggable window
   oslAddLog('create lookup results DIV');
   oslWindow = document.createElement('div');
   oslWindow.id = "oslWindow";
   oslWindow.style.position = 'absolute';
   oslWindow.style.border = '1px solid #BBDDBB';
   oslWindow.style.borderRadius = '4px';
   oslWindow.style.overflow = 'hidden';
   oslWindow.style.zIndex = 2000;
   oslWindow.style.opacity = 0;
   oslWindow.style.transitionProperty = "opacity";
   oslWindow.style.transitionDuration = "1000ms";
   oslWindow.style.webkitTransitionProperty = "opacity";
   oslWindow.style.webkitTransitionDuration = "1000ms";
   oslWindow.style.boxShadow = '5px 5px 10px Silver';
   document.body.appendChild(oslWindow);

   // dragbar div
   oslAddLog('create dragbar DIV');
   oslDragBar = document.createElement('div');
   oslDragBar.id = "oslDragBar";
   oslDragBar.style.backgroundColor = '#D0D0D0';
   oslDragBar.style.padding = '4px';
   oslDragBar.style.fontSize = '16px';
   oslDragBar.style.lineHeight = '18px';
   oslWindow.appendChild(oslDragBar);

   // OS results div
   oslAddLog('create results DIV');
   oslOSLDiv = document.createElement('div');
   oslOSLDiv.id = "oslOSLDiv";
   oslOSLDiv.style.backgroundColor = '#DDFFDD';
   oslOSLDiv.style.padding = '2px';
   oslOSLDiv.style.fontSize = '14px';
   oslOSLDiv.style.lineHeight = '16px';
   oslOSLDiv.style.display = 'none';

   subDivHTML = "<div id='oslRoadNameMatches'></div>";
   oslOSLDiv.innerHTML = oslGenerateCollapsibleSubDiv("OS Open Names", subDivHTML);

   oslWindow.appendChild(oslOSLDiv);

   // Segment geometry control div
   oslAddLog('create SegGeo control DIV');
   oslSegGeoUIDiv = document.createElement('div');
   oslSegGeoUIDiv.id = "oslSegGeoUIDiv";
   oslSegGeoUIDiv.style.backgroundColor = '#40A040';
   oslSegGeoUIDiv.style.padding = '2px';
   oslSegGeoUIDiv.style.fontSize = '14px';
   oslSegGeoUIDiv.style.lineHeight = '16px';
   oslSegGeoUIDiv.style.display = 'none';

   subDivHTML = oslGenerateOpenRoadsCBHTML(true, "_cbOpenRoadsEnabled", "Highlight by Classification:", 0) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbSegGeoMotorway", "Motorway", 1) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbSegGeoARoad", "A Road", 1) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbSegGeoBRoad", "B Road", 1) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbSegGeoMinor", "Minor Road", 1) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbSegGeoLocal", "Local Road", 1) + '<br>';
   if(oslAdvancedMode == true)
   {
      subDivHTML += oslGenerateOpenRoadsCBHTML(false, "_cbSegGeoLocalAccess", "Local Access Road", 1) + '<br>';
      subDivHTML += oslGenerateOpenRoadsCBHTML(false, "_cbSegGeoRestricted", "Restricted Access Road", 1) + '<br>';
      subDivHTML += oslGenerateOpenRoadsCBHTML(false, "_cbSegGeoSecondary", "Secondary Access Road", 1) + '<br>';
   }
   subDivHTML += '<br>' + oslGenerateOpenRoadsCBHTML(true, "_cbOpenRoadsUseGeo", "Show as polylines", 0) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbOpenRoadsEnhanceGeoVis", "Enhance polyline visibility", 1) + '<br><br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbOpenRoadsHighlightPRN", "Highlight PRN", 1);
   oslSegGeoUIDiv.innerHTML = oslGenerateCollapsibleSubDiv("OS Open Roads", subDivHTML);
   oslWindow.appendChild(oslSegGeoUIDiv);

   // Gazetteer Tags div
   oslAddLog('create GazTags DIV');
   oslGazTagsDiv = document.createElement('div');
   oslGazTagsDiv.id = "oslGazTagsDiv";
   oslGazTagsDiv.style.backgroundColor = '#FFFF80';
   oslGazTagsDiv.style.padding = '2px';
   oslGazTagsDiv.style.fontSize = '14px';
   oslGazTagsDiv.style.lineHeight = '16px';
   oslGazTagsDiv.style.display = 'none';

   subDivHTML = oslGenerateOpenRoadsCBHTML(true, "_cbGazTagsEnabled", "Show by type:", 0) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbGazTagsCity", "City", 1) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbGazTagsTown", "Town", 1) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbGazTagsVillage", "Village", 1) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbGazTagsHamlet", "Hamlet", 1) + '<br>';
   subDivHTML += oslGenerateOpenRoadsCBHTML(true, "_cbGazTagsOther", "Other", 1);
   oslGazTagsDiv.innerHTML = oslGenerateCollapsibleSubDiv("Gazetteer Tags", subDivHTML);
   oslWindow.appendChild(oslGazTagsDiv);

   // NameCheck div
   oslAddLog('create NameCheck DIV');
   oslNCDiv = document.createElement('div');
   oslNCDiv.id = "oslNCDiv";
   oslNCDiv.style.backgroundColor = '#DDDDFF';
   oslNCDiv.style.padding = '2px';
   oslNCDiv.style.fontSize = '14px';
   oslNCDiv.style.lineHeight = '16px';
   oslNCDiv.style.display = 'none';

   subDivHTML = '<label style="display:inline;"><input type="checkbox" id="_cbNCEnabled" />Highlight potential naming errors</label>';
   subDivHTML += '<br><i>Note: only active at at zoom level 16 and above</i>';
   oslNCDiv.innerHTML = oslGenerateCollapsibleSubDiv("NameCheck", subDivHTML);
   oslWindow.appendChild(oslNCDiv);

   // external links div
   oslAddLog('create extern links DIV');
   oslMLCDiv = document.createElement('div');
   oslMLCDiv.id = "oslMLCDiv";
   oslMLCDiv.style.backgroundColor = '#EEFFEE';
   oslMLCDiv.style.padding = '2px';
   oslMLCDiv.style.fontSize = '14px';
   oslMLCDiv.style.lineHeight = '16px';
   
   // add the anchors and auto-track checkboxes for external sites.  Note that some urls are blank at this stage,
   // they'll be filled in by oslMouseMoveAndUp()...
   subDivHTML = '<div id="_extlinksUK" style="display: none;">';
   subDivHTML += '<a href="https://labs.os.uk/prototyping/osel-framework/template/openlayers.html" id="_linkOSOD" target="_osopendata">OS OpenData</a> <input type="checkbox" id="_cbAutoTrackOSOD"></input> | ';
   subDivHTML += '<a href="https://one.network?showlinks=true" id="_linkRWO" target="_roadworksorg">one.network</a> <input type="checkbox" id="_cbAutoTrackRWO"></input><br>';
   subDivHTML += '<div id="lrrCtrls"><a href="https://public.londonworks.gov.uk/roadworks/home" id="_linkLRR" target="_londonregister">London Roadworks</a> <input type="checkbox" id="_cbAutoTrackLRR"></input></div>';
   subDivHTML += '</div>';
   subDivHTML += '<br>(Checkboxes enable auto-tracking)';
   subDivHTML += '<br><br><a href="" id="_linkPermalink">Permalink</a>';
   oslMLCDiv.innerHTML = oslGenerateCollapsibleSubDiv("External resources", subDivHTML);
   oslWindow.appendChild(oslMLCDiv);
}
function oslSetupEventListeners()
{
   oslAddLog('adding event listeners');
   oslDragBar.addEventListener('mousedown', oslOSLDivMouseDown, false);
   oslDragBar.addEventListener('mouseup', oslOSLDivMouseUp, false);
   W.map.events.register("zoomend", null, oslMouseMoveAndUp);
   W.map.events.register("mouseout", null, oslMouseOut);
   document.getElementById('_cbNCEnabled').addEventListener('click', oslNCStateChange, false);
   document.getElementById('_linkOSOD').addEventListener('click', oslOSODClick, false);
   document.getElementById('_linkRWO').addEventListener('click', oslUpdateOneKey, false);
   document.getElementById('_linkLRR').addEventListener('click', oslUpdateLRRKey, false);

   for(let idx = 0; idx < document.getElementsByName('oslOpenRoads_std').length; ++idx)
   {
      document.getElementsByName('oslOpenRoads_std')[idx].addEventListener('click', oslOpenRoadsStateChange, false);
   }
   if(oslAdvancedMode == true)
   {
      for(let idx = 0; idx < document.getElementsByName('oslOpenRoads_adv').length; ++idx)
      {
         document.getElementsByName('oslOpenRoads_adv')[idx].addEventListener('click', oslOpenRoadsStateChange, false);
      }
   }

   for(let idx = 0; idx < document.getElementsByName('oslMinMaxToggle').length; ++idx)
   {
      document.getElementsByName('oslMinMaxToggle')[idx].addEventListener('click', oslUIMinMax, false);
   }

   W.map.events.register("mousemove", null, oslMouseMoveAndUp);
   W.map.events.register("mouseup", null, oslMouseMoveAndUp);

   W.map.getLayerByUniqueName('segments').events.register("featuresadded", null, oslNameCheckTrigger);
   W.map.getLayerByUniqueName('segments').events.register("featuresremoved", null, oslNameCheckTrigger);
}
function oslSetupUIPosition()
{
   oslAddLog('adjusting UI position...');
   document.body.style.overflow = 'hidden';

   let vpHeight = window.innerHeight;
   let vpWidth = window.innerWidth;

   if
   (
      (oslUserPrefs.ui.y === null)||
      (oslUserPrefs.ui.x === null)||
      (oslUserPrefs.ui.y > vpHeight)||
      (oslUserPrefs.ui.x > vpWidth)||
      (oslUserPrefs.ui.y < 0)||
      (oslUserPrefs.ui.x < 0)
   )
   {
      oslOSLDivTop = document.getElementById('sidebar').getBoundingClientRect().top + (document.getElementById('sidebar').getBoundingClientRect().height / 2);
      oslOSLDivLeft = 8;
   }
   else
   {
      oslOSLDivTop = oslUserPrefs.ui.y;
      oslOSLDivLeft = oslUserPrefs.ui.x;
   }

   if(oslUserPrefs.ui.state === undefined) oslUserPrefs.ui.state = 'maximised';
   oslOSLDivTopMinimised = oslOSLDivTop;
   oslWindow.style.left = oslOSLDivLeft+'px';
   oslWindow.style.top = oslOSLDivTop+'px';
   if(oslUserPrefs.ui.state == 'maximised') oslWindowMaximise();
   else oslWindowMinimise();

   oslWindow.style.opacity = 1;   
}
function oslSetLayerZIndices()
{
   oslAddLog('setting layer zIndices...');
   for(let i=0; i < W.map.layers.length; i++)
   {
      if((W.map.layers[i].uniqueName == 'satellite_imagery')||(W.map.layers[i].name == 'satellite_imagery'))
      {
         oslBBDiv.style.zIndex = parseInt(W.map.layers[i].div.style.zIndex) + 1;
         oslSegGeoDiv.style.zIndex = parseInt(W.map.layers[i].div.style.zIndex) + 2;
         oslNamesDiv.style.zIndex = parseInt(W.map.layers[i].div.style.zIndex) + 1000;
      }
      if(W.map.layers[i].name == 'Spotlight') oslOSLMaskLayer = W.map.layers[i].div;
   }
}
function oslSetupAccessKey()
{
   oslAccessKeyUpdate();

   GM_addValueChangeListener("_rwoPositionToWME", function()
   {
      if(arguments[3] === true)
      {
         oslAddLog('WME reposition request from one.network...');
         let tPos = new OpenLayers.LonLat();
         let posBits = arguments[2].split(',');
         tPos.lat = parseFloat(posBits[2]);
         tPos.lon = parseFloat(posBits[3]);
         tPos.transform(new OpenLayers.Projection("EPSG:4326"),new OpenLayers.Projection("EPSG:900913"));
         W.map.setCenter(tPos);
         let tZoom = parseInt(posBits[4]);
         if(tZoom < 12) tZoom = 12;
         if(tZoom > 22) tZoom = 22;

         let cZoom = W.map.getZoom();
         if(tZoom > cZoom)
         {
            oslAddLog('Zoom in from '+cZoom+' to '+tZoom);
            for(let i = 0; i < (tZoom - cZoom); ++i)
            {
               W.map.zoomIn();
            }
         }
         else if(tZoom < cZoom)
         {
            oslAddLog('Zoom out from '+cZoom+' to '+tZoom);
            for(let i = 0; i < (cZoom - tZoom); ++i)
            {
               W.map.zoomOut();
            }
         }
      }
   });

   window.setInterval(oslAccessKeyUpdate,5000);
}
function oslLoadOrInitPrefs()
{
   // See if this system already has something in localStorage
   let userPrefs = localStorage.oslUserPrefs;
   if(userPrefs != undefined)
   {
      oslUserPrefs = JSON.parse(userPrefs);
   }

   // Fill out any undefined parts of the prefs object with default values - this works both to initialise
   // the object if nothing at all is stored in localStorage, and can also be used to initialise any newer
   // parameters that might get added later on which aren't yet defined in the localStorage copy
   if(oslUserPrefs.minmax == undefined) oslUserPrefs.minmax = [];
   if(oslUserPrefs.openRoads == undefined) oslUserPrefs.openRoads = [];
   if(oslUserPrefs.ui == undefined) oslUserPrefs.ui = {};
   if(oslUserPrefs.ui.x == undefined) oslUserPrefs.ui.x = null;
   if(oslUserPrefs.ui.y == undefined) oslUserPrefs.ui.y = null;
   if(oslUserPrefs.ui.state == undefined) oslUserPrefs.ui.state = null;

   // Import any older preferences set on this system, then remove from localStorage - this should only ever
   // occur the first time this version of the script is run after updating an existing setup, so there
   // wouldn't be anything already stored in these three properties that would get wiped out by the import.
   if(localStorage.oslOSLDivState != undefined)
   {
      oslUserPrefs.ui.state = localStorage.oslOSLDivState;
      localStorage.removeItem("oslOSLDivState");
   }
   if(localStorage.oslOSLDivLeft != undefined)
   {
      oslUserPrefs.ui.x = localStorage.oslOSLDivLeft;
      localStorage.removeItem("oslOSLDivLeft");
   }
   if(localStorage.oslOSLDivTop != undefined)
   {
      oslUserPrefs.ui.y = localStorage.oslOSLDivTop;
      localStorage.removeItem("oslOSLDivTop");
   }
}
function oslApplyPrefs()
{
   // subDiv states
   for(let i = 0; i < oslUserPrefs.minmax.length; ++i)
   {
      oslSubDivSetState(oslUserPrefs.minmax[i][0], oslUserPrefs.minmax[i][1]);
   }

   // Open Roads options
   for(let i = 0; i < oslUserPrefs.openRoads.length; ++i)
   {
      let cbID = oslUserPrefs.openRoads[i][0];
      if(document.getElementById(cbID) != undefined)
      {
         document.getElementById(cbID).checked = oslUserPrefs.openRoads[i][1];
      }
   } 
   // Force-disable Open Roads display on startup to avoid problems if the current WME viewport is 
   // showing a more densely mapped area than before...
   document.getElementById('_cbOpenRoadsEnabled').checked = false;
   // Similarly, force-disable Gazetteer Tags display - this isn't quite as resource-sapping as
   // the Open Roads display could be, but it's still preferable to leave it off until the user is
   // ready to show the tags during this session
   document.getElementById('_cbGazTagsEnabled').checked = false;
}
function oslWriteUserPrefs()
{
   localStorage.oslUserPrefs = JSON.stringify(oslUserPrefs);
}
function oslUpdateMinMax(key, value)
{
   let found = false;
   for(let i = 0; i < oslUserPrefs.minmax.length; ++i)
   {
      if(oslUserPrefs.minmax[i][0] == key)
      {
         oslUserPrefs.minmax[i][1] = value;
         found = true;
         break;
      }
   }
   if(found == false)
   {
      oslUserPrefs.minmax.push([key, value]);
   }
   oslWriteUserPrefs();
}
function oslUpdateHighlightCBs(key)
{
   let found = false;
   let value = document.getElementById(key).checked;
   for(let i = 0; i < oslUserPrefs.openRoads.length; ++i)
   {
      if(oslUserPrefs.openRoads[i][0] == key)
      {
         oslUserPrefs.openRoads[i][1] = value;
         found = true;
         break;
      }
   }
   if(found == false)
   {
      oslUserPrefs.openRoads.push([key, value]);
   }
   oslWriteUserPrefs();
}
function oslFinaliseSetup()
{
   oslWazeMapElement = document.getElementById(W.map.getLayerByUniqueName('segments').div.parentElement.id);

   oslEnableAdvancedOptions();
   oslLoadOrInitPrefs();

   oslSetupBBDiv();
   oslSetupORDiv();
   oslSetupNamesDiv();
   oslSetupUI();
   oslSetupEventListeners();      
   oslSetupUIPosition();
   oslSetLayerZIndices();
   oslSetupAccessKey();

   oslApplyPrefs();

   oslOffsetToolbar = document.getElementById('map').contains(document.getElementById('toolbar'));
   window.setInterval(oslTenthSecondTick,100);
   oslDoneOnload = true;
}
function oslWaitForW()
{
   if(document.getElementsByClassName("sandbox").length > 0)
   {
      oslAddLog('WME practice mode detected, script is disabled...');
      return;
   }
   if(document.location.href.indexOf('user') !== -1)
   {
      oslAddLog('User profile page detected, script is disabled...');
      return;
   }

   if(typeof W === "undefined")
   {
      window.setTimeout(oslWaitForW, 1000);
      return;
   }

   if (W.userscripts?.state?.isReady)
   {
      oslFinaliseSetup();
   } 
   else 
   {
      document.addEventListener("wme-ready", oslFinaliseSetup, {once: true});
   }
}
function oslAccessKeyUpdate()
{
   GM_setValue('_rwoAccessKey',Date.now());
}
function oslSNATest()
{
   // Checks the street name abbreviation processing code in oslWazeifyStreetName(),
   // to make sure it still correctly handles names of a form that are particularly
   // problematic, whilst also still correctly handling regular forms...
   const testNames =
   [
      // Standard forms for all the defined abbreviations
      ['Electric Avenue', 'Electric Ave'],
      ['Hollywood Boulevard', 'Hollywood Blvd'],
      ['Ealing Broadway', 'Ealing Bdwy'],
      ['Hillingdon Circus', 'Hillingdon Cir'],
      ['Glenn Close', 'Glenn Cl'],
      ['Crown Court', 'Crown Ct'],
      ['Red Crescent', 'Red Cr'],
      ['Test Drive', 'Test Dr'],
      ['Graham Garden', 'Graham Gdn'],
      ['Kensington Gardens', 'Kensington Gdns'],
      ['Theresa Green', 'Theresa Gn'],
      ['Byker Grove', 'Byker Gr'],
      ['Lois Lane', 'Lois Ln'],
      ['My Cat Mews', 'My Cat Mews'],
      ['Tripod Mount', 'Tripod Mt'],
      ['Last Place', 'Last Pl'],
      ['Gosford Park', 'Gosford Pk'],
      ['Sophy Ridge', 'Sophy Rdg'],
      ['Take The High Road', 'Take The High Rd'],
      ['Four Square', 'Four Sq'],
      ['Baker Street', 'Baker St'],
      ['No Idea Terrace', 'No Idea Ter'],
      ['Happy Valley', 'Happy Val'],
      ['Hyperspace By-pass', 'Hyperspace Bypass'],
      ['This Is The Way', 'This Is The Way'],
      ['Damon Hill', 'Damon Hill'],

      // Forms including cardinals as prefixes
      ["North Street", "North St"],
      ["South Street", "South St"],
      ["East Street", "East St"],
      ["West Street", "West St"],
      ["North Town Street", "North Town St"],
      ["South Town Street", "South Town St"],
      ["East Town Street", "East Town St"],
      ["West Town Street", "West Town St"],
      ["West View Lane", "West View Ln"],

      // Forms including cardinals as suffixes
      ["Aston Boulevard West", "Aston Blvd W"],
      ["Breakspear Road North", "Breakspear Rd N"],
      ["Breakspear Road South", "Breakspear Rd S"],
      ["Breakspear Road East", "Breakspear Rd E"],
      ["Breakspear Road West", "Breakspear Rd W"],

      // Forms including non-cardinal suffixes
      ["High Road Ickenham", "High Rd Ickenham"],

      // Forms including "The"
      ["Orchard On The Green", "Orchard On The Green"],
      ["The Orchard On The Green", "The Orchard On The Green"],
      ["The Avenue", "The Avenue"],

      // Forms including abbreviatable words elsewhere in the name
      ["Green Street", "Green St"],
      ["Kensal Green Way", "Kensal Green Way"],
      ["Green Lane Hill", "Green Lane Hill"],
      ["Court Court", "Court Ct"],
      ["The Street Road", "The Street Rd"],
      ["Great North Road", "Great North Rd"],
      ["North Park Brook Road", "North Park Brook Rd"],

      // Forms where abbreviatable words are found at the start of longer words
      ["Westway Avenue", "Westway Ave"],
      ["Parkway Park", "Parkway Pk"],
      ["Parkway Crescent", "Parkway Cr"],

      // Test for correct abbreviation of Saint -> St.
      ["Saint Albans Way", "St. Albans Way"]
   ];

   let nTests = testNames.length;
   for(let i = 0; i < nTests; ++i)
   {
      let result = oslWazeifyStreetName(testNames[i][0], false);
      if(result !== testNames[i][1])
      {
         console.log(oslWazeifyStreetName(testNames[i][0], true));
      }
   }
}
function oslInitialise()
{
   oslAddLog('initialise()');

   oslSNATest();

   // inject gazetteer data
   let gazscript = document.createElement("script");
   gazscript.setAttribute('type','text/javascript');
   gazscript.setAttribute('charset','UTF-8');
   gazscript.src = oslModifySrc(oslGazetteerURL);
   document.head.appendChild(gazscript);
   oslMergeGazData = true;

   // initialise persistent vars
   sessionStorage.zoom = 0;
   sessionStorage.lat = '';
   sessionStorage.lon = '';
   sessionStorage.myCity = '';
   sessionStorage.prevCity = '';
   sessionStorage.cityChangeEastings = 0;
   sessionStorage.cityChangeNorthings = 0;
   sessionStorage.cityNameRB = 'optUseExisting';
   sessionStorage.oslTabCreated = 0;

   oslWaitForW();
}

// External site helper functions
const hlp_ONE =
{
   // one.network helper functions
   lat: 0,
   lng: 0,
   zoom: 0,
   checkInterval: 10000,
   addLog: function(logtext)
   {
      console.log('ONE: '+logtext);
   },
   checkDatalink: function()
   {
      let lastAccessKey = GM_getValue('_oneAccessKey', null);
      if(lastAccessKey !== null)
      {
         let keyBits = lastAccessKey.split(',');
         if(keyBits.length === 4)
         {
            let keyTS = parseInt(keyBits[0]);
            if((Date.now() - keyTS) <= hlp_ONE.checkInterval)
            {
               // the access key was written recently enough to imply an active WME tab, so reposition if required based on the lat/lon/zoom values in the key
               let lat = parseFloat(keyBits[1]);
               let lng = parseFloat(keyBits[2]);
               let zoom = parseFloat(keyBits[3]);
               if((lat != hlp_ONE.lat) || (lng != hlp_ONE.lng) || (zoom != hlp_ONE.zoom))
               {
                  hlp_ONE.lat = lat;
                  hlp_ONE.lng = lng;
                  hlp_ONE.zoom = zoom - 1;
                  hlp_ONE.relocate();
               }
               hlp_ONE.checkInterval = 1000; // reduce the interval after the first check, to minimise the period where manually panning/zooming is overridden
            }
         }
      }
      setTimeout(hlp_ONE.checkDatalink, 50);
   },
   relocate: function()
   {
      Elgin.map.setCenter([hlp_ONE.lng, hlp_ONE.lat]);
      Elgin.map.setZoom(hlp_ONE.zoom);
   },
   clickToWME: function()
   {
      let elginLat = Elgin.map.getCenter().lat;
      let elginLon = Elgin.map.getCenter().lng;
      let elginZoom = Elgin.map.getZoom();
      
      if(elginZoom < 12) elginZoom = 12;
      if(elginZoom > 22) elginZoom = 22;
      elginZoom += 1;

      // check for an active WME tab...
      let tabFound = false;
      let lastAccessKey = GM_getValue('_rwoAccessKey', null);
      if(lastAccessKey !== null)
      {
         lastAccessKey = parseInt(lastAccessKey);
         if((Date.now() - lastAccessKey) <= 10000)
         {
            // the access key was written within the last 10s which implies an active tab, so send it the repositioning data
            let toWrite = Date.now()+','+lastAccessKey+','+elginLat+','+elginLon+','+elginZoom;
            GM_setValue('_rwoPositionToWME',toWrite);
            tabFound = true;
         }
      }

      if(tabFound === false)
      {
         // couldn't find a WME tab to reposition, so open a new one...
         let wmeURL = 'https://www.waze.com/editor?';
         wmeURL += 'lon='+elginLon;
         wmeURL += '&lat='+elginLat;
         wmeURL += '&zoomLevel='+elginZoom;
         window.open(wmeURL);
      }
   },
   init: function()
   {
      hlp_ONE.addLog('initialise()');
      hlp_ONE.addLog('waiting for map objects...');
      let waitSomeMore = false;

      try
      {
         if(Elgin === undefined)
         {
            waitSomeMore = true;
         }
         else if (Elgin.map === undefined)
         {
            waitSomeMore = true;
         }

         if(google === undefined)
         {
            waitSomeMore = true;
         }
         else if(google.maps === undefined)
         {
            waitSomeMore = true;
         }
      }
      catch
      {
         waitSomeMore = true;
      }

      if(document.getElementById('map-canvas') === null)
      {
         waitSomeMore = true;
      }

      if(waitSomeMore)
      {
         window.setTimeout(hlp_ONE.init,500);
         return;
      }

      hlp_ONE.addLog('all required objects found...');

      // Add "open in WME" button...
      let tBtnDiv = document.createElement('div');
      tBtnDiv.id = 'rwoWMELink';
      let tHTML = '<div class="gmnoprint" draggable="false" controlwidth="40" controlheight="40" style="margin: 10px; user-select: none; position: absolute; bottom: 200px; right: 40px;">';
      tHTML += '<div class="gmnoprint" controlwidth="40" controlheight="40" style="position: absolute; left: 0px; top: 0px;">';
      tHTML += '<div draggable="false" style="user-select: none; box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 4px -1px; border-radius: 2px; cursor: pointer; background-color: rgb(255, 255, 128); width: 40px; height: 40px;">';
      tHTML += '<button draggable="false" title="Open in WME" aria-label="Open in WME" type="button" class="gm-control-active" style="background: none; display: block; border: 0px; margin: 0px; padding: 0px; position: relative; cursor: pointer; user-select: none; overflow: hidden; width: 40px; height: 40px; top: 0px; left: 0px;"><strong>WME</strong></button>';
      tHTML += '</div></div></div>';
      tBtnDiv.innerHTML = tHTML;
      document.getElementById('map-canvas').firstChild.appendChild(tBtnDiv);

      document.getElementById('rwoWMELink').addEventListener('click', hlp_ONE.clickToWME, false);
      setTimeout(hlp_ONE.checkDatalink, 500);
   }
};

const hlp_LRR =
{
   // London Roadworks Register helper functions
   lat: 0,
   lng: 0,
   zoom: 0,
   checkInterval: 10000,
   addLog: function(logtext)
   {
      console.log('LRR: '+logtext);
   },
   checkDatalink: function()
   {
      let lastAccessKey = GM_getValue('_lrrAccessKey', null);
      if(lastAccessKey !== null)
      {
         let keyBits = lastAccessKey.split(',');
         if(keyBits.length === 4)
         {
            let keyTS = parseInt(keyBits[0]);
            if((Date.now() - keyTS) <= hlp_LRR.checkInterval)
            {
               // the access key was written recently enough to imply an active WME tab, so reposition if required based on the lat/lon/zoom values in the key
               let lat = parseFloat(keyBits[1]);
               let lng = parseFloat(keyBits[2]);
               let zoom = parseFloat(keyBits[3]);
               if((lat != hlp_LRR.lat) || (lng != hlp_LRR.lng) || (zoom != hlp_LRR.zoom))
               {
                  hlp_LRR.lat = lat;
                  hlp_LRR.lng = lng;
                  hlp_LRR.zoom = zoom - 12;
                  if(hlp_LRR.zoom > 5)
                  {
                     hlp_LRR.zoom = 5;
                  }
                  hlp_LRR.relocate();
               }
               hlp_LRR.checkInterval = 1000; // reduce the interval after the first check, to minimise the period where manually panning/zooming is overridden
            }
         }
      }
      setTimeout(hlp_LRR.checkDatalink, 500);
   },
   relocate: function()
   {
      let waitSomeMore = false;

      if(map === undefined)
      {
         waitSomeMore = true;
      }
      if(OpenLayers === undefined)
      {
         waitSomeMore = true;
      }
      if(waitSomeMore)
      {
         return;
      }

      map.setCenter(new OpenLayers.LonLat(hlp_LRR.lng,hlp_LRR.lat));
      map.zoomTo(hlp_LRR.zoom);
   },
   init: function()
   {
      // trap the page reload that occurs when the map view is generated...
      if(document.location.href.indexOf('home') == -1)
      {
         hlp_LRR.addLog('page reloaded during map generation...');
         setTimeout(hlp_LRR.checkDatalink, 500);   
         return;
      }

      hlp_LRR.addLog('initialise()');
      hlp_LRR.addLog('waiting for search page to load...');
      var waitSomeMore = false;
      if(document.getElementsByName('mapResults')[0].length < 1)
      {
         waitSomeMore = true;
      }
      if(document.getElementsByName('postCode')[0].length < 1)
      {
         waitSomeMore = true;
      }

      if(waitSomeMore === true)
      {
         window.setTimeout(hlp_LRR.init,500);
         return;
      }

      hlp_LRR.addLog('accessing map...');
      // first set a "safe" postcode as the search criteria - whilst asking for the map view to be generated without any
      // search terms usually works OK, from time to time the site throws a wobbler and refuses to do anything without
      // having the search narrowed down a bit...
      document.getElementsByName('postCode')[0].value="EC1A 1AA";
      document.getElementsByName('mapResults')[0].click();

      setTimeout(hlp_LRR.checkDatalink, 500);
   }
};

const hlp_ODM =
{
   // OS OpenData map helper functions
   lat: 0,
   lng: 0,
   zoom: 0,
   checkInterval: 10000,
   addLog: function(logtext)
   {
      console.log('ODM: '+logtext);
   },
   checkDatalink: function()
   {
      let lastAccessKey = GM_getValue('_osodAccessKey', null);
      if(lastAccessKey !== null)
      {
         let keyBits = lastAccessKey.split(',');
         if(keyBits.length === 4)
         {
            let keyTS = parseInt(keyBits[0]);
            if((Date.now() - keyTS) <= hlp_ODM.checkInterval)
            {
               // the access key was written recently enough to imply an active WME tab, so reposition if required based on the lat/lon/zoom values in the key
               let lat = parseFloat(keyBits[1]);
               let lng = parseFloat(keyBits[2]);
               let zoom = parseFloat(keyBits[3]);
               if((lat != hlp_ODM.lat) || (lng != hlp_ODM.lng) || (zoom != hlp_ODM.zoom))
               {
                  hlp_ODM.lat = lat;
                  hlp_ODM.lng = lng;
                  hlp_ODM.zoom = zoom;
                  hlp_ODM.relocate();
               }
               hlp_ODM.checkInterval = 1000; // reduce the interval after the first check, to minimise the period where manually panning/zooming is overridden
            }
         }
      }
      setTimeout(hlp_ODM.checkDatalink, 500);
   },
   fakeOnload: function()
   {
      // maximise the map view
      document.getElementsByClassName('content')[0].style.top='0px';
      document.getElementsByClassName('content')[0].style.height='100%';
      map.updateSize();
      document.getElementsByClassName('osel-sliding-side-panel panel-left')[0].style.display='none';
      document.getElementsByClassName('header')[0].style.display='none';
      if(document.getElementsByClassName('ol-layer').length > 1)
      {
         document.getElementsByClassName('ol-layer')[1].style.display='none';
      }
      document.getElementsByClassName('osel-control-container container-bottom container-right')[0].style.display='none';
      document.getElementsByClassName('layer-element')[0].children[0].click();
      // set the desired map style...
      document.getElementsByClassName('datahub-os-maps road')[0].click();
      setTimeout(hlp_ODM.checkDatalink, 500);
   },
   relocate: function()
   {
      map.getView().setCenter([hlp_ODM.lng, hlp_ODM.lat]);
      map.getView().setZoom(hlp_ODM.zoom);
   },
   init: function()
   {
      hlp_ODM.addLog('initialise()');
      if(map === undefined) window.setTimeout(hlp_ODM.init,100);
      else hlp_ODM.fakeOnload();
   }
};

oslBootstrap();