leaflet-map.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  6. <title>天地图</title>
  7. <!-- Leaflet CSS -->
  8. <link rel="stylesheet" href="./leaflet.css" />
  9. <!-- Leaflet MarkerCluster CSS -->
  10. <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
  11. <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
  12. <style>
  13. * {
  14. margin: 0;
  15. padding: 0;
  16. box-sizing: border-box;
  17. }
  18. html, body {
  19. width: 100%;
  20. height: 100%;
  21. overflow: hidden;
  22. }
  23. #map {
  24. width: 100%;
  25. height: 100%;
  26. }
  27. /* 自定义标记点样式 */
  28. .custom-marker {
  29. display: flex;
  30. flex-direction: column;
  31. align-items: center;
  32. cursor: pointer;
  33. }
  34. .custom-marker-icon {
  35. width: 32px;
  36. height: 32px;
  37. object-fit: contain;
  38. }
  39. .custom-marker-label {
  40. margin-top: 2px;
  41. font-size: 12px;
  42. font-weight: bold;
  43. color: #000000;
  44. text-align: center;
  45. white-space: nowrap;
  46. /* 文字白色描边 */
  47. text-shadow:
  48. -1px -1px 0 #ffffff,
  49. 1px -1px 0 #ffffff,
  50. -1px 1px 0 #ffffff,
  51. 1px 1px 0 #ffffff,
  52. -1px 0 0 #ffffff,
  53. 1px 0 0 #ffffff,
  54. 0 -1px 0 #ffffff,
  55. 0 1px 0 #ffffff;
  56. }
  57. /* 位置标记点样式 - 圆形白底 */
  58. .location-marker {
  59. display: flex;
  60. flex-direction: column;
  61. align-items: center;
  62. cursor: pointer;
  63. }
  64. .location-marker-circle {
  65. width: 35px;
  66. height: 35px;
  67. background-color: #ffffff;
  68. border-radius: 50%;
  69. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  70. display: flex;
  71. justify-content: center;
  72. align-items: center;
  73. }
  74. .location-marker-icon {
  75. width: 25px;
  76. height: 25px;
  77. object-fit: contain;
  78. }
  79. </style>
  80. </head>
  81. <body>
  82. <div id="map"></div>
  83. <!-- Leaflet JS -->
  84. <script src="./leaflet.js"></script>
  85. <!-- Leaflet MarkerCluster JS -->
  86. <script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
  87. <script>
  88. console.log('=== Script started ===');
  89. console.log('window.location:', window.location.href);
  90. // 确保 Leaflet 完全加载后再初始化
  91. (function() {
  92. console.log('=== IIFE executed ===');
  93. // 等待 Leaflet 加载完成
  94. function initMap() {
  95. console.log('initMap called, typeof L:', typeof L);
  96. if (typeof L === 'undefined') {
  97. console.log('Waiting for Leaflet to load...');
  98. setTimeout(initMap, 100);
  99. return;
  100. }
  101. console.log('✓ Leaflet loaded, initializing map...');
  102. 'use strict';
  103. // 解析URL参数
  104. const urlParams = new URLSearchParams(window.location.search);
  105. const initialLat = parseFloat(urlParams.get('lat')) || 32.556211;
  106. const initialLng = parseFloat(urlParams.get('lng')) || 111.494811;
  107. const initialZoom = parseInt(urlParams.get('zoom')) || 13;
  108. const initialLayerType = urlParams.get('layer') || 'tianditu-img';
  109. const tiandituKey = urlParams.get('key') || '4c9c8a818d3cb25bf5ccbd749e7e67c8';
  110. // 天地图图层配置(使用固定子域名,避免 {s} 变量在鸿蒙系统的兼容性问题)
  111. const layerConfigs = {
  112. 'tianditu-vec': {
  113. tile: 'https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + tiandituKey,
  114. annotation: 'https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + tiandituKey,
  115. name: '天地图矢量'
  116. },
  117. 'tianditu-img': {
  118. tile: 'https://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + tiandituKey,
  119. annotation: 'https://t0.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + tiandituKey,
  120. name: '天地图影像'
  121. },
  122. 'tianditu-ter': {
  123. tile: 'https://t0.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + tiandituKey,
  124. annotation: 'https://t0.tianditu.gov.cn/cta_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cta&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + tiandituKey,
  125. name: '天地图地形'
  126. }
  127. };
  128. // 初始化地图
  129. console.log('Creating map with center:', initialLat, initialLng, 'zoom:', initialZoom);
  130. const map = L.map('map', {
  131. center: [initialLat, initialLng],
  132. zoom: initialZoom,
  133. zoomControl: false, // 禁用默认缩放控件
  134. attributionControl: false,
  135. preferCanvas: true // 使用 Canvas 渲染,性能更好
  136. });
  137. console.log('✓ Map created:', map);
  138. // 当前图层
  139. let currentLayerType = initialLayerType;
  140. let currentTileLayer = null;
  141. let currentAnnotationLayer = null;
  142. console.log('Initial layer type:', currentLayerType);
  143. // 标记点集合
  144. const markers = {};
  145. // 标记点聚合图层
  146. let markerClusterGroup = null;
  147. // 覆盖物集合
  148. const overlays = {
  149. polylines: [],
  150. circles: [],
  151. polygons: []
  152. };
  153. // 业务图层集合(用于叠加显示)
  154. const businessLayers = {};
  155. // 初始化标记点聚合图层
  156. function initMarkerCluster() {
  157. if (!markerClusterGroup) {
  158. markerClusterGroup = L.markerClusterGroup({
  159. maxClusterRadius: 80,
  160. spiderfyOnMaxZoom: true,
  161. showCoverageOnHover: false,
  162. zoomToBoundsOnClick: true
  163. });
  164. map.addLayer(markerClusterGroup);
  165. }
  166. }
  167. // 设置图层
  168. function setLayer(layerType) {
  169. const config = layerConfigs[layerType];
  170. if (!config) {
  171. console.error('Unknown layer type:', layerType);
  172. return;
  173. }
  174. // 移除旧图层
  175. if (currentTileLayer) {
  176. map.removeLayer(currentTileLayer);
  177. }
  178. if (currentAnnotationLayer) {
  179. map.removeLayer(currentAnnotationLayer);
  180. }
  181. // 添加新图层(不使用 subdomains,因为 URL 已是固定子域名)
  182. currentTileLayer = L.tileLayer(config.tile, {
  183. attribution: config.name,
  184. maxZoom: 18,
  185. minZoom: 3
  186. }).addTo(map);
  187. currentAnnotationLayer = L.tileLayer(config.annotation, {
  188. maxZoom: 18,
  189. minZoom: 3
  190. }).addTo(map);
  191. currentLayerType = layerType;
  192. }
  193. // 初始化图层
  194. setLayer(initialLayerType);
  195. // 处理图片路径,确保在所有平台都能正确显示
  196. function resolveImagePath(iconPath) {
  197. if (!iconPath) {
  198. return window.location.origin + '/static/images/map/project.png';
  199. }
  200. // 如果已经是完整 URL,直接返回
  201. if (iconPath.startsWith('http://') || iconPath.startsWith('https://') || iconPath.startsWith('data:') || iconPath.startsWith('file://')) {
  202. return iconPath;
  203. }
  204. // 如果是 /static/ 开头的路径
  205. if (iconPath.startsWith('/static/')) {
  206. // 判断运行环境
  207. const origin = window.location.origin;
  208. const protocol = window.location.protocol;
  209. // 如果是 file:// 协议(安卓原生 WebView)
  210. if (protocol === 'file:') {
  211. // 安卓 WebView 中,需要使用相对于 HTML 文件的相对路径
  212. // leaflet-map.html 在 /static/leaflet/ 目录
  213. // project.png 在 /static/images/map/ 目录
  214. // 所以相对路径是 ../images/map/project.png
  215. return iconPath.replace('/static/', '../');
  216. } else {
  217. // Web 环境,使用完整 URL
  218. return origin + iconPath;
  219. }
  220. }
  221. // 如果是相对路径,使用当前页面的 base URL
  222. const baseUrl = window.location.href.substring(0, window.location.href.lastIndexOf('/'));
  223. return baseUrl + '/' + iconPath.replace(/^\.\//, '');
  224. }
  225. // 添加标记点
  226. function addMarker(markerData) {
  227. // 初始化聚合图层
  228. initMarkerCluster();
  229. // 处理图片路径
  230. const resolvedIconPath = resolveImagePath(markerData.iconPath);
  231. // 检测是否为位置标记点
  232. const isLocationMarker = markerData.iconPath && markerData.iconPath.includes('location.png');
  233. let iconHtml = '';
  234. let iconSize = [32, 52];
  235. let iconAnchor = [16, 52];
  236. let popupAnchor = [0, -52];
  237. if (isLocationMarker) {
  238. // 位置标记点:圆形白底,图片在中间
  239. iconHtml = `
  240. <div class="location-marker">
  241. <div class="location-marker-circle">
  242. <img class="location-marker-icon" src="${resolvedIconPath}" alt="${markerData.title || ''}">
  243. </div>
  244. </div>
  245. `;
  246. iconSize = [48, 48];
  247. iconAnchor = [24, 24]; // 锚点在圆心
  248. popupAnchor = [0, -24];
  249. } else {
  250. // 普通标记点:图片在上,文字在下
  251. iconHtml = `
  252. <div class="custom-marker">
  253. <img class="custom-marker-icon" src="${resolvedIconPath}" alt="${markerData.title || ''}">
  254. <div class="custom-marker-label">${markerData.title || ''}</div>
  255. </div>
  256. `;
  257. iconSize = [markerData.width || 32, (markerData.height || 32) + 20];
  258. iconAnchor = [(markerData.width || 32) / 2, (markerData.height || 32) + 20];
  259. popupAnchor = [0, -((markerData.height || 32) + 20)];
  260. }
  261. const marker = L.marker([markerData.latitude, markerData.longitude], {
  262. icon: L.divIcon({
  263. html: iconHtml,
  264. className: '', // 移除默认样式
  265. iconSize: iconSize,
  266. iconAnchor: iconAnchor,
  267. popupAnchor: popupAnchor
  268. })
  269. });
  270. marker.bindPopup(markerData.title || '');
  271. marker.on('click', function() {
  272. postMessage({
  273. type: 'markerTap',
  274. markerId: markerData.id
  275. });
  276. });
  277. // 位置标记点不加入聚合图层
  278. if (isLocationMarker) {
  279. marker.addTo(map);
  280. } else {
  281. markerClusterGroup.addLayer(marker);
  282. }
  283. markers[markerData.id] = marker;
  284. }
  285. // 批量添加标记点
  286. function addMarkers(markersData) {
  287. initMarkerCluster();
  288. const markerArray = [];
  289. markersData.forEach(function(markerData) {
  290. // 处理图片路径
  291. const resolvedIconPath = resolveImagePath(markerData.iconPath);
  292. // 创建自定义 HTML 标记
  293. const iconHtml = `
  294. <div class="custom-marker">
  295. <img class="custom-marker-icon" src="${resolvedIconPath}" alt="${markerData.title || ''}">
  296. <div class="custom-marker-label">${markerData.title || ''}</div>
  297. </div>
  298. `;
  299. const marker = L.marker([markerData.latitude, markerData.longitude], {
  300. icon: L.divIcon({
  301. html: iconHtml,
  302. className: '', // 移除默认样式
  303. iconSize: [markerData.width || 32, (markerData.height || 32) + 20], // 高度增加以容纳文字
  304. iconAnchor: [(markerData.width || 32) / 2, (markerData.height || 32) + 20], // 锚点在底部
  305. popupAnchor: [0, -((markerData.height || 32) + 20)]
  306. })
  307. });
  308. marker.bindPopup(markerData.title || '');
  309. marker.on('click', function() {
  310. postMessage({
  311. type: 'markerTap',
  312. markerId: markerData.id
  313. });
  314. });
  315. markerArray.push(marker);
  316. markers[markerData.id] = marker;
  317. });
  318. // 批量添加到聚合图层,性能更好
  319. markerClusterGroup.addLayers(markerArray);
  320. }
  321. // 清除所有标记点
  322. function clearMarkers() {
  323. if (markerClusterGroup) {
  324. markerClusterGroup.clearLayers();
  325. }
  326. for (var id in markers) {
  327. delete markers[id];
  328. }
  329. }
  330. // 移除单个标记点
  331. function removeMarker(markerId) {
  332. if (markers[markerId]) {
  333. // 尝试从聚合图层移除
  334. if (markerClusterGroup && markerClusterGroup.hasLayer(markers[markerId])) {
  335. markerClusterGroup.removeLayer(markers[markerId]);
  336. } else {
  337. // 如果不在聚合图层(如位置标记点),直接从地图移除
  338. map.removeLayer(markers[markerId]);
  339. }
  340. delete markers[markerId];
  341. }
  342. }
  343. // 添加折线
  344. function addPolyline(data) {
  345. const latlngs = data.points.map(p => [p.latitude, p.longitude]);
  346. const polyline = L.polyline(latlngs, {
  347. color: data.color || '#007aff',
  348. weight: data.width || 4
  349. }).addTo(map);
  350. overlays.polylines.push(polyline);
  351. }
  352. // 添加圆形
  353. function addCircle(data) {
  354. const circle = L.circle([data.latitude, data.longitude], {
  355. radius: data.radius || 100,
  356. color: data.strokeColor || '#007aff',
  357. fillColor: data.fillColor || 'rgba(0, 122, 255, 0.2)',
  358. fillOpacity: 0.2,
  359. weight: 2
  360. }).addTo(map);
  361. overlays.circles.push(circle);
  362. }
  363. // 添加多边形
  364. function addPolygon(data) {
  365. const latlngs = data.points.map(p => [p.latitude, p.longitude]);
  366. const polygon = L.polygon(latlngs, {
  367. color: data.strokeColor || '#007aff',
  368. fillColor: data.fillColor || 'rgba(0, 122, 255, 0.2)',
  369. fillOpacity: 0.2,
  370. weight: 2
  371. }).addTo(map);
  372. overlays.polygons.push(polygon);
  373. }
  374. // 清除所有覆盖物
  375. function clearOverlays() {
  376. // 清除折线
  377. overlays.polylines.forEach(function(polyline) {
  378. map.removeLayer(polyline);
  379. });
  380. overlays.polylines = [];
  381. // 清除圆形
  382. overlays.circles.forEach(function(circle) {
  383. map.removeLayer(circle);
  384. });
  385. overlays.circles = [];
  386. // 清除多边形
  387. overlays.polygons.forEach(function(polygon) {
  388. map.removeLayer(polygon);
  389. });
  390. overlays.polygons = [];
  391. }
  392. // 设置地图中心
  393. function setCenter(lat, lng, zoom) {
  394. // zoom 可能为 null 或 undefined,都使用当前缩放级别
  395. const finalZoom = (zoom != null && zoom !== undefined) ? zoom : map.getZoom();
  396. // 禁用动画,避免安卓 WebView 闪白屏
  397. map.setView([lat, lng], finalZoom, {
  398. animate: false,
  399. duration: 0
  400. });
  401. }
  402. // 设置缩放级别
  403. function setZoom(zoom) {
  404. // 禁用动画,避免安卓 WebView 闪白屏
  405. map.setZoom(zoom, {
  406. animate: false
  407. });
  408. }
  409. // 切换图层
  410. function switchLayer(layerType) {
  411. setLayer(layerType);
  412. postMessage({
  413. type: 'layerChange',
  414. layerType: layerType
  415. });
  416. }
  417. // 添加业务图层
  418. function addBusinessLayer(layerCode, layerUrl, baseUrl, token) {
  419. // 如果图层已存在,先移除
  420. if (businessLayers[layerCode]) {
  421. map.removeLayer(businessLayers[layerCode]);
  422. }
  423. // 构建完整的 URL
  424. let fullUrl = baseUrl + layerUrl;
  425. // 添加 token 参数
  426. if (token && token.length > 0) {
  427. // 检查 URL 中是否已有查询参数
  428. const separator = fullUrl.indexOf('?') > -1 ? '&' : '?';
  429. fullUrl += separator + 'token=' + encodeURIComponent(token);
  430. }
  431. console.log('Adding business layer:', layerCode, fullUrl);
  432. // 创建图层
  433. const layer = L.tileLayer(fullUrl, {
  434. maxZoom: 18,
  435. minZoom: 3
  436. }).addTo(map);
  437. // 保存图层引用
  438. businessLayers[layerCode] = layer;
  439. }
  440. // 移除业务图层
  441. function removeBusinessLayer(layerCode) {
  442. const layer = businessLayers[layerCode];
  443. if (layer) {
  444. map.removeLayer(layer);
  445. delete businessLayers[layerCode];
  446. console.log('Removed business layer:', layerCode);
  447. }
  448. }
  449. // 向 uni-app 发送消息
  450. function postMessage(data) {
  451. console.log('Sending postMessage:', JSON.stringify(data));
  452. console.log('window.uni exists:', typeof window.uni !== 'undefined');
  453. console.log('window.uni.postMessage exists:', window.uni && typeof window.uni.postMessage !== 'undefined');
  454. if (window.uni && window.uni.postMessage) {
  455. window.uni.postMessage({
  456. data: data
  457. });
  458. console.log('postMessage sent successfully');
  459. } else {
  460. console.warn('window.uni.postMessage not available');
  461. }
  462. }
  463. // 监听地图事件
  464. map.on('moveend', function() {
  465. const center = map.getCenter();
  466. postMessage({
  467. type: 'regionChange',
  468. latitude: center.lat,
  469. longitude: center.lng,
  470. zoom: map.getZoom()
  471. });
  472. });
  473. map.on('zoomend', function() {
  474. const center = map.getCenter();
  475. postMessage({
  476. type: 'regionChange',
  477. latitude: center.lat,
  478. longitude: center.lng,
  479. zoom: map.getZoom()
  480. });
  481. });
  482. // 暴露全局方法供 uni-app 调用
  483. window.leafletMap = {
  484. addMarker: addMarker,
  485. addMarkers: addMarkers,
  486. clearMarkers: clearMarkers,
  487. removeMarker: removeMarker,
  488. addPolyline: addPolyline,
  489. addCircle: addCircle,
  490. addPolygon: addPolygon,
  491. clearOverlays: clearOverlays,
  492. setCenter: setCenter,
  493. setZoom: setZoom,
  494. switchLayer: switchLayer,
  495. addBusinessLayer: addBusinessLayer,
  496. removeBusinessLayer: removeBusinessLayer,
  497. getCenter: function() {
  498. const center = map.getCenter();
  499. return {
  500. latitude: center.lat,
  501. longitude: center.lng,
  502. zoom: map.getZoom()
  503. };
  504. }
  505. };
  506. // 地图加载完成
  507. console.log('Setting up map.whenReady callback...');
  508. map.whenReady(function() {
  509. console.log('Map is ready!');
  510. postMessage({
  511. type: 'ready',
  512. layerType: currentLayerType
  513. });
  514. });
  515. // 监听来自 uni-app 的消息
  516. window.addEventListener('message', function(e) {
  517. try {
  518. const message = e.data;
  519. if (!message || !message.type) return;
  520. switch(message.type) {
  521. case 'addMarker':
  522. if (message.data) addMarker(message.data);
  523. break;
  524. case 'addPolyline':
  525. if (message.data) addPolyline(message.data);
  526. break;
  527. case 'addCircle':
  528. if (message.data) addCircle(message.data);
  529. break;
  530. case 'addPolygon':
  531. if (message.data) addPolygon(message.data);
  532. break;
  533. case 'clearOverlays':
  534. clearOverlays();
  535. break;
  536. case 'setCenter':
  537. if (message.data) {
  538. setCenter(message.data.latitude, message.data.longitude, message.data.zoom);
  539. }
  540. break;
  541. case 'setZoom':
  542. if (message.data && message.data.zoom !== undefined) {
  543. setZoom(message.data.zoom);
  544. }
  545. break;
  546. case 'switchLayer':
  547. if (message.data && message.data.layerType) {
  548. switchLayer(message.data.layerType);
  549. }
  550. break;
  551. }
  552. } catch (error) {
  553. console.error('Error handling message:', error);
  554. postMessage({
  555. type: 'error',
  556. error: error.message
  557. });
  558. }
  559. });
  560. console.log('✓ Leaflet map initialized with layer:', currentLayerType);
  561. }
  562. // 启动初始化
  563. console.log('document.readyState:', document.readyState);
  564. if (document.readyState === 'loading') {
  565. console.log('Waiting for DOMContentLoaded...');
  566. document.addEventListener('DOMContentLoaded', function() {
  567. console.log('DOMContentLoaded fired, calling initMap');
  568. initMap();
  569. });
  570. } else {
  571. console.log('DOM already loaded, calling initMap directly');
  572. initMap();
  573. }
  574. })();
  575. console.log('=== Script end ===');
  576. </script>
  577. </body>
  578. </html>