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.

  1. // ==UserScript==
  2. // @name Craigslist apartment map view with filters
  3. // @namespace http://ivanjoukov.com/
  4. // @version 0.1
  5. // @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.
  6. // @author Ivan Joukov
  7. // @include http://*.craigslist.tld/search/apa
  8. // @require https://code.jquery.com/jquery-1.11.3.min.js
  9. // @grant none
  10. // ==/UserScript==
  11. /* jshint -W097 */
  12.  
  13. this.$ = this.jQuery = jQuery.noConflict(true);
  14.  
  15. (function (window, document, $, undefined) {
  16. 'use strict';
  17.  
  18. // Sanity check that this has any chance of working
  19. if (!(CL && CL.banish && CL.maps)) {
  20. return;
  21. }
  22.  
  23. var minAgeSlider, maxAgeSlider, $minDaysSpan, $maxDaysSpan, $filteringProgress;
  24. // Create and set up the slider elements that will form our UI
  25. minAgeSlider = document.createElement("INPUT");
  26. minAgeSlider.setAttribute("type", "range");
  27. minAgeSlider.setAttribute("min", "0");
  28. minAgeSlider.setAttribute("max", "30");
  29. minAgeSlider.value = 0;
  30. minAgeSlider.id = "minAgeSlider";
  31.  
  32. maxAgeSlider = document.createElement("INPUT");
  33. maxAgeSlider.setAttribute("type", "range");
  34. maxAgeSlider.setAttribute("min", "0");
  35. maxAgeSlider.setAttribute("max", "30");
  36. maxAgeSlider.value = 30;
  37. maxAgeSlider.id = "maxAgeSlider";
  38.  
  39. // Replace the default posted today checkbox with our UI
  40. $('.postedToday > input').remove();
  41. $('.postedToday').append("<div>Post min age <span id='minDays'>0</span> (days)<div>").append(minAgeSlider);
  42. $('.postedToday').append("<div>Posting max age <span id='maxDays'>30</span>(days)<div>").append(maxAgeSlider);
  43. $('.postedToday').append("<div>Filtering progress: <span id='filteringProgress'>100</span>%<div>");
  44.  
  45. $minDaysSpan = $("#minDays");
  46. $maxDaysSpan = $("#maxDays");
  47. $filteringProgress = $("#filteringProgress");
  48.  
  49.  
  50. //Borrowed from https://davidwalsh.name/javascript-debounce-function
  51. // Because filtering is a pretty expensive operation, let's delay it until the user has finished adjusting the sliders
  52. function debounce(func, wait, immediate) {
  53. var timeout;
  54. return function () {
  55. var context = this,
  56. args = arguments,
  57. later = function () {
  58. timeout = null;
  59. if (!immediate) {
  60. func.apply(context, args);
  61. }
  62. },
  63. callNow = immediate && !timeout;
  64. window.clearTimeout(timeout);
  65. timeout = window.setTimeout(later, wait);
  66. if (callNow) {
  67. func.apply(context, args);
  68. }
  69. };
  70. }
  71.  
  72. // Inspired by http://stackoverflow.com/a/10344560
  73. // This prevents locking the UI while doing the pretty slow/expensive filtering
  74. // The basic idea is rather than iterating over all the (possibly thousands) of listings in a single blocking call
  75. // We can break up the processing into small chunks, pausing often enough to allow the UI thread to run to prevent
  76. // UI locking from the user's perspective
  77. function processLargeArrayAsync(array, fn, maxTimePerChunk, context, done) {
  78. context = context || window;
  79. maxTimePerChunk = maxTimePerChunk || 200;
  80. var index = 0;
  81.  
  82. function now() {
  83. return new Date().getTime();
  84. }
  85.  
  86. function doChunk() {
  87. var startTime = now();
  88. while (index < array.length && (now() - startTime) <= maxTimePerChunk) {
  89. // callback called with args (value, index, array)
  90. fn.call(context, array[index], index, array);
  91. ++index;
  92. }
  93. if (index < array.length) {
  94. // set Timeout for async iteration
  95. window.setTimeout(doChunk, 1);
  96. } else {
  97. done.call(context);
  98. }
  99. }
  100. doChunk();
  101. }
  102.  
  103. function inDateRange(dateToCheck, newestDate, oldestDate) {
  104. return dateToCheck > oldestDate && dateToCheck < newestDate;
  105. }
  106.  
  107. function hideByDate(newestDate, oldestDate) {
  108. var containingPIDKeys = Object.keys(CL.maps.marker.containingPID),
  109. byIDKeys = Object.keys(CL.maps.marker.byID),
  110. totalLength = containingPIDKeys.length + byIDKeys.length,
  111. totalProcessed = 0,
  112. processMarker = function (key, index, keyArray) {
  113. var marker = this[key];
  114. if (!inDateRange(marker.marker.options.posteddate, newestDate, oldestDate)) {
  115. CL.banish.ban(key);
  116. } else {
  117. CL.banish.unban(key);
  118. }
  119. $filteringProgress.text(Math.round(100 * ++totalProcessed / totalLength));
  120. },
  121. doneCallback = function () {
  122. CL.banish.hide();
  123. };
  124. processLargeArrayAsync(containingPIDKeys, processMarker, 100, CL.maps.marker.containingPID, doneCallback);
  125. processLargeArrayAsync(byIDKeys, processMarker, 100, CL.maps.marker.byID, doneCallback);
  126. }
  127.  
  128. // Do the cheap UI changes in real time
  129. $('#minAgeSlider, #maxAgeSlider').on('input change', function () {
  130. $minDaysSpan.text(minAgeSlider.value);
  131. $maxDaysSpan.text(maxAgeSlider.value);
  132. });
  133.  
  134. // But debounce and offload the really expensive filtering operation
  135. var debouncedHandleSliderChange = debounce(function () {
  136. var newestDate = Date.now() - (1000 * 60 * 60 * minAgeSlider.value),
  137. oldestDate = Date.now() - (1000 * 60 * 60 * maxAgeSlider.value);
  138. hideByDate(newestDate, oldestDate);
  139. }, 500);
  140.  
  141. $('#minAgeSlider, #maxAgeSlider').on('change', debouncedHandleSliderChange);
  142.  
  143. })(window, document, jQuery);