ReRentalAlert
Checking 12 sources. Last check just now.

Browse re-rentals

Browse the map

Listings

Browse re-rentals

About listing data

Some landlords don't publish income limits. We show those listings instead of hiding them, so check the flyer before ruling yourself out. Reside listings come from their application board, which doesn't include rent or unit details.

Bedrooms
Status
Borough
More
More filters

Shows listings at or below this share of area median income.

308 listings match

Yesterday

$795 to $3,352 New per month Building rent range
Studio
Studio in Bronx
1115 Commonwealth Ave (100% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 19h ago
Email
$1,602 per month
Studio
Studio in Bronx
1115 Commonwealth Ave (60% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$1,713 per month
1BR
1 bedroom in Bronx
1115 Commonwealth Ave (60% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$2,231 per month
1BR
1 bedroom in Bronx
1115 Commonwealth Ave (100% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$795 to $3,352 per month Building rent range
2BR
2 bedrooms in Bronx
1115 Commonwealth Ave (100% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$2,859 per month
Studio
Studio in Jamaica
147-36 94TH AVENUE (120% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$3,574 per month
Studio
Studio in Jamaica
147-36 94TH AVENUE (165% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$2,910 per month
1BR
1 bedroom in Jamaica
147-36 94TH AVENUE (120% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$3,821 per month
1BR
1 bedroom in Jamaica
147-36 94TH AVENUE (165% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$3,475 per month
2BR
2 bedrooms in Jamaica
147-36 94TH AVENUE (120% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$4,568 per month
2BR
2 bedrooms in Jamaica
147-36 94TH AVENUE (165% AMI)
Available unit
Listed by NYC Housing Development Corporation, Found 1d ago
Email
$795 to $1,003 per month
Studios to 2 bedrooms
Studios to 2 bedrooms in Mount Hope
2028 CRESTON AVENUE (30% AMI)
Waitlist open
Listed by NYC Housing Development Corporation, Found 1d ago
Waitlist
$3,220 per month
not published
Apartment in Melrose
1135 Teller Ave #Apt 3E
Available unit
Listed by Reside New York, Found 1d ago
Online form
$1,601 per month
not published
Apartment in Crotona Park East
1281 Hoe Ave #Apt 7E
Available unit
Listed by Reside New York, Found 1d ago
Online form
$1,601 per month
not published
Apartment in Crotona Park East
1281 Hoe Ave #Apt 8A
Available unit
Listed by Reside New York, Found 1d ago
Online form
$3,397 per month
not published
Apartment in Bronx
138 Bruckner Blvd #Apt 522
Available unit
Listed by Reside New York, Found 1d ago
Online form
$3,323 per month
not published
Apartment in Bronx
1433 Teller Ave #Apt 3D
Available unit
Listed by Reside New York, Found 1d ago
Online form
$3,862 per month
not published
Apartment in Brooklyn
1718 President Street #Apt 6D
Available unit
Listed by Reside New York, Found 1d ago
Online form
$3,261 per month
not published
Apartment (location not listed)
20 & 30 Halletts Point
Available unit Location couldn't be verified
Listed by Reside New York, Found 1d ago
Online form
$3,128 per month
not published
Apartment in Bronx
2180 Walton Avenue #Apt 10B
Available unit
Listed by Reside New York, Found 1d ago
Online form
$2,883 per month
not published
Apartment in Brownsville
254 Amboy Street #Apt 2B
Available unit
Listed by Reside New York, Found 1d ago
Online form
$3,222 per month
not published
Apartment in Bedford Park
3001 Grand Concourse #Apt 6D
Available unit
Listed by Reside New York, Found 1d ago
Online form
$2,705 per month
not published
Apartment in Bronx
3923 Carpenter Ave #Apt 3C
Available unit
Listed by Reside New York, Found 1d ago
Online form
$2,800 per month
not published
Apartment in East New York-New Lots
647 Sheffield Ave #Apt 1A
Available unit
Listed by Reside New York, Found 1d ago
Online form
$1,608 per month
not published
Apartment in Hunts Point
661 Manida Street #Apt 3C
Available unit
Listed by Reside New York, Found 1d ago
Online form
$1,802 per month
not published
Apartment in Hunts Point
661 Manida Street #Apt 4A
Available unit
Listed by Reside New York, Found 1d ago
Online form
$3,037 per month
not published
Apartment in Williamsbridge-Olinville
765 East 214 #Apt 302
Available unit Location couldn't be verified
Listed by Reside New York, Found 1d ago
Online form
$2,158 per month
not published
Apartment in Brooklyn
875 4th Avenue #Apt 302
Available unit
Listed by Reside New York, Found 1d ago
Online form

Earlier this week

Earlier listings

$1,260 per month
Studio
Studio in Lower East Side
#Lower East Side - Studio - 80% (80% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,519 per month
1BR
1 bedroom in Lower East Side
#Lower East Side - 1 bedroom - 80% (80% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,955 per month
2BR
2 bedrooms in Lower East Side
#Lower East Side - 2 bedrooms - 80% (80% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,013 per month
Studio
Studio in Chinatown-Two Bridges
#Chinatown - Studio - 50% (50% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,285 per month
Studio
Studio in Chinatown-Two Bridges
#Chinatown - Studio - 60% (60% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,519 per month
1BR
1 bedroom in Chinatown-Two Bridges
#Chinatown - 1 bedroom - 60% (60% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,955 per month
2BR
2 bedrooms in Chinatown-Two Bridges
#Chinatown - 2 bedrooms - 60% (60% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,013 per month
Studio
Studio in Manhattan
#Harlem - Studio - 50% (50% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,081 per month
1BR
1 bedroom in Manhattan
#Harlem - 1 bedroom - 50% (50% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,289 per month
2BR
2 bedrooms in Manhattan
#Harlem - 2 bedrooms - 50% (50% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,285 per month
Studio
Studio in Manhattan
#Harlem - Studio - 60% (60% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,372 per month
1BR
1 bedroom in Manhattan
#Harlem - 1 bedroom - 60% (60% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
$1,639 per month
2BR
2 bedrooms in Manhattan
#Harlem - 2 bedrooms - 60% (60% AMI)
Available unit
Listed by Stanton Norfolk Inc., Found 15h ago
Online form
Show more

Showing 50 of 308

(function () { var listwrap = document.getElementById('listwrap'); var mapwrap = document.getElementById('mapwrap'); var mapEl = document.getElementById('map'); if (!listwrap || !mapwrap || !mapEl) return; // Status -> dot color + label. Labels come from the server (STATUS_LABELS // in app.py) so the key can never drift from the list cards. Colors are // Okabe-Ito colorblind-safe; "new" is an overlay halo, not a color, so a // listing is never two colors at once. var STATUS_META = [ { key: 'available', color: '#2F7A4D' }, { key: 'building', color: '#0072B2' }, { key: 'waitlist', color: '#E69F00' }, { key: 'on_hold', color: '#9AA0A6' } ]; // Most actionable status wins when several units share one building pin: // "can I apply right now?" reads at a glance. var STATUS_RANK = { available: 0, building: 1, waitlist: 2, on_hold: 3 }; var statusLabels = {}; try { statusLabels = JSON.parse(mapEl.getAttribute('data-status-labels') || '{}'); } catch (e) { statusLabels = {}; } function statusLabel(key) { return statusLabels[key] || key; } function statusColor(key) { for (var i = 0; i < STATUS_META.length; i++) { if (STATUS_META[i].key === key) return STATUS_META[i].color; } return '#2F7A4D'; } // Tile layer config lives in one constant so the provider can be swapped // without a code change. We use Esri's World Light Gray canvas: // muted, light, minimal-label cartography (the Zillow/Redfin look) that // lets the price pills carry the visual weight. Free to use with visible // attribution; no API key needed. Terms: // https://www.esri.com/en-us/legal/terms/full-master-agreement // (CartoDB Positron was evaluated first but now serves an "API KEY // REQUIRED" watermark to unauthenticated clients, so it is unusable // without a key. OSM standard tiles were the previous look; the // client-side grayscale filter over them read as muddy gray.) var TILE_URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}'; // Attribution must name this service's actual providers (Esri's // copyrightText for Canvas/World_Light_Gray_Base). The previous // "Esri, DeLorme, NAVTEQ" was World_Street_Map's attribution. var TILE_ATTRIB = 'Tiles © Esri — Esri, HERE, Garmin, © OpenStreetMap contributors, and the GIS user community'; var map = null, inited = false, loading = false; // Markers built by buildMap, keyed to list rows in commit 3 (row hover). var pinRefs = []; function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } function pins() { try { return JSON.parse(mapEl.getAttribute('data-pins') || '[]'); } catch (e) { return []; } } // Tile failure (or a CDN failure, below): hide the map pane, keep the // list, show a notice. #maperr lives outside #mapwrap so it survives. function mapFailed(msg) { mapwrap.setAttribute('hidden', ''); } // The map hero is full-width at the top of the page on every viewport; // it loads eagerly on page load. function layoutMode() { mapwrap.removeAttribute('hidden'); initMap(); if (map) map.invalidateSize(); } function initMap() { if (inited || loading) return; if (window.L) { buildMap(); return; } loading = true; loadCss('https://unpkg.com/[email protected]/dist/leaflet.css'); // MarkerCluster (same CDN, same lazy load): clusters pins on Browse so // 300 listings don't render 300 overlapping markers (QA 0.9). If it // fails to load we fall back to plain markers - never a broken map. loadCss('https://unpkg.com/[email protected]/dist/MarkerCluster.css'); loadCss('https://unpkg.com/[email protected]/dist/MarkerCluster.Default.css'); loadScript('https://unpkg.com/[email protected]/dist/leaflet.js', function (ok) { if (!ok) return leafletFailed(); loadScript('https://unpkg.com/[email protected]/dist/leaflet.markercluster.js', function () { loading = false; buildMap(); }); }); } function loadCss(href) { var css = document.createElement('link'); css.rel = 'stylesheet'; css.href = href; document.head.appendChild(css); } function loadScript(src, done) { var s = document.createElement('script'); s.src = src; s.onload = function () { done(true); }; s.onerror = function () { done(false); }; document.head.appendChild(s); } function leafletFailed() { loading = false; mapLoadError('Map could not load (check your connection). Showing the list instead.'); } function mapLoadError(msg) { var e = document.getElementById('maperr'); if (e) { e.textContent = msg; e.removeAttribute('hidden'); } mapFailed(msg); } function buildMap() { inited = true; var ps = pins(); if (!ps.length) { mapEl.innerHTML = '

None of these listings have map locations yet. Try widening your filters.

'; return; } map = L.map('map', { scrollWheelZoom: false, zoomControl: false }); var tiles = L.tileLayer(TILE_URL, { maxZoom: 19, // Esri's Canvas/World_Light_Gray_Base publishes real tiles only to // z16: deeper zooms return HTTP 200 with a "Map data not yet // available" placard baked into the pixels (not a 404, so the // tileerror fallback never fires). maxNativeZoom makes Leaflet // upscale z16 tiles past that instead of requesting placards. // (P0 2026-09-28: fitBounds zooms tight result sets to z17+, which // tiled the whole mobile map with placards.) maxNativeZoom: 16, attribution: TILE_ATTRIB }); // QA 0.9: degrade to the list if the TILES fail (not just the Leaflet // CDN). A handful of bad tiles is normal; sustained failure means the // tile server is unreachable, so fall back with a notice. var tileErrors = 0; tiles.on('tileerror', function () { tileErrors++; if (tileErrors >= 6) { mapLoadError("Map tiles couldn't load. Showing the list instead."); } }); tiles.addTo(map); // Zoom control sits bottom-right: the status legend occupies // top-left, and the stacked default controls covered pins there // on narrow screens (owner interface note 2026-09-28). L.control.zoom({ position: 'bottomright' }).addTo(map); // Group pins that share a building (identical coordinates) into one // marker. Map v2: the cluster is a STATUS RING, not a price pill - an // inline SVG ring shows the mix of application statuses inside // ("can I apply here?"), with the count in the middle. SVG arcs, not // conic-gradient: identical rendering in every engine (P0 2026-09-28: // the gradient build rendered as thin bars on iOS Safari and taps // were dead). The divIcon is a real 46x46 box anchored at its center, // so iOS hit-testing always has a true tappable target. var groups = groupPins(ps); var group = (window.L && L.markerClusterGroup) ? L.markerClusterGroup({ maxClusterRadius: 56, iconCreateFunction: function (cluster) { var kids = cluster.getAllChildMarkers(); var counts = {}; kids.forEach(function (mk) { var s = mk.options.status || 'available'; counts[s] = (counts[s] || 0) + 1; }); // Bubble size ramps with count: a "2" cluster no longer dwarfs // single pins (owner interface note 2026-09-28). var n = kids.length; var size = n < 10 ? 40 : (n < 50 ? 48 : 56); return L.divIcon({ className: 'scluster-wrap', html: clusterRingHtml(counts, n, size), iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); } }) : null; var bounds = []; groups.forEach(function (g) { var m = L.marker([g.lat, g.lng], { icon: statusDotIcon(g), title: pinLabel(g), status: g.status // tallied by the cluster ring above }); m.bindPopup(popupHtml(g)); m.on('click', function () { selectPin(m); }); if (group) group.addLayer(m); else m.addTo(map); bounds.push([g.lat, g.lng]); pinRefs.push({ marker: m, urls: g.members.map(function (p) { return p.url.split('?')[0]; }) }); }); if (group) map.addLayer(group); map.fitBounds(bounds, { padding: [30, 30] }); addLegend(map); window.RR_MAP = map; // handle for tests/debug tooling // The container may have settled to its final size while Leaflet was // loading; re-sync so no gray gaps remain. setTimeout(function () { map.invalidateSize(); }, 50); setTimeout(placeCallout, 300); map.on('moveend zoomend', placeCallout); // Don't hijack page scroll: zoom with wheel only while the map is focused. map.on('focus', function () { map.scrollWheelZoom.enable(); }); map.on('blur', function () { map.scrollWheelZoom.disable(); }); wireRowHover(); } function rentVal(r) { return parseFloat(String(r == null ? '' : r).replace(/[^0-9.]/g, '')) || Infinity; } // Status ring for clusters: inline SVG arc segments proportional to the // statuses inside, count in the middle. Glanceable, ownable, no prices. // SVG (not conic-gradient): deterministic in every engine, including // iOS Safari where the gradient build broke (P0 2026-09-28). function clusterRingHtml(counts, total, size) { var RC = 2 * Math.PI * 17.5; // r=17.5 in the 46x46 viewBox var acc = 0, circles = ''; function ringArc(color, offset, len) { // +0.9px overlap per segment hides dash anti-aliasing seams; later // segments paint over earlier ones so the overlap stays invisible. return ''; } STATUS_META.forEach(function (sm) { var n = counts[sm.key] || 0; if (!n) return; var len = n / total * RC; circles += ringArc(sm.color, acc, len); acc += len; }); if (acc < RC - 0.001) { // unknown status keys fall back to the green slice circles += ringArc('#2F7A4D', acc, RC - acc); } var bits = STATUS_META.filter(function (sm) { return counts[sm.key]; }) .map(function (sm) { return counts[sm.key] + ' ' + statusLabel(sm.key).toLowerCase(); }); var label = total + (total === 1 ? ' listing: ' : ' listings: ') + bits.join(', '); return '' + '' + '' + total + ''; } // The legend/key: dot color = application status, exactly the labels the // list cards use. Collapsed on narrow screens so it never covers the map. function addLegend(map) { var legend = L.control({ position: 'topleft' }); legend.onAdd = function () { var d = L.DomUtil.create('details', 'map-legend'); if (window.innerWidth >= 640) d.setAttribute('open', ''); // The color key lives in the summary itself: on narrow screens the // details element starts collapsed, so without inline dots the key // was invisible until tapped (owner interface note 2026-09-28). The // chevron is the tap affordance (the native marker is hidden in CSS). var h = 'Listing status' + '' + STATUS_META.map(function (sm) { return ''; }).join('') + '
    '; STATUS_META.forEach(function (sm) { h += '
  • ' + esc(statusLabel(sm.key)) + '
  • '; }); h += '
  • Added in the last 24 hours
'; d.innerHTML = h; L.DomEvent.disableClickPropagation(d); L.DomEvent.disableScrollPropagation(d); return d; }; legend.addTo(map); } function groupPins(ps) { // Group pins that belong to the SAME building: same cleaned address // AND same rounded coordinate. Two distinct addresses that round to // the same point stay separate pins (owner P0 2026-09-28: the // Brownsville/Manhattan popup merged two boroughs into one pin). var byCoord = {}; ps.forEach(function (p) { var addr = (p.addr_key || "").trim().toLowerCase(); var k = (addr ? "a:" + addr : "x") + "|" + p.lat.toFixed(4) + ',' + p.lng.toFixed(4); (byCoord[k] = byCoord[k] || []).push(p); }); return Object.keys(byCoord).map(function (k) { var members = byCoord[k]; var priced = members.filter(function (m) { return m.rent; }) .sort(function (a, b) { return rentVal(a.rent) - rentVal(b.rent); }); // Group status: the most actionable member status wins, so a building // with one open unit never reads as "waitlist" at a glance. var gstatus = 'available', grank = 99, gnew = false; members.forEach(function (m) { var s = m.status || 'available'; var r = (STATUS_RANK[s] == null) ? 99 : STATUS_RANK[s]; if (r < grank) { grank = r; gstatus = s; } if (m.new) gnew = true; }); return { lat: members[0].lat, lng: members[0].lng, members: members, rent: priced.length ? priced[0].rent : null, count: members.length, status: gstatus, isNew: gnew }; }); } function pinLabel(g) { var p = g.members[0]; return (g.rent || 'Rent not listed') + (g.count > 1 ? ', ' + g.count + ' units' : ', ' + p.beds) + (p.hood ? ', ' + p.hood : '') + ', ' + statusLabel(g.status); } // Map v2: the pin is a color-coded status DOT. Price is gone from the pin // face; it lives in the hover title, the tap popup, and the list cards. // New listings (first seen < 24h) get a halo ring: an overlay, never a // second color, so a dot is always exactly one status. // The divIcon is a real 16x16 box anchored at its center (P0 2026-09-28: // 0x0 icons left iOS Safari with no tappable target). function statusDotIcon(g) { var cls = 'sdot sdot-' + (g.status || 'available') + (g.isNew ? ' is-new' : ''); var html = ''; return L.divIcon({ className: 'sdot-wrap', html: html, iconSize: [16, 16], iconAnchor: [8, 8] }); } function popupHtml(g) { var h = '
'; // Status first: the map's whole language is "can I apply right now?". // Price stays prominent right below it: never lost, just not on the pin. h += '
' + esc(statusLabel(g.status)) + '
'; if (g.count === 1) { var p = g.members[0]; h += '
' + esc(p.addr || '') + '
' + '
' + esc(p.rent || 'Rent not listed') + '
' + '
' + esc(p.beds) + '
' + // Reside enrichment (DECISIONS #65): enriched rent/bedrooms are // labeled so they never read as tracker-sourced fact. (p.enrich_note ? '
' + esc(p.enrich_note) + '
' : '') + (p.hood ? '
' + esc(p.hood) + '
' : '') + 'View listing'; } else { h += '
' + esc(g.rent || 'Rent not listed') + '
' + '
' + g.count + ' units
' + (g.members[0].hood ? '
' + esc(g.members[0].hood) + '
' : ''); g.members.slice(0, 5).forEach(function (p) { h += '' + '' + esc(p.rent || 'Rent not listed') + '' + '' + esc(p.beds) + ''; }); if (g.count > 5) h += '
+' + (g.count - 5) + ' more at this building
'; } return h + '
'; } function selectPin(m) { document.querySelectorAll('.sdot.sel').forEach(function (x) { x.classList.remove('sel'); }); var el = m.getElement && m.getElement(); var chip = el && el.querySelector('.sdot'); if (chip) chip.classList.add('sel'); flashRow(m); } // List <-> map linking: hovering (or keyboard-focusing) a result row // highlights its pin; clicking a pin opens the popup (Leaflet's bindPopup) // and flashes the matching row WITHOUT scrolling. Map users stay in map // context; the popup carries the detail (owner UX 2026-09-29). Row hrefs // carry household query params; pin urls are bare, so matching strips // the query string on both sides. function wireRowHover() { if (!listwrap) return; function hrefOf(el) { var a = el && el.closest ? el.closest('.lrow') : null; return a ? a.getAttribute('href').split('?')[0] : null; } listwrap.addEventListener('mouseover', function (ev) { setPinHover(hrefOf(ev.target)); }); listwrap.addEventListener('mouseleave', function () { setPinHover(null); }); listwrap.addEventListener('focusin', function (ev) { setPinHover(hrefOf(ev.target)); }); listwrap.addEventListener('focusout', function () { setPinHover(null); }); } function setPinHover(path) { pinRefs.forEach(function (ref) { var on = !!path && ref.urls.indexOf(path) !== -1; var el = ref.marker.getElement && ref.marker.getElement(); if (!el) return; // clustered pins aren't rendered; nothing to highlight var chip = el.querySelector('.sdot'); if (!chip) return; chip.classList.toggle('hl', on); el.classList.toggle('hl-above', on); }); } function flashRow(m) { // Pin tap highlights the matching row WITHOUT scrolling: the map stays // put and the popup shows the summary. Never call scrollIntoView here. if (mapwrap.hasAttribute('hidden') || listwrap.hasAttribute('hidden')) return; var ref = null; for (var i = 0; i < pinRefs.length; i++) { if (pinRefs[i].marker === m) { ref = pinRefs[i]; break; } } if (!ref || !ref.urls.length) return; var row = null; try { row = listwrap.querySelector('.lrow[href^="' + ref.urls[0].replace(/"/g, '') + '"]'); } catch (e) { row = null; } if (!row) return; row.classList.add('flash'); setTimeout(function () { row.classList.remove('flash'); }, 1600); } // Design/homepage-v2: the hero callout. A floating card anchored by a // connector line to ONE real listing pin (the server picks it and passes // its bare detail URL in data-hero-url). Positioned from the pin's live // container point, re-anchored on pan/zoom, hidden when the pin is // off-screen, dismissible so it can never trap taps. var callout = document.getElementById('hero-callout'); var calloutSvg = document.getElementById('hero-callout-svg'); var calloutClose = document.getElementById('hero-callout-close'); var heroUrl = mapEl.getAttribute('data-hero-url') || ''; var calloutDismissed = false; if (calloutClose) { calloutClose.addEventListener('click', function (ev) { ev.stopPropagation(); calloutDismissed = true; callout.setAttribute('hidden', ''); if (calloutSvg) calloutSvg.innerHTML = ''; }); } function heroMarker() { if (!heroUrl || !pinRefs.length) return null; for (var i = 0; i < pinRefs.length; i++) { if (pinRefs[i].urls.indexOf(heroUrl) !== -1) return pinRefs[i].marker; } return null; } function placeCallout() { if (!callout || calloutDismissed || !map) return; var mk = heroMarker(); if (!mk) { callout.setAttribute('hidden', ''); return; } var pt, size; try { pt = map.latLngToContainerPoint(mk.getLatLng()); size = map.getSize(); } catch (e) { return; } if (pt.x < 0 || pt.y < 0 || pt.x > size.x || pt.y > size.y) { callout.setAttribute('hidden', ''); if (calloutSvg) calloutSvg.innerHTML = ''; return; } callout.removeAttribute('hidden'); // Anchor up-right of the pin, clamped inside the map frame. var cw = callout.offsetWidth || 240, ch = callout.offsetHeight || 160; var x = Math.min(Math.max(pt.x + 28, 8), Math.max(size.x - cw - 8, 8)); var y = Math.max(pt.y - ch - 28, 8); callout.style.left = x + 'px'; callout.style.top = y + 'px'; // Connector: pin center to the callout's bottom-left corner. if (calloutSvg) { calloutSvg.setAttribute('width', size.x); calloutSvg.setAttribute('height', size.y); calloutSvg.innerHTML = ''; } } // The map hero is full-width at the top of the page on every viewport, so // there is no viewport-dependent layout to re-run; just reveal and init once. layoutMode(); })();