Craigslist apartment map view with filters

Craigslist apartment search is most useful on the map view, since after all real estate is about location, location, location, but other factors matter too. For example you probably want to see listings that are reasonably new but not just from today, but the current UI only lets you pick "Listed today" or no filter. This tampermonkey script lets you eliminate listings by a configurable age range.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Craigslist apartment map view with filters
// @namespace    http://ivanjoukov.com/
// @version      0.1
// @description  Craigslist apartment search is most useful on the map view, since after all real estate is about location, location, location, but other factors matter too.  For example you probably want to see listings that are reasonably new but not just from today, but the current UI only lets you pick "Listed today" or no filter.  This tampermonkey script lets you eliminate listings by a configurable age range.
// @author       Ivan Joukov
// @include      http://*.craigslist.tld/search/apa
// @require      https://code.jquery.com/jquery-1.11.3.min.js
// @grant        none
// ==/UserScript==
/* jshint -W097 */

this.$ = this.jQuery = jQuery.noConflict(true);

(function (window, document, $, undefined) {
    'use strict';

	// Sanity check that this has any chance of working
	if (!(CL && CL.banish && CL.maps)) {
		return;
	}

    var minAgeSlider, maxAgeSlider, $minDaysSpan, $maxDaysSpan, $filteringProgress;
    
    // Create and set up the slider elements that will form our UI
    minAgeSlider = document.createElement("INPUT");
    minAgeSlider.setAttribute("type", "range");
    minAgeSlider.setAttribute("min", "0");
    minAgeSlider.setAttribute("max", "30");
    minAgeSlider.value = 0;
    minAgeSlider.id = "minAgeSlider";

    maxAgeSlider = document.createElement("INPUT");
    maxAgeSlider.setAttribute("type", "range");
    maxAgeSlider.setAttribute("min", "0");
    maxAgeSlider.setAttribute("max", "30");
    maxAgeSlider.value = 30;
    maxAgeSlider.id = "maxAgeSlider";

    // Replace the default posted today checkbox with our UI
    $('.postedToday > input').remove();
    $('.postedToday').append("<div>Post min age <span id='minDays'>0</span> (days)<div>").append(minAgeSlider);
    $('.postedToday').append("<div>Posting max age <span id='maxDays'>30</span>(days)<div>").append(maxAgeSlider);
    $('.postedToday').append("<div>Filtering progress: <span id='filteringProgress'>100</span>%<div>");

    $minDaysSpan = $("#minDays");
    $maxDaysSpan = $("#maxDays");
    $filteringProgress = $("#filteringProgress");


    //Borrowed from https://davidwalsh.name/javascript-debounce-function
    // Because filtering is a pretty expensive operation, let's delay it until the user has finished adjusting the sliders
    function debounce(func, wait, immediate) {
        var timeout;
        return function () {
            var context = this,
				args = arguments,
				later = function () {
					timeout = null;
					if (!immediate) {
						func.apply(context, args);
					}
				},
				callNow = immediate && !timeout;
            window.clearTimeout(timeout);
            timeout = window.setTimeout(later, wait);
            if (callNow) {
				func.apply(context, args);
			}
        };
    }

    // Inspired by http://stackoverflow.com/a/10344560
    // This prevents locking the UI while doing the pretty slow/expensive filtering
    // The basic idea is rather than iterating over all the (possibly thousands) of listings in a single blocking call
    // We can break up the processing into small chunks, pausing often enough to allow the UI thread to run to prevent
    // UI locking from the user's perspective
    function processLargeArrayAsync(array, fn, maxTimePerChunk, context, done) {
        context = context || window;
        maxTimePerChunk = maxTimePerChunk || 200;
        var index = 0;

        function now() {
            return new Date().getTime();
        }

        function doChunk() {
            var startTime = now();
            while (index < array.length && (now() - startTime) <= maxTimePerChunk) {
                // callback called with args (value, index, array)
                fn.call(context, array[index], index, array);
                ++index;
            }
            if (index < array.length) {
                // set Timeout for async iteration
                window.setTimeout(doChunk, 1);
            } else {
                done.call(context);
            }
        }
        doChunk();
    }

	function inDateRange(dateToCheck, newestDate, oldestDate) {
        return dateToCheck > oldestDate && dateToCheck < newestDate;
    }

    function hideByDate(newestDate, oldestDate) {
        var containingPIDKeys = Object.keys(CL.maps.marker.containingPID),
			byIDKeys = Object.keys(CL.maps.marker.byID),
			totalLength = containingPIDKeys.length + byIDKeys.length,
			totalProcessed = 0,
			processMarker = function (key, index, keyArray) {
				var marker = this[key];
				if (!inDateRange(marker.marker.options.posteddate, newestDate, oldestDate)) {
					CL.banish.ban(key);
				} else {
					CL.banish.unban(key);
				}
				$filteringProgress.text(Math.round(100 * ++totalProcessed / totalLength));
			},
			doneCallback = function () {
				CL.banish.hide();
			};
        processLargeArrayAsync(containingPIDKeys, processMarker, 100, CL.maps.marker.containingPID, doneCallback);
        processLargeArrayAsync(byIDKeys, processMarker, 100, CL.maps.marker.byID, doneCallback);
    }

    // Do the cheap UI changes in real time    
    $('#minAgeSlider, #maxAgeSlider').on('input change', function () {
        $minDaysSpan.text(minAgeSlider.value);
        $maxDaysSpan.text(maxAgeSlider.value);
    });

    // But debounce and offload the really expensive filtering operation
    var debouncedHandleSliderChange = debounce(function () {
        var newestDate = Date.now() - (1000 * 60 * 60 * minAgeSlider.value),
			oldestDate = Date.now() - (1000 * 60 * 60 * maxAgeSlider.value);
        hideByDate(newestDate, oldestDate);
    }, 500);

    $('#minAgeSlider, #maxAgeSlider').on('change', debouncedHandleSliderChange);

})(window, document, jQuery);