Remove tracked generated files, fix map link on status page #4
114
regen_map.py
114
regen_map.py
@@ -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()
|
||||
@@ -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 += (
|
||||
'<div style="display:flex;align-items:center;gap:6px;margin:6px 0 0 0;'
|
||||
'padding-top:6px;border-top:1px solid #eee;">'
|
||||
'<span style="width:18px;height:18px;border-radius:50%;background:#66BB6A;'
|
||||
'display:inline-block;box-shadow:0 1px 4px rgba(0,0,0,0.35);flex-shrink:0;"></span>'
|
||||
'<span>Nové (z dnešního scrapu) — větší</span></div>'
|
||||
'<span style="display:inline-flex;align-items:center;gap:3px;flex-shrink:0;">'
|
||||
'<span style="width:14px;height:14px;border-radius:50%;background:#66BB6A;'
|
||||
'display:inline-block;box-shadow:0 1px 3px rgba(0,0,0,0.3);"></span>'
|
||||
'<span style="font-size:8px;font-weight:700;background:#FFD600;color:#333;'
|
||||
'padding:1px 3px;border-radius:2px;">NEW</span>'
|
||||
'</span>'
|
||||
'<span>Nové (≤ 1 den)</span></div>'
|
||||
)
|
||||
|
||||
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 = (
|
||||
'<span style="margin-left:6px;font-size:11px;background:#FFD600;color:#333;'
|
||||
@@ -603,12 +609,12 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
|
||||
.heart-icon-fav svg path {{ stroke: gold !important; stroke-width: 2.5 !important; filter: drop-shadow(0 0 4px rgba(255,193,7,0.7)); }}
|
||||
.heart-icon-rej {{ opacity: 0.4 !important; filter: grayscale(1); }}
|
||||
.reject-overlay {{ background: none !important; border: none !important; pointer-events: none !important; }}
|
||||
@keyframes pulse-new {{
|
||||
0% {{ stroke-opacity: 1; stroke-width: 3px; r: 11; }}
|
||||
50% {{ stroke-opacity: 0.4; stroke-width: 6px; r: 12; }}
|
||||
100% {{ stroke-opacity: 1; stroke-width: 3px; r: 11; }}
|
||||
.new-badge-icon {{ background: none !important; border: none !important; pointer-events: none !important; }}
|
||||
.new-badge {{
|
||||
font-size: 9px; font-weight: 700; color: #333; background: #FFD600;
|
||||
padding: 1px 4px; border-radius: 3px; white-space: nowrap;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.3); letter-spacing: 0.5px;
|
||||
}}
|
||||
.marker-new {{ animation: pulse-new 2s ease-in-out infinite; }}
|
||||
.info-panel {{
|
||||
position: absolute; top: 10px; right: 10px; z-index: 1000;
|
||||
background: white; padding: 16px; border-radius: 10px;
|
||||
@@ -683,12 +689,9 @@ def generate_map(estates: list[dict], output_path: str = "mapa_bytu.html"):
|
||||
</div>
|
||||
<div style="margin-top:6px;">
|
||||
<label>Max cena:
|
||||
<select id="max-price" onchange="applyFilters()">
|
||||
<option value="13500000">13 500 000 Kč</option>
|
||||
<option value="12000000">12 000 000 Kč</option>
|
||||
<option value="10000000">10 000 000 Kč</option>
|
||||
<option value="8000000">8 000 000 Kč</option>
|
||||
</select>
|
||||
<input type="number" id="max-price" value="13500000" max="14000000" step="500000"
|
||||
style="width:130px;padding:2px 4px;border:1px solid #ccc;border-radius:3px;"
|
||||
onchange="applyFilters()" onkeyup="applyFilters()"> Kč
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-top:6px;">
|
||||
@@ -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: '<span class="new-badge">NEW</span>',
|
||||
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);
|
||||
}}
|
||||
}});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user