-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.js
More file actions
896 lines (735 loc) · 33.8 KB
/
main.js
File metadata and controls
896 lines (735 loc) · 33.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
const DongguanMapApp = (function () {
// ==========================================
// 第一部分:私有状态变量
// ==========================================
// 说明:这些变量只在模块内部使用,外部无法访问
// 好处:避免命名冲突,防止被意外修改
let rootURL = document.baseURI
let root = document.querySelector(':root')
const body = document.querySelector('body');
// state related to scrolling and interaction
let currentP = 0.0; // 滚动进度
let mX = 0.0;
let mY = 0.0;
// app state
let currentLang = 'cn'; //
let showModal = false; // 模态框显示状态
let isDarkMode = true
// state related to town display
let towns = [];
let currentTown = null;
let showTown = true;
let previousCurrentTownId = null; // 存储上一个 currentTown 的 id
// scroll-related state for SVG highlighting
let townStart = 0;
let townEnd = 0;
let aboutContent =
"网页设计 & 开发:<a href='https://www.eddiehe.top/' target='_blank'>Eddie He</a><br/><br/>\n" +
"<strong>参考信息:</strong><br/>\n" +
"· <a target='_blank' href='https://jjying.com/nurburgring/'>Nürburgring Map</a><br/><br/>\n" +
"<strong>页面源码:</strong><br/>\n" +
"· <a target='_blank' href='https://github.com/eddiehe99/dongguan-map'>@GitHub</a>";
// ==========================================
// 第二部分:初始化函数
// ==========================================
/**
* 初始化深色模式
* 检查用户系统偏好设置
*/
function initDarkMode() {
// 检查用户系统是否偏好深色模式
// window.matchMedia('(prefers-color-scheme: dark)') 返回一个 MediaQueryList 对象
// .matches 为 true 表示系统使用深色主题
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
isDarkMode = true; // 设置状态为深色模式
root.classList.add('dark'); // 给根元素添加 'dark' 类(CSS 会根据这个类改变颜色)
}
}
async function loadAppData() {
try {
const response = await fetch('assets/data.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 填充数据
towns = data.towns || [];
return true;
} catch (error) {
console.error('加载数据失败:', error);
return false;
}
}
function openModal(type, img = null) {
let modal, modalBody;
if (type === 'text') {
modal = document.getElementById('text-modal');
modalBody = document.getElementById('text-modal-body');
} else if (type === 'image' && img) {
modal = document.getElementById('image-modal');
modalBody = document.getElementById('image-modal-body');
} else {
console.error("Invalid modal type or missing image data for image type.");
return;
}
if (modal && modalBody) {
modalBody.innerHTML = ''; // 清空之前的内容
console.log("openModal called with type:", type, "and img:", img);
if (type === 'text') {
modalBody.innerHTML = aboutContent;
} else if (type === 'image' && img) {
let imgHtml = `<img src='${img.url}'/>`;
if (img.url && img.author) {
imgHtml += `<div class='source-in-modal'>@<a href='${img.url}' target='_blank'>${img.author}</a></div>`;
}
modalBody.innerHTML = imgHtml;
}
// 显示对应的模态框
modal.classList.add('show');
} else {
console.error("Modal or modal-body element not found for type:", type);
}
}
function closeModal(modalElement) {
if (modalElement && modalElement.classList.contains('modal')) {
modalElement.classList.remove('show'); // 如果使用 class
} else {
// 如果没有传入元素,或者你想一次性关闭所有模态框
document.getElementById('text-modal').classList.remove('show');
document.getElementById('image-modal').classList.remove('show');
}
}
// --- 定义 innerModal 函数 (防止点击内容区域关闭模态框) ---
function innerModal(event) {
// 这个函数的目的是阻止事件冒泡到模态框背景 (modal),从而不触发关闭
// 在 HTML 中已经通过 onclick="innerModal(event)" 绑定
event.stopPropagation();
}
function updatePageHeight() {
if (window.innerHeight < window.innerWidth) {
// horizontal screen
body.classList.remove("vertical")
body.classList.add("horizontal")
}
else {
// vertical screen
body.classList.remove("horizontal")
body.classList.add("vertical")
}
}
function updateTownIntroduction() {
const container = document.getElementById('currentTownInfoContainer');
const nameEl = document.getElementById('currentTownName');
const moreEl = document.getElementById('currentTownAbout');
// 获取 thumbs 容器
const thumbsContainer = document.getElementById('thumbsContainer');
if (container && nameEl && moreEl && thumbsContainer) {
const currentCornerId = (showTown && currentTown) ? currentTown.id : null;
const needsUpdate = (currentCornerId !== previousCurrentTownId) ||
(currentCornerId !== null && previousLang !== currentLang);
if (needsUpdate) {
// Update the stored ID
previousCurrentTownId = currentCornerId;
previousLang = currentLang; // 记录当前语言
// 2. 清空 *所有* 相关容器内容 (清理旧状态) - 最佳位置
nameEl.textContent = '';
nameEl.className = 'primary skew-n title-font'; // 重置类名
moreEl.innerHTML = ''; // 注意:使用 innerHTML 有 XSS 风险,仅用于显示可信的 HTML
thumbsContainer.innerHTML = ''; // 清空缩略图内容
thumbsContainer.classList.add('hidden-area'); // 默认隐藏 thumbs 容器
if (showTown && currentTown) {
if (currentLang === 'cn' && currentTown.town_name_cn) {
nameEl.textContent = currentTown.town_name_cn;
nameEl.classList.add('title-font');
} else if (currentLang === 'en' && currentTown.town_name_en) {
nameEl.textContent = currentTown.town_name_en;
nameEl.classList.remove('title-font');
}
// 显示更多描述 (中文)
if (currentLang === 'cn' && currentTown.about_cn) {
moreEl.innerHTML = currentTown.about_cn; // 注意:使用 innerHTML 有 XSS 风险
} else if (currentLang === 'en' && currentTown.about_en) {
moreEl.innerHTML = currentTown.about_en; // 注意:使用 innerHTML 有 XSS 风险
}
// --- 新增:显示缩略图 ---
// 检查 currentTown.imgs 是否存在且为数组
if (currentTown.imgs && Array.isArray(currentTown.imgs) && currentTown.imgs.length > 0) {
// 遍历 imgs 数组
currentTown.imgs.forEach(imgObj => {
// 创建 thumb 容器 div
const thumbDiv = document.createElement('div');
thumbDiv.className = 'thumb'; // 添加基础类名
// 如果 img.url 存在,添加 'has-author' 类
if (imgObj.url) {
thumbDiv.classList.add('has-author');
}
// 创建 img 元素
const imgElement = document.createElement('img');
// 构建图片 URL (注意移除 URL 中的多余空格)
imgElement.src = imgObj.url;
imgElement.className = 'skew-n';
imgElement.loading = 'lazy'; // 设置懒加载
// 为图片添加点击事件
imgElement.addEventListener('click', () => {
// 调用 openModal 函数,传入 'image' 类型和当前的 img 对象
window.openModal('image', imgObj);
});
// 将 img 元素添加到 thumb 容器
thumbDiv.appendChild(imgElement);
// 如果 img.url 存在,创建并添加 source 链接
if (imgObj.url) {
const sourceLink = document.createElement('a');
sourceLink.href = imgObj.url;
sourceLink.target = '_blank';
sourceLink.title = '查看照片来源'; // 设置链接标题
sourceLink.className = 'thumb-source';
const authorSpan = document.createElement('span');
authorSpan.className = 'skew-n';
authorSpan.textContent = imgObj.author; // 设置作者名称
sourceLink.appendChild(authorSpan);
thumbDiv.appendChild(sourceLink); // 将链接添加到 thumb 容器
}
// 将整个 thumb 容器添加到 thumbsContainer
thumbsContainer.appendChild(thumbDiv);
});
// 所有缩略图添加完毕后,显示 thumbsContainer
thumbsContainer.classList.remove('hidden-area'); // 移除隐藏类
}
} else {
// container.style.display = 'none'; // 隐藏整个容器
moreEl.innerHTML = '';
}
}
}
}
function updateIntroMessageDisplay(progress) {
const container = document.getElementById('midDiv'); // 选择容器
if (!container) return; // 如果容器不存在,退出
// 检查滚动进度是否为 0
const shouldShow = progress === 0; // 模拟 Vue 的 v-if="p == 0"
// 控制整个容器的显示/隐藏
container.classList.toggle('show', shouldShow);
container.classList.toggle('hidden', !shouldShow);
// 如果容器是显示状态,则根据语言更新内部内容
if (shouldShow) {
const currentLang = window.currentLang || 'cn';
// 选择所有带 lang-cn 类的元素
const cnElements = container.querySelectorAll('.lang-cn');
// 选择所有带 lang-en 类的元素
const enElements = container.querySelectorAll('.lang-en');
// 为当前语言的元素添加 active 类,为非当前语言的元素移除 active 类
cnElements.forEach(el => {
el.classList.toggle('active-cn', currentLang === 'cn');
// 如果需要,确保非活跃语言的类被移除(虽然 display: none; 会覆盖)
if (currentLang !== 'cn') {
el.classList.remove('active-cn');
}
});
enElements.forEach(el => {
el.classList.toggle('active-en', currentLang === 'en');
if (currentLang !== 'en') {
el.classList.remove('active-en');
}
});
} else {
// 如果容器是隐藏的,确保内部元素的 active 类也被清除(可选,但更干净)
container.querySelectorAll('.lang-cn').forEach(el => el.classList.remove('active-cn'));
container.querySelectorAll('.lang-en').forEach(el => el.classList.remove('active-en'));
}
}
function updateSvgHighlight() {
// 获取当前所有 corner-group 元素 (即包含 path 的 <g>)
const allCornerGroups = document.querySelectorAll('g.corner'); // 选择所有拥有 corner 类的 <g>
// 遍历所有 corner-group 元素
allCornerGroups.forEach(gElement => {
// 获取该 g 对应的 GeoJSON feature 的 id (通过 g 的 id 推断)
// 这里假设 g 的 id 格式是 `corner-group-${featureId}`
const gId = gElement.id;
const pathId = gId.replace('corner-group-', ''); // 从 g 的 id 中提取 path 的 id
// 检查当前的 g 元素是否对应于 main.js 中找到的 currentTown
if (showTown && currentTown && pathId === currentTown.id) {
// 获取 g 内的 path 元素
const pathElement = gElement.querySelector('path.path'); // 选择 g 内拥有 path 类的 path
const progressPathElement = gElement.querySelector('path.town-progress'); // 选择 g 内拥有 progress 类的 path
if (pathElement) {
// 1. 设置 path 的 CSS 变量 --st 和 --ed (来自 main.js 的计算结果)
pathElement.style.setProperty('--st', window.townStart);
pathElement.style.setProperty('--ed', window.townEnd);
// 注意:--full 应该在加载 GeoJSON 时设置在 SVG 或 <g> 上,或者 pathGenerator 生成的每个 path 都知道自己的长度
// 如果每个 path 的长度不同且需要精确控制,这会变得复杂。通常 --full 是整个赛道的总长度。
// 你可能需要为每个 path 计算其相对于总长度的比例,或者重新考虑 --full 的定义。
// 简单起见,如果所有 path 都基于同一个坐标系统,可以设置一个全局的 --full。
// 例如:svgElement.style.setProperty('--full', totalTrackLength);
// 2. 控制 g 元素的显示/隐藏 (class)
gElement.classList.remove('hidden'); // 移除 hidden 类
gElement.classList.add('show'); // 添加 show 类
}
if (progressPathElement) {
// --- 应用 Progress 效果 ---
// 1. 设置进度相关的样式属性 (模拟 .progress 类)
// 注意:你需要知道这个 progressPathElement 的 *自身* 长度,或者一个相对于其长度的比例
// 假设我们想让这个区域的进度条根据 currentP 从 0% 变化到 100%
// 那么 stroke-dasharray 应该是 path 的长度,stroke-dashoffset 应该是 (1 - currentP) * 长度
// 但更简单的方式是,如果 currentP 在 [st, ed] 范围内,我们将其映射到 [0, 1] 作为这个区域的局部进度
// 局部进度 = (currentP - townStart) / (townEnd - townStart)
// 但 currentP 可能超出 [townStart, townEnd] 范围,所以需要 clamping
const localP = Math.max(0, Math.min(1, (currentP - townStart) / (townEnd - townStart)));
// 获取路径的总长度
const pathLength = progressPathElement.getTotalLength();
// 设置 stroke-dasharray 为 [可见长度, 隐藏长度],其中可见长度是 localP * pathLength
// 或者设置为 [pathLength, pathLength],然后用 offset 控制
// 推荐使用 [pathLength, pathLength] 方式,更稳定
progressPathElement.style.setProperty('stroke-dasharray', `${pathLength} ${pathLength}`);
// 设置 stroke-dashoffset,当 localP 为 1 时 offset 为 0 (完全显示),当 localP 为 0 时 offset 为 -pathLength (完全隐藏)
progressPathElement.style.setProperty('stroke-dashoffset', `${pathLength * (1 - localP)}`);
// 设置其他进度条样式
progressPathElement.style.setProperty('stroke', 'var(--bg1)'); // 使用进度条颜色
progressPathElement.style.setProperty('stroke-width', '1px');
progressPathElement.style.setProperty('stroke-linecap', 'round');
progressPathElement.style.setProperty('transition', 'stroke-dashoffset 0.2s ease-out'); // 添加过渡效果
}
} else {
// 如果该 g 的 id 不匹配当前 corner,或者没有当前 corner,则隐藏它
gElement.classList.remove('show');
gElement.classList.add('hidden');
}
});
updateTownNamesDiv();
}
function updateScrollDistance() {
// 重置状态
// showTown = false;
// currentTown = null;
// 计算当前滚动进度
const progress = window.scrollY / (body.scrollHeight - window.innerHeight);
currentP = Math.min(1, Math.max(0, progress)); // 确保 p 在 [0, 1] 范围内
// 设置 CSS 变量 (如果需要)
body.style.setProperty('--p', currentP);
// 查找当前 town
for (const town of towns) {
if (currentP > town.st && currentP < town.ed) {
showTown = true;
townStart = town.st;
townEnd = town.ed;
currentTown = town;
break; // 找到第一个匹配的就退出循环
}
}
// --- 新增:更新介绍消息的显示 ---
updateIntroMessageDisplay(currentP);
// --- 关键:调用 updateTownIntroduction 来更新显示 ---
updateTownIntroduction();
// 如果还有其他需要根据滚动更新的 UI,也可以在这里调用对应的函数
// 例如,更新 "The End" 消息的显示
updateEndingDisplay(currentP); // 假设有这个函数
// --- 新增:更新 SVG 高亮显示 ---
updateSvgHighlight();
}
function updateEndingDisplay(progress) {
const container = document.getElementById('endingMessageContainer');
const messageElement = document.getElementById('endingMessage'); // 通常不需要显式隐藏/显示这个,因为它在 container 内部
const startOverBtn = document.getElementById('startOverBtn');
const cnTextElement = document.getElementById('startOverBtnTextCn');
const enTextElement = document.getElementById('startOverBtnTextEn');
if (container && startOverBtn && cnTextElement && enTextElement) {
// 检查滚动进度是否接近 1 (模拟 Vue 的 v-if="p > 0.999")
if (progress > 0.98) {
container.style.display = ''; // 显示整个容器
// 根据当前语言显示按钮文本 (模拟 Vue 的 v-if="lang == 'cn'" 和 v-if="lang == 'en'")
if (currentLang === 'cn') {
cnTextElement.style.display = '';
enTextElement.style.display = 'none';
} else { // 假设不是 'cn' 就是 'en'
cnTextElement.style.display = 'none';
enTextElement.style.display = '';
}
} else {
container.style.display = 'none'; // 隐藏整个容器
}
}
}
function setP(percentage) {
currentP = percentage; // 更新内部状态变量 (如果需要)
// 计算目标滚动位置
const scrollTarget = (body.scrollHeight - window.innerHeight) * percentage;
// 执行滚动
window.scrollTo({ top: scrollTarget, behavior: 'smooth' }); // 使用平滑滚动,或者 'auto' 立即滚动
// 重要:滚动后,需要触发 updateScrollDistance 来同步状态和 UI
updateScrollDistance(); // 这行很关键,确保滚动后 UI 更新
document.body.classList.remove("scrolled");
}
function updateTownNamesDiv() {
const container = document.getElementById('town-names-container');
if (!container) return;
// 确保容器有正确的定位
if (container.style.position !== 'absolute') {
container.style.position = 'absolute';
container.style.left = '0';
container.style.top = '0';
container.style.width = '100%';
container.style.height = '100%';
container.style.pointerEvents = 'none'; // 允许点击穿透到 SVG
}
// 使用 Set 来跟踪已处理的元素,避免重复创建
const processedIds = new Set();
// 遍历 towns 数组
towns.forEach(town => {
const townId = town.id;
processedIds.add(townId);
// 查找或创建对应的 corner-name div 元素
let element = container.querySelector(`.corner-name[data-id="${townId}"]`);
if (!element) {
// 如果元素不存在,则创建它
element = document.createElement('div');
element.className = 'corner-name';
element.setAttribute('data-id', townId);
// 创建内部结构
const innerDiv1 = document.createElement('div');
const innerDiv2 = document.createElement('div');
innerDiv2.textContent = town.town_name_cn;
innerDiv1.appendChild(innerDiv2);
element.appendChild(innerDiv1);
// 添加点击事件
element.style.pointerEvents = 'auto';
element.addEventListener('click', (event) => {
event.stopPropagation();
if (typeof setP === 'function') {
setP((town.st + town.ed) / 2);
}
});
container.appendChild(element);
}
// 更新文本内容
const textElement = element.querySelector('div div');
if (textElement) {
textElement.textContent = currentLang === 'cn' ? town.town_name_cn : (town.town_name_en || town.town_name_cn);
}
// --- 修改此处的逻辑 ---
// 计算是否应该显示 (基于滚动、进度和全局设置)
const isScrolled = window.scrollY > 2; // 检查是否滚动超过 2px
const isPassed = town.st < currentP; // 检查是否已超过该区域的起始点
const shouldShowBasedOnScroll = isScrolled; // 根据滚动状态决定是否显示
// 你可以选择以下几种方式之一:
// 1. 只根据滚动状态 (最符合你的要求)
let shouldShow = shouldShowBasedOnScroll;
// 2. 或者,滚动后显示已过的 + 全部显示 (结合滚动和进度)
// let shouldShow = (isScrolled && isPassed) || window.showAllCornerNames;
// 3. 或者,滚动后显示全部,未滚动时按原逻辑 (优先考虑滚动)
// let shouldShow = isScrolled || shouldShowBasedOnProgressOrAll;
// 根据你的具体需求选择一种 shouldShow 逻辑
// 这里选择第 1 种:只根据滚动状态
shouldShow = shouldShowBasedOnScroll;
// --- end 修改 ---
const isHighlighted = currentP > town.st && currentP <= town.ed;
element.classList.toggle('show', shouldShow);
element.classList.toggle('hidden', !shouldShow);
element.classList.toggle('highlighted', isHighlighted);
// --- 修复坐标计算 ---
const centroid = window.featureCentroids?.get(townId);
if (centroid) {
const [svgX, svgY] = centroid;
const svgElement = document.getElementById('svg-track');
const containerElement = document.querySelector('.track-map > .inner');
if (svgElement && containerElement) {
// 获取 SVG 的 viewBox
const viewBox = svgElement.viewBox.baseVal;
// 获取容器的 clientWidth/Height (内容区域尺寸,不包括 padding 和 border)
const containerWidth = containerElement.clientWidth;
const containerHeight = containerElement.clientHeight;
// 计算 SVG 坐标相对于 viewBox 的比例
const xRatio = (svgX - viewBox.x) / viewBox.width;
const yRatio = (svgY - viewBox.y) / viewBox.height;
// 计算在容器内容区域内的像素位置
const pixelX = xRatio * containerWidth;
const pixelY = yRatio * containerHeight;
// 设置位置 - 使用 transform 定位
// translate 可以精确控制,且不依赖于容器的定位属性
// 需要减去元素自身的一半宽高以实现中心对齐
// element.style.transform = `translate(${pixelX}px, ${pixelY}px)`;
// 为了居中,可以使用 CSS 的 transform-origin 或者 JS 计算偏移
// 更好的方式是在 CSS 中设置 transform-origin: center center;
element.style.transform = `translate(${pixelX}px, ${pixelY}px)`;
// 移除可能的 left/top 干扰
element.style.left = '0';
element.style.top = '0';
}
}
});
// 清理不再需要的元素(如果需要)
// const allElements = container.querySelectorAll('.corner-name');
// allElements.forEach(el => {
// const id = el.getAttribute('data-id');
// if (!processedIds.has(id)) {
// el.remove();
// }
// });
}
// --- 定义更新 UI 的函数 ---
function updateUIBasedOnLanguage(lang) {
// a. 更新语言切换按钮的文本
const langToggleTextElement = document.getElementById('langToggleText');
if (langToggleTextElement) {
langToggleTextElement.textContent = lang === 'cn' ? '中文' : 'Chinese';
}
// b. 更新语言切换按钮的 CSS 类 (on/off)
const langToggleGroupElement = document.getElementById('langToggleGroup');
if (langToggleGroupElement) {
if (lang === 'cn') {
langToggleGroupElement.classList.add('on');
langToggleGroupElement.classList.remove('off');
} else {
langToggleGroupElement.classList.add('off');
langToggleGroupElement.classList.remove('on');
}
}
// c. 更新 #app 元素的类 (如果 main.css 依赖它来切换语言相关样式)
const appElement = document.getElementById('app');
if (appElement) {
appElement.classList.remove('cn', 'en'); // 移除旧类
appElement.classList.add(lang); // 添加新类
}
// d. 更新其他依赖语言的内容 (如果有的话)
// 例如,更新 Logo 标题 (假设 HTML 结构允许)
const logoTitleElement = document.getElementById('logoTitle');
if (logoTitleElement) {
// 注意:这里直接替换整个 innerHTML,如果结构复杂可能需要更精细的操作
logoTitleElement.innerHTML = lang === 'cn' ? '东莞市地图(政区版)' : 'Dongguan Map';
}
// e. 更新其他控制按钮的文本 (如果 HTML 中给它们加了 id)
const darkModeTextElement = document.getElementById('darkModeToggleText');
if (darkModeTextElement) {
darkModeTextElement.textContent = lang === 'cn' ? '深色模式' : 'Dark Mode';
}
const aboutTextElement = document.getElementById('aboutLinkText');
if (aboutTextElement) {
aboutTextElement.textContent = lang === 'cn' ? '关于本站' : 'About';
}
}
function toggleLang() {
if (currentLang === "en") {
currentLang = "cn";
} else {
currentLang = "en";
}
window.currentLang = currentLang;
window.lang = currentLang;
console.log("Language toggled to:", currentLang); // 可选:调试用
updateUIBasedOnLanguage(currentLang);
updateTownIntroduction();
updateTownNamesDiv();
}
function updateUIBasedOnDarkMode(darkModeEnabled) {
// a. 更新深色模式按钮的 CSS 类 (on/off)
const darkModeToggleGroupElement = document.getElementById('darkModeToggleGroup');
if (darkModeToggleGroupElement) {
if (darkModeEnabled) {
darkModeToggleGroupElement.classList.add('on');
darkModeToggleGroupElement.classList.remove('off');
} else {
darkModeToggleGroupElement.classList.add('off');
darkModeToggleGroupElement.classList.remove('on');
}
}
// b. 更新页面或应用的主体样式 (例如,切换 body 或 #app 的类)
// 这通常需要 CSS 配合,例如 .dark-mode { background-color: #333; color: #fff; }
const appElement = document.getElementById('app'); // 或者是 document.body
if (appElement) {
appElement.classList.toggle('dark-mode', darkModeEnabled); // 根据 darkModeEnabled 的布尔值添加或移除 'dark-mode' 类
}
}
function toggleDarkMode() {
// 1. 切换深色模式状态
isDarkMode = !isDarkMode;
console.log("Dark mode toggled to:", isDarkMode);
if (isDarkMode == true) {
root.classList.add("dark")
}
else {
root.classList.remove("dark")
}
// 2. 更新 UI
updateUIBasedOnDarkMode(isDarkMode);
// 注意:语言切换可能也会影响这个按钮的文本,所以最好也调用一次语言更新
}
function bindEventListeners() {
// 1. 滚动时添加状态类
document.addEventListener('scroll', function (e) {
if (window.scrollY > 2) {
body.classList.add("scrolled");
} else {
body.classList.remove("scrolled");
}
updateScrollDistance();
});
// 2. 窗口大小变化时更新布局
window.addEventListener('resize', function () {
updateScrollDistance();
updatePageHeight();
});
// 3. 按 Escape 键关闭模态框
window.addEventListener('keyup', function (e) {
if (e.key === "Escape") {
closeModal();
}
});
// 4. 鼠标在地图上移动(用于交互效果)
const trackInner = document.querySelector('.track-map > .inner');
if (trackInner) {
trackInner.addEventListener('mousemove', function (event) {
const innerRect = this.getBoundingClientRect();
mX = (event.clientX - innerRect.left) / innerRect.width;
mY = (event.clientY - innerRect.top) / innerRect.height;
});
}
// 5. 模态框背景点击关闭
const modal = document.getElementById('modal');
if (modal) {
modal.addEventListener('click', function (event) {
if (event.target === modal) {
closeModal();
}
});
}
// 6. 关于链接
const aboutLinkElement = document.getElementById('aboutLinkText');
if (aboutLinkElement) {
aboutLinkElement.addEventListener('click', function () {
openModal('text');
});
}
// 7. 语言切换按钮
const langToggleElement = document.getElementById('langToggleGroup');
if (langToggleElement) {
langToggleElement.addEventListener('click', toggleLang);
}
// 8. 深色模式切换按钮
const darkModeToggleElement = document.getElementById('darkModeToggleGroup');
if (darkModeToggleElement) {
darkModeToggleElement.addEventListener('click', toggleDarkMode);
}
// 9. 回到起点按钮
const startOverBtn = document.getElementById('startOverBtn');
if (startOverBtn) {
startOverBtn.addEventListener('click', function () {
setP(0.0);
body.classList.remove("scrolled");
});
}
}
function initD3Map() {
if (typeof d3 === 'undefined') {
console.error("D3.js not loaded!");
return;
}
const svg = d3.select("#svg-track");
d3.json("441900.geojson").then(function (geojsonData) {
console.log("GeoJSON loaded:", geojsonData);
const projection = d3.geoMercator()
.fitSize([660, 530], geojsonData);
// --- 计算并存储每个 feature 的 SVG 坐标 ---
const featureCentroids = new Map(); // 使用 Map 存储 id -> [x, y] 的映射
geojsonData.features.forEach(feature => {
const centroid = d3.geoCentroid(feature);
const [x, y] = projection(centroid);
featureCentroids.set(feature.properties.id, [x, y]);
});
// 将坐标映射存储到全局变量或方便访问的地方,供 update 函数使用
window.featureCentroids = featureCentroids;
const pathGenerator = d3.geoPath().projection(projection);
const bounds = pathGenerator.bounds(geojsonData);
const dx = bounds[1][0] - bounds[0][0];
const dy = bounds[1][1] - bounds[0][1];
const padding = 5;
const x = bounds[0][0] - padding;
const y = bounds[0][1] - padding;
const width = dx + 2 * padding;
const height = dy + 2 * padding;
svg.attr("viewBox", `${x} ${y} ${width} ${height}`);
svg.append("path")
.datum(geojsonData)
.attr("id", "track")
// .attr("class", "geojson-track")
.attr("d", pathGenerator)
.attr("fill", "none")
// .attr("stroke", "white")
// .attr("stroke-width", 2);
const features = geojsonData.features;
// --- 修改部分:为每个 Feature 创建 <g> 和 <path> ---
// 选择 <svg> 容器
const svgSelection = svg; // 或者 svg.select('g') 如果你有特定的组容器
// 为每个 Feature 创建一个 <g> 元素
// The class attribute `corner` is originated from the CSS selector `.corner .path` in main.css, which is written by JJ Ying. I don not want to chang the CSS, so I keep using `corner` as the class name for the <g> element, and add an additional `path` class to the <path> element inside it to match the CSS selector.
const townGroups = svgSelection.selectAll("g.corner")
.data(features)
.enter()
.append("g") // 创建 <g> 元素
.attr("class", "corner") // 给 <g> 添加 'corner' 类和一个基于 id 的唯一类
.attr("id", d => `corner-group-${d.properties.id}`); // 给 <g> 添加一个唯一的 ID
// 在每个 <g> 内部添加对应的 <path> 元素
townGroups
.append("path")
.attr("id", d => d.properties.id) // 保持你原来的 path ID
.attr("class", "path") // 给 <path> 添加 'path' 类,使其匹配 CSS
.attr("d", pathGenerator) // 使用 pathGenerator 生成路径
.attr("fill", "none")
// 注意:不要在这里设置 stroke,因为 CSS 会控制 stroke
// .attr("stroke", ...) // 移除或注释掉
// .attr("stroke-width", ...) // 移除或注释掉,CSS 会控制
;
townGroups
.append("path")
.attr("id", d => `${d.properties.id}-progress`) // 添加 -progress 后缀
.attr("class", "town-progress") // 用于进度的类
.attr("d", pathGenerator) // 使用 pathGenerator 生成路径
.attr("fill", "none");
}).catch(function (error) {
console.error("Error loading GeoJSON:", error);
}).finally(function () {
// --- 关键:在 D3.js Promise 完成(无论成功或失败)后,调用 updateTownNamesDiv ---
// 确保 main.js 中的 updateTownNamesDiv 函数已定义且 window.featureCentroids 已设置
if (typeof updateTownNamesDiv === 'function' && window.featureCentroids) {
updateTownNamesDiv();
} else {
console.error("updateTownNamesDiv function or featureCentroids not available.");
}
});
}
async function initApp() {
bindEventListeners();
// init darkMode befor loading data
initDarkMode();
//
const dataLoaded = await loadAppData();
if (!dataLoaded) {
// 数据加载失败的处理
console.error('加载数据失败:', error);
return;
}
initD3Map();
// 初始化其他功能
updatePageHeight();
updateScrollDistance();
updateUIBasedOnDarkMode(isDarkMode); // 确保 UI 根据当前深色模式状态正确显示
updateUIBasedOnLanguage(currentLang); // 确保 UI 根据当前语言正确显示
updateTownNamesDiv()
window.dispatchEvent(new Event('scroll'));
}
return {
initApp: initApp,
openModal: openModal,
closeModal: closeModal,
innerModal: innerModal,
// 状态切换
toggleLang: toggleLang,
toggleDarkMode: toggleDarkMode,
// 滚动控制
setP: setP,
updateScrollDistance: updateScrollDistance,
// 更新相关
updateTownIntroduction: updateTownIntroduction,
updateTownNamesDiv: updateTownNamesDiv,
updateUIBasedOnDarkMode: updateUIBasedOnDarkMode,
updateUIBasedOnLanguage: updateUIBasedOnLanguage,
}
})();
document.addEventListener('DOMContentLoaded', DongguanMapApp.initApp);