// // @ts-nocheck import crossfilter from "crossfilter2"; import * as d3 from "d3"; import mapboxgl from "mapbox-gl"; import 'mapbox-gl/dist/mapbox-gl.css'; // this hides the map for some reason import { useEffect, useRef } from "react"; import { renderToString } from 'react-dom/server'; import "../assets/Map.css"; import { Metric, type ParameterValues } from "./Parameters"; import { Button } from "./ui/button"; import { ScrollArea } from "./ui/scroll-area"; import { Separator } from "./ui/separator"; export function Map( props: { listingData: any; queryParameters: ParameterValues | null; } ) { const data = props.listingData; var crossData = data.features.map(function (d, i) { //clone properties var props = clone(d['properties']); props['index'] = i; return props; }); const cf = crossfilter(crossData); const qmDim = cf.dimension(function (d) { return d.qm; }); const cityDim = cf.dimension(function (d) { return d.city; }); const countryDim = cf.dimension(function (d) { return d.country; }); const rentDim = cf.dimension(function (d) { return d.total_price; }); const roomsDim = cf.dimension(function (d) { return d.rooms; }); const urlDim = cf.dimension(function (d) { return d.url; }); const indexDim = cf.dimension(function (d) { return d.index; }); let heatmap = null; // rivet var filter = { city: 'London', country: null, mode: Metric.qmprice }; // filter['countries'] = Array.from(new Set(data.features.map(function (d) { return d['properties']['country'] }))); if (props.queryParameters) { filter['mode'] = props.queryParameters.metric; } // rivets.bind(document.getElementById('overlay'), { filter: filter }); const mapRef = useRef(mapboxgl.Map) const mapContainerRef = useRef('map-container') useEffect(() => { mapboxgl.accessToken = 'pk.eyJ1IjoiZGktdG8iLCJhIjoiY2o0bnBoYXcxMW1mNzJ3bDhmc2xiNWttaiJ9.ZccatVk_4shzoAsEUXXecA'; mapRef.current = new mapboxgl.Map({ container: mapContainerRef.current, style: 'mapbox://styles/mapbox/light-v9', center: [13.38032, 49.994210], zoom: 5 }); mapRef.current.on('load', function () { update() }) mapRef.current.on('click', function (e) { openListingsDialog(e.lngLat.lng, e.lngLat.lat); }) return () => { mapRef.current.remove() } }, [data]) function clone(d) { return JSON.parse(JSON.stringify(d)); } function percentile(arr, p) { if (arr.length === 0) return 0; if (typeof p !== 'number') throw new TypeError('p must be a number'); if (p <= 0) return arr[0]; if (p >= 1) return arr[arr.length - 1]; var index = arr.length * p, lower = Math.floor(index), upper = lower + 1, weight = index % 1; if (upper >= arr.length) return arr[lower]; return arr[lower] * (1 - weight) + arr[upper] * weight; } function update() { // init heatmap heatmap = new HexgridHeatmap(mapRef.current, "hexgrid-heatmap", "waterway-label"); heatmap.setIntensity(9); // dunno yet heatmap.setSpread(0.05); // dunno yet heatmap.setCellDensity(0.5); // small value == bigger hexagons heatmap.setPropertyName(filter.mode); if (filter.mode === Metric.qmprice) { // if we visualize sqm based data, remove properties where we have no data qmDim.filter(function (d) { return d > 0; }); } // set filter if (filter.city) { cityDim.filterExact(filter.city); } else if (filter.country) { countryDim.filterExact(filter.country); } else { alert('nothing loadable'); } filter.count = cityDim.top(Infinity).length; var subset = { "type": "FeatureCollection", "features": [] }; indexDim.top(Infinity).forEach(function (i) { subset.features.push(data.features[i.index]); }); loadData(heatmap, subset); } function loadData(heatmap, subset) { heatmap.setData(subset); var values = subset.features.map(function (d) { return d['properties'][filter.mode] }); values = values.sort(function (a, b) { return a - b; }); // setting the color stops, min is at 5th percentile, max at 95percentile var min = values[Math.round(values.length * 0.05)]; var max = values[Math.round(values.length * 0.95)]; var colorStopsPerc = [ [0, "rgba(0,185,243,0)"], [25, "rgba(0,185,243,0.24)"], [60, "rgba(255,223,0,0.3)"], [100, "rgba(255,105,0,0.3)"], ]; makeLegend(colorStopsPerc, min, max); var colorStopsValue = colorStopsPerc.map(function (d) { return [min + d[0] * (max - min) / 100, d[1]]; }); heatmap.setColorStops(colorStopsValue); heatmap.update(); //get bounding box and zoom to that area // we use a 1% percentile since some data can be corrupt var longitudes = subset.features.map(function (d) { return d.geometry.coordinates[0]; }).sort(function (a, b) { return a - b; }); var latitudes = subset.features.map(function (d) { return d.geometry.coordinates[1]; }).sort(function (a, b) { return a - b; }); var minlng = percentile(longitudes, 0.01); var maxlng = percentile(longitudes, 0.99); var minlat = percentile(latitudes, 0.01); var maxlat = percentile(latitudes, 0.99); mapRef.current.fitBounds([ [minlng, minlat], [maxlng, maxlat] ]); } function makeLegend(colorstops, minValue, maxValue) { /** * colorstops: [[0, 'green'], [100, 'red']] * @type {number} */ var svg_height = 300, svg_width = 70; // clear svg before starting d3.select('#svg').selectAll('*').remove(); // create a new SVG element const svg = d3.select('#svg'); var defs = svg .attr('height', svg_height) .attr('width', svg_width); var linearGradient = svg.append("defs") .append("linearGradient") .attr("id", "linear-gradient"); linearGradient .attr("x1", "0%") .attr("y1", "100%") .attr("x2", "0%") .attr("y2", "0%"); svg.append("rect") .attr("width", svg_width * 0.4) .attr("height", svg_height) .attr('rx', 4) .style("fill", "url(#linear-gradient)"); colorstops.forEach(function (d) { linearGradient.append("stop") .attr("offset", d[0] + "%") .attr("stop-color", d[1]); }); var xScale = d3.scaleLinear().range([svg_height - 20, 0]).domain([minValue, maxValue]); var xAxis = d3.axisRight(xScale).ticks(5); svg.append("g") .attr("class", "axis") .attr("transform", "translate(" + svg_width / 2 + "," + (10) + ")") .call(xAxis); } function openListingsDialog(longtitude, latitude) { const searchBuffer = 0.001 // ~100m const properties = heatmap._tree.search({ minX: longtitude - searchBuffer, maxX: longtitude + searchBuffer, minY: latitude - searchBuffer, maxY: latitude + searchBuffer }) if (properties.length > 0) { const listingDialogPopup = getListingDialog(properties); new mapboxgl.Popup() .setLngLat([longtitude, latitude]) .setHTML(renderToString(listingDialogPopup)) .setMaxWidth("500px") .addTo(mapRef.current); } } function getListingDialog(properties) { let listingComponents = []; for (let property of properties) { listingComponents.push(getPropertyComponent(property)); } return
Showing {properties.length} properties
{listingComponents.map((item) => { const scrollDiv =
{item}
; return scrollDiv })}
; } function getPropertyComponent(property) { const priceHistoryHTMLs = property.properties.price_history.map((d) => { return
  • ${d.last_seen.split('T')[0]}: £${d.price}
  • ; }); let priceHistoryHTML = <>; if (priceHistoryHTMLs.length > 1) { priceHistoryHTML =
    Price history:
    } const lastSeenStr = property.properties.last_seen.split('T')[0]; const lastSeenDays = Math.round((new Date() - new Date(lastSeenStr)) / (1000 * 60 * 60 * 24)); return
    Available from:
    {property.properties.available_from}
    Price:
    £{property.properties.total_price}
    {priceHistoryHTML}
    Rooms:
    {property.properties.rooms}
    Area:
    {property.properties.qm} m²
    Price per area:
    £{property.properties.qmprice}/m²
    Last seen:
    {lastSeenDays} days ago
    Agency:
    {property.properties.agency}
    } return <>
    }