This commit is contained in:
tamakyi
2026-09-19 19:50:27 +08:00
parent 824c1e3b3d
commit 6b023fb9fd
12 changed files with 1610 additions and 0 deletions
+856
View File
@@ -0,0 +1,856 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>rond-with-you · 演示版</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ctext y='26' font-size='26'%3E%F0%9F%A7%AD%3C/text%3E%3C/svg%3E">
<link rel="stylesheet" href="vendor/leaflet/leaflet.css">
<link rel="stylesheet" href="vendor/markercluster/MarkerCluster.css">
<link rel="stylesheet" href="vendor/markercluster/MarkerCluster.Default.css">
<style>
:root{
--bg:#f5f6f8; --card:#ffffff; --text:#1b1d21; --text-2:#4a4f57; --text-3:#8b9099;
--border:#e7e9ed; --border-2:#eef0f3; --accent:#1c9d57; --accent-2:#15803d;
--accent-soft:rgba(28,157,87,.12); --shadow:0 1px 2px rgba(16,24,40,.04),0 2px 12px rgba(16,24,40,.05);
--radius:13px;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",Roboto,Helvetica,Arial,sans-serif;
background:var(--bg); color:var(--text); font-size:14px; line-height:1.55; -webkit-font-smoothing:antialiased;
}
a{color:var(--accent-2); text-decoration:none}
a:hover{text-decoration:underline}
h1{font-size:21px;margin:0;font-weight:700;letter-spacing:.2px}
h2{font-size:15.5px;margin:0 0 2px;font-weight:650}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:7px 13px;border:1px solid var(--border);background:#fff;color:var(--text);border-radius:9px;font-size:13px;cursor:pointer;transition:.12s}
.btn:hover{background:#f4f6f8;text-decoration:none}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn.primary:hover{background:var(--accent-2)}
.btn.sm{padding:5px 10px;font-size:12.5px}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.92em}
.muted{color:var(--text-3)}
.num{text-align:right;font-variant-numeric:tabular-nums}
/* nav */
.nav{position:sticky;top:0;z-index:1200;background:rgba(255,255,255,.92);backdrop-filter:saturate(180%) blur(8px);border-bottom:1px solid var(--border)}
.nav-inner{max-width:1080px;margin:0 auto;display:flex;align-items:center;gap:14px;padding:10px 16px}
.brand{display:flex;align-items:center;gap:8px;font-weight:700;color:var(--text);font-size:15px}
.brand:hover{text-decoration:none}
.avatar{width:28px;height:28px;border-radius:9px;background:var(--accent-soft);display:inline-flex;align-items:center;justify-content:center;font-size:16px}
.nav-links{display:flex;gap:2px;overflow-x:auto;scrollbar-width:none;flex:1}
.nav-links::-webkit-scrollbar{display:none}
.nav-links a{padding:6px 11px;border-radius:8px;color:var(--text-2);font-size:13.5px;white-space:nowrap}
.nav-links a:hover{background:#f0f2f5;text-decoration:none}
.nav-links a.on{background:var(--accent-soft);color:var(--accent-2);font-weight:600}
.nav-right{display:flex;align-items:center;gap:8px}
.nav-right .btn{font-size:12.5px}
@media(max-width:520px){
.nav-inner{padding:9px 12px;gap:8px}
.nav-links a{padding:6px 8px;font-size:13px}
.brand{font-size:14px}
.nav-right .btn{padding:5px 9px;font-size:12px}
}
.banner{background:linear-gradient(90deg,#fff7e6,#fff);border-bottom:1px solid #f3e3bf;color:#8a6d1f;font-size:12.5px;text-align:center;padding:6px 12px}
.banner b{color:#a06a00}
.wrap{max-width:1080px;margin:0 auto;padding:18px 16px 60px}
.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;margin:4px 0 16px;flex-wrap:wrap}
.page-head .sub{color:var(--text-3);font-size:13px;margin-top:2px}
.row{display:flex;gap:10px;flex-wrap:wrap}
.grid{display:grid;gap:16px}
.grid.g2{grid-template-columns:repeat(2,1fr)}
@media(max-width:720px){.grid.g2{grid-template-columns:1fr}}
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;box-shadow:var(--shadow);margin-bottom:16px}
.card-sub{color:var(--text-3);font-size:12.5px;margin-bottom:12px}
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
@media(max-width:720px){.kpis{grid-template-columns:repeat(2,1fr)}}
.kpi{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:12px 13px;box-shadow:var(--shadow)}
.kpi .v{font-size:24px;font-weight:750;line-height:1.1;font-variant-numeric:tabular-nums}
.kpi .v small{font-size:13px;font-weight:600;color:var(--text-3);margin-left:2px}
.kpi .k{font-size:12px;color:var(--text-3);margin-top:3px}
/* bars */
.bars{display:flex;flex-direction:column;gap:9px}
.bar-row{display:grid;grid-template-columns:120px 1fr auto;align-items:center;gap:10px;font-size:13px}
.bar-row .nm{color:var(--text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bar-track{height:9px;background:var(--border-2);border-radius:6px;overflow:hidden}
.bar-fill{height:100%;border-radius:6px;background:var(--accent)}
.bar-row .vv{color:var(--text-3);font-size:12px;white-space:nowrap;font-variant-numeric:tabular-nums}
@media(max-width:520px){.bar-row{grid-template-columns:84px 1fr auto}.bar-row .nm{font-size:12px}}
/* chips */
.chips{display:flex;flex-wrap:wrap;gap:7px}
.chip{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border:1px solid var(--border);border-radius:999px;background:#fff;color:var(--text-2);font-size:12.5px;cursor:pointer}
.chip.on{background:var(--accent-soft);border-color:transparent;color:var(--accent-2);font-weight:600}
.chip .dot{width:9px;height:9px;border-radius:50%;display:inline-block}
/* calendar heatmap */
.heat{display:flex;gap:3px;overflow-x:auto;padding-bottom:4px}
.wk{display:flex;flex-direction:column;gap:3px}
.d{width:12px;height:12px;border-radius:3px;background:#ebedf0}
.d.l1{background:#cdeed6}.d.l2{background:#9bdcb0}.d.l3{background:#5cc488}.d.l4{background:#1c9d57}
.heat-months{display:flex;gap:3px;margin-top:4px;font-size:10.5px;color:var(--text-3)}
.heat-months span{width:12px}
.heat-months .ml{width:auto;white-space:nowrap;margin-right:14px}
.heat-legend{display:flex;align-items:center;gap:4px;font-size:11.5px;color:var(--text-3);margin-top:10px}
/* monthly / hourly cols */
.cols{display:flex;align-items:flex-end;gap:3px;height:120px}
.col{flex:1;background:var(--accent);border-radius:3px 3px 0 0;min-height:2px;opacity:.85;transition:.15s}
.col:hover{opacity:1}
.cols-x{display:flex;gap:3px;margin-top:5px;font-size:10px;color:var(--text-3)}
.cols-x span{flex:1;text-align:center;white-space:nowrap;overflow:hidden}
/* tables */
.tw{overflow-x:auto}
.tb{width:100%;border-collapse:collapse;font-size:13px}
.tb th,.tb td{padding:8px 10px;border-bottom:1px solid var(--border-2);text-align:left}
.tb th{color:var(--text-3);font-weight:600;font-size:12px}
.tb tbody tr:hover{background:#fafbfc}
.tag{display:inline-block;padding:1px 7px;border-radius:999px;background:var(--accent-soft);color:var(--accent-2);font-size:11.5px;margin-right:3px}
/* list (recent) */
.list{display:flex;flex-direction:column}
.item{display:flex;gap:11px;align-items:center;padding:10px 0;border-bottom:1px solid var(--border-2)}
.item:last-child{border-bottom:none}
.item .ic{width:34px;height:34px;border-radius:9px;display:inline-flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0}
.item .t{font-weight:600}
.item .m{font-size:12px;color:var(--text-3)}
.item .r{margin-left:auto;text-align:right;font-size:12.5px;font-weight:600;white-space:nowrap}
.item .r .muted{font-weight:400}
/* map */
.map-page{position:relative;height:calc(100vh - 111px);min-height:420px}
#map{position:absolute;inset:0;border-radius:0}
.map-preview{height:340px;border-radius:11px;overflow:hidden;border:1px solid var(--border);cursor:pointer}
.map-panel{position:absolute;top:12px;left:12px;z-index:800;width:268px;max-height:calc(100% - 24px);overflow-y:auto;background:#fff;border:1px solid var(--border);border-radius:12px;box-shadow:var(--shadow);padding:13px}
.map-panel h3{font-size:12.5px;margin:14px 0 7px;color:var(--text-2)}
.map-panel .row{display:flex;gap:7px}
.map-panel input,.map-panel select{width:100%;padding:7px 9px;border:1px solid var(--border);border-radius:8px;font-size:13px;background:#fff;color:var(--text)}
.map-panel .filter-actions{margin-top:12px}
.map-panel .map-count{font-size:12px;margin-top:11px}
.panel-fold{display:none;width:100%;align-items:center;justify-content:space-between;gap:8px;padding:9px 11px;border:1px solid var(--border);background:#fff;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;color:var(--text)}
.panel-fold .pf-n{font-weight:400;color:var(--text-3);font-size:12px}
.panel-fold .pf-c{color:var(--text-3)}
.map-legend{position:absolute;right:12px;bottom:12px;z-index:800;background:#fff;border:1px solid var(--border);border-radius:11px;box-shadow:var(--shadow);padding:11px 13px;font-size:12.5px;max-width:200px}
.map-legend .lg{display:flex;align-items:center;gap:7px;margin-top:5px;color:var(--text-2)}
.map-legend .lg i{width:10px;height:10px;border-radius:3px;display:inline-block}
.timeline-bar{position:absolute;left:50%;transform:translateX(-50%);bottom:14px;z-index:800;display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--border);border-radius:999px;box-shadow:var(--shadow);padding:8px 14px;width:min(560px,calc(100% - 280px))}
.timeline-bar input[type=range]{flex:1}
.tl-play,.tl-reset{border:1px solid var(--border);background:#fff;border-radius:8px;padding:3px 9px;cursor:pointer;font-size:12.5px}
.tl-label{font-size:12px;color:var(--text-2);min-width:130px;text-align:right}
@media(max-width:720px){
.map-panel{width:calc(100% - 24px)}
.panel-fold{display:flex}
.map-panel.collapsed .panel-body{display:none}
.map-panel.collapsed h3{display:none}
.timeline-bar{width:calc(100% - 24px)}
.map-legend{display:none}
}
.leaflet-popup-content{font-size:13px;margin:10px 12px}
.pop b{font-size:14px}
.pop .pm{color:var(--text-3);font-size:12px;margin-top:2px}
/* cluster */
.cluster{background:var(--accent);border-radius:50%;color:#fff;font-weight:700;font-size:12px;text-align:center;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.06)}
/* demo pins via divIcon */
.pin{width:14px;height:14px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.3)}
/* places view */
.toolbar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
.toolbar input,.toolbar select{padding:8px 11px;border:1px solid var(--border);border-radius:9px;font-size:13px;background:#fff}
.toolbar input[type=text]{flex:1;min-width:180px}
.pager{display:flex;align-items:center;gap:10px;justify-content:center;margin-top:14px;font-size:13px}
.pager a{border:1px solid var(--border);padding:5px 12px;border-radius:8px;background:#fff}
.pager .cur{color:var(--text-3)}
/* timeline view */
.tl-day{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:13px 15px;margin-bottom:12px;box-shadow:var(--shadow)}
.tl-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:9px}
.tl-head .dt{font-weight:700;font-size:14.5px}
.tl-head .m{color:var(--text-3);font-size:12.5px}
.tl-item{display:flex;gap:10px;align-items:center;padding:6px 0;border-top:1px solid var(--border-2)}
.tl-item .ic{width:26px;height:26px;border-radius:7px;display:inline-flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
.tl-item .tn{font-weight:600}
.tl-item .tm{font-size:12px;color:var(--text-3)}
.tl-item .tr{margin-left:auto;font-size:12px;color:var(--text-2);white-space:nowrap}
.empty{padding:30px 10px;text-align:center;color:var(--text-3)}
.ft{max-width:1080px;margin:0 auto;padding:22px 16px;color:var(--text-3);font-size:12.5px;text-align:center}
.note{font-size:12px;color:var(--text-3);margin-top:10px}
</style>
</head>
<body>
<div class="banner">🧭 <b>演示版</b> · 本页全部为程序生成的示例数据,仅用于展示界面与交互,并非真实足迹记录。地图底图来自公共瓦片服务,需要联网。</div>
<nav class="nav">
<div class="nav-inner">
<a class="brand" href="#home"><span class="avatar">🧭</span><span>足迹地图 · Demo</span></a>
<div class="nav-links">
<a href="#home" data-view="home">主页</a>
<a href="#map" data-view="map">地图</a>
<a href="#places" data-view="places">地点</a>
<a href="#timeline" data-view="timeline">时间线</a>
<a href="#stats" data-view="stats">统计</a>
</div>
<div class="nav-right">
<a class="btn sm" href="https://github.com/tamakyi/rond-with-you" target="_blank" rel="noopener">GitHub</a>
</div>
</div>
</nav>
<main>
<section id="view-home" class="wrap"></section>
<section id="view-map" style="display:none">
<div class="map-page">
<div id="map"></div>
<div class="map-panel" id="map-panel">
<button class="panel-fold" id="panel-fold" type="button" aria-expanded="true"><span>筛选</span><span class="pf-n" id="pf-count">0 个地点</span><span class="pf-c" id="pf-caret" aria-hidden="true">▾</span></button>
<div class="panel-body">
<h3>时间范围</h3>
<div class="chips presets" id="map-presets"></div>
<div class="row" style="margin:10px 0">
<input type="date" id="m-from" style="flex:1">
<input type="date" id="m-to" style="flex:1">
</div>
<h3>地点类型</h3>
<div class="chips" id="map-types" style="margin-bottom:4px"></div>
<details class="adv-fold">
<summary style="cursor:pointer;font-size:12.5px;color:var(--text-2);margin:12px 0 6px">更多筛选 / 图层</summary>
<div class="row" style="margin-bottom:8px">
<select id="m-city" style="flex:1"><option value="">全部城市</option></select>
<select id="m-min" style="flex:1">
<option value="0">不限停留</option><option value="30">≥30 分钟</option>
<option value="60">≥1 小时</option><option value="180">≥3 小时</option>
</select>
</div>
<input type="text" id="m-q" placeholder="关键词(地点/城市)" style="margin-bottom:10px">
<div class="chips" style="margin-bottom:4px">
<label class="chip"><input type="checkbox" id="t-track" style="accent-color:var(--accent)"> 行程轨迹</label>
<label class="chip"><input type="checkbox" id="t-heat" style="accent-color:var(--accent)"> 到访热力</label>
<label class="chip"><input type="checkbox" id="t-city" style="accent-color:var(--accent)"> 城市聚合</label>
</div>
</details>
<div class="map-count muted">当前显示 <b id="pt-count">0</b> 个地点</div>
<div class="row filter-actions">
<button class="btn" id="m-apply" style="flex:1">应用筛选</button>
<button class="btn" id="m-reset">重置</button>
</div>
</div>
</div>
<div class="timeline-bar" id="timeline-bar" hidden>
<button class="tl-play" id="tl-play" type="button" aria-label="播放">▶</button>
<input type="range" id="tl-range" min="0" max="100" value="100" aria-label="时间轴">
<span class="tl-label" id="tl-label"></span>
<button class="tl-reset" id="tl-reset" type="button">全部</button>
</div>
<div class="map-legend" id="map-legend"></div>
</div>
</section>
<section id="view-places" class="wrap" style="display:none"></section>
<section id="view-timeline" class="wrap" style="display:none"></section>
<section id="view-stats" class="wrap" style="display:none"></section>
</main>
<footer class="ft">rond-with-you 演示页 · 纯前端静态页面,数据由浏览器本地生成 · 原项目:Go + PostgreSQL + Leaflet,零外部 CDN</footer>
<script src="vendor/leaflet/leaflet.js"></script>
<script src="vendor/markercluster/leaflet.markercluster.js"></script>
<script src="vendor/heat/leaflet-heat.js"></script>
<script>
"use strict";
/* ============ 确定性随机数(保证每次刷新数据一致) ============ */
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
var rnd=mulberry32(20240919);
function rand(a,b){return a+(b-a)*rnd();}
function randint(a,b){return Math.floor(rand(a,b+1));}
function pick(a){return a[Math.floor(rnd()*a.length)];}
function weighted(items,wf){var t=0;for(var i=0;i<items.length;i++)t+=wf(items[i]);var r=rnd()*t;for(var j=0;j<items.length;j++){r-=wf(items[j]);if(r<=0)return items[j];}return items[items.length-1];}
/* ============ 基础数据 ============ */
var CITIES=[
{name:'北京',prov:'北京',lat:39.9042,lng:116.4074,w:4,base:true},
{name:'上海',prov:'上海',lat:31.2304,lng:121.4737,w:4,base:true},
{name:'广州',prov:'广东',lat:23.1291,lng:113.2644,w:3},
{name:'深圳',prov:'广东',lat:22.5431,lng:114.0579,w:3},
{name:'成都',prov:'四川',lat:30.5728,lng:104.0668,w:3},
{name:'杭州',prov:'浙江',lat:30.2741,lng:120.1551,w:2},
{name:'西安',prov:'陕西',lat:34.3416,lng:108.9398,w:2},
{name:'东京',prov:'东京',lat:35.6762,lng:139.6503,w:2,abroad:true},
{name:'新加坡',prov:'新加坡',lat:1.3521,lng:103.8198,w:2,abroad:true}
];
var ACTS=[
{id:1,name:'家',key:'home',icon:'🏠',color:'#94a3b8',w:5},
{id:2,name:'工作',key:'work',icon:'💼',color:'#3b82f6',w:5},
{id:3,name:'餐饮',key:'food',icon:'🍜',color:'#f97316',w:6},
{id:4,name:'咖啡',key:'coffee',icon:'☕',color:'#b45309',w:3},
{id:5,name:'购物',key:'shopping',icon:'🛍️',color:'#ec4899',w:3},
{id:6,name:'景点',key:'sight',icon:'📸',color:'#14b8a6',w:3},
{id:7,name:'公园',key:'park',icon:'🌳',color:'#22c55e',w:2},
{id:8,name:'交通',key:'transit',icon:'🚉',color:'#8b5cf6',w:2},
{id:9,name:'酒店',key:'hotel',icon:'🏨',color:'#6366f1',w:2},
{id:10,name:'医院',key:'hospital',icon:'🏥',color:'#ef4444',w:1}
];
var ACT={}; ACTS.forEach(function(a){ACT[a.id]=a;});
var NAME_POOL={
home:['家','爸妈家'], work:['公司','分部办公室'], food:['巷口面馆','街角火锅','老地方餐馆','夜市大排档','社区食堂'],
coffee:['转角咖啡','河畔咖啡馆','写字楼咖啡','书店咖啡'], shopping:['万象城','百货商场','生活超市','街区小店'],
sight:['古城墙','博物馆','地标广场','老街','观景台'], park:['城市公园','滨江绿道','植物园','湖边步道'],
transit:['火车站','机场','客运站','地铁站'], hotel:['商务酒店','民宿','度假酒店'], hospital:['市第一医院','社区诊所']
};
var TAGS=[{id:1,name:'美食',color:'#f97316'},{id:2,name:'出差',color:'#3b82f6'},{id:3,name:'旅行',color:'#14b8a6'},{id:4,name:'周末',color:'#22c55e'},{id:5,name:'推荐',color:'#ec4899'}];
var MODES=[{name:'步行',icon:'🚶',color:'#22c55e',max:1.5},{name:'骑行',icon:'🚲',color:'#14b8a6',max:5},{name:'地铁',icon:'🚇',color:'#3b82f6',max:25},{name:'公交',icon:'🚌',color:'#8b5cf6',max:25},{name:'驾车',icon:'🚗',color:'#f97316',max:300},{name:'高铁',icon:'🚄',color:'#ec4899',max:1200},{name:'飞机',icon:'✈️',color:'#ef4444',max:99999}];
/* ============ 坐标:WGS-84 → GCJ-02(与高德底图对齐) ============ */
var XPI=3.14159265358979324, XA=6378245.0, XEE=0.00669342162296594323;
function outOfChina(lng,lat){return !(lng>73.66&&lng<135.05&&lat>3.86&&lat<53.55);}
function tLat(x,y){var r=-100+2*x+3*y+0.2*y*y+0.1*x*y+0.2*Math.sqrt(Math.abs(x));r+=(20*Math.sin(6*x*XPI)+20*Math.sin(2*x*XPI))*2/3;r+=(20*Math.sin(y*XPI)+40*Math.sin(y/3*XPI))*2/3;r+=(160*Math.sin(y/12*XPI)+320*Math.sin(y*XPI/30))*2/3;return r;}
function tLng(x,y){var r=300+x+2*y+0.1*x*x+0.1*x*y+0.1*Math.sqrt(Math.abs(x));r+=(20*Math.sin(6*x*XPI)+20*Math.sin(2*x*XPI))*2/3;r+=(20*Math.sin(x*XPI)+40*Math.sin(x/3*XPI))*2/3;r+=(150*Math.sin(x/12*XPI)+300*Math.sin(x/30*XPI))*2/3;return r;}
function wgs2gcj(lng,lat){if(outOfChina(lng,lat))return [lng,lat];var dLat=tLat(lng-105,lat-35),dLng=tLng(lng-105,lat-35);var rl=lat/180*XPI,m=Math.sin(rl);m=1-XEE*m*m;var sm=Math.sqrt(m);dLat=(dLat*180)/((XA*(1-XEE))/(m*sm)*XPI);dLng=(dLng*180)/(XA/sm*Math.cos(rl)*XPI);return [lng+dLng,lat+dLat];}
/* ============ 生成地点 ============ */
var PLACES=[], pid=1, placeById={}, usedName={};
CITIES.forEach(function(c){
var n=randint(9,15);
for(var i=0;i<n;i++){
var act = c.base ? weighted(ACTS,function(a){return a.w;}) : weighted(ACTS.filter(function(a){return a.id>2;}),function(a){return a.w;});
var pool=NAME_POOL[act.key];
var name=pick(pool)+(rnd()<0.4?(' '+(randint(1,9))+'号'):'');
if(!usedName[c.name]) usedName[c.name]={};
var base=name,k=2;
while(usedName[c.name][name]){name=base+' '+(k++);} // 同城同名去重
usedName[c.name][name]=1;
var g=wgs2gcj(c.lng+rand(-0.045,0.045), c.lat+rand(-0.035,0.035));
var p={id:pid++,name:name,city:c.name,prov:c.prov,lat:g[1],lng:g[0],actId:act.id,visits:[]};
PLACES.push(p); placeById[p.id]=p;
}
});
/* ============ 生成到访 ============ */
var START=new Date('2024-01-01T00:00:00').getTime();
var END=new Date('2025-12-31T23:59:59').getTime();
var VISIT_COUNT=760, visits=[];
var raw=[], curCity=CITIES[0];
for(var i=0;i<VISIT_COUNT;i++){
if(i>0 && rnd()<0.05) curCity=weighted(CITIES,function(c){return c.w;}); // 偶尔出行换城市,其余时间同城活动
var cityPlaces=PLACES.filter(function(p){return p.city===curCity.name;});
var place=weighted(cityPlaces,function(p){return ACT[p.actId].w;});
var act=ACT[place.actId];
var dmin = act.id===1?rand(180,600): act.id===2?rand(240,540): act.id===8?rand(5,90): rand(20,180);
var tags=[];
if(rnd()<0.28) tags.push(pick(TAGS).id);
if(rnd()<0.12) tags.push(pick(TAGS).id);
raw.push({place:place,dmin:dmin,gap:rand(150,2200),tags:tags});
}
// 按时间顺序铺到 2024–2025:相邻两次到访间隔为 gap 分钟,整体归一化铺满两年
var sumD=0,sumG=0; raw.forEach(function(r){sumD+=r.dmin;sumG+=r.gap;});
var spanMin=(END-START)/60000, gscale=sumG>0?(spanMin-sumD)/sumG:1; if(gscale<=0)gscale=0.01;
var tcur=START;
raw.forEach(function(r){
var arrival=tcur, departure=arrival+r.dmin*60000;
var v={placeId:r.place.id,arrival:arrival,departure:departure,dur:r.dmin,tags:r.tags};
visits.push(v); r.place.visits.push(v);
tcur=departure+r.gap*gscale*60000;
});
/* ============ 工具 ============ */
function hav(la1,lo1,la2,lo2){var R=6371,dLa=(la2-la1)*Math.PI/180,dLo=(lo2-lo1)*Math.PI/180,a=Math.sin(dLa/2)**2+Math.cos(la1*Math.PI/180)*Math.cos(la2*Math.PI/180)*Math.sin(dLo/2)**2;return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));}
function num(n){return (n==null?0:n).toLocaleString('zh-CN');}
function dur(min){if(min==null)return '—';min=Math.round(min);if(min<60)return min+' 分钟';var h=Math.floor(min/60),m=min%60;if(h<24)return (h?h+' 小时':'')+(m?m+' 分':'');var d=Math.floor(h/24),hh=h%24;return d+' 天'+(hh?hh+' 小时':'');}
function km(v){return (v==null?0:v).toLocaleString('zh-CN',{maximumFractionDigits:0});}
function pct(a,b){return b?Math.max(2,Math.round(a/b*100)):0;}
function since(ts){var d=Math.floor((Date.now()-ts)/86400000);if(d<=0)return '今天';if(d<30)return d+' 天前';if(d<365)return Math.floor(d/30)+' 个月前';return Math.floor(d/365)+' 年前';}
function fdate(ts){var d=new Date(ts);return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}
function fhm(ts){var d=new Date(ts);return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');}
function tagName(id){for(var i=0;i<TAGS.length;i++)if(TAGS[i].id===id)return TAGS[i].name;return '';}
/* ============ 聚合统计 ============ */
var S={};
(function(){
var cities={}, provs={}, types={}, days={}, hourly=new Array(24).fill(0), weekday=new Array(7).fill(0), dwellTotal=0, distance=0, firstAt=Infinity, lastAt=0;
var cityVisit={}, cityPlace={};
visits.forEach(function(v){
var p=placeById[v.placeId];
cities[p.city]=(cities[p.city]||0)+1;
if(!cityPlace[p.city]) cityPlace[p.city]={};
cityPlace[p.city][p.id]=1;
var pr=provs[p.prov]||(provs[p.prov]={cities:{},places:{},count:0});
pr.cities[p.city]=1; pr.places[p.id]=1; pr.count++;
var t=types[p.actId]||(types[p.actId]={count:0,dwell:0});
t.count++; t.dwell+=v.dur;
var d=new Date(v.arrival); var key=fdate(v.arrival);
days[key]=(days[key]||0)+1;
hourly[d.getHours()]++; weekday[d.getDay()]++;
dwellTotal+=v.dur; firstAt=Math.min(firstAt,v.arrival); lastAt=Math.max(lastAt,v.departure);
});
// 出行里程 = 相邻到访地点不同的直线距离之和
for(var i=1;i<visits.length;i++){var a=placeById[visits[i-1].placeId],b=placeById[visits[i].placeId];if(a.id!==b.id)distance+=hav(a.lat,a.lng,b.lat,b.lng);}
S.places=PLACES.length;
S.cities=Object.keys(cities).length;
S.provinces=Object.keys(provs).length;
S.days=Object.keys(days).length;
S.visits=visits.length;
S.dwell=dwellTotal; S.distance=distance; S.firstAt=firstAt; S.lastAt=lastAt;
S.types=ACTS.map(function(a){var t=types[a.id]||{count:0,dwell:0};return {id:a.id,name:a.name,icon:a.icon,color:a.color,count:t.count,dwell:t.dwell};}).filter(function(x){return x.count>0;}).sort(function(a,b){return b.count-a.count;});
S.cityStats=Object.keys(cities).map(function(c){return {city:c,count:cities[c],places:Object.keys(cityPlace[c]).length};}).sort(function(a,b){return b.count-a.count;});
S.provStats=Object.keys(provs).map(function(p){var x=provs[p];return {prov:p,cities:Object.keys(x.cities).length,places:Object.keys(x.places).length,count:x.count};}).sort(function(a,b){return b.count-a.count;});
// 月度(最近 24 个月)
var months=[]; var cur=new Date(START); cur.setDate(1);
var endM=new Date(END); endM.setDate(1);
var mcount={};
visits.forEach(function(v){var d=new Date(v.arrival);var k=d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0');mcount[k]=(mcount[k]||0)+1;});
while(cur<=endM){var key=cur.getFullYear()+'-'+String(cur.getMonth()+1).padStart(2,'0');months.push({key:key,label:(cur.getMonth()+1)+'月',count:mcount[key]||0});cur.setMonth(cur.getMonth()+1);}
var mmax=Math.max.apply(null,months.map(function(m){return m.count;}));
months.forEach(function(m){m.pct=pct(m.count,mmax);});
S.monthly=months;
// 活跃日历(最近 ~53 周)
var calStart=END-371*86400000; calStart=calStart-calStart%86400000; // 对齐到天
var calDays={}; visits.forEach(function(v){var k=fdate(v.arrival);calDays[k]=(calDays[k]||0)+1;});
var weeks=[], dayMax=0;
for(var t=calStart;t<=END;t+=86400000){var dt=new Date(t);dayMax=Math.max(dayMax,calDays[fdate(t)]||0);}
// 对齐到周日
var firstDow=new Date(calStart).getDay(); var t0=calStart-firstDow*86400000;
var curWeek=null, monthsRow=[];
for(var t2=t0;t2<=END+6*86400000;t2+=86400000){
var wd=new Date(t2).getDay();
if(wd===0){curWeek=[];weeks.push(curWeek);}
var c=calDays[fdate(t2)]||0;
var lvl=0; if(c>0){lvl=c<dayMax*0.25?1:c<dayMax*0.5?2:c<dayMax*0.75?3:4;}
curWeek.push({date:t2,count:c,level:lvl,title:fdate(t2)+':'+(c||'无')+' 次到访'});
if(wd===0){var md=new Date(t2);monthsRow.push({i:weeks.length-1,label:md.getMonth()===0?md.getFullYear()+'':(md.getMonth()+1)+'月'});}
}
S.calendar={weeks:weeks,dayMax:dayMax,months:monthsRow};
var hmax=Math.max.apply(null,hourly);
S.hourly=hourly.map(function(c,h){return {h:h,count:c,pct:pct(c,hmax)};});
var wmax=Math.max.apply(null,weekday);
var wlab=['周日','周一','周二','周三','周四','周五','周六'];
S.weekday=weekday.map(function(c,idx){return {label:wlab[idx],count:c,pct:pct(c,wmax)};});
// 出行方式(相邻不同地点的位移段)
var segs=[];
for(var k=1;k<visits.length;k++){var pa=placeById[visits[k-1].placeId],pb=placeById[visits[k].placeId];if(pa.id===pb.id)continue;var gap=(visits[k].arrival-visits[k-1].departure)/60000;if(gap<3||gap>600)continue;var dist=hav(pa.lat,pa.lng,pb.lat,pb.lng);var mi=dist<1.5?0:dist<5?1:dist<25?(rnd()<0.5?2:3):dist<300?4:dist<1200?5:6;segs.push({a:pa,b:pb,dist:dist,min:gap,mode:mi});}
var mgroup={}; segs.forEach(function(s){var g=mgroup[s.mode]||(mgroup[s.mode]={count:0,dist:0,dwell:0});g.count++;g.dist+=s.dist;g.dwell+=s.min;});
S.transports=MODES.map(function(m,i){var g=mgroup[i]||{count:0,dist:0,dwell:0};return {name:m.name,icon:m.icon,color:m.color,count:g.count,dist:g.dist,dwell:g.dwell};}).filter(function(x){return x.count>0;}).sort(function(a,b){return b.count-a.count;});
S.segs=segs;
// 标签
var tc={};
visits.forEach(function(v){v.tags.forEach(function(id){tc[id]=(tc[id]||0)+1;});});
S.tags=TAGS.map(function(t){return {id:t.id,name:t.name,color:t.color,count:tc[t.id]||0};}).filter(function(x){return x.count>0;}).sort(function(a,b){return b.count-a.count;});
// 最近足迹
S.recent=visits.slice(-12).reverse().map(function(v){var p=placeById[v.placeId];return {name:p.name,city:p.city,act:ACT[p.actId],dur:v.dur,arrival:v.arrival,tags:v.tags};});
// 天气(确定性合成)
var CONDS=[{c:'晴',i:'☀️',at:23},{c:'多云',i:'⛅',at:19},{c:'阴',i:'☁️',at:16},{c:'小雨',i:'🌦️',at:14},{c:'雷阵雨',i:'⛈️',at:25},{c:'雪',i:'❄️',at:0},{c:'雾',i:'🌫️',at:10}];
S.weather=CONDS.map(function(x){var h=Math.round(rand(180,2600));return {c:x.c,i:x.i,count:h,avg:x.at+Math.round(rand(-4,4)),min:x.at-Math.round(rand(3,8)),max:x.at+Math.round(rand(3,8))};}).sort(function(a,b){return b.count-a.count;});
// 世界迷雾(mock)
var cells=Math.round(rand(480000,620000));
var litRate=rand(0.58,0.78);
var litPlaces=Math.round(S.places*litRate);
S.fog={blocks:Math.round(cells/64),cells:cells,areaKm2:+(cells*0.0000912).toFixed(1),litPlaces:litPlaces,places:S.places,litRate:Math.round(litRate*100)};
S.fogCities=S.cityStats.map(function(c){var total=c.places;var lit=Math.round(total*rand(0.45,0.92));return {name:c.city,lit:lit,total:total};}).sort(function(a,b){return b.lit-a.lit;});
})();
/* ============ 渲染:主页 ============ */
function kpi(v,u,k){return '<div class="kpi"><div class="v">'+v+(u?'<small>'+u+'</small>':'')+'</div><div class="k">'+k+'</div></div>';}
function barRow(nm,fill,w,vv,extra){return '<div class="bar-row"'+(extra||'')+'><div class="nm" title="'+nm+'">'+nm+'</div><div class="bar-track"><div class="bar-fill" style="width:'+w+'%;'+(fill?('background:'+fill):'')+'"></div></div><div class="vv">'+vv+'</div></div>';}
function renderHome(){
var h='';
h+='<div class="page-head"><div><h1>🧭 我的足迹</h1><div class="sub">示例数据 · 覆盖 '+S.cities+' 座城市 · '+fdate(S.firstAt)+' 起记录</div></div>'+
'<div class="row"><a class="btn" href="#map">全屏地图</a><a class="btn" href="#stats">数据统计</a></div></div>';
h+='<div class="kpis" style="margin-bottom:16px">';
h+=kpi(num(S.places),'个','去过的地点');
h+=kpi(num(S.cities),'座','到过的城市');
h+=kpi(num(S.provinces),'个','覆盖省份');
h+=kpi(num(S.days),'天','有记录的日子');
h+=kpi(num(S.visits),'次','累计到访');
h+=kpi(dur(S.dwell),'','累计停留');
h+=kpi(km(S.distance),'km','出行里程');
h+=kpi(since(S.firstAt),'','开始记录于');
h+='</div>';
h+='<div class="card"><h2>足迹地图</h2><div class="card-sub">共 '+num(S.places)+' 个地点(示例)。点击进入全屏模式查看筛选与时间轴。</div><div id="home-map" class="map-preview"></div></div>';
h+='<div class="grid g2">';
h+='<div class="card"><h2>去过的类型</h2><div class="card-sub">按到访次数排序</div><div class="bars">';
var maxA=S.types[0]?S.types[0].count:1;
S.types.forEach(function(t){h+=barRow(t.icon+' '+t.name,t.color,pct(t.count,maxA),num(t.count)+' 次');});
h+='</div></div>';
h+='<div class="card"><h2>城市足迹</h2><div class="card-sub">按到访次数排序</div><div class="bars">';
var maxC=S.cityStats[0]?S.cityStats[0].count:1;
S.cityStats.forEach(function(c){h+=barRow(c.city,'',pct(c.count,maxC),num(c.count)+' 次');});
h+='</div><div class="note">共覆盖 '+num(S.cities)+' 座城市、'+num(S.provinces)+' 个省级行政区。</div></div>';
h+='</div>';
// 月度活跃度
h+='<div class="card"><h2>月度活跃度</h2><div class="card-sub">每月到访次数,最高 '+num(Math.max.apply(null,S.monthly.map(function(m){return m.count;})))+' 次</div><div class="cols">';
S.monthly.forEach(function(m){h+='<div class="col" style="height:'+m.pct+'%" title="'+m.key+':'+m.count+' 次"></div>';});
h+='</div><div class="cols-x">';
S.monthly.forEach(function(m){h+='<span>'+(m.label.replace('月',''))+'</span>';});
h+='</div></div>';
// 活跃日历
var cal=S.calendar;
var ml=[];
cal.months.forEach(function(mo){ml[mo.i]='<span class="ml">'+mo.label+'</span>';});
var mrow=''; for(var wi=0;wi<cal.weeks.length;wi++){mrow+=(ml[wi]||'<span></span>');}
h+='<div class="card"><h2>活跃日历</h2><div class="card-sub">每一列是一周,颜色越深记录越多</div><div class="heat">';
cal.weeks.forEach(function(w){h+='<div class="wk">';w.forEach(function(d){h+='<div class="d '+(d.level?'l'+d.level:'')+'" title="'+d.title+'"></div>';});h+='</div>';});
h+='</div><div class="heat-months">'+mrow+'</div><div class="heat-legend">少 <span class="d"></span><span class="d l1"></span><span class="d l2"></span><span class="d l3"></span><span class="d l4"></span> 多</div></div>';
h+='<div class="grid g2">';
h+='<div class="card"><h2>出行方式</h2><div class="card-sub">按次数排序</div><div class="bars">';
var maxT=S.transports[0]?S.transports[0].count:1;
S.transports.forEach(function(t){h+=barRow(t.icon+' '+t.name,t.color,pct(t.count,maxT),num(t.count)+' 次');});
h+='</div></div>';
h+='<div class="card"><h2>我的标签</h2><div class="card-sub">记录时打上的自定义标签</div><div class="chips">';
S.tags.forEach(function(t){h+='<span class="chip"><span class="dot" style="background:'+t.color+'"></span>'+t.name+' <span class="muted">'+num(t.count)+'</span></span>';});
h+='</div></div>';
h+='</div>';
// 天气
h+='<div class="card"><h2>天气足迹</h2><div class="card-sub">记录期间经历过的天气(示例)</div><div class="tw"><table class="tb"><thead><tr><th>天气</th><th class="num">记录小时数</th><th class="num">平均气温</th><th class="num">范围</th></tr></thead><tbody>';
S.weather.forEach(function(w){h+='<tr><td>'+w.i+' '+w.c+'</td><td class="num">'+num(w.count)+'</td><td class="num">'+w.avg+' ℃</td><td class="num">'+w.min+' ~ '+w.max+' ℃</td></tr>';});
h+='</tbody></table></div></div>';
// 最近足迹
h+='<div class="card"><h2>最近足迹</h2><div class="card-sub">最新 '+S.recent.length+' 条到访记录</div><div class="list">';
S.recent.forEach(function(r){var tags=r.tags.map(function(id){return '<span class="tag">'+tagName(id)+'</span>';}).join(' ');
h+='<div class="item"><div class="ic" style="background:'+r.act.color+'22">'+r.act.icon+'</div><div><div class="t">'+r.name+'</div><div class="m">'+fdate(r.arrival)+' · '+fhm(r.arrival)+' · '+r.city+' · '+r.act.name+(tags?' · '+tags:'')+'</div></div><div class="r">'+dur(r.dur)+'<br><span class="muted">'+since(r.arrival)+'</span></div></div>';});
h+='</div><div style="margin-top:12px"><a class="btn sm" href="#timeline">查看完整时间线 →</a></div></div>';
document.getElementById('view-home').innerHTML=h;
}
/* ============ 渲染:地点库 ============ */
var placeState={q:'',sort:'visits',page:1,pageSize:12};
function renderPlaces(){
var q=placeState.q.trim().toLowerCase();
var rows=PLACES.map(function(p){return {p:p,visits:p.visits.length,dwell:p.visits.reduce(function(s,v){return s+v.dur;},0),last:p.visits.length?Math.max.apply(null,p.visits.map(function(v){return v.arrival;})):0};});
if(q) rows=rows.filter(function(r){return r.p.name.toLowerCase().indexOf(q)>=0||r.p.city.toLowerCase().indexOf(q)>=0||r.p.prov.toLowerCase().indexOf(q)>=0;});
rows.sort(function(a,b){if(placeState.sort==='name')return a.p.name.localeCompare(b.p.name,'zh');if(placeState.sort==='city')return a.p.city.localeCompare(b.p.city,'zh')||b.visits-a.visits;if(placeState.sort==='last')return b.last-a.last;return b.visits-a.visits;});
var total=rows.length, pages=Math.max(1,Math.ceil(total/placeState.pageSize));
if(placeState.page>pages)placeState.page=pages;
var start=(placeState.page-1)*placeState.pageSize;
var slice=rows.slice(start,start+placeState.pageSize);
var h='<div class="page-head"><div><h1>地点库</h1><div class="sub">共 '+num(total)+' 个地点</div></div></div>';
h+='<div class="toolbar"><input type="text" id="p-q" placeholder="搜索地点 / 城市 / 省份" value="'+placeState.q+'">'+
'<select id="p-sort"><option value="visits">按到访次数</option><option value="city">按城市</option><option value="name">按名称</option><option value="last">按最近到访</option></select>'+
'<a class="btn" href="#map">在地图上查看</a></div>';
h+='<div class="card" style="padding:0;overflow:hidden"><div class="tw"><table class="tb"><thead><tr><th>地点</th><th>城市</th><th>类型</th><th class="num">到访</th><th class="num">累计停留</th><th class="num">最近到访</th></tr></thead><tbody>';
slice.forEach(function(r){var a=ACT[r.p.actId];
h+='<tr><td>'+r.p.name+'</td><td>'+r.p.city+'</td><td><span class="dot" style="background:'+a.color+'"></span> '+a.name+'</td><td class="num">'+num(r.visits)+'</td><td class="num">'+dur(r.dwell)+'</td><td class="num">'+fdate(r.last)+'</td></tr>';
});
h+='</tbody></table></div></div>';
h+='<div class="pager"><a href="#" id="p-prev">上一页</a><span class="cur">第 '+placeState.page+' / '+pages+' 页</span><a href="#" id="p-next">下一页</a></div>';
var el=document.getElementById('view-places'); el.innerHTML=h;
document.getElementById('p-q').addEventListener('input',function(e){placeState.q=e.target.value;placeState.page=1;renderPlaces();});
document.getElementById('p-sort').addEventListener('change',function(e){placeState.sort=e.target.value;placeState.page=1;renderPlaces();});
document.getElementById('p-prev').addEventListener('click',function(e){e.preventDefault();if(placeState.page>1){placeState.page--;renderPlaces();}});
document.getElementById('p-next').addEventListener('click',function(e){e.preventDefault();if(placeState.page<pages){placeState.page++;renderPlaces();}});
}
/* ============ 渲染:时间线 ============ */
function renderTimeline(){
var recent=visits.slice(-260).reverse();
var groups={}, order=[];
recent.forEach(function(v){var k=fdate(v.arrival);if(!groups[k]){groups[k]={date:v.arrival,count:0,dwell:0,items:[]};order.push(k);}var g=groups[k];g.count++;g.dwell+=v.dur;var p=placeById[v.placeId];g.items.push({p:p,v:v});});
var h='<div class="page-head"><div><h1>时间线</h1><div class="sub">按天分组的到访流水(最近 260 条)</div></div></div>';
order.forEach(function(k){var g=groups[k];
h+='<div class="tl-day"><div class="tl-head"><span class="dt">'+k+' '+['周日','周一','周二','周三','周四','周五','周六'][new Date(g.date).getDay()]+'</span><span class="m">'+g.count+' 次 · 累计 '+dur(g.dwell)+'</span></div>';
g.items.forEach(function(it){var a=ACT[it.p.actId];var tags=it.v.tags.map(function(id){return '<span class="tag">'+tagName(id)+'</span>';}).join(' ');
h+='<div class="tl-item"><div class="ic" style="background:'+a.color+'22">'+a.icon+'</div><div><div class="tn">'+it.p.name+'</div><div class="tm">'+fhm(it.v.arrival)+' · '+it.p.city+' · '+a.name+(tags?' · '+tags:'')+'</div></div><div class="tr">'+dur(it.v.dur)+'</div></div>';
});
h+='</div>';
});
document.getElementById('view-timeline').innerHTML=h;
}
/* ============ 渲染:统计 ============ */
function renderStats(){
var h='<div class="page-head"><div><h1>数据统计</h1><div class="sub">示例数据 · 2024–2025</div></div></div>';
h+='<div class="kpis" style="margin-bottom:16px">';
h+=kpi(num(S.visits),'次','到访记录');
h+=kpi(num(S.places),'个','不同地点');
h+=kpi(dur(S.dwell),'','累计停留');
h+=kpi(km(S.distance),'km','出行里程');
h+='</div>';
// 月度分布
var mmax=Math.max.apply(null,S.monthly.map(function(m){return m.count;}));
h+='<div class="card"><h2>月度分布</h2><div class="card-sub">每月到访次数,最高 '+num(mmax)+' 次</div><div class="cols">';
S.monthly.forEach(function(m){h+='<div class="col" style="height:'+m.pct+'%" title="'+m.key+':'+m.count+' 次"></div>';});
h+='</div><div class="cols-x">';S.monthly.forEach(function(m){h+='<span>'+(m.label.replace('月',''))+'</span>';});h+='</div></div>';
h+='<div class="grid g2">';
h+='<div class="card"><h2>类型分布</h2><div class="card-sub">按到访次数排序</div><div class="bars">';
var maxA=S.types[0].count;
S.types.forEach(function(t){h+=barRow(t.icon+' '+t.name,t.color,pct(t.count,maxA),num(t.count)+' 次 · '+dur(t.dwell));});
h+='</div></div>';
h+='<div class="card"><h2>一天中的分布</h2><div class="card-sub">按到访开始时间统计</div><div class="cols" style="height:130px">';
S.hourly.forEach(function(x){h+='<div class="col" style="height:'+x.pct+'%" title="'+x.h+' 时:'+x.count+' 次"></div>';});
h+='</div><div class="cols-x">';S.hourly.forEach(function(x){h+='<span>'+(x.h===0?'0':x.h===6?'6':x.h===12?'12':x.h===18?'18':' ')+'</span>';});h+='</div></div>';
h+='</div>';
h+='<div class="grid">';
h+='<div class="card"><h2>城市排行</h2><div class="card-sub">到访次数</div><div class="bars">';
var maxC=S.cityStats[0].count;
S.cityStats.forEach(function(c){h+=barRow(c.city,'',pct(c.count,maxC),num(c.count)+' 次 · '+num(c.places)+' 地点');});
h+='</div></div>';
h+='<div class="card"><h2>省级行政区</h2><div class="card-sub">覆盖情况</div><div class="tw"><table class="tb"><thead><tr><th>省份</th><th class="num">城市</th><th class="num">地点</th><th class="num">到访</th></tr></thead><tbody>';
S.provStats.forEach(function(p){h+='<tr><td>'+p.prov+'</td><td class="num">'+num(p.cities)+'</td><td class="num">'+num(p.places)+'</td><td class="num">'+num(p.count)+'</td></tr>';});
h+='</tbody></table></div></div>';
h+='</div>';
h+='<div class="grid">';
h+='<div class="card"><h2>星期分布</h2><div class="card-sub">一周中的到访规律</div><div class="bars">';
var maxW=S.weekday[0].count;
S.weekday.forEach(function(w){h+=barRow(w.label,'',pct(w.count,maxW),num(w.count)+' 次');});
h+='</div></div>';
h+='<div class="card"><h2>出行方式</h2><div class="card-sub">次数与估算里程</div><div class="tw"><table class="tb"><thead><tr><th>方式</th><th class="num">次数</th><th class="num">里程</th><th class="num">在途时间</th></tr></thead><tbody>';
S.transports.forEach(function(t){h+='<tr><td><span class="dot" style="display:inline-block;background:'+t.color+'"></span> '+t.icon+' '+t.name+'</td><td class="num">'+num(t.count)+'</td><td class="num">'+km(t.dist)+' km</td><td class="num">'+dur(t.dwell)+'</td></tr>';});
h+='</tbody></table></div></div>';
h+='</div>';
h+='<div class="grid">';
h+='<div class="card"><h2>标签统计</h2><div class="card-sub">自定义标签的使用情况</div><div class="bars">';
var maxTag=S.tags[0]?S.tags[0].count:1;
S.tags.forEach(function(t){h+=barRow('<span class="dot" style="display:inline-block;background:'+t.color+'"></span> '+t.name,'',pct(t.count,maxTag),num(t.count)+' 次');});
h+='</div></div>';
h+='<div class="card"><h2>天气与气温</h2><div class="card-sub">记录期间经历的小时级天气</div><div class="tw"><table class="tb"><thead><tr><th>天气</th><th class="num">小时数</th><th class="num">平均气温</th></tr></thead><tbody>';
S.weather.forEach(function(w){h+='<tr><td>'+w.i+' '+w.c+'</td><td class="num">'+num(w.count)+'</td><td class="num">'+w.avg+' ℃</td></tr>';});
h+='</tbody></table></div></div>';
h+='</div>';
h+='<div class="card"><h2>世界迷雾覆盖</h2><div class="card-sub">迷雾快照 '+num(S.fog.blocks)+' 块 / '+num(S.fog.cells)+' 格,估算已探索 <b>'+S.fog.areaKm2+'</b> km²;地点落在已探索格内的比例即「点亮率」</div>';
h+='<div class="kpis" style="margin-bottom:12px">';
h+=kpi(S.fog.areaKm2,'km²','已探索面积');
h+=kpi(S.fog.litRate,'%','地点点亮率');
h+=kpi(S.fog.litPlaces,'/'+num(S.fog.places),'点亮地点');
h+='</div><div class="bars">';
S.fogCities.forEach(function(c){h+=barRow(c.name,'',pct(c.lit,c.total),c.lit+'/'+c.total);});
h+='</div></div>';
document.getElementById('view-stats').innerHTML=h;
}
/* ============ 地图 ============ */
var BASEMAPS={
street:{url:'https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',attr:'© 高德地图 AutoNavi',name:'街道',sub:'1234'},
roadnet:{url:'https://wprd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}',attr:'© 高德地图 AutoNavi',name:'纯路网',sub:'1234'},
satellite:{url:'https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',attr:'© 高德地图 AutoNavi',name:'卫星',sub:'1234'},
dark:{url:'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}',attr:'Tiles © Esri',name:'暗色',sub:'abcd'}
};
var PREVIEW=null, FULLMAP=null, FULL_INIT=false;
var cluster=null, heatLayer=null, cityLayer=null, trackLayer=null;
var allLatLngs=[];
function pinIcon(color){return L.divIcon({className:'',html:'<div class="pin" style="background:'+color+'"></div>',iconSize:[14,14],iconAnchor:[7,14]});}
function clusterIcon(c){var n=c.getChildCount();var s=n<10?34:n<50?42:n<100?50:58;return L.divIcon({html:'<div style="width:'+s+'px;height:'+s+'px;line-height:'+s+'px">'+n+'</div>',className:'cluster',iconSize:[s,s]});}
function makeMarker(p){
var a=ACT[p.actId];
var m=L.marker([p.lat,p.lng],{icon:pinIcon(a.color)});
m.bindPopup('<div class="pop"><b>'+p.name+'</b><div class="pm">'+p.city+' · '+a.icon+' '+a.name+'</div><div class="pm">'+num(p.visits.length)+' 次到访 · 最近 '+fdate(p.visits.length?Math.max.apply(null,p.visits.map(function(v){return v.arrival;})):0)+'</div></div>');
return m;
}
function ensurePreview(){
if(PREVIEW) return;
PREVIEW=L.map('home-map',{scrollWheelZoom:false,zoomControl:true,dragging:true});
L.tileLayer(BASEMAPS.street.url,{attribution:BASEMAPS.street.attr,subdomains:'1234',maxZoom:19}).addTo(PREVIEW);
var c=L.markerClusterGroup({showCoverageOnHover:false,maxClusterRadius:40,iconCreateFunction:clusterIcon});
PLACES.forEach(function(p){c.addLayer(makeMarker(p));});
PREVIEW.addLayer(c);
PREVIEW.fitBounds(boundsOf(PLACES),{padding:[18,18]});
PREVIEW.on('click',function(){location.hash='#map';});
}
function boundsOf(places){var b=[];places.forEach(function(p){b.push([p.lat,p.lng]);});return b;}
/* 地图筛选状态 */
var mapState={from:'',to:'',acts:new Set(),city:'',min:0,q:'',track:false,heat:false,cityAgg:false};
function filteredPlaces(){
var from=mapState.from?new Date(mapState.from).getTime():-Infinity;
var to=mapState.to?new Date(mapState.to).getTime()+86400000-1:Infinity;
var q=mapState.q.trim().toLowerCase();
return PLACES.filter(function(p){
if(mapState.acts.size && !mapState.acts.has(p.actId)) return false;
if(mapState.city && p.city!==mapState.city) return false;
if(q && !(p.name.toLowerCase().indexOf(q)>=0||p.city.toLowerCase().indexOf(q)>=0||p.prov.toLowerCase().indexOf(q)>=0)) return false;
var ok=false;
for(var i=0;i<p.visits.length;i++){var v=p.visits[i];if(v.arrival>=from&&v.arrival<=to&&v.dur>=mapState.min){ok=true;break;}}
return ok;
});
}
function rebuildMap(keepView){
if(!FULLMAP) return;
var list=filteredPlaces();
if(cluster){FULLMAP.removeLayer(cluster);}
cluster=L.markerClusterGroup({showCoverageOnHover:false,maxClusterRadius:45,iconCreateFunction:clusterIcon});
list.forEach(function(p){cluster.addLayer(makeMarker(p));});
FULLMAP.addLayer(cluster);
document.getElementById('pt-count').textContent=list.length;
var pfc=document.getElementById('pf-count'); if(pfc)pfc.textContent=list.length+' 个地点';
if(list.length && !keepView) FULLMAP.fitBounds(boundsOf(list),{padding:[40,40],maxZoom:12});
if(mapState.heat) drawHeat(list); else if(heatLayer){FULLMAP.removeLayer(heatLayer);heatLayer=null;}
if(mapState.cityAgg) drawCityAgg(); else if(cityLayer){FULLMAP.removeLayer(cityLayer);cityLayer=null;}
}
function drawHeat(list){
if(heatLayer)FULLMAP.removeLayer(heatLayer);
var pts=[];list.forEach(function(p){var w=Math.min(1,p.visits.length/8+0.2);pts.push([p.lat,p.lng,w]);});
heatLayer=L.heatLayer(pts,{radius:26,blur:18,maxZoom:13,gradient:{0.2:'#1c9d57',0.5:'#f7b32b',0.8:'#ef4444',1.0:'#b91c1c'}});
heatLayer.addTo(FULLMAP);
}
function drawCityAgg(){
if(cityLayer)FULLMAP.removeLayer(cityLayer);
cityLayer=L.layerGroup();
var maxC=S.cityStats[0].count;
S.cityStats.forEach(function(cs){
var c=CITIES.filter(function(x){return x.name===cs.city;})[0];if(!c)return;
var r=4000+ (cs.count/maxC)*26000;
L.circleMarker([c.lat,c.lng],{radius:Math.sqrt(cs.count)*2.6+6,color:'#1c9d57',fillColor:'#1c9d57',fillOpacity:.18,weight:2}).bindPopup('<b>'+cs.city+'</b><br>'+num(cs.count)+' 次到访 · '+num(cs.places)+' 个地点').addTo(cityLayer);
});
cityLayer.addTo(FULLMAP);
}
function drawTrack(){
if(trackLayer)FULLMAP.removeLayer(trackLayer);
if(!mapState.track){trackLayer=null;return;}
trackLayer=L.layerGroup();
S.segs.forEach(function(s){var m=MODES[s.mode];L.polyline([[s.a.lat,s.a.lng],[s.b.lat,s.b.lng]],{color:m.color,weight:2.5,opacity:.7}).addTo(trackLayer);});
trackLayer.addTo(FULLMAP);
}
function ensureMap(){
if(FULL_INIT) return; FULL_INIT=true;
FULLMAP=L.map('map',{zoomControl:true});
var baseCtl=L.control.layers({},{},{position:'topright'});
var layers={};
Object.keys(BASEMAPS).forEach(function(k){(function(k){var l=L.tileLayer(BASEMAPS[k].url,{attribution:BASEMAPS[k].attr,subdomains:BASEMAPS[k].sub,maxZoom:19});layers[BASEMAPS[k].name]=l;})(k);});
baseCtl.addBaseLayer(layers['街道'],'街道');
baseCtl.addBaseLayer(layers['纯路网'],'纯路网');
baseCtl.addBaseLayer(layers['卫星'],'卫星');
baseCtl.addBaseLayer(layers['暗色'],'暗色');
baseCtl.addTo(FULLMAP);
layers['街道'].addTo(FULLMAP);
// 预设
var presets=[{l:'近 30 天',f:daysAgo(30)},{l:'近 90 天',f:daysAgo(90)},{l:'今年',f:new Date(new Date().getFullYear()+'-01-01')},{l:'全部',f:null}];
var ph='';presets.forEach(function(p,idx){ph+='<span class="chip'+(idx===presets.length-1?' on':'')+'" data-from="'+(p.f?fdate(p.f):'')+'">'+p.l+'</span>';});
document.getElementById('map-presets').innerHTML=ph;
document.getElementById('map-presets').querySelectorAll('.chip').forEach(function(ch){ch.addEventListener('click',function(){document.getElementById('map-presets').querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');});ch.classList.add('on');mapState.from=ch.dataset.from;mapState.to='';document.getElementById('m-from').value='';document.getElementById('m-to').value='';rebuildMap();});});
// 类型 chips
var th='';S.types.forEach(function(t){th+='<span class="chip" data-act="'+t.id+'"><span class="dot" style="background:'+t.color+'"></span>'+t.icon+' '+t.name+'</span>';});
document.getElementById('map-types').innerHTML=th;
document.getElementById('map-types').querySelectorAll('.chip').forEach(function(ch){ch.addEventListener('click',function(){ch.classList.toggle('on');if(ch.classList.contains('on'))mapState.acts.add(+ch.dataset.act);else mapState.acts.delete(+ch.dataset.act);rebuildMap();});});
// 城市下拉
var citySel=document.getElementById('m-city');
S.cityStats.forEach(function(c){var o=document.createElement('option');o.value=c.city;o.textContent=c.city;citySel.appendChild(o);});
citySel.addEventListener('change',function(){mapState.city=this.value;rebuildMap();});
document.getElementById('m-min').addEventListener('change',function(){mapState.min=+this.value;rebuildMap();});
document.getElementById('m-q').addEventListener('input',function(){mapState.q=this.value;rebuildMap();});
document.getElementById('m-apply').addEventListener('click',function(){mapState.from=document.getElementById('m-from').value;mapState.to=document.getElementById('m-to').value;rebuildMap();});
document.getElementById('m-reset').addEventListener('click',function(){mapState={from:'',to:'',acts:new Set(),city:'',min:0,q:''};document.getElementById('m-from').value='';document.getElementById('m-to').value='';document.getElementById('m-city').value='';document.getElementById('m-min').value='0';document.getElementById('m-q').value='';document.getElementById('map-types').querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');});document.getElementById('map-presets').querySelectorAll('.chip').forEach(function(x,i){x.classList.toggle('on',i===3);});rebuildMap();});
document.getElementById('t-track').addEventListener('change',function(){mapState.track=this.checked;drawTrack();});
document.getElementById('t-heat').addEventListener('change',function(){mapState.heat=this.checked;if(mapState.heat)drawHeat(filteredPlaces());else if(heatLayer){FULLMAP.removeLayer(heatLayer);heatLayer=null;}});
document.getElementById('t-city').addEventListener('change',function(){mapState.cityAgg=this.checked;if(mapState.cityAgg)drawCityAgg();else if(cityLayer){FULLMAP.removeLayer(cityLayer);cityLayer=null;}});
// 图例
var lg='<div style="font-weight:600;color:var(--text-2)">类型分布</div>';
S.types.slice(0,8).forEach(function(t){lg+='<div class="lg"><i style="background:'+t.color+'"></i>'+t.icon+' '+t.name+' <span class="muted">'+num(t.count)+'</span></div>';});
document.getElementById('map-legend').innerHTML=lg;
// 窄屏折叠筛选面板(桌面端按钮不显示,面板常开)
var panel=document.getElementById('map-panel'), foldBtn=document.getElementById('panel-fold'), caret=document.getElementById('pf-caret');
var mq=window.matchMedia('(max-width:720px)');
function foldApply(c){panel.classList.toggle('collapsed',c);foldBtn.setAttribute('aria-expanded',String(!c));if(caret)caret.textContent=c?'▸':'▾';}
foldApply(mq.matches);
foldBtn.addEventListener('click',function(){foldApply(!panel.classList.contains('collapsed'));});
rebuildMap();
// 时间轴回放
setupTimeline();
}
function daysAgo(n){return new Date(Date.now()-n*86400000);}
var tlAll=[],tlMin=0,tlMax=0,tlDay=86400000,tlLastDay=null,tlTimer=null;
function setupTimeline(){
tlAll=PLACES.filter(function(p){return p.visits.length>0;}).map(function(p){return {p:p,ft:Math.min.apply(null,p.visits.map(function(v){return v.arrival;}))};});
if(tlAll.length<2){document.getElementById('timeline-bar').hidden=true;return;}
tlAll.forEach(function(x){if(x.ft<tlMin||tlMin===0)tlMin=x.ft;if(x.ft>tlMax)tlMax=x.ft;});
tlMin=Math.floor(tlMin/tlDay)*tlDay; tlMax=Math.ceil(tlMax/tlDay)*tlDay;
var rng=document.getElementById('tl-range'); rng.min=tlMin; rng.max=tlMax; rng.step=tlDay; rng.value=tlMax;
tlLastDay=null; document.getElementById('timeline-bar').hidden=false; applyTimeline();
}
function applyTimeline(){
if(!tlAll.length)return;
var cut=+document.getElementById('tl-range').value;
var day=Math.floor(cut/tlDay); if(day===tlLastDay)return; tlLastDay=day;
var sub=[],shown=0;
tlAll.forEach(function(x){if(x.ft<cut+tlDay){sub.push(x.p);shown++;}});
if(cluster)FULLMAP.removeLayer(cluster);
cluster=L.markerClusterGroup({showCoverageOnHover:false,maxClusterRadius:45,iconCreateFunction:clusterIcon});
sub.forEach(function(p){cluster.addLayer(makeMarker(p));});
FULLMAP.addLayer(cluster);
document.getElementById('pt-count').textContent=shown;
var pfc2=document.getElementById('pf-count'); if(pfc2)pfc2.textContent=shown+' 个地点';
document.getElementById('tl-label').textContent=(new Date(cut)).getFullYear()+'-'+String((new Date(cut)).getMonth()+1).padStart(2,'0')+' · '+shown+'/'+tlAll.length+' 个点';
}
document.getElementById('tl-range').addEventListener('input',function(){if(tlTimer)return;tlTimer=requestAnimationFrame(function(){tlTimer=null;applyTimeline();});});
document.getElementById('tl-range').addEventListener('change',function(){tlLastDay=null;applyTimeline();});
document.getElementById('tl-reset').addEventListener('click',function(){stopPlay();document.getElementById('tl-range').value=tlMax;tlLastDay=null;applyTimeline();});
var tlPlayTimer=null;
document.getElementById('tl-play').addEventListener('click',function(){
var btn=document.getElementById('tl-play');
if(tlPlayTimer){stopPlay();return;}
if(+document.getElementById('tl-range').value>=tlMax){document.getElementById('tl-range').value=tlMin;tlLastDay=null;}
btn.textContent='⏸';
var steps=Math.max(1,Math.round((tlMax-+document.getElementById('tl-range').value)/tlDay));
var per=Math.max(1,Math.ceil(steps/240));
tlPlayTimer=setInterval(function(){var next=+document.getElementById('tl-range').value+per*tlDay;if(next>=tlMax){next=tlMax;stopPlay();}document.getElementById('tl-range').value=next;applyTimeline();},50);
});
function stopPlay(){if(tlPlayTimer){clearInterval(tlPlayTimer);tlPlayTimer=null;}document.getElementById('tl-play').textContent='▶';}
/* ============ 路由 ============ */
var SECTIONS=['home','map','places','timeline','stats'];
function currentHash(){var h=location.hash.replace('#','');return SECTIONS.indexOf(h)>=0?h:'home';}
function showView(v){
SECTIONS.forEach(function(s){document.getElementById('view-'+s).style.display=(s===v?'block':'none');});
document.querySelectorAll('.nav-links a').forEach(function(a){a.classList.toggle('on',a.dataset.view===v);});
if(v==='map'){ensureMap();setTimeout(function(){if(FULLMAP)FULLMAP.invalidateSize();},40);}
if(v==='home'){ensurePreview();setTimeout(function(){if(PREVIEW)PREVIEW.invalidateSize();},40);}
window.scrollTo(0,0);
}
window.addEventListener('hashchange',function(){showView(currentHash());});
/* ============ 首次渲染 ============ */
renderHome(); renderPlaces(); renderTimeline(); renderStats();
showView(currentHash());
</script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
/*
(c) 2014, Vladimir Agafonkin
simpleheat, a tiny JavaScript library for drawing heatmaps with Canvas
https://github.com/mourner/simpleheat
*/
!function(){"use strict";function t(i){return this instanceof t?(this._canvas=i="string"==typeof i?document.getElementById(i):i,this._ctx=i.getContext("2d"),this._width=i.width,this._height=i.height,this._max=1,void this.clear()):new t(i)}t.prototype={defaultRadius:25,defaultGradient:{.4:"blue",.6:"cyan",.7:"lime",.8:"yellow",1:"red"},data:function(t,i){return this._data=t,this},max:function(t){return this._max=t,this},add:function(t){return this._data.push(t),this},clear:function(){return this._data=[],this},radius:function(t,i){i=i||15;var a=this._circle=document.createElement("canvas"),s=a.getContext("2d"),e=this._r=t+i;return a.width=a.height=2*e,s.shadowOffsetX=s.shadowOffsetY=200,s.shadowBlur=i,s.shadowColor="black",s.beginPath(),s.arc(e-200,e-200,t,0,2*Math.PI,!0),s.closePath(),s.fill(),this},gradient:function(t){var i=document.createElement("canvas"),a=i.getContext("2d"),s=a.createLinearGradient(0,0,0,256);i.width=1,i.height=256;for(var e in t)s.addColorStop(e,t[e]);return a.fillStyle=s,a.fillRect(0,0,1,256),this._grad=a.getImageData(0,0,1,256).data,this},draw:function(t){this._circle||this.radius(this.defaultRadius),this._grad||this.gradient(this.defaultGradient);var i=this._ctx;i.clearRect(0,0,this._width,this._height);for(var a,s=0,e=this._data.length;e>s;s++)a=this._data[s],i.globalAlpha=Math.max(a[2]/this._max,t||.05),i.drawImage(this._circle,a[0]-this._r,a[1]-this._r);var n=i.getImageData(0,0,this._width,this._height);return this._colorize(n.data,this._grad),i.putImageData(n,0,0),this},_colorize:function(t,i){for(var a,s=3,e=t.length;e>s;s+=4)a=4*t[s],a&&(t[s-3]=i[a],t[s-2]=i[a+1],t[s-1]=i[a+2])}},window.simpleheat=t}(),/*
(c) 2014, Vladimir Agafonkin
Leaflet.heat, a tiny and fast heatmap plugin for Leaflet.
https://github.com/Leaflet/Leaflet.heat
*/
L.HeatLayer=(L.Layer?L.Layer:L.Class).extend({initialize:function(t,i){this._latlngs=t,L.setOptions(this,i)},setLatLngs:function(t){return this._latlngs=t,this.redraw()},addLatLng:function(t){return this._latlngs.push(t),this.redraw()},setOptions:function(t){return L.setOptions(this,t),this._heat&&this._updateOptions(),this.redraw()},redraw:function(){return!this._heat||this._frame||this._map._animating||(this._frame=L.Util.requestAnimFrame(this._redraw,this)),this},onAdd:function(t){this._map=t,this._canvas||this._initCanvas(),t._panes.overlayPane.appendChild(this._canvas),t.on("moveend",this._reset,this),t.options.zoomAnimation&&L.Browser.any3d&&t.on("zoomanim",this._animateZoom,this),this._reset()},onRemove:function(t){t.getPanes().overlayPane.removeChild(this._canvas),t.off("moveend",this._reset,this),t.options.zoomAnimation&&t.off("zoomanim",this._animateZoom,this)},addTo:function(t){return t.addLayer(this),this},_initCanvas:function(){var t=this._canvas=L.DomUtil.create("canvas","leaflet-heatmap-layer leaflet-layer"),i=L.DomUtil.testProp(["transformOrigin","WebkitTransformOrigin","msTransformOrigin"]);t.style[i]="50% 50%";var a=this._map.getSize();t.width=a.x,t.height=a.y;var s=this._map.options.zoomAnimation&&L.Browser.any3d;L.DomUtil.addClass(t,"leaflet-zoom-"+(s?"animated":"hide")),this._heat=simpleheat(t),this._updateOptions()},_updateOptions:function(){this._heat.radius(this.options.radius||this._heat.defaultRadius,this.options.blur),this.options.gradient&&this._heat.gradient(this.options.gradient),this.options.max&&this._heat.max(this.options.max)},_reset:function(){var t=this._map.containerPointToLayerPoint([0,0]);L.DomUtil.setPosition(this._canvas,t);var i=this._map.getSize();this._heat._width!==i.x&&(this._canvas.width=this._heat._width=i.x),this._heat._height!==i.y&&(this._canvas.height=this._heat._height=i.y),this._redraw()},_redraw:function(){var t,i,a,s,e,n,h,o,r,d=[],_=this._heat._r,l=this._map.getSize(),m=new L.Bounds(L.point([-_,-_]),l.add([_,_])),c=void 0===this.options.max?1:this.options.max,u=void 0===this.options.maxZoom?this._map.getMaxZoom():this.options.maxZoom,f=1/Math.pow(2,Math.max(0,Math.min(u-this._map.getZoom(),12))),g=_/2,p=[],v=this._map._getMapPanePos(),w=v.x%g,y=v.y%g;for(t=0,i=this._latlngs.length;i>t;t++)if(a=this._map.latLngToContainerPoint(this._latlngs[t]),m.contains(a)){e=Math.floor((a.x-w)/g)+2,n=Math.floor((a.y-y)/g)+2;var x=void 0!==this._latlngs[t].alt?this._latlngs[t].alt:void 0!==this._latlngs[t][2]?+this._latlngs[t][2]:1;r=x*f,p[n]=p[n]||[],s=p[n][e],s?(s[0]=(s[0]*s[2]+a.x*r)/(s[2]+r),s[1]=(s[1]*s[2]+a.y*r)/(s[2]+r),s[2]+=r):p[n][e]=[a.x,a.y,r]}for(t=0,i=p.length;i>t;t++)if(p[t])for(h=0,o=p[t].length;o>h;h++)s=p[t][h],s&&d.push([Math.round(s[0]),Math.round(s[1]),Math.min(s[2],c)]);this._heat.data(d).draw(this.options.minOpacity),this._frame=null},_animateZoom:function(t){var i=this._map.getZoomScale(t.zoom),a=this._map._getCenterOffset(t.center)._multiplyBy(-i).subtract(this._map._getMapPanePos());L.DomUtil.setTransform?L.DomUtil.setTransform(this._canvas,a,i):this._canvas.style[L.DomUtil.TRANSFORM]=L.DomUtil.getTranslateString(a)+" scale("+i+")"}}),L.heatLayer=function(t,i){return new L.HeatLayer(t,i)};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

+661
View File
@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
File diff suppressed because one or more lines are too long
+60
View File
@@ -0,0 +1,60 @@
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
/* IE 6-8 fallback colors */
.leaflet-oldie .marker-cluster-small {
background-color: rgb(181, 226, 140);
}
.leaflet-oldie .marker-cluster-small div {
background-color: rgb(110, 204, 57);
}
.leaflet-oldie .marker-cluster-medium {
background-color: rgb(241, 211, 87);
}
.leaflet-oldie .marker-cluster-medium div {
background-color: rgb(240, 194, 12);
}
.leaflet-oldie .marker-cluster-large {
background-color: rgb(253, 156, 115);
}
.leaflet-oldie .marker-cluster-large div {
background-color: rgb(241, 128, 23);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}
+14
View File
@@ -0,0 +1,14 @@
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
.leaflet-cluster-spider-leg {
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}
File diff suppressed because one or more lines are too long