WME OpenData

Provides access to certain OS OpenData products within the WME environment

当前为 2023-03-22 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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*
// @include             https://editor-beta.waze.com/*
// @include             https://beta.waze.com/*
// @include             https://one.network/*
// @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.21
// ==/UserScript==

// Contains Ordnance Survey data Crown copyright and database right 2022
//
// 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
=======================================================================================================================
WME compatibility fix

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


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


=======================================================================================================================
Proposed functionality
=======================================================================================================================
Restrict city name uniqueness testing to within each county rather than country-wide


=======================================================================================================================
New functionality in progress
======================================================================================================================= 

*/

/* 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 */
/* jshint bitwise: false */
/* jshint evil: true */
/* jshint esversion: 11 */

const oslVersion = '3.21';
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 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'
);

// include names that don't get abbreviated, so that for names such as "Somewhere Green Way", the
// abbreviation function is able to detect the "Way" part of the name as the part that ought to
// be abbreviated, rather than treating it as a non-abbreviatable suffix and instead abbreviating
// the "Green" part instead...
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'
);


var oslUserPrefs = {};

var oslGazetteerData = [];

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

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

var oslBlocksToLoad = [];
var oslBlocksToTest = [];

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

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

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

var oslWazeBitsPresent = 0;

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

var oslOffsetToolbar = false;
var oslMOAdded = false;

var oslLocatorElements = null;
var oslUsingNewName = false;
var oslUseName = false;
var oslCityName = '';
var oslCountyName = '';
var oslUseAlt = false;
var oslAltNamesClearing = false;

var oslRORCenter = null;
var oslRORZoom = null;

var oslWaitingForMapToStopMoving = false;


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);
}

//-----------------------------------------------------------------------------------------------------------------------------------------
// 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 ([email protected])
// *
// * 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 = 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);

   lonlat_sw.transform(new OpenLayers.Projection("EPSG:4326"),new OpenLayers.Projection("EPSG:900913"));
   lonlat_se.transform(new OpenLayers.Projection("EPSG:4326"),new OpenLayers.Projection("EPSG:900913"));
   lonlat_nw.transform(new OpenLayers.Projection("EPSG:4326"),new OpenLayers.Projection("EPSG:900913"));
   lonlat_ne.transform(new OpenLayers.Projection("EPSG:4326"),new OpenLayers.Projection("EPSG:900913"));

   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 == 1) || (mode == 2))
   {
      boxPos = oslGetBBCornerPixels(boxW, boxE, boxS, boxN);
   }

   if(mode === 0)
   {
      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 == 1)
   {
      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 == 2)
   {
      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 == 3)
   {
      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);
   }

   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);
   }
   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],2);
                  }
               }
            }
         }
      }
   }
}
function oslRadioClick()
{
   let oslElements = document.getElementById('oslRoadNameMatches');
   let selectedName = '';

   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')
                     {
                        roadData = unsafeWindow['locatorData_'+oslID[1]+'_'+oslID[2]][oslID[3]];
                     }
                     else
                     {
                        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,0);
                        oslVisualiseBoundingBox(locatorElements[OSL_ELM.BoundW],locatorElements[OSL_ELM.BoundE],locatorElements[OSL_ELM.BoundS],locatorElements[OSL_ELM.BoundN],1);

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

   if(selectedName === '')
   {
      oslBBDiv.innerHTML = '';
      return;
   }
   oslVisualiseBoundingBox(0,0,0,0,3);
}
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');
   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')
                     {
                        roadData = unsafeWindow['locatorData_'+oslID[1]+'_'+oslID[2]][oslID[3]];
                     }
                     else
                     {
                        for(let i = 0; i < OSL_ELM.MAX; ++i)
                        {
                           roadData += ':';
                        }
                     }
                     oslLocatorElements = oslSplitEntry(roadData);

                     // auto-click the Edit Address link...
                     document.getElementsByClassName('full-address')[0].click();
                     oslAltNamesClearing = false;
                     oslWaitAddressElementOpen();
                  }
               }
            }
         }
      }
   }
}

// To get rid of that damned annoying list of suggested names, which if left open prevents
// the newly entered name from being applied, we wait just a short time to let things
// settle, then call a helper function which first returns focus to the street field itself,
// and then double-clicks on the TTS button to toggle the TTS playback popup open and closed.
//
// If the auto-complete list for either the street name or city name is left open when the
// apply button is clicked, the names entered in these fields won't actually get applied.  To
// get around this slighy annoyance of how the segment naming UI now behaves, we persuade the
// list to close by first focussing on the name entry field again, and then double clicking on
// the TTS button - this acts to take focus completely away from the name being edited, thus
// causing the list to close just as if the user had manually clicked away from the name field.
//
// Why, you may ask, don't I just therefore do a scripted click away from the field?  Having
// asked, you may now feel free to assume that, given the script doesn't simply do this, I've
// already tried it and found the edit UI resistant to such a simplistic attempt at getting
// one up on it...
//
// Do the WME devs deliberately think up weird and wonderful UI interaction pathways like
// this to annoy script writers, or is it just an unintentional bonus they get out of the way
// the native UI now behaves?

let oslCanAutoApply = false;
function oslClearSegNameAutoComplete()
{
   let osr = document.getElementsByClassName("street-name")[0].shadowRoot;
   let nameElm = osr.getElementById('text-input');
   nameElm.focus();
   oslDoubleClickTTS();
}
function oslClearCityNameAutoComplete()
{
   let osr = document.getElementsByClassName("city-name")[0].shadowRoot;
   let snelm = osr.getElementById('text-input');
   snelm.focus();
   oslDoubleClickTTS();
}
function oslClearAltSegNameAutoComplete()
{
   let osr = document.getElementsByClassName("alt-street-name")[0].shadowRoot;
   let nameElm = osr.getElementById('text-input');
   nameElm.focus();
   oslDoubleClickTTS();
}
function oslClearAltCityNameAutoComplete()
{
   let osr = document.getElementsByClassName("alt-city-name")[0].shadowRoot;
   let snelm = osr.getElementById('text-input');
   snelm.focus();
   oslDoubleClickTTS();
   oslCanAutoApply = true;
}
function oslDoubleClickTTS()
{
   let ttsBtn = document.getElementsByClassName('tts-button')[0];
   ttsBtn.disabled = false;
   ttsBtn.click();
   ttsBtn.click();
}
function oslAutoApplyNameChange()
{
   if(oslCanAutoApply === true)
   {
      document.getElementsByClassName('address-form')[0].getElementsByClassName('save-button')[0].click();
   }
   else
   {
      window.setTimeout(oslAutoApplyNameChange, 100);
   }
}
function oslApplyAltStreetName()
{
   let proceed = null;
   let osr = null;
   let nameElm = null;

   proceed = (document.getElementsByClassName('alt-street-name').length > 0);
   if(proceed === true)
   {
      osr = document.getElementsByClassName('alt-street-name')[0].shadowRoot;
      proceed = proceed && (osr !== null);
   }
   if(proceed === true)
   {
      nameElm = osr.getElementById('text-input');
      proceed = proceed && (nameElm !== null);
   }

   if(proceed === true)
   {
      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);
      nameElm.disabled = false;
      nameElm.value = altName;
      nameElm.dispatchEvent(new Event('input', { 'bubbles': true }));
      window.setTimeout(oslClearAltSegNameAutoComplete, 10);

      let snelm = document.getElementsByClassName('empty-city')[1];
      let osr = document.getElementsByClassName('alt-city-name')[0].shadowRoot;
      if(oslUsingNewName)
      {
         if(oslUseName)
         {
            snelm.checked = false;
            snelm.dispatchEvent(new Event('change', { 'bubbles': true }));

            snelm = osr.getElementById('text-input');
            snelm.focus();
            snelm.value = oslCityName;
            snelm.disabled = false;
            snelm.dispatchEvent(new Event('input', { 'bubbles': true }));
            snelm.blur();
            snelm.click();            
         }
      }
      else if(document.getElementById('optClearExisting').checked === true)
      {
         /*
         snelm.disabled = true;
         snelm.dispatchEvent(new Event('change', { 'bubbles': true }));
         */
         snelm = document.getElementsByClassName('empty-city')[1];
         snelm.checked = true;
         snelm.dispatchEvent(new Event('change', { 'bubbles': true }));
      }
      window.setTimeout(oslClearAltCityNameAutoComplete, 10);
   }
   else
   {
      window.setTimeout(oslApplyAltStreetName, 10);
   }
}
function oslWaitAddressElementOpen()
{
   let editPanel = document.getElementsByClassName('address-form')[0];
   if(editPanel.getBoundingClientRect().height == 0)
   {
      window.setTimeout(oslWaitAddressElementOpen, 500);
      return;
   }

   // Delete any altnames that are already present
   let anDels = document.getElementsByClassName('alt-street-delete w-icon w-icon-trash');
   if(anDels.length > 0)
   {
      for(let i = (anDels.length - 1); i >= 0; --i)
      {
         anDels[i].click();
      }
      window.setTimeout(oslWaitAddressElementOpen, 500);
      return;
   }

   // make sure the Country field is set to "United Kingdom"
   let snelm = document.getElementsByClassName('country-id')[0];
   if(snelm != undefined)
   {
      if(snelm.value != 234) snelm.value = 234;
      snelm.dispatchEvent(new Event('change', { 'bubbles': true }));
   }

   // fill in the City field
   snelm = document.getElementsByClassName('city-name')[0];
   if(snelm != undefined)
   {
      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)
         {
            snelm = document.getElementsByClassName('empty-city')[0];
            snelm.checked = false;
            snelm.dispatchEvent(new Event('change', { 'bubbles': true }));

            let osr = document.getElementsByClassName("city-name")[0].shadowRoot;
            snelm = osr.getElementById('text-input');
            snelm.focus();
            snelm.value = oslCityName;
            snelm.disabled = false;
            snelm.dispatchEvent(new Event('input', { 'bubbles': true }));
            snelm.blur();
            snelm.click();
            window.setTimeout(oslClearCityNameAutoComplete, 10);
            
            if(oslCountyName !== '')
            {
               // set the county field if one was provided with the selected city name
               let cOpts = document.getElementsByClassName('state-id')[0].getElementsByTagName('wz-option');
               for(let countyIdx = 0; countyIdx < cOpts.length; countyIdx++)
               {
                  if(cOpts[countyIdx].innerText == oslCountyName)
                  {
                     document.getElementsByClassName('state-id')[0].value = cOpts[countyIdx].value;
                     document.getElementsByClassName('state-id')[0].dispatchEvent(new Event('change', { 'bubbles': true }));
                     break;
                  }
               }
            }
         }
      }
      else if(document.getElementById('optClearExisting').checked === true)
      {
         sessionStorage.cityNameRB = 'optClearExisting';
         snelm.disabled = true;
         snelm.dispatchEvent(new Event('change', { 'bubbles': true }));
         snelm = document.getElementsByClassName('empty-city')[0];
         snelm.checked = true;
         snelm.dispatchEvent(new Event('change', { 'bubbles': true }));
      }
      else
      {
         sessionStorage.cityNameRB = 'optUseExisting';
      }
   }

   if(oslUseAlt === true)
   {
      let tName = oslLocatorElements[OSL_ELM.RoadName];
      oslLocatorElements[OSL_ELM.RoadName] = oslLocatorElements[OSL_ELM.AltName];
      oslLocatorElements[OSL_ELM.AltName] = tName;
   }

   // finally 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;
      
      snelm = document.getElementsByClassName('empty-street')[0];
      snelm.checked = false;
      snelm.dispatchEvent(new Event('change', { 'bubbles': true }));

      let osr = document.getElementsByClassName("street-name")[0].shadowRoot;
      let nameElm = osr.getElementById('text-input');
      nameElm.disabled = false;
      nameElm.value = oslName;
      nameElm.dispatchEvent(new Event('input', { 'bubbles': true }));
      window.setTimeout(oslClearSegNameAutoComplete, 10);
   }
   else
   {
      snelm = document.getElementsByClassName('street-name')[0];
      snelm.value = '';
      snelm.disabled = true;
      snelm.dispatchEvent(new Event('change', { 'bubbles': true }));
      snelm = document.getElementsByClassName('empty-street')[0];
      snelm.checked = true;
      snelm.dispatchEvent(new Event('change', { 'bubbles': true }));
   }

   oslCanAutoApply = true;
   if(oslLocatorElements[OSL_ELM.AltName] != '')
   {
      oslCanAutoApply = false;
      document.getElementsByClassName('add-alt-street-btn')[0].click();
      window.setTimeout(oslApplyAltStreetName, 10);
   }

   if(oslAdvancedMode)
   {
      // auto-click the Apply button, after a suitable delay to allow the UI changes
      // to percolate through to where they're needed before we try applying anything...
      window.setTimeout(oslAutoApplyNameChange, 100);
   }
}
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;

   // 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);
   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 = [];
            for(let cityObj in W.model.cities.objects)
            {
               if(W.model.cities.objects.hasOwnProperty(cityObj))
               {
                  let cityname = W.model.cities.objects[cityObj].attributes.name;
                  if(cityname !== '')
                  {
                     let countyID = [];
                     countyID[0] = W.model.cities.objects[cityObj].attributes.stateID;
                     let countyName = W.model.states.getByIds(countyID)[0].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 = 0;

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

   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","9");
         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 = document.getElementById(seg.geometry.id);
            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(true);
   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);
   geoCenter.transform(new OpenLayers.Projection("EPSG:900913"),new OpenLayers.Projection("EPSG:4326"));
   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;

         if
         (
            (seg.geometry.bounds.left > mapExtents.right) ||
            (seg.geometry.bounds.right < mapExtents.left) ||
            (seg.geometry.bounds.top < mapExtents.bottom) ||
            (seg.geometry.bounds.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.name;
               let gid = seg.geometry.id;
               let pline = document.getElementById(gid);

               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.geometry.components[0].x,seg.geometry.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.geometry.components[seg.geometry.components.length-1].x,seg.geometry.components[seg.geometry.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 = [];
   if(W.selectionManager.selectedItems === undefined)
   {
      oslSelectedItems = W.selectionManager._selectedFeatures;
   }
   else
   {
      oslSelectedItems = W.selectionManager.selectedItems;
   }
   if(oslSelectedItems.length == 1)
   {
      if(oslPrevSelected === null) oslDoOSLUpdate = true;
      else if(oslSelectedItems[0].model.attributes.id != oslPrevSelected) oslDoOSLUpdate = true;
      oslPrevSelected = oslSelectedItems[0].model.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(oslRefreshAutoTrack)
   {
      // refresh any of the site tabs/windows we've checked for auto-tracking and which don't use the datalink
      if(oslInUK === true)
      {
         if(oslInLondon === true)
         {
            if(document.getElementById('_cbAutoTrackLRR').checked == 1) window.open(oslLRR_url,'_londonregister');
         }
      }
      oslRefreshAutoTrack = false;
   }

   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(true);
      }
   }   
}
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);
   lonlat.transform(new OpenLayers.Projection("EPSG:4326"),new OpenLayers.Projection("EPSG:900913"));
   return W.map.getPixelFromLonLat(lonlat);
}
function oslOSGridRefToPixel(osEast, osNorth)
{
   oslOSGBtoWGS(osEast,osNorth);
   let lonlat = new OpenLayers.LonLat(oslLongitude,oslLatitude);
   lonlat.transform(new OpenLayers.Projection("EPSG:4326"),new OpenLayers.Projection("EPSG:900913"));
   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 === 0) || (mode == 1))
   {
      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 === 0)
      {         
         // 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 == 2)
   {
      // finalise SVG
      oslSegGeoDivInnerHTML += '</svg>';
      oslSegGeoDiv.innerHTML = oslSegGeoDivInnerHTML;
   }
   else if(mode == 3)
   {
      // erase SVG
      oslSegGeoDiv.innerHTML = '';
   }

   return 1;
}
function oslRepositionOverlays()
{
   let divTop = getComputedStyle(W.map.segmentLayer.div).top;
   let divLeft = getComputedStyle(W.map.segmentLayer.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);
   return W.map.getLonLatFromPixel(pixelPos);
}
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)) return;
   let mouseX = e.pageX - document.getElementById('map').getBoundingClientRect().left;
   let mouseY = e.pageY - document.getElementById('map').getBoundingClientRect().top;
  
   oslMousePixelpos = new OpenLayers.Pixel(mouseX, mouseY);
   oslLastViewportWidth = W.map.getViewport().getBoundingClientRect().width;
   oslMousepos = oslGetCorrectedLonLatFromPixelPos(mouseX, mouseY, true).transform(new OpenLayers.Projection("EPSG:900913"),new OpenLayers.Projection("EPSG:4326"));
   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 = false;
      if(W.selectionManager.selectedItems !== undefined)
      {
         noneSelected = (W.selectionManager.selectedItems.length === 0);
      }
      else
      {
         noneSelected = (W.selectionManager._selectedFeatures.length === 0);
      }
      if(noneSelected === true)
      {
         for(let slIdx=0; slIdx < W.map.segmentLayer.features.length; slIdx++)
         {
            if(W.map.segmentLayer.features[slIdx].renderIntent == 'highlight')
            {
               if(slIdx != oslPrevHighlighted)
               {
                  oslPrevHighlighted = slIdx;
               }
               oslDoOSLUpdate = true;
               oslSegmentHighlighted = true;
            }
         }
      }
   }

   let gomc = oslGetOffsetMapCentre();
   let geoCenterTransform = gomc.transform(new OpenLayers.Projection("EPSG:900913"),new OpenLayers.Projection("EPSG:4326"));
   let lat = geoCenterTransform.lat;
   let lon = geoCenterTransform.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 = null;
      if((W.model.countries.top === undefined)||(W.model.countries.top === null))
      {
         if((W.model.countries.additionalInfo !== null) && (W.model.countries.additionalInfo !== undefined))
         {
            country = W.model.countries.additionalInfo[0].name;
         }
      }
      else
      {
         country = W.model.countries.top.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)
      {
         // calculate the OS eastings/northings for the current WME centrepoint - this is required by the London Roadworks site...
         var osCoords = oslToOSGrid(lat,lon,OSL_MODE.Conversion);

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

         // translate the zoom level between WME and London Roadworks Register
         var lrrzoom = zoom - 12;
         if(lrrzoom > 5) lrrzoom = 5;
         // generate the roadworks register URL
         oslLRR_url = 'http://public.londonworks.gov.uk/roadworks/home'+osCoords+'&z='+lrrzoom;
      }

      // 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);

      // update the link URLs
      if(oslInUK === true)
      {
         document.getElementById("_linkLRR").href = oslLRR_url;
      }
      document.getElementById('_linkPermalink').href = document.getElementsByClassName('WazeControlPermalink')[0].getElementsByClassName('permalink')[0].href;

      // refreshing the tabs within the event handler causes Chrome to switch focus to the tabs, so we
      // simply set the flag here and let the refresh occur within the 100ms tick handler as before
      oslRefreshAutoTrack = true;
   }
   
   if((e.type == "mouseup") || (e.type == "zoomend"))
   {
      oslMapIsStaticProcessing(false);
   }
}
function oslSetAccessKey(keyName, requiresTransform)
{
   let gomc = oslGetOffsetMapCentre();
   if(requiresTransform === true)
   {
      gomc = gomc.transform(new OpenLayers.Projection("EPSG:900913"),new OpenLayers.Projection("EPSG:4326"));
   }
   let zoom = W.map.getZoom();
   GM_setValue(keyName,Date.now()+','+gomc.lat.toFixed(4)+','+gomc.lon.toFixed(4)+','+zoom);
}
function oslOSODClick()
{
   oslSetAccessKey('_osodAccessKey', false);
}
function oslUpdateOneKey()
{
   oslSetAccessKey('_oneAccessKey', true);
}
function oslMapIsStaticProcessing(forceUpdate)
{
   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)
      )
      {
         oslWaitingForMapToStopMoving = true;
         oslRORCenter = tCenter;
         oslRORZoom = tZoom;
         window.setTimeout(oslMapIsStaticProcessing, 250);
         return;
      }

      if((oslWaitingForMapToStopMoving == false) && (forceUpdate == false))
      {
         return;
      }
      oslWaitingForMapToStopMoving = false;

      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,3);
         }
         
         oslGetVisibleCityNames();
      }
      else
      {
         oslHighlightOpenRoads(0,0,3);
      }
      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(false);
   }
}
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(true);
}
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.segmentLayer.div).top;
   oslBBDiv.style.left = getComputedStyle(W.map.segmentLayer.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.segmentLayer.div).top;
   oslSegGeoDiv.style.left = getComputedStyle(W.map.segmentLayer.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.segmentLayer.div).top;
   oslNamesDiv.style.left = getComputedStyle(W.map.segmentLayer.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="" 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);

   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.segmentLayer.events.register("featuresadded", null, oslNameCheckTrigger);
   W.map.segmentLayer.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.segmentLayer.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 oslWazeBits()
{
   oslAddLog('adding WazeBits...'+oslWazeBitsPresent);
   if((oslWazeBitsPresent & 0x01) === 0)
   {
      if(W.selectionManager != undefined)
      {
         oslAddLog('   W.selectionManager OK');
         oslWazeBitsPresent |= 0x01;
      }
   }

   if((oslWazeBitsPresent & 0x02) === 0)
   {
      if(OpenLayers != undefined)
      {
         oslAddLog('   OpenLayers OK');
         oslWazeBitsPresent |= 0x02;
      }
   }

   if((oslWazeBitsPresent & 0x04) === 0)
   {
      if(W.map?.segmentLayer?.div?.parentElement?.id != undefined)
      {
         if(document.getElementById(W.map.segmentLayer.div.parentElement.id) !== null)
         {
            oslAddLog('   WazeMap OK');
            oslWazeBitsPresent |= 0x04;
            oslWazeMapElement = document.getElementById(W.map.segmentLayer.div.parentElement.id);
         }
      }
   }

   if((oslWazeBitsPresent & 0x08) === 0)
   {
      if(W.model?.countries != undefined)
      {
         oslAddLog('   W.model.countries OK');
         oslWazeBitsPresent |= 0x08;
      }
   }

   if((oslWazeBitsPresent & 0x10) === 0)
   {
      if(W.map?.layers != undefined)
      {
         oslAddLog('   W.map.layers OK');
         oslWazeBitsPresent |= 0x10;
      }
   }

   if(oslWazeBitsPresent != 0x1F)
   {
      window.setTimeout(oslWazeBits,250);
   }
   else
   {
      oslFinaliseSetup();
   }
}
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(W === undefined)
   {
      window.setTimeout(oslWaitForW, 1000);
      return;
   }

   if(parseFloat(W.version.split("-")[0].split("v")[1]) < 2.140)
   {
      oslAddLog("WME version < 2.140, using legacy startup code");
      oslWazeBits();
   }
   else
   {
      oslAddLog("WME version >= 2.140, using new startup code");
      if (W.userscripts?.state?.isReady)
      {
         oslFinaliseSetup();
      } 
      else 
      {
         document.addEventListener("wme-ready", oslFinaliseSetup, {once: true});
      }
   }   
}
function oslAccessKeyUpdate()
{
   GM_setValue('_rwoAccessKey',Date.now());
}
function oslInitialise()
{
   oslAddLog('initialise()');

   let nameTestMode = false;
   // oslWazeifyStreetName() functionality test code
   if(nameTestMode === true)
   {
      console.log(oslWazeifyStreetName("North Street", true));
      console.log(oslWazeifyStreetName("South Street", true));
      console.log(oslWazeifyStreetName("East Street", true));
      console.log(oslWazeifyStreetName("West Street", true));
      console.log(oslWazeifyStreetName("North Town Street", true));
      console.log(oslWazeifyStreetName("South Town Street", true));
      console.log(oslWazeifyStreetName("East Town Street", true));
      console.log(oslWazeifyStreetName("West Town Street", true));
      console.log(oslWazeifyStreetName("Green Street", true));
      console.log(oslWazeifyStreetName("The Boulevard", true));
      console.log(oslWazeifyStreetName("Aston Boulevard West", true));
      console.log(oslWazeifyStreetName("Saint Albans Way", true));
      console.log(oslWazeifyStreetName("Orchard On The Green", true));
      console.log(oslWazeifyStreetName("The Orchard On The Green", true));
      console.log(oslWazeifyStreetName("The Avenue", true));
      console.log(oslWazeifyStreetName("High Road Ickenham", true));
      console.log(oslWazeifyStreetName("Westway Avenue", true));
      console.log(oslWazeifyStreetName("Parkway Park", true));
      console.log(oslWazeifyStreetName("Parkway Crescent", true));
      console.log(oslWazeifyStreetName("Breakspear Road North", true));
      console.log(oslWazeifyStreetName("Breakspear Road South", true));
      console.log(oslWazeifyStreetName("Breakspear Road East", true));
      console.log(oslWazeifyStreetName("Breakspear Road West", true));
      console.log(oslWazeifyStreetName("Kensal Green Way", true));
      console.log(oslWazeifyStreetName("Great North Road", true));
      return;
   }

   // inject gazetteer data
   let gazscript = document.createElement("script");
   gazscript.setAttribute('type','text/javascript');
   gazscript.setAttribute('charset','UTF-8');
   gazscript.src = 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, 500);
   },
   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: 170px; 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
   addLog: function(logtext)
   {
      console.log('LRR: '+logtext);
   },
   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 - redirecting to second stage initialisation...');
         hlp_LRR.initPartDeux();
         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;
      }

      // extract the coords/zoom from the url...
      let userloc = document.location.href;
      let epos = userloc.indexOf("?e=");
      let npos = userloc.indexOf("&n=");
      let zpos = userloc.indexOf("&z=");
      if((epos != -1)&&(npos != -1)&&(zpos != -1))
      {
         let lrrLon = userloc.substr(epos+3,npos-(epos+3));
         let lrrLat = userloc.substr(npos+3,zpos-(npos+3));
         let lrrZoom = userloc.substr(zpos+3,2);
         sessionStorage.setItem('lrrLon',lrrLon);
         sessionStorage.setItem('lrrLat',lrrLat);
         sessionStorage.setItem('lrrZoom',lrrZoom);

         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();
      }
   },
   initPartDeux: function()
   {
      hlp_LRR.addLog('waiting for map objects...');
      let waitSomeMore = false;

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

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

      if(waitSomeMore)
      {
         window.setTimeout(hlp_LRR.initPartDeux,500);
         return;
      }

      hlp_LRR.addLog('all required objects found...');
      let lrrLon = parseInt(sessionStorage.getItem('lrrLon'));
      let lrrLat = parseInt(sessionStorage.getItem('lrrLat'));
      let lrrZoom = parseInt(sessionStorage.getItem('lrrZoom'));
      map.setCenter(new OpenLayers.LonLat(lrrLon,lrrLat));
      map.zoomTo(lrrZoom);
      hlp_LRR.addLog('map repositioned');
   }
};

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();