From 57a9f6f21a2f2e44ef15ff11f829cf9a368a1a1f Mon Sep 17 00:00:00 2001 From: Marie Michalova Date: Thu, 26 Feb 2026 21:14:48 +0100 Subject: [PATCH] Add NEW badge for recent listings, text input for price filter, cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New listings (≤1 day) show yellow NEW badge instead of oversized marker - Price filter changed from dropdown to text input (max 14M) - Cap price filter at 14M in JS - Remove unused regen_map.py - Remove unused HTMLParser import in scrape_idnes.py Co-Authored-By: Claude Opus 4.6 --- regen_map.py | 114 ---------------------------------------------- scrape_and_map.py | 67 +++++++++++++++++---------- scrape_idnes.py | 1 - 3 files changed, 43 insertions(+), 139 deletions(-) delete mode 100644 regen_map.py diff --git a/regen_map.py b/regen_map.py deleted file mode 100644 index 0d8f6f3..0000000 --- a/regen_map.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -""" -Přegeneruje mapu z již stažených dat (byty_sreality.json). -Doplní chybějící plochy ze Sreality API, opraví URL, aplikuje filtry. -""" -from __future__ import annotations - -import json -import time -import urllib.request -from pathlib import Path - -from scrape_and_map import ( - generate_map, format_price, MIN_AREA, HEADERS, DETAIL_API -) - - -def api_get(url: str) -> dict: - req = urllib.request.Request(url, headers=HEADERS) - with urllib.request.urlopen(req, timeout=30) as resp: - return json.loads(resp.read().decode("utf-8")) - - -def fix_sreality_url(estate: dict) -> str: - """Fix the Sreality URL to include disposition segment (only if missing).""" - disp = estate.get("disposition", "") - slug_map = { - "1+kk": "1+kk", "1+1": "1+1", "2+kk": "2+kk", "2+1": "2+1", - "3+kk": "3+kk", "3+1": "3+1", "4+kk": "4+kk", "4+1": "4+1", - "5+kk": "5+kk", "5+1": "5+1", "6+": "6-a-vice", "Atypický": "atypicky", - } - slug = slug_map.get(disp, "byt") - old_url = estate.get("url", "") - parts = old_url.split("/") - try: - byt_idx = parts.index("byt") - # Only insert if disposition slug is not already there - if byt_idx + 1 < len(parts) and parts[byt_idx + 1] == slug: - return old_url # already correct - parts.insert(byt_idx + 1, slug) - return "/".join(parts) - except ValueError: - return old_url - - -def fetch_area(hash_id: int) -> int | None: - """Fetch area from detail API.""" - try: - url = DETAIL_API.format(hash_id) - detail = api_get(url) - for item in detail.get("items", []): - name = item.get("name", "") - if "žitná ploch" in name or "zitna ploch" in name.lower(): - return int(item["value"]) - except Exception: - pass - return None - - -def main(): - json_path = Path("byty_sreality.json") - if not json_path.exists(): - print("Soubor byty_sreality.json nenalezen. Nejprve spusť scrape_and_map.py") - return - - estates = json.loads(json_path.read_text(encoding="utf-8")) - print(f"Načteno {len(estates)} bytů z byty_sreality.json") - - # Step 1: Fetch missing areas - missing_area = [e for e in estates if e.get("area") is None] - print(f"Doplňuji plochu u {len(missing_area)} bytů...") - - for i, e in enumerate(missing_area): - time.sleep(0.3) - area = fetch_area(e["hash_id"]) - if area is not None: - e["area"] = area - if (i + 1) % 50 == 0: - print(f" {i + 1}/{len(missing_area)} ...") - - # Count results - with_area = sum(1 for e in estates if e.get("area") is not None) - print(f"Plocha doplněna: {with_area}/{len(estates)}") - - # Step 2: Fix URLs - for e in estates: - e["url"] = fix_sreality_url(e) - - # Step 3: Filter by min area - filtered = [] - excluded = 0 - for e in estates: - area = e.get("area") - if area is not None and area < MIN_AREA: - excluded += 1 - continue - filtered.append(e) - - print(f"Vyloučeno (< {MIN_AREA} m²): {excluded}") - print(f"Zbývá: {len(filtered)} bytů") - - # Save updated data - filtered_path = Path("byty_sreality.json") - filtered_path.write_text( - json.dumps(filtered, ensure_ascii=False, indent=2), - encoding="utf-8", - ) - - # Generate map - generate_map(filtered) - - -if __name__ == "__main__": - main() diff --git a/scrape_and_map.py b/scrape_and_map.py index 3594b09..c129bb8 100644 --- a/scrape_and_map.py +++ b/scrape_and_map.py @@ -13,7 +13,7 @@ import math import time import urllib.request import urllib.parse -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from scraper_stats import write_stats @@ -448,9 +448,13 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"): price_legend_items += ( '
' - '' - 'Nové (z dnešního scrapu) — větší
' + '' + '' + 'NEW' + '' + 'Nové (≤ 1 den)' ) markers_js = "" @@ -476,7 +480,9 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"): first_seen = e.get("first_seen", "") last_changed = e.get("last_changed", "") - is_new = first_seen == datetime.now().strftime("%Y-%m-%d") + today = datetime.now().strftime("%Y-%m-%d") + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + is_new = first_seen in (today, yesterday) new_badge = ( '
@@ -784,19 +787,28 @@ function addMarker(lat, lon, color, popup, hashId, firstSeen, lastChanged) {{ function addNewMarker(lat, lon, color, popup, hashId, firstSeen, lastChanged) {{ var marker = L.circleMarker([lat, lon], {{ - radius: 12, + radius: 8, fillColor: color, - color: color, - weight: 4, - opacity: 0.35, - fillOpacity: 0.95, + color: '#fff', + weight: 2, + opacity: 1, + fillOpacity: 0.85, }}).bindPopup(popup); marker._data = {{ lat: lat, lon: lon, color: color, hashId: hashId, isNew: true, firstSeen: firstSeen || '', lastChanged: lastChanged || '' }}; allMarkers.push(marker); marker.addTo(map); - marker.on('add', function() {{ - if (marker._path) marker._path.classList.add('marker-new'); + var badge = L.marker([lat, lon], {{ + icon: L.divIcon({{ + className: 'new-badge-icon', + html: 'NEW', + iconSize: [32, 14], + iconAnchor: [-6, 7], + }}), + interactive: false, + pane: 'markerPane', }}); + badge.addTo(map); + marker._newBadge = badge; }} function heartIcon(color) {{ @@ -899,6 +911,7 @@ function applyMarkerStyle(marker, status) {{ }} else {{ if (status === 'fav') {{ removeRejectStrike(marker); + if (marker._newBadge && map.hasLayer(marker._newBadge)) map.removeLayer(marker._newBadge); if (!marker._data._origCircle) marker._data._origCircle = true; var popup = marker.getPopup(); var popupContent = popup ? popup.getContent() : ''; @@ -922,6 +935,7 @@ function applyMarkerStyle(marker, status) {{ }} // Add strikethrough line over the marker addRejectStrike(marker); + if (marker._newBadge && map.hasLayer(marker._newBadge)) map.removeLayer(marker._newBadge); }} else {{ if (marker._data._origCircle && !(marker instanceof L.CircleMarker)) {{ revertToCircle(marker, {{ radius: 8, fillColor: marker._data.color, color: '#fff', weight: 2, fillOpacity: 0.85 }}); @@ -934,6 +948,7 @@ function applyMarkerStyle(marker, status) {{ }} if (marker._path) marker._path.classList.remove('marker-rejected'); removeRejectStrike(marker); + if (marker._newBadge && !map.hasLayer(marker._newBadge)) marker._newBadge.addTo(map); }} }} }} @@ -1089,7 +1104,9 @@ map.on('popupopen', function(e) {{ // ── Filters ──────────────────────────────────────────────────── function applyFilters() {{ var minFloor = parseInt(document.getElementById('min-floor').value); - var maxPrice = parseInt(document.getElementById('max-price').value); + var maxPriceEl = document.getElementById('max-price'); + var maxPrice = parseInt(maxPriceEl.value) || 14000000; + if (maxPrice > 14000000) {{ maxPrice = 14000000; maxPriceEl.value = 14000000; }} var hideRejected = document.getElementById('hide-rejected').checked; var daysFilter = parseInt(document.getElementById('days-filter').value) || 0; var ratings = loadRatings(); @@ -1130,10 +1147,12 @@ function applyFilters() {{ visible++; // Show strike line if rejected and visible if (m._rejectStrike && !map.hasLayer(m._rejectStrike)) m._rejectStrike.addTo(map); + if (m._newBadge && !map.hasLayer(m._newBadge)) m._newBadge.addTo(map); }} else {{ if (map.hasLayer(m)) map.removeLayer(m); // Hide strike line when marker hidden if (m._rejectStrike && map.hasLayer(m._rejectStrike)) map.removeLayer(m._rejectStrike); + if (m._newBadge && map.hasLayer(m._newBadge)) map.removeLayer(m._newBadge); }} }}); diff --git a/scrape_idnes.py b/scrape_idnes.py index 820ded6..88f17ff 100644 --- a/scrape_idnes.py +++ b/scrape_idnes.py @@ -15,7 +15,6 @@ import re import time import urllib.request import urllib.parse -from html.parser import HTMLParser from pathlib import Path from scraper_stats import write_stats