1// ç»ååºåå°ç¨ã®ä¸å¯è¦å°å³ã管çããã¯ã©ã¹ 2 3import { waitTilesLoaded } from './capture.js'; 4import { drawCanvasMarker, MARKER_CONFIG } from './marker-utils.js'; 5 6// ããªã©ã¤ã³ã®ã¹ã¿ã¤ã«è¨å® 7const POLYLINE_CONFIG = { 8 color: '#0066CC', 9 width: 4 10}; 11 12// ããã£ã³ã°è¨å®ï¼ãã¼ã«ã¼ï¼ã³ã¡ã³ãå¹ãåºããè¦åããªãããã«ï¼ 13const PADDING_PIXELS = 80; 14 15export class ExportMapManager { 16 constructor() { 17 this.exportMap = null; 18 this.container = null; 19 } 20 21 /** 22 * åºåç¨ã®ä¸å¯è¦å°å³ã使 23 * @param {maplibregl.Map} displayMap - 表示ç¨å°å³ï¼è¨å®ãã³ãã¼ããå ï¼ 24 */ 25 async createExportMap(displayMap) { 26 console.log('Creating export map...'); 27 28 // 1. é ãã³ã³ããã使 29 this.container = document.createElement('div'); 30 this.container.id = 'export-map-container'; 31 32 // ç»åã¨ã¯ã¹ãã¼ãç¨ã®ãªãã¹ã¯ãªã¼ã³ã³ã³ããã 33 // ã¦ã¼ã¶ã¼ã«è¡¨ç¤ºãããªãèæ¯ããããæç»ãããã¼ã«ã¼ä»ãå°å³ç»åãéãã«çæããããã«ä½¿ç¨ã 34 const EXPORT_CONTAINER_WIDTH = 800; 35 const EXPORT_CONTAINER_HEIGHT = 600; 36 const EXPORT_CONTAINER_HIDDEN_POSITION = -9999; 37 this.container.style.cssText = ` 38 position: absolute; 39 left: ${EXPORT_CONTAINER_HIDDEN_POSITION}px; 40 width: ${EXPORT_CONTAINER_WIDTH}px; 41 height: ${EXPORT_CONTAINER_HEIGHT}px; 42 visibility: hidden; 43 `; 44 document.body.appendChild(this.container); 45 46 // 2. ç¾å¨ã®è¡¨ç¤ºå°å³ã®è¨å®ãåå¾ 47 const center = displayMap.getCenter(); 48 const zoom = displayMap.getZoom(); 49 const style = displayMap.getStyle(); 50 51 // ç¾å¨è¡¨ç¤ºä¸ã®ã¬ã¤ã¤ã¼ãç¹å® 52 let visibleLayer = 'pale-layer'; 53 const layers = ['pale-layer', 'photo-layer', 'std-layer', 'osm-layer', 'blank-layer', 'esry-layer']; 54 for (const layer of layers) { 55 if (displayMap.getLayoutProperty(layer, 'visibility') === 'visible') { 56 visibleLayer = layer; 57 break; 58 } 59 } 60 61 // 3. åºåç¨å°å³ã使 62 this.exportMap = new maplibregl.Map({ 63 container: this.container, 64 style: style, 65 center: center, 66 zoom: zoom, 67 interactive: false, // ã¤ã³ã¿ã©ã¯ã·ã§ã³ç¡å¹ 68 trackResize: false, // ãªãµã¤ãºè¿½è·¡ç¡å¹ 69 preserveDrawingBuffer: true, // Canvasåºåç¨ 70 attributionControl: false 71 }); 72 73 // 4. ã¹ã¿ã¤ã«èªã¿è¾¼ã¿å®äºãå¾ æ© 74 await new Promise(resolve => { 75 this.exportMap.once('load', () => { 76 console.log('Export map loaded'); 77 78 // 表示ã¬ã¤ã¤ã¼ã®è¨å®ãåæ 79 layers.forEach(layer => { 80 const visibility = layer === visibleLayer ? 'visible' : 'none'; 81 if (this.exportMap.getLayer(layer)) { 82 this.exportMap.setLayoutProperty(layer, 'visibility', visibility); 83 } 84 }); 85 86 resolve(); 87 }); 88 }); 89 } 90 91 /** 92 * ãã¦ã³ãã£ã³ã°ããã¯ã¹ãè¨ç® 93 * @param {Array} coordinates - 座æ¨ã®é å 94 * @returns {Object} bounds { minLng, maxLng, minLat, maxLat } 95 */ 96 calculateBounds(coordinates) { 97 if (coordinates.length === 0) { 98 return null; 99 } 100 101 let minLng = Infinity; 102 let maxLng = -Infinity; 103 let minLat = Infinity; 104 let maxLat = -Infinity; 105 106 for (const coord of coordinates) { 107 if (coord.lng < minLng) minLng = coord.lng; 108 if (coord.lng > maxLng) maxLng = coord.lng; 109 if (coord.lat < minLat) minLat = coord.lat; 110 if (coord.lat > maxLat) maxLat = coord.lat; 111 } 112 113 return { minLng, maxLng, minLat, maxLat }; 114 } 115 116 /** 117 * ããªã©ã¤ã³å ¨ä½ãåã¾ãåä¸ç»åããã£ãã㣠118 * @param {number} width - ç»åå¹ 119 * @param {number} height - ç»åé«ã 120 * @param {Array} allCoordinates - ãã¹ã¦ã®ã«ã¼ãåº§æ¨ 121 * @returns {Promise<string>} Base64ã¨ã³ã³ã¼ããããç»åãã¼ã¿ 122 */ 123 async captureFullRouteImage(width, height, allCoordinates) { 124 console.log('Capturing full route image'); 125 126 // ãµã¤ãºè¨å® 127 this.container.style.width = `${width}px`; 128 this.container.style.height = `${height}px`; 129 this.exportMap.resize(); 130 131 // ãã¦ã³ãã£ã³ã°ããã¯ã¹ãè¨ç® 132 const bounds = this.calculateBounds(allCoordinates); 133 if (!bounds) { 134 throw new Error('No coordinates to capture'); 135 } 136 137 // ããªã©ã¤ã³å ¨ä½ãåã¾ãããã«ãã¥ã¼ãè¨å® 138 this.exportMap.fitBounds( 139 [[bounds.minLng, bounds.minLat], [bounds.maxLng, bounds.maxLat]], 140 { 141 padding: PADDING_PIXELS, 142 duration: 0 143 } 144 ); 145 146 // ã¿ã¤ã«èªã¿è¾¼ã¿å¾ æ© 147 await new Promise(r => this.exportMap.once('idle', r)); 148 149 // ããªã©ã¤ã³ã追å 150 this.addPolyline(allCoordinates); 151 await new Promise(r => this.exportMap.once('idle', r)); 152 153 // ãã£ãã㣠154 await waitTilesLoaded(this.exportMap); 155 156 // Canvas ãåå¾ãã¦ãã¼ã«ã¼ãç´æ¥æç» 157 const mapCanvas = this.exportMap.getCanvas(); 158 const outputCanvas = document.createElement('canvas'); 159 outputCanvas.width = mapCanvas.width; 160 outputCanvas.height = mapCanvas.height; 161 const ctx = outputCanvas.getContext('2d'); 162 163 // å°å³ãæç» 164 ctx.drawImage(mapCanvas, 0, 0); 165 166 // ãã¼ã«ã¼ã Canvas ã«ç´æ¥æç» 167 this.drawMarkersOnCanvas(ctx, allCoordinates); 168 169 const dataURL = outputCanvas.toDataURL('image/png'); 170 171 // ããªã©ã¤ã³ãåé¤ 172 this.removePolyline(); 173 174 return dataURL; 175 } 176 177 /** 178 * ããªã©ã¤ã³ã¬ã¤ã¤ã¼ã追å 179 * @param {Array} coordinates - ã«ã¼ã座æ¨ã®é å 180 */ 181 addPolyline(coordinates) { 182 // 座æ¨ã2ã¤æªæºã®å ´åã¯ããªã©ã¤ã³ãæç»ããªã 183 if (coordinates.length < 2) { 184 return; 185 } 186 187 // GeoJSON LineString ãã¼ã¿ã使 188 const geojsonData = { 189 type: 'Feature', 190 geometry: { 191 type: 'LineString', 192 coordinates: coordinates.map(coord => [coord.lng, coord.lat]) 193 } 194 }; 195 196 // ã½ã¼ã¹ã追å 197 if (!this.exportMap.getSource('export-polyline')) { 198 this.exportMap.addSource('export-polyline', { 199 type: 'geojson', 200 data: geojsonData 201 }); 202 } else { 203 this.exportMap.getSource('export-polyline').setData(geojsonData); 204 } 205 206 // ã©ã¤ã³ã¬ã¤ã¤ã¼ã追å 207 if (!this.exportMap.getLayer('export-polyline-layer')) { 208 this.exportMap.addLayer({ 209 id: 'export-polyline-layer', 210 type: 'line', 211 source: 'export-polyline', 212 layout: { 213 'line-join': 'round', 214 'line-cap': 'round' 215 }, 216 paint: { 217 'line-color': POLYLINE_CONFIG.color, 218 'line-width': POLYLINE_CONFIG.width 219 } 220 }); 221 } 222 } 223 224 /** 225 * ããªã©ã¤ã³ã¬ã¤ã¤ã¼ãåé¤ 226 */ 227 removePolyline() { 228 if (this.exportMap.getLayer('export-polyline-layer')) { 229 this.exportMap.removeLayer('export-polyline-layer'); 230 } 231 if (this.exportMap.getSource('export-polyline')) { 232 this.exportMap.removeSource('export-polyline'); 233 } 234 } 235 236 /** 237 * Canvas ã«ç´æ¥ãã¼ã«ã¼ãæç» 238 * @param {CanvasRenderingContext2D} ctx - Canvas 2D ã³ã³ããã¹ã 239 * @param {Array} coordinates - ã«ã¼ã座æ¨ã®é å 240 */ 241 drawMarkersOnCanvas(ctx, coordinates) { 242 if (coordinates.length === 0) { 243 return; 244 } 245 246 const { width, height } = MARKER_CONFIG; 247 // MapLibre GL ã使ç¨ãã¦ãã pixelRatio ãåå¾
248 // map.getPixelRatio() ã§ MapLibre å é¨ã®å¤ãåå¾ï¼window.devicePixelRatio ã¨ç°ãªãå ´åãããï¼ 249 const dpr = this.exportMap.getPixelRatio(); 250 const scaledWidth = width * dpr; 251 const scaledHeight = height * dpr; 252 253 coordinates.forEach((coord, index) => { 254 // 座æ¨ããã¯ã»ã«ä½ç½®ã«å¤æ 255 // project() 㯠CSS ãã¯ã»ã«åº§æ¨ãè¿ããããç©çãã¯ã»ã«ã«å¤æãå¿ è¦ 256 const point = this.exportMap.project([coord.lng, coord.lat]); 257 const physicalX = point.x * dpr; 258 const physicalY = point.y * dpr; 259 260 // ãã¼ã«ã¼ã®æç»ä½ç½®ï¼ãã³ã®å 端ã座æ¨ä½ç½®ãæãï¼ 261 const x = physicalX - scaledWidth / 2; 262 const y = physicalY - scaledHeight; 263 264 // 䏿ç㪠Canvas ã使ãã¦ãã¼ã«ã¼ãæç»ï¼dpr ã§ã¹ã±ã¼ãªã³ã°ï¼ 265 const markerCanvas = document.createElement('canvas'); 266 markerCanvas.width = scaledWidth; 267 markerCanvas.height = scaledHeight; 268 const markerCtx = markerCanvas.getContext('2d'); 269 markerCtx.scale(dpr, dpr); 270 271 drawCanvasMarker(markerCtx, index + 1); 272 273 // ã¡ã¤ã³ã® Canvas ã«ãã¼ã«ã¼ãæç» 274 ctx.drawImage(markerCanvas, x, y); 275 276 // ã³ã¡ã³ããããå ´åã¯ã©ãã«ãæç» 277 if (coord.comment && coord.comment.trim()) { 278 this.drawCommentLabel(ctx, physicalX, physicalY - scaledHeight, coord.comment, dpr); 279 } 280 }); 281 } 282 283 /** 284 * ã³ã¡ã³ãã©ãã«ã Canvas ã«æç»ï¼ç¢å°ä»ãå¹ãåºãï¼ 285 * @param {CanvasRenderingContext2D} ctx - Canvas 2D ã³ã³ããã¹ã 286 * @param {number} x - ã©ãã«ã®ä¸å¿X座æ¨ï¼ç©çãã¯ã»ã«ï¼ 287 * @param {number} y - ã©ãã«ã®ä¸ç«¯Y座æ¨ï¼ç¢å°ã®å 端ãç©çãã¯ã»ã«ï¼ 288 * @param {string} text - ã©ãã«ããã¹ã 289 * @param {number} dpr - ããã¤ã¹ãã¯ã»ã«æ¯ 290 */ 291 drawCommentLabel(ctx, x, y, text, dpr = 1) { 292 // CSS ãã¯ã»ã«å¤ã dpr ã§ã¹ã±ã¼ãªã³ã° 293 const padding = 6 * dpr; 294 const fontSize = 12 * dpr; 295 const borderRadius = 4 * dpr; 296 const arrowWidth = 8 * dpr; 297 const arrowHeight = 8 * dpr; 298 299 ctx.font = `${fontSize}px Arial, sans-serif`; 300 const metrics = ctx.measureText(text); 301 const textWidth = metrics.width; 302 const textHeight = fontSize; 303 304 const boxWidth = textWidth + padding * 2; 305 const boxHeight = textHeight + padding * 2; 306 const boxX = x - boxWidth / 2; 307 const boxY = y - arrowHeight - boxHeight; 308 309 // å¹ãåºãå ¨ä½ã®ãã¹ï¼è§ä¸¸ããã¯ã¹ + ç¢å°ï¼ 310 ctx.beginPath(); 311 // å·¦ä¸ããéå§ï¼è§ä¸¸ï¼ 312 ctx.moveTo(boxX + borderRadius, boxY); 313 // ä¸è¾º 314 ctx.lineTo(boxX + boxWidth - borderRadius, boxY); 315 // å³ä¸è§ä¸¸ 316 ctx.arcTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + borderRadius, borderRadius); 317 // å³è¾º 318 ctx.lineTo(boxX + boxWidth, boxY + boxHeight - borderRadius); 319 // å³ä¸è§ä¸¸ 320 ctx.arcTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - borderRadius, boxY + boxHeight, borderRadius); 321 // ä¸è¾ºï¼ç¢å°ã¾ã§ï¼ 322 ctx.lineTo(x + arrowWidth / 2, boxY + boxHeight); 323 // ç¢å° 324 ctx.lineTo(x, boxY + boxHeight + arrowHeight); 325 ctx.lineTo(x - arrowWidth / 2, boxY + boxHeight); 326 // ä¸è¾ºï¼ç¢å°ããå·¦ä¸è§ä¸¸ã¾ã§ï¼ 327 ctx.lineTo(boxX + borderRadius, boxY + boxHeight); 328 // å·¦ä¸è§ä¸¸ 329 ctx.arcTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - borderRadius, borderRadius); 330 // 左辺 331 ctx.lineTo(boxX, boxY + borderRadius); 332 // å·¦ä¸è§ä¸¸ 333 ctx.arcTo(boxX, boxY, boxX + borderRadius, boxY, borderRadius); 334 ctx.closePath(); 335 336 // èæ¯ï¼ç½ï¼ 337 ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; 338 ctx.fill(); 339 340 // æ ç·ï¼éï¼ 341 ctx.strokeStyle = '#1976D2'; 342 ctx.lineWidth = 1 * dpr; 343 ctx.stroke(); 344 345 // ããã¹ã 346 ctx.fillStyle = '#333'; 347 ctx.textAlign = 'center'; 348 ctx.textBaseline = 'middle'; 349 ctx.fillText(text, x, boxY + boxHeight / 2); 350 } 351 352 /** 353 * å°å³ã¨ã³ã³ãããç ´æ£ 354 */ 355 destroy() { 356 console.log('Destroying export map...'); 357 358 if (this.exportMap) { 359 this.exportMap.remove(); 360 this.exportMap = null; 361 } 362 363 if (this.container) { 364 this.container.remove(); 365 this.container = null; 366 } 367 } 368}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.