// AuctionSearchKit - "Rightmove on Google Maps" User Script
// Version 2.0
// 2009-10-21
// Copyright (c) 2009, Auction Search Kit. All rights reserved.
// Feedback to auctionsearchkit@gmail.com is welcome.
//
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install, you need Greasemonkey: http://greasemonkey.mozdev.org/
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// To uninstall, go to Tools/Manage User Scripts,
// select "Rightmove on Google Maps", and click Uninstall.
//
// --------------------------------------------------------------------
//
// ==UserScript==
// @name          Rightmove on Google Maps
// @namespace     http://www.auctionsearchkit.co.uk
// @description   Adds links to Rightmove pages to show properties on a Google Map. From v1.7 includes other mapping tools and websites mentioned in a MoneySavingExpert article.
// @include       http://www.rightmove.co.uk/*
// ==/UserScript==

// Constants
var DEBUG_MODE = false;
var CHAR_CODE_A = 65; // The ASCII code for A, which will be the first marker on the map

var ABOUT_MY_PLACE_URL_BASE = 'http://www.aboutmyplace.co.uk/showmap?mt=i&type=property&poi=property&id=';
var GOOGLE_MAP_URL_BASE = 'http://maps.google.co.uk/maps?t=m&f=q';
var MULTIMAP_SINGLE_URL_BASE = 'http://www.multimap.com/maps/?zoom=16';
var BING_MULTI_URL_BASE = 'http://www.bing.com/maps/default.aspx?rtp=';
var BING_SINGLE_URL_BASE = 'http://www.bing.com/maps/?v=2&style=r&where1=';
var BING_3D_URL_BASE = 'http://www.bing.com/maps/?v=2&style=b&cp=';
var FLOODINGMAP_URL_BASE = 'http://maps.environment-agency.gov.uk/wiyby/wiybyController?ep=map&lang=_e&x=<x>&y=<y>&topic=floodmap#x=<x>&y=<y>&lg=1,&scale=5';
var FLOODRISK_URL_BASE = 'http://maps.environment-agency.gov.uk/wiyby/wiybyController?ep=query&floodrisk=0&lang=_e&topic=floodmap&floodX=<x>&floodY=<y>';
var HACIENDA_URL_BASE = 'http://www.hacienda.co.uk/map.aspx?zoom=15&t=s0&r=0&l=0&u=200000000';
var HOMETRACK_URL_BASE = 'http://www.hometrack.co.uk/hometrack/products/property-search/index.cfm?searchKey=';
var HOUSEPRICESCOUK_BASE = 'http://www.houseprices.co.uk/e.php?n=100&q=';
var LANDREG_URL_BASE = 'http://www1.landregistry.gov.uk/houseprices/housepriceindex/report/default.asp?step=2&locationType=0&area=&image3.x=22&image3.y=5&postcode=';
var MOUSEPRICE_URL_START = 'http://www.mouseprice.com/house-prices/land-registry/';
var MOUSEPRICE_URL_END = '?Mode=SP';
var NEIGHBOURHOODSTATS_URL_BASE = 'http://www.neighbourhood.statistics.gov.uk/dissemination/NeighbourhoodSummary.do?width=1280&a=7&r=1&i=1001&m=0&s=1254947383742&enc=1&profileSearchText=';
var NETHOUSEPRICES_URL_BASE = 'http://www.nethouseprices.com/index.php?con=sold_prices_street&cCode=EW'
var OFSTED_URL_START = 'http://www.ofsted.gov.uk/oxcare_providers/postcode_search/%28type%29/1,31/%28typename%29/Childminders/%28typeclass%29/care/%28postcode%29/';
var OFSTED_URL_END = '/%28radius%29/5%20miles/%28object_step%29/10?type=4096|Primary+schools|edu&register=&radius=5+miles&postcode=';
var ORDNANCE_SURVEY_URL_BASE = 'http://getamap.ordnancesurvey.co.uk/getamap/frames.htm?mapAction=gaz&gazName=g&gazString=';
var STREETMAP_REV_GEO_URL = 'http://www.streetmap.co.uk/streetmap.dll';
var STREETMAP_REV_GEO_PARAMS = 'MfcISAPICommand=GridConvert&type=LatLong&name=';
var STREETMAP_REV_GEO_PC_REGEXP = /<strong>Nearest Post Code<\/strong> <\/td> <td width="50%" align="center" valign="middle">\s*([^<]*)/i;
var STREETMAP_SINGLE_URL_BASE = 'http://www.streetmap.co.uk/map.srf?';
var UPMYSTREET_URL_BASE = 'http://www.upmystreet.com/properties/house-prices/search?year=2000&location=';
var UKLOCALAREA_URL_BASE = 'http://www.uklocalarea.com/index.php?q=';
var YAHOO_MAP_URL_BASE = 'http://uk.maps.yahoo.com/#mvt=m';
var ZOOPLA_URL_START = 'http://www.zoopla.co.uk/house-prices/';
var ZOOPLA_URL_END = '/?sold_price_types=all&sold_price_years=all&so=date&sd=desc';

// If in DEBUG mode and FireBug is installed, define a logging function using the FireBug console
if ((DEBUG_MODE === true) && (unsafeWindow.console)) {
  // Log a variable's value via the Firebug console (if debug mode is turned on)
  function fbLog(name, value) {
    if (DEBUG_MODE === true) {
      switch (typeof value) {
        case 'undefined':
          unsafeWindow.console.log(name + ' is undefined');
          break;
        case 'object':
          if (value === null) {
            unsafeWindow.console.log(name + ' is null');
          } else {
            if (value.constructor === Date) {
              unsafeWindow.console.log(name + ' = ' + value);
            } else if (typeof value.length == 'undefined') {
              unsafeWindow.console.log(name + ':');
            } else if (value.length === 0) {
              unsafeWindow.console.log(name + ' is empty (length = 0)');
            } else {
              unsafeWindow.console.log(name + ' (length = ' + value.length + '):');
            }
            unsafeWindow.console.dir(value);
          }
          break;
        case 'string':
          unsafeWindow.console.log(name + ' = "' + value + '"');
          break;
        default:
          unsafeWindow.console.log(name + ' = ' + value);
          break;
      }
    }
  }
} else {
  // Assign a function that does nothing whenever a logging call is made
  function fbLog() {}
}

// Enclose everything in a try...catch block for ease of debugging
try {

  // Initialise globals
  var mapLinks = [];
  var propertyLinks = [];
  var coordsToIdMap = [];
  var mapLinksReceived = 0;

  // Helper functions for converting latitude/longitude to OS Grid References below.
  // Taken from http://www.bdcc.co.uk/LatLngToOSGB.js
  // Linked to from http://mapki.com/wiki/Tools:Snippets

  function OGBLatLng(lat, lon)
  {
    this.lat = lat;
    this.lon = lon;
  }

  function OGBNorthEast(east, north)
  {
    this.north = north;
    this.east = east;
  }

  //convert WGS84 Latitude and Longitude to Ordnance Survey 1936 Latitude Longitude

  function WGS84ToOGB(WGlat, WGlon, height)
  {
  var deg2rad = Math.PI / 180;
  var rad2deg = 180.0 / Math.PI;

  //first off convert to radians
  var radWGlat = WGlat * deg2rad;
  var radWGlon = WGlon * deg2rad;
  //these are the values for WGS84(GRS80) to OSGB36(Airy)
  var a = 6378137; // WGS84_AXIS
  var e = 0.00669438037928458; // WGS84_ECCENTRIC
  var h = height; // height above datum (from $GPGGA sentence)
  var a2 = 6377563.396; // OSGB_AXIS
  var e2 = 0.0066705397616; // OSGB_ECCENTRIC 
  var xp = -446.448;
  var yp = 125.157;
  var zp = -542.06;
  var xr = -0.1502;
  var yr = -0.247;
  var zr = -0.8421;
  var s = 20.4894;

  // convert to cartesian; lat, lon are in radians
  var sf = s * 0.000001;
  var v = a / (Math.sqrt(1 - (e * (Math.sin(radWGlat) * Math.sin(radWGlat)))));
  var x = (v + h) * Math.cos(radWGlat) * Math.cos(radWGlon);
  var y = (v + h) * Math.cos(radWGlat) * Math.sin(radWGlon);
  var z = ((1 - e) * v + h) * Math.sin(radWGlat);

  // transform cartesian
  var xrot = (xr / 3600) * deg2rad;
  var yrot = (yr / 3600) * deg2rad;
  var zrot = (zr / 3600) * deg2rad;
  var hx = x + (x * sf) - (y * zrot) + (z * yrot) + xp;
  var hy = (x * zrot) + y + (y * sf) - (z * xrot) + yp;
  var hz = (-1 * x * yrot) + (y * xrot) + z + (z * sf) + zp;

  // Convert back to lat, lon
  var newLon = Math.atan(hy / hx);
  var p = Math.sqrt((hx * hx) + (hy * hy));
  var newLat = Math.atan(hz / (p * (1 - e2)));
  v = a2 / (Math.sqrt(1 - e2 * (Math.sin(newLat) * Math.sin(newLat))));
  var errvalue = 1.0;
  var lat0 = 0;
  while (errvalue > 0.001)
  {
  lat0 = Math.atan((hz + e2 * v * Math.sin(newLat)) / p);
  errvalue = Math.abs(lat0 - newLat);
  newLat = lat0;
  }

  //convert back to degrees
  newLat = newLat * rad2deg;
  newLon = newLon * rad2deg;

  return new OGBLatLng(newLat, newLon);

  }

  //converts lat and lon (OSGB36) to OS northings and eastings
  function LLtoNE(lat, lon)
  {
  var deg2rad = Math.PI / 180;
  var rad2deg = 180.0 / Math.PI;

  var phi = lat * deg2rad; // convert latitude to radians
  var lam = lon * deg2rad; // convert longitude to radians
  var a = 6377563.396; // OSGB semi-major axis
  var b = 6356256.91; // OSGB semi-minor axis
  var e0 = 400000; // easting of false origin
  var n0 = -100000; // northing of false origin
  var f0 = 0.9996012717; // OSGB scale factor on central meridian
  var e2 = 0.0066705397616; // OSGB eccentricity squared
  var lam0 = -0.034906585039886591; // OSGB false east
  var phi0 = 0.85521133347722145; // OSGB false north
  var af0 = a * f0;
  var bf0 = b * f0;

  // easting
  var slat2 = Math.sin(phi) * Math.sin(phi);
  var nu = af0 / (Math.sqrt(1 - (e2 * (slat2))));
  var rho = (nu * (1 - e2)) / (1 - (e2 * slat2));
  var eta2 = (nu / rho) - 1;
  var p = lam - lam0;
  var IV = nu * Math.cos(phi);
  var clat3 = Math.pow(Math.cos(phi), 3);
  var tlat2 = Math.tan(phi) * Math.tan(phi);
  var V = (nu / 6) * clat3 * ((nu / rho) - tlat2);
  var clat5 = Math.pow(Math.cos(phi), 5);
  var tlat4 = Math.pow(Math.tan(phi), 4);
  var VI = (nu / 120) * clat5 * ((5 - (18 * tlat2)) + tlat4 + (14 * eta2) - (58 * tlat2 * eta2));
  var east = e0 + (p * IV) + (Math.pow(p, 3) * V) + (Math.pow(p, 5) * VI);

  // northing
  var n = (af0 - bf0) / (af0 + bf0);
  var M = Marc(bf0, n, phi0, phi);
  var I = M + (n0);
  var II = (nu / 2) * Math.sin(phi) * Math.cos(phi);
  var III = ((nu / 24) * Math.sin(phi) * Math.pow(Math.cos(phi), 3)) * (5 - Math.pow(Math.tan(phi), 2) + (9 * eta2));
  var IIIA = ((nu / 720) * Math.sin(phi) * clat5) * (61 - (58 * tlat2) + tlat4);
  var north = I + ((p * p) * II) + (Math.pow(p, 4) * III) + (Math.pow(p, 6) * IIIA);

  // make whole number values
  east = Math.round(east); // round to whole number of meters
  north = Math.round(north); 

  return new OGBNorthEast(east, north);
  }

  //convert northing and easting to letter and number grid system
  function NE2NGR( east,  north)
  {
  east = Math.round(east);
  north = Math.round(north);
  var eX = east / 500000;
  var nX = north / 500000;
  var tmp = Math.floor(eX) - 5.0 * Math.floor(nX) + 17.0; 
  nX = 5 * (nX - Math.floor(nX));
  eX = 20 - 5.0 * Math.floor(nX) + Math.floor(5.0 * (eX - Math.floor(eX)));
  if (eX > 7.5) eX = eX + 1; // I is not used
  if (tmp > 7.5) tmp = tmp + 1; // I is not used

  var eing = east - (Math.floor(east / 100000)*100000);
  var ning = north - (Math.floor(north / 100000)*100000);
  var estr = eing.toString();
  var nstr = ning.toString();
  while(estr.length < 5)
    estr = "0" + estr;
  while(nstr.length < 5)
    nstr = "0" + nstr;

  var ngr = String.fromCharCode(tmp + 65) + 
            String.fromCharCode(eX + 65) + 
            estr + nstr; // Note: Spaces removed here for compatibility with Ordnance Survery URL

  return ngr;
  }

  //helper
  function Marc( bf0,  n,  phi0,  phi)
  {
  return bf0 * (((1 + n + ((5 / 4) * (n * n)) + ((5 / 4) * (n * n * n))) * (phi - phi0))
  - (((3 * n) + (3 * (n * n)) + ((21 / 8) * (n * n * n))) * (Math.sin(phi - phi0)) * (Math.cos(phi + phi0)))
  + ((((15 / 8) * (n * n)) + ((15 / 8) * (n * n * n))) * (Math.sin(2 * (phi - phi0))) * (Math.cos(2 * (phi + phi0))))
  - (((35 / 24) * (n * n * n)) * (Math.sin(3 * (phi - phi0))) * (Math.cos(3 * (phi + phi0)))));
  }

  // My own helper function for easily converting from latitude/longitude to OS grid Eastings/Northings
  function WGS84ToOGBNorthEast(lat, lon) {
    var ogbLatLng = WGS84ToOGB(lat, lon, 0);
    var ogbNorthEast = LLtoNE(ogbLatLng.lat, ogbLatLng.lon);
    return ogbNorthEast;
  }

  // Inserts either a space or a line break after the specified link object, depending on context
  function InsertSpaceOrBR(linkObj, useParentsParent) {
    if (linkObj.element.id == 'link-map') {
      var newBr = document.createElement('span');
      newBr.innerHTML = '&nbsp;';
    } else {
      var newBr = document.createElement('br');
    }
    
    if (useParentsParent === true) {
      linkObj.element.parentNode.parentNode.insertBefore(newBr, linkObj.element.parentNode.nextSibling);
    } else {
      linkObj.element.parentNode.insertBefore(newBr, linkObj.element.nextSibling);
    }
  }
  
  // Find all map and property links
  for (var linkIndex = 0; linkIndex < document.links.length; linkIndex++) {
    var link = document.links[linkIndex];
    // To avoid repeated links, only process those that do not have an image contained within
    if (link.getElementsByTagName('img').length == 0) {
      // Find mapping links using aboutmyplace
      if (/aboutmyplace\.co\.uk/.test(link.href) == true) {
        mapLinks[mapLinks.length] = {
            element: link,
            url: link.href};
      // Find Rightmove property links.
      // To avoid repeated links, only process those that have a span contained within them.
      } else if (link.getElementsByTagName('span').length > 0) {
        // Look for a Rightmove property ID in the URL
        var matches = /property-([0-9]+)\.html/.exec(link.href);
        // If the ID was successfully extracted, construct a new map link to aboutmyplace from it
        if ((matches != null) && (matches.length > 1)) { 
          propertyLinks[propertyLinks.length] = {
              element: link,
              url: ABOUT_MY_PLACE_URL_BASE + matches[1]};
        }
      }
    }
  }

  fbLog('mapLinks', mapLinks);
  fbLog('propertyLinks', propertyLinks);

  if (mapLinks.length > 0) {
    var pageHeaderElement = document.getElementById('pageheader');
    if (pageHeaderElement != null) {
      var spanElements = pageHeaderElement.getElementsByClassName('address');
      if (spanElements.length > 0) {
        var origHeaderElement = spanElements[0];
        var origHeaderText = origHeaderElement.innerHTML;
        origHeaderElement.innerHTML += ' [Looking up postcode...]';
      }
    }
  }
  for (var linkIndex = 0; linkIndex < mapLinks.length; linkIndex++) {
    (function(linkObj) {
      GM_xmlhttpRequest({
        method:	'GET',
        url: linkObj.url,
        onload: function(responseDetails) {
          // Change aboutmyplace link text
          linkObj.element.innerHTML = '[aboutmyplace]';

          var matches = /GetPointMap\('([-.0-9]+)', '([-.0-9]+)'/.exec(responseDetails.responseText);
          if ((matches != null) && (matches.length > 2)) {
            fbLog("matches[2] + ',' + matches[1]", matches[2] + ',' + matches[1]);
            if (linkObj.element.id != 'link-map') {
              // If this is the link object on the right pane of the screen, remove the text after the link.
              // Do this 3 times to remove all the text.
              for (var index = 0; index < 5; index++) {
                linkObj.element.parentNode.removeChild(linkObj.element.nextSibling);
              }
            }

            // Create new link to Yahoo Maps
            var newLink = document.createElement('a');
            newLink.href = YAHOO_MAP_URL_BASE + '&zoom=16&q1=' + matches[2] + ',' + matches[1] + '&lat=' + matches[2] + ',' + '&lon=' + matches[1];
            newLink.innerHTML = '[Yahoo Map]';
            newLink.rel = 'nofollow';
            newLink.target = 'yahooMapWindow';
            newLink.title = 'View this property on a Yahoo Map (opens new window)';
            
            // Add new link to Yahoo Maps
            linkObj.element.parentNode.insertBefore(newLink, linkObj.element.nextSibling);
            InsertSpaceOrBR(linkObj, false);
            
            // As I'm relying on code I haven't tested I'll put this in a try...catch
            try {
              var ogbNorthEast = WGS84ToOGBNorthEast(matches[2], matches[1]);
              var ngr = NE2NGR(ogbNorthEast.east, ogbNorthEast.north);

              // Create new link to Streetmap
              var newLink = document.createElement('a');
              newLink.href = STREETMAP_SINGLE_URL_BASE + 'x=' + ogbNorthEast.east + '&y=' + ogbNorthEast.north;
              newLink.innerHTML = '[Streetmap]';
              newLink.rel = 'nofollow';
              newLink.target = 'streetmapWindow';
              newLink.title = 'View this property on Streetmap (opens new window)';
              
              // Add new link to Streetmap
              linkObj.element.parentNode.insertBefore(newLink, linkObj.element.nextSibling);
              InsertSpaceOrBR(linkObj, false);

              // Create new link to Ordnance Survery
              var newLink = document.createElement('a');
              newLink.href = ORDNANCE_SURVEY_URL_BASE + ngr;
              newLink.innerHTML = '[OS Map]';
              newLink.rel = 'nofollow';
              newLink.target = 'ordnanceSurveyWindow';
              newLink.title = 'View this property on an Ordnance Survey map (opens new window)';
              
              // Add new link to Ordnance Survey
              linkObj.element.parentNode.insertBefore(newLink, linkObj.element.nextSibling);
              InsertSpaceOrBR(linkObj, false);
            } catch(err) {
              fbLog('Error', err);
            }
            
            // Create new link to Multimap
            var newLink = document.createElement('a');
            newLink.href = MULTIMAP_SINGLE_URL_BASE + '&qs=' + matches[2] + ',' + matches[1];
            newLink.innerHTML = '[Multimap]';
            newLink.rel = 'nofollow';
            newLink.target = 'multimapWindow';
            newLink.title = 'View this property on a Multimap (opens new window)';
            
            // Add new link to Multimap
            linkObj.element.parentNode.insertBefore(newLink, linkObj.element.nextSibling);
            InsertSpaceOrBR(linkObj, false);
            
            // Create new link to Hacienda
            var newLink = document.createElement('a');
            newLink.href = HACIENDA_URL_BASE + '&lat=' + matches[2] + '&lng=' + matches[1];
            newLink.innerHTML = '[Hacienda]';
            newLink.rel = 'nofollow';
            newLink.target = 'haciendaWindow';
            newLink.title = 'View this property on a Hacienda Amenities Map (opens new window)';
            
            // Add new link to Hometrack
            linkObj.element.parentNode.insertBefore(newLink, linkObj.element.nextSibling);
            InsertSpaceOrBR(linkObj, false);

            // Create new link to Google Maps
            var newLink = document.createElement('a');
            newLink.href = GOOGLE_MAP_URL_BASE + '&z=15&q=' + matches[2] + ',' + matches[1];
            newLink.innerHTML = '[Google Map]';
            newLink.rel = 'nofollow';
            newLink.target = 'googleMapWindow';
            newLink.title = 'View this property on a Google Map (opens new window)';
            
            // Add new link to Google Maps
            linkObj.element.parentNode.insertBefore(newLink, linkObj.element.nextSibling);
            InsertSpaceOrBR(linkObj, false);
            
            // Create new link to flooding map
            if ((typeof ogbNorthEast == 'object') && (ogbNorthEast != null)) {
              var newLink = document.createElement('a');
              newLink.href = FLOODINGMAP_URL_BASE.replace(/<x>/g, ogbNorthEast.east).replace(/<y>/g, ogbNorthEast.north);
              newLink.innerHTML = '[Flooding Map]';
              newLink.rel = 'nofollow';
              newLink.target = 'floodingMapWindow';
              newLink.title = 'View a map of flooding risk near this postal code (opens new window)';
              
              // Add new link to flooding map
              linkObj.element.parentNode.insertBefore(newLink, linkObj.element.nextSibling);
              InsertSpaceOrBR(linkObj, false);
            }

            // Create new link to Bing 3D Map
            var newLink = document.createElement('a');
            newLink.href = BING_3D_URL_BASE + matches[2] + '~' + matches[1];
            newLink.innerHTML = '[Bing 3D Map]';
            newLink.rel = 'nofollow';
            newLink.target = 'bingMapWindow';
            newLink.title = 'View this property on a Bing 3D Map (opens new window)';
            
            // Add new link to Bing 3D Map
            linkObj.element.parentNode.insertBefore(newLink, linkObj.element.nextSibling);
            InsertSpaceOrBR(linkObj, false);
            
            // Get nearest postal code to the latitude/longitude.
            // Ensure this is only done once for the page by checking the link ID.
            if (linkObj.element.id != 'link-map') {
              (function(matches) {
                GM_xmlhttpRequest({
                  method:	'POST',
                  url: STREETMAP_REV_GEO_URL,
                  headers:{'Content-type':'application/x-www-form-urlencoded'},
                  data: encodeURI(STREETMAP_REV_GEO_PARAMS + matches[2] + ',' + matches[1]),
                  onload: function(responseDetails) {
                    var postalCodeUpdated = false;
                    if (responseDetails != null) {
                      var postcodeMatches = STREETMAP_REV_GEO_PC_REGEXP.exec(responseDetails.responseText);
                      if (postcodeMatches != null) {
                        // Trim all leading/trailing spaces from the postal code
                        var postalCode = postcodeMatches[1].toUpperCase().replace(/^\s+|\s+$/g, '');
                        if (typeof spanElements == 'object') {
                          // Check whether the current header ends with the first part of the postcode - if so only add on
                          // the end of the postcode, otherwise add on all of it with a comma.
                          if ((postalCode.length > 4) &&
                              (origHeaderText.toUpperCase().substring(origHeaderText.length - (postalCode.length - 4)) ==
                               postalCode.substring(0, postalCode.length - 4))) {
                            spanElements[0].innerHTML = origHeaderText.substring(0, origHeaderText.length - (postalCode.length - 4)) + postalCode;
                          } else {
                            spanElements[0].innerHTML = origHeaderText + ', ' + postalCode;
                          }
                          postalCodeUpdated = true;
                        }

                        // Strip space(s) from postal code
                        var postalCodeNoSpace = postalCode.replace(' ', '');
                        
                        // Create new link to Zoopla
                        var newLink = document.createElement('a');
                        newLink.href = ZOOPLA_URL_START + postalCodeNoSpace + ZOOPLA_URL_END;
                        newLink.innerHTML = '[Zoopla]';
                        newLink.rel = 'nofollow';
                        newLink.target = 'zooplaWindow';
                        newLink.title = 'View past house prices for this postal code (opens new window)';
                        
                        // Add new link to Zoopla
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        // Create new link to Upmystreet
                        var newLink = document.createElement('a');
                        newLink.href = UPMYSTREET_URL_BASE + postalCodeNoSpace;
                        newLink.innerHTML = '[Upmystreet]';
                        newLink.rel = 'nofollow';
                        newLink.target = 'upmystreetWindow';
                        newLink.title = 'View past house prices etc for this postal code (opens new window)';
                        
                        // Add new link to Upmystreet
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        // Create new link to UK Local Area
                        var newLink = document.createElement('a');
                        newLink.href = UKLOCALAREA_URL_BASE + postalCodeNoSpace;
                        newLink.innerHTML = '[UK Local Area]';
                        newLink.rel = 'nofollow';
                        newLink.target = 'ukLocalAreaWindow';
                        newLink.title = 'View local area statistics for this postal code (opens new window)';
                        
                        // Add new link to UK Local Area
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        // Create new link to Ofsted
                        var newLink = document.createElement('a');
                        newLink.href = OFSTED_URL_START + postalCodeNoSpace + OFSTED_URL_END + postalCodeNoSpace;
                        newLink.innerHTML = '[Ofsted Schools]';
                        newLink.rel = 'nofollow';
                        newLink.target = 'ofstedWindow';
                        newLink.title = 'View schools near this postal code (opens new window)';
                        
                        // Add new link to Ofsted
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        if (postalCodeNoSpace.length > 3) {
                          var outcode = postalCodeNoSpace.substring(0, postalCodeNoSpace.length - 3);
                          var incode = postalCodeNoSpace.substring(postalCodeNoSpace.length - 3);

                          // Create new link to Nethouseprices
                          var newLink = document.createElement('a');
                          newLink.href = NETHOUSEPRICES_URL_BASE + '&outcode=' + outcode + '&incode=' + incode;
                          newLink.innerHTML = '[Nethouseprices]';
                          newLink.rel = 'nofollow';
                          newLink.target = 'nethousepricesWindow';
                          newLink.title = 'View past house prices for roads in this area (opens new window)';
                        }
                        
                        // Add new link to Nethouseprices
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        // Create new link to Neighbourhood Statistics
                        var newLink = document.createElement('a');
                        newLink.href = NEIGHBOURHOODSTATS_URL_BASE + postalCodeNoSpace;
                        newLink.innerHTML = '[Neighbourhood Statistics]';
                        newLink.rel = 'nofollow';
                        newLink.target = 'neighbourhoodStatsWindow';
                        newLink.title = 'View neighbourhood statistics for this postal code (opens new window)';
                        
                        // Add new link to Neighbourhood Statistics
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        // Create new link to Mouseprice
                        var newLink = document.createElement('a');
                        newLink.href = MOUSEPRICE_URL_START + postalCodeNoSpace + MOUSEPRICE_URL_END;
                        newLink.innerHTML = '[Mouseprice]';
                        newLink.rel = 'nofollow';
                        newLink.target = 'mousepriceWindow';
                        newLink.title = 'View past house prices for this postal code (opens new window)';
                        
                        // Add new link to Mouseprice
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        // Create new link to the Land Registry
                        var newLink = document.createElement('a');
                        newLink.href = LANDREG_URL_BASE + postalCodeNoSpace;
                        newLink.innerHTML = '[Land Registry]';
                        newLink.rel = 'nofollow';
                        newLink.target = 'landRegistryWindow';
                        newLink.title = 'View Land Registry house price index for this region (opens new window)';
                        
                        // Add new link to the Land Registry
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        // Create new link to houseprices.co.uk
                        var newLink = document.createElement('a');
                        newLink.href = HOUSEPRICESCOUK_BASE + postalCodeNoSpace;
                        newLink.innerHTML = '[Houseprices.co.uk]';
                        newLink.rel = 'nofollow';
                        newLink.target = 'housepricescoukWindow';
                        newLink.title = 'View past house prices for this postal code (opens new window)';
                        
                        // Add new link to houseprices.co.uk
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        // Create new link to Hometrack
                        var newLink = document.createElement('a');
                        newLink.href = HOMETRACK_URL_BASE + postalCodeNoSpace;
                        newLink.innerHTML = '[Hometrack]';
                        newLink.rel = 'nofollow';
                        newLink.target = 'hometrackWindow';
                        newLink.title = 'View past house prices for this postal code (opens new window)';
                        
                        // Add new link to Hometrack
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                        
                        if ((typeof ogbNorthEast == 'object') && (ogbNorthEast != null)) {
                          // Create new link to flooding risk report
                          var newLink = document.createElement('a');
                          newLink.href = FLOODRISK_URL_BASE.replace(/<x>/g, ogbNorthEast.east).replace(/<y>/g, ogbNorthEast.north);
                          newLink.innerHTML = '[Flooding Risk]';
                          newLink.rel = 'nofollow';
                          newLink.target = 'floodingriskWindow';
                          newLink.title = 'View a report of the flooding risk at this postal code (opens new window)';
                          
                          // Add new link to flooding risk report
                          linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                          InsertSpaceOrBR(linkObj, true);
                        }

                        // Create new link to flooding risk report
                        var newLink = document.createElement('strong');
                        newLink.innerHTML = 'Useful links (see <a target="moneysavingexpert" href="http://www.moneysavingexpert.com/mortgages/free-house-prices">moneysavingexpert</a>):';
                        
                        // Add new link to flooding risk report
                        linkObj.element.parentNode.parentNode.insertBefore(newLink, linkObj.element.parentNode.nextSibling);
                        InsertSpaceOrBR(linkObj, true);
                      }
                    }
                
                    if ((postalCodeUpdated == false) && (typeof spanElements == 'object')) {
                      spanElements[0].innerHTML = origHeaderText;
                    }
                  }
                });
              })(matches);
            }
          }
        }
      });
    })(mapLinks[linkIndex]);
  }

  var numPropertiesElement = document.getElementById('numberOfProperties');
  if ((numPropertiesElement != null) && (propertyLinks.length > 0)) {
    // Set base Yahoo Map URL
    var propertyYahooMapUrl = YAHOO_MAP_URL_BASE;

    // Set base Google Map URL
    var propertyGoogleMapUrl = GOOGLE_MAP_URL_BASE;
    
    // Set base Bing Map URL depending on whether there are one or more properties
    var propertyBingUrl = (propertyLinks.length > 1) ? 
                          BING_MULTI_URL_BASE : 
                          BING_SINGLE_URL_BASE;

    // Create Yahoo Map link placeholder
    var yahooMapPropsElement = document.createElement('a');
    yahooMapPropsElement.innerHTML = '';

    // Create Google Maps link placeholder
    var googleMapPropsElement = document.createElement('a');
    googleMapPropsElement.innerHTML = '[Mapping properties...0/' + propertyLinks.length + ']';
    
    // Create Bing Map link placeholder
    var bingPropsElement = document.createElement('a');
    bingPropsElement.innerHTML = '';

    // Add Yahoo Maps link placeholder
    numPropertiesElement.parentNode.insertBefore(yahooMapPropsElement, numPropertiesElement.nextSibling);
    var newBr = document.createElement('span');
    newBr.innerHTML = '&nbsp;';
    numPropertiesElement.parentNode.insertBefore(newBr, numPropertiesElement.nextSibling);

    // Add Google Maps link placeholder
    numPropertiesElement.parentNode.insertBefore(googleMapPropsElement, numPropertiesElement.nextSibling);
    var newBr = document.createElement('span');
    newBr.innerHTML = '&nbsp;';
    numPropertiesElement.parentNode.insertBefore(newBr, numPropertiesElement.nextSibling);

    // Add Bing Map link placeholder
    numPropertiesElement.parentNode.insertBefore(bingPropsElement, numPropertiesElement.nextSibling);
    var newBr = document.createElement('span');
    newBr.innerHTML = '&nbsp;';
    numPropertiesElement.parentNode.insertBefore(newBr, numPropertiesElement.nextSibling);

    for (var linkIndex = 0; linkIndex < propertyLinks.length; linkIndex++) {
      (function(linkObj) {
        GM_xmlhttpRequest({
          method:	'GET',
          url: linkObj.url,
          onload: function(responseDetails) {
            mapLinksReceived++;
            googleMapPropsElement.innerHTML = '[Mapping properties...' + mapLinksReceived + '/' + propertyLinks.length + ']';
            
            // Add the map coordinates to the link object
            var matches = /GetPointMap\('([-.0-9]+)', '([-.0-9]+)'/.exec(responseDetails.responseText);
            if ((matches != null) && (matches.length > 2)) {
              linkObj.coords = matches[2] + ',' + matches[1];
            }
            
            // If this is the last property link, create a link to display all the properties on Google Maps
            if (mapLinksReceived == propertyLinks.length) {
              // Add the coordinates for each link to the Google Maps URL
              var coordsToMarkerNum = [];
              var numOfMarkers = 0;
              for (var linkIndex = 0; linkIndex < propertyLinks.length; linkIndex++) {
                if (typeof propertyLinks[linkIndex].coords != 'undefined') {
                  // Find out whether these coordinates have already been seen. If not, add them
                  // to the Google Maps URL and increment the current marker number
                  var markerNum = coordsToMarkerNum[propertyLinks[linkIndex].coords];
                  if (typeof markerNum == 'undefined') {
                    markerNum = numOfMarkers;
                    coordsToMarkerNum[propertyLinks[linkIndex].coords] = markerNum;
                    var propertyTitle = '';
                    var propertyPrice = '';
                    try {
                      propertyTitle = propertyLinks[linkIndex].element.innerHTML
                                          .replace(/<[^>]*>/g, ' ')
                                          .replace(/[^-, A-Z0-9]/gi, '')
                                          .replace(/^\s+|\s+$/g, '')
                                          .replace(/\s+/g, ' ')
                                          .replace(' house ', ' ')
                                          .replace('bedroom', 'bed')
                                          .replace('semi-detached', 'semi')
                                          .replace(' for sale', '');
                      propertyPrice = propertyLinks[linkIndex].element.parentNode
                                          .nextSibling.nextSibling.innerHTML
                                          .replace(/[^,0-9]/g, '');
                    } catch(err) {
                      fbLog('Error', err);
                    }
                    
                    // Update Bing URL
                    if (propertyLinks.length > 1) {
                      var titleForBing = (propertyPrice == '' ? '' : propertyPrice + ': ') + propertyTitle;
                      propertyBingUrl += ((numOfMarkers > 0) ? '~' : '') + 'pos.'
                      propertyBingUrl += propertyLinks[linkIndex].coords.replace(',', '_')
                                       + '_' + titleForBing;
                    } else {
                      propertyBingUrl += propertyLinks[linkIndex].coords;
                    }

                    // Update Google Map URL
                    var titleForGoogle = (propertyPrice == '' ? '' : '£' + propertyPrice + ': ') + propertyTitle;
                    if (numOfMarkers == 0) {
                      propertyGoogleMapUrl += '&saddr=' + titleForGoogle + '@' + propertyLinks[linkIndex].coords;
                    } else if (numOfMarkers == 1) {
                      propertyGoogleMapUrl += '&daddr=' + titleForGoogle + '@' + propertyLinks[linkIndex].coords;
                    } else {
                      propertyGoogleMapUrl += '+to:' + titleForGoogle + '@' + propertyLinks[linkIndex].coords;
                    }

                    // Update Yahoo Map URL
                    propertyYahooMapUrl += '&q' + (numOfMarkers + 1) + '=' + propertyLinks[linkIndex].coords;
                    
                    numOfMarkers++;
                    
                    // Google Maps only supports up to 25 markers using its "Get Directions" feature
                    if (numOfMarkers == 25) {
                      break;
                    }
                  }
                
                  // Add marker reference to aboutmyplace link text
                  propertyLinks[linkIndex].element.innerHTML += 
                      '<font color="red">[' + String.fromCharCode(CHAR_CODE_A + markerNum) + ']</font>';
                }
              }

              // Create new link to Bing
              bingPropsElement.href = propertyBingUrl;
              bingPropsElement.innerHTML = '[Bing Map]';
              bingPropsElement.rel = 'nofollow';
              bingPropsElement.target = 'bingWindow';
              bingPropsElement.title = 'View all these properties on a Bing Map (opens new window)';
              
              // Create new link to Google Maps
              googleMapPropsElement.href = propertyGoogleMapUrl;
              googleMapPropsElement.innerHTML = '[Google Map]';
              googleMapPropsElement.rel = 'nofollow';
              googleMapPropsElement.target = 'googleMapWindow';
              googleMapPropsElement.title = 'View all these properties on a Google Map (opens new window)';

              // Create new link to Yahoo Maps
              yahooMapPropsElement.href = propertyYahooMapUrl;
              yahooMapPropsElement.innerHTML = '[Yahoo Map]';
              yahooMapPropsElement.rel = 'nofollow';
              yahooMapPropsElement.target = 'yahooMapWindow';
              yahooMapPropsElement.title = 'View all these properties on a Yahoo Map (opens new window)';
            }
          }
        });
      })(propertyLinks[linkIndex]);
    };
  }
} catch(err) {
  fbLog('Error', err);
}
