Consolidated ASP Classic MVC framework from best components
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

814 lignes
34KB

  1. <div class="container mt-4">
  2. <nav aria-label="breadcrumb" class="d-print-none">
  3. <ol class="breadcrumb">
  4. <li class="breadcrumb-item"><a href="/territories">Territories</a></li>
  5. <li class="breadcrumb-item active" aria-current="page"><%= Server.HTMLEncode(TerritoryController.territory.Name) %></li>
  6. </ol>
  7. </nav>
  8. <% Flash().ShowSuccessIfPresent %>
  9. <% Flash().ShowErrorsIfPresent %>
  10. <div class="card">
  11. <div class="card-header d-flex justify-content-between align-items-center">
  12. <h2 class="mb-0"><%= Server.HTMLEncode(TerritoryController.territory.Name) %></h2>
  13. <div class="d-print-none">
  14. <button type="button" class="btn btn-secondary" onclick="printTerritory()"><i class="bi bi-printer"></i> Print</button>
  15. <a href="/territories/<%= TerritoryController.territory.Id %>/edit" class="btn btn-warning">Edit</a>
  16. <form method="post" action="/territories/<%= TerritoryController.territory.Id %>/delete" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this territory?');">
  17. <button type="submit" class="btn btn-danger">Delete</button>
  18. </form>
  19. </div>
  20. </div>
  21. <div class="card-body">
  22. <div class="row">
  23. <div class="col-md-4 territory-details">
  24. <dl>
  25. <dt>ID</dt>
  26. <dd><%= TerritoryController.territory.Id %></dd>
  27. <dt>Name</dt>
  28. <dd><%= Server.HTMLEncode(TerritoryController.territory.Name) %></dd>
  29. <dt>Description</dt>
  30. <dd><%= Server.HTMLEncode(TerritoryController.territory.Description & "") %></dd>
  31. </dl>
  32. </div>
  33. <div class="col-md-8 map-container">
  34. <div id="map" style="height: 400px; width: 100%; border-radius: 8px;"></div>
  35. </div>
  36. </div>
  37. </div>
  38. </div>
  39. <div class="mt-3 d-print-none">
  40. <a href="/territories" class="btn btn-secondary">Back to List</a>
  41. </div>
  42. </div>
  43. <style>
  44. @media print {
  45. /* Hide navigation and non-essential elements */
  46. .d-print-none,
  47. nav.navbar,
  48. footer,
  49. .breadcrumb {
  50. display: none !important;
  51. }
  52. /* Set page size and margins */
  53. @page {
  54. size: letter portrait;
  55. margin: 0.5in;
  56. }
  57. /* Reset body for print */
  58. body {
  59. margin: 0 !important;
  60. padding: 0 !important;
  61. }
  62. .container {
  63. max-width: 100% !important;
  64. padding: 0 !important;
  65. margin: 0 !important;
  66. }
  67. .card {
  68. border: none !important;
  69. box-shadow: none !important;
  70. }
  71. .card-header {
  72. background-color: transparent !important;
  73. border-bottom: 2px solid #000 !important;
  74. padding: 0 0 10px 0 !important;
  75. margin-bottom: 10px !important;
  76. }
  77. .card-body {
  78. padding: 0 !important;
  79. }
  80. /* Territory title styling for print */
  81. h2 {
  82. font-size: 24pt !important;
  83. margin: 0 !important;
  84. text-align: center !important;
  85. }
  86. /* Hide details section in print, show only title and map */
  87. .territory-details {
  88. display: none !important;
  89. }
  90. /* Map takes 3/4 of 8.5x11 page (11in - 1in margins = 10in, 3/4 = 7.5in) */
  91. #map {
  92. height: 7.5in !important;
  93. width: 100% !important;
  94. page-break-inside: avoid;
  95. border: 1px solid #ccc !important;
  96. }
  97. .map-container {
  98. flex: 0 0 100% !important;
  99. max-width: 100% !important;
  100. }
  101. .row {
  102. display: block !important;
  103. }
  104. /* Ensure text is black for printing */
  105. body, .card-body, dt, dd, h2 {
  106. color: #000 !important;
  107. }
  108. }
  109. </style>
  110. <%
  111. Dim mapProvider, googleMapsKey, mapTilerKey, mapTilerStyle, mapTilerSdkJsUrl, mapTilerSdkCssUrl
  112. mapProvider = LCase(Trim(GetAppSetting("MapProvider") & ""))
  113. If mapProvider <> "maptiler" Then mapProvider = "google"
  114. googleMapsKey = Trim(GetAppSetting("GoogleMapsApiKey") & "")
  115. mapTilerKey = Trim(GetAppSetting("MapTilerApiKey") & "")
  116. mapTilerStyle = Trim(GetAppSetting("MapTilerStyle") & "")
  117. If mapTilerStyle = "" Or LCase(mapTilerStyle) = "nothing" Then mapTilerStyle = "streets-v2"
  118. mapTilerSdkJsUrl = Trim(GetAppSetting("MapTilerSdkJsUrl") & "")
  119. mapTilerSdkCssUrl = Trim(GetAppSetting("MapTilerSdkCssUrl") & "")
  120. If mapTilerSdkJsUrl = "" Or LCase(mapTilerSdkJsUrl) = "nothing" Then mapTilerSdkJsUrl = "https://cdn.maptiler.com/maptiler-sdk-js/v3.0.0/maptiler-sdk.umd.min.js"
  121. If mapTilerSdkCssUrl = "" Or LCase(mapTilerSdkCssUrl) = "nothing" Then mapTilerSdkCssUrl = "https://cdn.maptiler.com/maptiler-sdk-js/v3.0.0/maptiler-sdk.css"
  122. %>
  123. <%
  124. Dim coordsJson
  125. coordsJson = Trim(TerritoryController.territory.Coordinates & "")
  126. If coordsJson = "" Then coordsJson = "[]"
  127. ' Build street names array for JavaScript
  128. Dim streetIter, streetName, streetsJson, isFirst
  129. streetsJson = "["
  130. isFirst = True
  131. If Not TerritoryController.territoryStreets Is Nothing Then
  132. Set streetIter = TerritoryController.territoryStreets.Iterator()
  133. Do While streetIter.HasNext()
  134. streetName = streetIter.GetNext()
  135. If Not isFirst Then streetsJson = streetsJson & ","
  136. streetsJson = streetsJson & """" & Replace(streetName, """", "\""") & """"
  137. isFirst = False
  138. Loop
  139. End If
  140. streetsJson = streetsJson & "]"
  141. ' Get border streets from the Description field
  142. Dim borderStreets
  143. borderStreets = Trim(TerritoryController.territory.Description & "")
  144. %>
  145. <script>
  146. var territoryCoordinates = <%= coordsJson %>;
  147. var territoryName = "<%= Replace(TerritoryController.territory.Name, """", "\""") %>";
  148. var territoryBorderStreets = "<%= Replace(Replace(borderStreets, """", "\"""), vbCrLf, ", ") %>";
  149. var territoryStreets = <%= streetsJson %>;
  150. var mapProvider = "<%= Replace(mapProvider, """", "\""") %>";
  151. var googleMapsKey = "<%= Replace(googleMapsKey, """", "\""") %>";
  152. var mapTilerKey = "<%= Replace(mapTilerKey, """", "\""") %>";
  153. var mapTilerStyle = "<%= Replace(mapTilerStyle, """", "\""") %>";
  154. </script>
  155. <% If mapProvider = "maptiler" Then %>
  156. <link rel="stylesheet" href="<%= Server.HTMLEncode(mapTilerSdkCssUrl) %>" />
  157. <script src="<%= Server.HTMLEncode(mapTilerSdkJsUrl) %>"></script>
  158. <% Else %>
  159. <script src="https://maps.googleapis.com/maps/api/js?key=<%= Server.HTMLEncode(googleMapsKey) %>&callback=initMap" async defer></script>
  160. <% End If %>
  161. <script>
  162. var map, polygon;
  163. var mapIsReady = false;
  164. function setMapMessage(message) {
  165. var mapEl = document.getElementById('map');
  166. if (mapEl) {
  167. mapEl.innerHTML = '<div class="alert alert-info">' + message + '</div>';
  168. }
  169. }
  170. function getLatLng(coord) {
  171. var lat, lng;
  172. if (coord && typeof coord === 'object' && (coord.lat !== undefined || coord.lng !== undefined)) {
  173. lat = parseFloat(coord.lat);
  174. lng = parseFloat(coord.lng);
  175. } else if (Array.isArray(coord) && coord.length >= 2) {
  176. var a = parseFloat(coord[0]);
  177. var b = parseFloat(coord[1]);
  178. if (!isNaN(a) && !isNaN(b)) {
  179. if (Math.abs(a) > 90 && Math.abs(b) <= 90) {
  180. lng = a;
  181. lat = b;
  182. } else {
  183. lat = a;
  184. lng = b;
  185. }
  186. }
  187. }
  188. return { lat: lat, lng: lng };
  189. }
  190. function initMap() {
  191. try {
  192. var coords = territoryCoordinates;
  193. if (!coords || !Array.isArray(coords) || coords.length === 0) {
  194. setMapMessage('No coordinates available for this territory.');
  195. return;
  196. }
  197. if (mapProvider === 'maptiler') {
  198. if (!mapTilerKey || mapTilerKey === 'nothing') {
  199. setMapMessage('MapTiler API key is missing.');
  200. return;
  201. }
  202. if (typeof maptilersdk === 'undefined') {
  203. setMapMessage('MapTiler SDK failed to load.');
  204. return;
  205. }
  206. maptilersdk.config.apiKey = mapTilerKey;
  207. var polygonCoords = coords.map(function(coord) {
  208. var ll = getLatLng(coord);
  209. return [ll.lng, ll.lat];
  210. }).filter(function(p) {
  211. return isFinite(p[0]) && isFinite(p[1]);
  212. });
  213. if (polygonCoords.length === 0) {
  214. setMapMessage('No valid coordinates available for this territory.');
  215. return;
  216. }
  217. if (polygonCoords.length > 0) {
  218. var firstCoord = polygonCoords[0];
  219. var lastCoord = polygonCoords[polygonCoords.length - 1];
  220. if (firstCoord[0] !== lastCoord[0] || firstCoord[1] !== lastCoord[1]) {
  221. polygonCoords.push([firstCoord[0], firstCoord[1]]);
  222. }
  223. }
  224. var bounds = new maptilersdk.LngLatBounds();
  225. polygonCoords.forEach(function(coord) {
  226. bounds.extend(coord);
  227. });
  228. var styleUrl = 'https://api.maptiler.com/maps/' + encodeURIComponent(mapTilerStyle) + '/style.json?key=' + encodeURIComponent(mapTilerKey);
  229. map = new maptilersdk.Map({
  230. container: 'map',
  231. style: styleUrl,
  232. center: bounds.getCenter(),
  233. zoom: 14,
  234. preserveDrawingBuffer: true
  235. });
  236. map.on('styleimagemissing', function(e) {
  237. if (map.hasImage(e.id)) return;
  238. var emptyCanvas = document.createElement('canvas');
  239. emptyCanvas.width = 1;
  240. emptyCanvas.height = 1;
  241. map.addImage(e.id, emptyCanvas);
  242. });
  243. map.on('load', function() {
  244. map.addSource('territory', {
  245. type: 'geojson',
  246. data: {
  247. type: 'Feature',
  248. geometry: {
  249. type: 'Polygon',
  250. coordinates: [polygonCoords]
  251. }
  252. }
  253. });
  254. map.addLayer({
  255. id: 'territory-fill',
  256. type: 'fill',
  257. source: 'territory',
  258. paint: {
  259. 'fill-color': '#ff0000',
  260. 'fill-opacity': 0.35
  261. }
  262. });
  263. map.addLayer({
  264. id: 'territory-line',
  265. type: 'line',
  266. source: 'territory',
  267. paint: {
  268. 'line-color': '#ff0000',
  269. 'line-width': 2
  270. }
  271. });
  272. map.fitBounds(bounds, { padding: 20 });
  273. });
  274. map.on('idle', function() {
  275. mapIsReady = true;
  276. });
  277. return;
  278. }
  279. // Convert coordinates to Google Maps format
  280. var polygonCoords = coords.map(function(coord) {
  281. var ll = getLatLng(coord);
  282. return { lat: ll.lat, lng: ll.lng };
  283. }).filter(function(p) {
  284. return isFinite(p.lat) && isFinite(p.lng);
  285. });
  286. if (polygonCoords.length === 0) {
  287. setMapMessage('No valid coordinates available for this territory.');
  288. return;
  289. }
  290. // Calculate bounds to center the map
  291. var bounds = new google.maps.LatLngBounds();
  292. polygonCoords.forEach(function(coord) {
  293. bounds.extend(coord);
  294. });
  295. map = new google.maps.Map(document.getElementById('map'), {
  296. zoom: 14,
  297. center: bounds.getCenter(),
  298. mapTypeId: 'roadmap'
  299. });
  300. // Draw the polygon
  301. polygon = new google.maps.Polygon({
  302. paths: polygonCoords,
  303. strokeColor: '#FF0000',
  304. strokeOpacity: 0.8,
  305. strokeWeight: 2,
  306. fillColor: '#FF0000',
  307. fillOpacity: 0.35
  308. });
  309. polygon.setMap(map);
  310. map.fitBounds(bounds);
  311. } catch (e) {
  312. setMapMessage('Map error: ' + e.message);
  313. }
  314. }
  315. var printWindow = null;
  316. function printTerritory() {
  317. var coords = territoryCoordinates;
  318. if (!coords || !Array.isArray(coords) || coords.length === 0) {
  319. alert('No coordinates available to print.');
  320. return;
  321. }
  322. if (mapProvider === 'maptiler') {
  323. function openPrintWindowShell() {
  324. // Build street lists HTML
  325. var borderStreetsHtml = '';
  326. if (territoryBorderStreets && territoryBorderStreets.trim() !== '') {
  327. borderStreetsHtml = '<div class="streets-section"><strong>Border Streets:</strong> ' + territoryBorderStreets + '</div>';
  328. }
  329. var insideStreetsHtml = '';
  330. if (territoryStreets && territoryStreets.length > 0) {
  331. insideStreetsHtml = '<div class="streets-section"><strong>Streets Inside:</strong> ' + territoryStreets.join(', ') + '</div>';
  332. }
  333. var printContent = '<!DOCTYPE html>' +
  334. '<html><head><title>Territory: ' + territoryName + '</title>' +
  335. '<style>' +
  336. '@page { size: letter portrait; margin: 0.25in; }' +
  337. 'body { margin: 0; padding: 0; font-family: Arial, sans-serif; }' +
  338. '.header { text-align: center; border-bottom: 2px solid #000; padding-bottom: 5px; margin-bottom: 10px; }' +
  339. '.header h1 { margin: 0; font-size: 20pt; }' +
  340. '.streets-section { font-size: 10pt; margin: 5px 0; text-align: left; }' +
  341. '.map-container { text-align: center; min-height: 200px; }' +
  342. '.map-container img { max-width: 100%; height: auto; max-height: 7in; }' +
  343. '.map-link { display: inline-block; margin-top: 10px; font-size: 12px; word-break: break-all; }' +
  344. '.print-btn { margin-top: 15px; padding: 10px 30px; font-size: 16px; cursor: pointer; }' +
  345. '.loading { color: #666; font-size: 14px; }' +
  346. '@media print { .no-print { display: none !important; } }' +
  347. '</style></head><body>' +
  348. '<div class="header"><h1>' + territoryName + '</h1></div>' +
  349. borderStreetsHtml +
  350. insideStreetsHtml +
  351. '<div class="map-container">' +
  352. '<div class="loading">Preparing map for print...</div>' +
  353. '<img id="print-map-img" src="" alt="Territory Map" style="display:none;">' +
  354. '<div class="map-link no-print"><a id="print-map-link" href="#" target="_blank" rel="noopener">Open image in new tab</a></div>' +
  355. '</div>' +
  356. '<div class="no-print" style="text-align: center; margin-top: 15px;">' +
  357. '<button class="print-btn" onclick="window.print();">Print</button> ' +
  358. '<button class="print-btn" onclick="window.close();">Close</button>' +
  359. '</div>' +
  360. '</body></html>';
  361. printWindow = window.open('', '_blank', 'width=900,height=1000');
  362. printWindow.document.write(printContent);
  363. printWindow.document.close();
  364. }
  365. function setPrintWindowImage(src) {
  366. if (!printWindow || printWindow.closed) {
  367. alert('Print window was blocked. Please allow popups and try again.');
  368. return;
  369. }
  370. var attempts = 0;
  371. function trySet() {
  372. attempts = attempts + 1;
  373. var doc = printWindow.document;
  374. if (!doc) {
  375. if (attempts < 20) return setTimeout(trySet, 100);
  376. alert('Print window did not finish loading.');
  377. return;
  378. }
  379. var img = doc.getElementById('print-map-img');
  380. var loading = doc.querySelector('.loading');
  381. var link = doc.getElementById('print-map-link');
  382. if (!img) {
  383. if (attempts < 20) return setTimeout(trySet, 100);
  384. alert('Print image element not found.');
  385. return;
  386. }
  387. if (loading) loading.style.display = 'none';
  388. img.style.display = 'block';
  389. img.src = src;
  390. if (link) link.href = src;
  391. }
  392. trySet();
  393. }
  394. openPrintWindowShell();
  395. function loadImage(url) {
  396. return new Promise(function(resolve, reject) {
  397. var img = new Image();
  398. img.crossOrigin = 'anonymous';
  399. img.onload = function() { resolve(img); };
  400. img.onerror = function() { reject(url); };
  401. img.src = url;
  402. });
  403. }
  404. function renderMapTilerImage() {
  405. return new Promise(function(resolve, reject) {
  406. if (!mapTilerKey || mapTilerKey === 'nothing') {
  407. reject('MapTiler API key is missing.');
  408. return;
  409. }
  410. var tileCoordSize = 512;
  411. if (map && typeof map.getStyle === 'function') {
  412. var styleObj = map.getStyle();
  413. if (styleObj && styleObj.sources) {
  414. for (var srcKey in styleObj.sources) {
  415. if (styleObj.sources.hasOwnProperty(srcKey)) {
  416. var src = styleObj.sources[srcKey];
  417. if (src && src.tileSize) {
  418. tileCoordSize = src.tileSize;
  419. break;
  420. }
  421. }
  422. }
  423. }
  424. }
  425. function buildTileUrl(z, x, y) {
  426. return 'https://api.maptiler.com/maps/' +
  427. encodeURIComponent(mapTilerStyle) + '/' +
  428. z + '/' + x + '/' + y + '.png?key=' +
  429. encodeURIComponent(mapTilerKey);
  430. }
  431. function buildImage(tileImageSize) {
  432. function lngToWorldX(lng, zoom) {
  433. var scale = tileCoordSize * Math.pow(2, zoom);
  434. return (lng + 180) / 360 * scale;
  435. }
  436. function latToWorldY(lat, zoom) {
  437. var rad = lat * Math.PI / 180;
  438. var scale = tileCoordSize * Math.pow(2, zoom);
  439. return (1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2 * scale;
  440. }
  441. var minLat = Infinity, maxLat = -Infinity;
  442. var minLng = Infinity, maxLng = -Infinity;
  443. coords.forEach(function(coord) {
  444. var ll = getLatLng(coord);
  445. var lat = ll.lat;
  446. var lng = ll.lng;
  447. if (!isFinite(lat) || !isFinite(lng)) return;
  448. if (lat < minLat) minLat = lat;
  449. if (lat > maxLat) maxLat = lat;
  450. if (lng < minLng) minLng = lng;
  451. if (lng > maxLng) maxLng = lng;
  452. });
  453. if (!isFinite(minLat) || !isFinite(minLng) || !isFinite(maxLat) || !isFinite(maxLng)) {
  454. reject('No valid coordinates available to print.');
  455. return;
  456. }
  457. var centerLat = (minLat + maxLat) / 2;
  458. var centerLng = (minLng + maxLng) / 2;
  459. var isWide = (maxLng - minLng) > (maxLat - minLat) * 1.2;
  460. var baseWidth = isWide ? 800 : 640;
  461. var baseHeight = isWide ? 640 : 800;
  462. var outputScale = tileImageSize > 0 ? (tileImageSize / tileCoordSize) : 1;
  463. var width = baseWidth * outputScale;
  464. var height = baseHeight * outputScale;
  465. var x1 = lngToWorldX(minLng, 0);
  466. var x2 = lngToWorldX(maxLng, 0);
  467. var y1 = latToWorldY(maxLat, 0);
  468. var y2 = latToWorldY(minLat, 0);
  469. var spanX0 = Math.abs(x2 - x1);
  470. var spanY0 = Math.abs(y2 - y1);
  471. var paddingFactor = 1.02;
  472. var zoomX = spanX0 > 0 ? Math.log2(baseWidth / (spanX0 * paddingFactor)) : 20;
  473. var zoomY = spanY0 > 0 ? Math.log2(baseHeight / (spanY0 * paddingFactor)) : 20;
  474. var zoom = Math.min(zoomX, zoomY);
  475. if (!isFinite(zoom)) zoom = 1;
  476. if (zoom < 1) zoom = 1;
  477. if (zoom > 20) zoom = 20;
  478. var zInt = Math.floor(zoom);
  479. var zScale = Math.pow(2, zoom - zInt);
  480. var worldCenterXint = lngToWorldX(centerLng, zInt);
  481. var worldCenterYint = latToWorldY(centerLat, zInt);
  482. var worldWidth = width / (outputScale * zScale);
  483. var worldHeight = height / (outputScale * zScale);
  484. var topLeftXint = worldCenterXint - worldWidth / 2;
  485. var topLeftYint = worldCenterYint - worldHeight / 2;
  486. var topLeftX = topLeftXint * outputScale * zScale;
  487. var topLeftY = topLeftYint * outputScale * zScale;
  488. var n = Math.pow(2, zInt);
  489. var tileXStart = Math.floor(topLeftXint / tileCoordSize);
  490. var tileYStart = Math.floor(topLeftYint / tileCoordSize);
  491. var tileXEnd = Math.floor((topLeftXint + worldWidth) / tileCoordSize);
  492. var tileYEnd = Math.floor((topLeftYint + worldHeight) / tileCoordSize);
  493. var promises = [];
  494. var tiles = [];
  495. var tileX, tileY;
  496. for (tileY = tileYStart; tileY <= tileYEnd; tileY++) {
  497. if (tileY < 0 || tileY >= n) continue;
  498. for (tileX = tileXStart; tileX <= tileXEnd; tileX++) {
  499. var wrappedX = ((tileX % n) + n) % n;
  500. var url = buildTileUrl(zInt, wrappedX, tileY);
  501. (function(x, y, u) {
  502. var p = loadImage(u).then(function(img) {
  503. tiles.push({ x: x, y: y, img: img });
  504. });
  505. promises.push(p);
  506. })(tileX, tileY, url);
  507. }
  508. }
  509. Promise.all(promises).then(function() {
  510. var canvas = document.createElement('canvas');
  511. canvas.width = width;
  512. canvas.height = height;
  513. var ctx = canvas.getContext('2d');
  514. var tileDrawSize = tileCoordSize * zScale * outputScale;
  515. tiles.forEach(function(t) {
  516. var dx = (t.x * tileCoordSize * zScale * outputScale) - topLeftX;
  517. var dy = (t.y * tileCoordSize * zScale * outputScale) - topLeftY;
  518. ctx.drawImage(t.img, dx, dy, tileDrawSize, tileDrawSize);
  519. });
  520. if (coords.length > 1) {
  521. ctx.save();
  522. ctx.strokeStyle = 'rgba(255,0,0,0.8)';
  523. ctx.fillStyle = 'rgba(255,0,0,0.25)';
  524. ctx.lineWidth = 3;
  525. ctx.beginPath();
  526. coords.forEach(function(coord, idx) {
  527. var ll = getLatLng(coord);
  528. var lat = ll.lat;
  529. var lng = ll.lng;
  530. if (!isFinite(lat) || !isFinite(lng)) return;
  531. var px = (lngToWorldX(lng, zInt) * zScale * outputScale) - topLeftX;
  532. var py = (latToWorldY(lat, zInt) * zScale * outputScale) - topLeftY;
  533. if (idx === 0) {
  534. ctx.moveTo(px, py);
  535. } else {
  536. ctx.lineTo(px, py);
  537. }
  538. });
  539. ctx.closePath();
  540. ctx.fill();
  541. ctx.stroke();
  542. ctx.restore();
  543. }
  544. var dataUrl = "";
  545. try {
  546. dataUrl = canvas.toDataURL('image/png');
  547. } catch (e) {
  548. reject('Unable to generate print image.');
  549. return;
  550. }
  551. if (!dataUrl || dataUrl.length < 1000) {
  552. reject('Generated print image is empty.');
  553. return;
  554. }
  555. resolve(dataUrl);
  556. }).catch(function() {
  557. reject('Failed to load map tiles for printing.');
  558. });
  559. }
  560. var testUrl = buildTileUrl(0, 0, 0);
  561. loadImage(testUrl).then(function(img) {
  562. var tileImageSize = (img && img.width) ? img.width : 256;
  563. buildImage(tileImageSize);
  564. }).catch(function() {
  565. buildImage(256);
  566. });
  567. });
  568. }
  569. renderMapTilerImage().then(function(dataUrl) {
  570. setPrintWindowImage(dataUrl);
  571. }).catch(function(errMsg) {
  572. alert(errMsg);
  573. });
  574. return;
  575. }
  576. // Build polygon path for Static Maps API
  577. var pathPoints = coords.map(function(coord) {
  578. var ll = getLatLng(coord);
  579. if (!isFinite(ll.lat) || !isFinite(ll.lng)) return null;
  580. return ll.lat + ',' + ll.lng;
  581. }).filter(function(p) { return p !== null; });
  582. // Close the polygon
  583. pathPoints.push(pathPoints[0]);
  584. var pathStr = pathPoints.join('|');
  585. // Calculate bounding box to determine optimal zoom
  586. var minLat = Infinity, maxLat = -Infinity;
  587. var minLng = Infinity, maxLng = -Infinity;
  588. coords.forEach(function(coord) {
  589. var ll = getLatLng(coord);
  590. var lat = ll.lat;
  591. var lng = ll.lng;
  592. if (!isFinite(lat) || !isFinite(lng)) return;
  593. if (lat < minLat) minLat = lat;
  594. if (lat > maxLat) maxLat = lat;
  595. if (lng < minLng) minLng = lng;
  596. if (lng > maxLng) maxLng = lng;
  597. });
  598. var centerLat = (minLat + maxLat) / 2;
  599. var centerLng = (minLng + maxLng) / 2;
  600. // Calculate the span of the polygon
  601. var latSpan = maxLat - minLat;
  602. var lngSpan = maxLng - minLng;
  603. // Calculate optimal zoom level to maximize polygon size
  604. // Map dimensions: 640x800 (width x height for portrait letter)
  605. // Each zoom level doubles the scale
  606. var zoom = 20; // Start with max zoom
  607. // Degrees per pixel at zoom level 0 is approximately 360/256 for lng
  608. // For latitude it varies, but we use equirectangular approximation
  609. var mapWidth = 640;
  610. var mapHeight = 800;
  611. // Add 10% padding around the polygon
  612. var paddedLatSpan = latSpan * 1.1;
  613. var paddedLngSpan = lngSpan * 1.1;
  614. // Calculate zoom based on longitude span
  615. if (paddedLngSpan > 0) {
  616. var lngZoom = Math.floor(Math.log2(360 / paddedLngSpan * mapWidth / 256));
  617. if (lngZoom < zoom) zoom = lngZoom;
  618. }
  619. // Calculate zoom based on latitude span
  620. if (paddedLatSpan > 0) {
  621. var latZoom = Math.floor(Math.log2(180 / paddedLatSpan * mapHeight / 256));
  622. if (latZoom < zoom) zoom = latZoom;
  623. }
  624. // Clamp zoom between 1 and 20
  625. if (zoom < 1) zoom = 1;
  626. if (zoom > 20) zoom = 20;
  627. var staticMapUrl;
  628. if (mapProvider === 'maptiler') {
  629. if (!mapTilerKey || mapTilerKey === 'nothing') {
  630. alert('MapTiler API key is missing.');
  631. return;
  632. }
  633. var mapWidth = 640;
  634. var mapHeight = 800;
  635. var pathParam = 'stroke:#ff0000|width:3|fill:none|' + pathStr;
  636. staticMapUrl = 'https://api.maptiler.com/maps/' +
  637. encodeURIComponent(mapTilerStyle) +
  638. '/static/auto/' +
  639. mapWidth + 'x' + mapHeight + '@2x.png' +
  640. '?path=' + encodeURIComponent(pathParam) +
  641. '&key=' + encodeURIComponent(mapTilerKey);
  642. } else {
  643. // Build Google Static Maps URL with scale=2 for higher resolution (1280x1600 actual pixels)
  644. staticMapUrl = 'https://maps.googleapis.com/maps/api/staticmap?' +
  645. 'size=640x800' +
  646. '&scale=2' +
  647. '&center=' + centerLat + ',' + centerLng +
  648. '&zoom=' + zoom +
  649. '&maptype=roadmap' +
  650. '&path=color:0xFF0000CC|weight:3|fillcolor:0xFF000044|' + pathStr +
  651. '&key=' + encodeURIComponent(googleMapsKey);
  652. }
  653. // Open print window with static map - landscape for wider polygons, portrait for taller
  654. var isWide = lngSpan > latSpan * 1.2;
  655. var pageSize = isWide ? 'letter landscape' : 'letter portrait';
  656. var imgMaxHeight = isWide ? '5in' : '7in';
  657. // Build street lists HTML
  658. var borderStreetsHtml = '';
  659. if (territoryBorderStreets && territoryBorderStreets.trim() !== '') {
  660. borderStreetsHtml = '<div class="streets-section"><strong>Border Streets:</strong> ' + territoryBorderStreets + '</div>';
  661. }
  662. var insideStreetsHtml = '';
  663. if (territoryStreets && territoryStreets.length > 0) {
  664. insideStreetsHtml = '<div class="streets-section"><strong>Streets Inside:</strong> ' + territoryStreets.join(', ') + '</div>';
  665. }
  666. var printContent = '<!DOCTYPE html>' +
  667. '<html><head><title>Territory: ' + territoryName + '</title>' +
  668. '<style>' +
  669. '@page { size: ' + pageSize + '; margin: 0.25in; }' +
  670. 'body { margin: 0; padding: 0; font-family: Arial, sans-serif; }' +
  671. '.header { text-align: center; border-bottom: 2px solid #000; padding-bottom: 5px; margin-bottom: 10px; }' +
  672. '.header h1 { margin: 0; font-size: 20pt; }' +
  673. '.streets-section { font-size: 10pt; margin: 5px 0; text-align: left; }' +
  674. '.map-container { text-align: center; }' +
  675. '.map-container img { max-width: 100%; height: auto; max-height: ' + imgMaxHeight + '; }' +
  676. '.print-btn { margin-top: 15px; padding: 10px 30px; font-size: 16px; cursor: pointer; }' +
  677. '@media print { .no-print { display: none !important; } }' +
  678. '</style></head><body>' +
  679. '<div class="header"><h1>' + territoryName + '</h1></div>' +
  680. borderStreetsHtml +
  681. insideStreetsHtml +
  682. '<div class="map-container">' +
  683. '<img src="' + staticMapUrl + '" alt="Territory Map" onerror="document.body.innerHTML=\'<p>Error loading map. Please try again.</p>\';">' +
  684. '</div>' +
  685. '<div class="no-print" style="text-align: center; margin-top: 15px;">' +
  686. '<button class="print-btn" onclick="window.print();">Print</button> ' +
  687. '<button class="print-btn" onclick="window.close();">Close</button>' +
  688. '</div>' +
  689. '</body></html>';
  690. printWindow = window.open('', '_blank', 'width=900,height=1000');
  691. printWindow.document.write(printContent);
  692. printWindow.document.close();
  693. }
  694. if (mapProvider === 'maptiler') {
  695. if (document.readyState === 'loading') {
  696. document.addEventListener('DOMContentLoaded', initMap);
  697. } else {
  698. initMap();
  699. }
  700. setTimeout(function() {
  701. if (!map) {
  702. setMapMessage('Map failed to load.');
  703. }
  704. }, 2000);
  705. } else {
  706. setTimeout(function() {
  707. if (typeof google === 'undefined') {
  708. setMapMessage('Google Maps failed to load.');
  709. }
  710. }, 2000);
  711. }
  712. </script>

Powered by TurnKey Linux.