Consolidated ASP Classic MVC framework from best components
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

814 рядки
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.