tinyjacky / Rightmove Map Enhanced

// ==UserScript==
// @name         Rightmove Map Enhanced
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Extra info in property map view
// @author       You
// @license      MIT
// @match        https://www.rightmove.co.uk/property-*/map.html*
// @icon         https://www.google.com/s2/favicons?domain=rightmove.co.uk
// @require      http://code.jquery.com/jquery-latest.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.core.js
// @connect      localhost
// @connect      bestmove.loophole.site
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @resource     MATERIAL_ICONS https://fonts.googleapis.com/icon?family=Material+Icons
// ==/UserScript==

/* globals $ _ */

const propertyCoordinatePair = (property) => `${property.location.latitude},${property.location.longitude}`;

const state = {
  currentPropertyId: null,
  propertiesById: {},
  scoresById: {},
  waitingPropertyInfo: false
};


const renderScore = (property) => {
    const propertyScore = state.scoresById[property.id];
    const coordinatePair = propertyCoordinatePair(property);
    const groceryListView = _.compact(propertyScore.scores?.groceries).map((grocery) => {
        return `
             <div class="grocery-item">
               <span class="material-icons">shopping_cart</span>
               <a href="https://www.google.com/maps/dir/${coordinatePair}/${grocery.latitude},${grocery.longitude}/data=!4m2!4m1!3e2" target="_blank">
                 <span class="score-text">${grocery.store_name} (${grocery.postcode}, ${grocery.size_band})</span>
               </a>
             </div>`;
    }).join('');

    $('.score-section').remove();
    $(
        `
               <div class="score-section">
                  <div class="imd-list">
                    <span class="material-icons">paid</span><span class="score-text">${propertyScore.scores?.income_score?.toFixed(1)}</span>
                    <span class="material-icons">work</span><span class="score-text">${propertyScore.scores?.employment_score?.toFixed(1)}</span>
                    <span class="material-icons">school</span><span class="score-text">${propertyScore.scores?.skill_score?.toFixed(1)}</span>
                    <span class="material-icons">security</span><span class="score-text">${propertyScore.scores?.crime_score?.toFixed(1)}</span>
                    <span class="material-icons">apartment</span><span class="score-text">${propertyScore.scores?.housing_barrier_score?.toFixed(1)}</span>
                    <span class="material-icons">park</span><span class="score-text">${propertyScore.scores?.environment_score?.toFixed(1)}</span>
                    IMD<span class="score-text">${propertyScore.scores?.imd_score?.toFixed(1)}</span>
                  </div>
                  <div class="grocery-list">${groceryListView}</div>
                  <a href="https://www.google.com/maps/dir/${coordinatePair}/birmingham+new+street+station/data=!4m2!4m1!3e3" target="_blank">
                    <span class="material-icons">directions_bus_filled</span><span class="score-text">${propertyScore.scores?.distance_to_town_center?.toFixed(1)}</span>
                  </a>
                  <a href="https://www.google.com/maps/search/${coordinatePair}" target="_blank"><span class="material-icons">map</span><span class="score-text">${propertyScore.postcode}</span></a>
                  <a href="https://epc.opendatacommunities.org/domestic/search?postcode=${propertyScore.postcode}" target="_blank"><span class="material-icons">bolt</span><span class="score-text">Cert</span></a>
                  <a href="https://www.streetcheck.co.uk/postcode/${propertyScore.postcode.split(' ').join('')}" target="_blank"><span class="material-icons">groups</span></a>
                </div>

                `
    ).insertAfter('.expPropCardDescription');
};


const loadScore = function(propertyId) {
    if (!propertyId) { return; }

    state.currentPropertyId = propertyId;

    const property = state.propertiesById[propertyId];

    if (!property) {
        // propertyInfo is not ready, waiting for ajax
        state.waitingPropertyInfo = true;
        return;
    }

    state.waitinfForPropertyInfo = false;

    const scoreParams = {
      coordinate_pairs: [property].map(propertyCoordinatePair),
      expand: ['groceries'],
    };

    if (state.scoresById[property.id]) {
        renderScore(property);
        return;
    }

    GM_xmlhttpRequest({
      method: 'GET',
      url: `http://bestmove.loophole.site/scores?${$.param(scoreParams)}`,
      responseType: 'json',
      onload: function(res) {
          const payload = res.response;

          const coordinatePair = propertyCoordinatePair(property);
          const propertyScore = payload[coordinatePair];

          state.scoresById[property.id] = propertyScore;

          renderScore(property);
      }
    });
};

(function() {
    'use strict';

    GM_addStyle(GM_getResourceText("MATERIAL_ICONS"));
    GM_addStyle(
      `
        .score-section .material-icons {
          font-size: 18px;
        }

        .score-section .score-text {
          vertical-align: top;
          margin-right: 5px;
        }

        .score-section a {
          color: inherit;
        }
      `
    );

    $('.mapSidePanel').bind('DOMSubtreeModified', function(e) {
        if (e.target.innerHTML.length === 0) {
            return;
        }

        const propertyId = $('.expPropCardPropertyLinkButton a').attr('href')?.split('/')[2];

        if (propertyId && propertyId !== state.currentPropertyId) {
            loadScore(propertyId);
        }
    });
})();

(function(open) {
    XMLHttpRequest.prototype.open = function() {
        this.addEventListener("readystatechange", function() {
            if(this.readyState !== 4 || !Array.isArray(this.response) || !this.response[0].id) {
                return;
            }

            this.response.forEach((property) => {
                state.propertiesById[property.id.toString()] = property;

                if (state.waitingPropertyInfo && property.id == state.currentPropertyId) {
                    loadScore(property.id.toString());
                }
            });
        }, false);
        open.apply(this, arguments);
    };
})(XMLHttpRequest.prototype.open);