From c90d559afa79d6955b3c9d5773404fe6522b4b7a Mon Sep 17 00:00:00 2001 From: aziz Date: Tue, 16 May 2023 15:53:43 +0200 Subject: [PATCH 01/24] feat(geo): Added rotate option and north direction to the map when printing --- .../map-browser/map-browser.component.scss | 11 + .../geo/src/lib/map/shared/map.interface.ts | 13 + packages/geo/src/lib/map/shared/map.ts | 17 +- .../geo/src/lib/print/shared/print.service.ts | 252 +++++++++++++----- 4 files changed, 230 insertions(+), 63 deletions(-) diff --git a/packages/geo/src/lib/map/map-browser/map-browser.component.scss b/packages/geo/src/lib/map/map-browser/map-browser.component.scss index 2b690e8b17..0bfb95fe3b 100644 --- a/packages/geo/src/lib/map/map-browser/map-browser.component.scss +++ b/packages/geo/src/lib/map/map-browser/map-browser.component.scss @@ -103,6 +103,17 @@ } } +:host ::ng-deep .ol-rotate { + right: $igo-margin; + background-color: rgba(255, 255, 255, 0); + display: none; + button { + width: 40px; + height: 40px; + background-color: rgba(255, 255, 255, 0); + } +} + :host ::ng-deep .ol-scale-line-inner { color: rgba(black, 1); border-color: black; diff --git a/packages/geo/src/lib/map/shared/map.interface.ts b/packages/geo/src/lib/map/shared/map.interface.ts index 83208faf6c..d6ce7e8c50 100644 --- a/packages/geo/src/lib/map/shared/map.interface.ts +++ b/packages/geo/src/lib/map/shared/map.interface.ts @@ -40,6 +40,7 @@ export interface MapOptions { export interface MapControlsOptions { attribution?: boolean | MapAttributionOptions; scaleLine?: boolean | MapScaleLineOptions; + rotate?: boolean | MapRotateOptions; } export interface MapScaleLineOptions { @@ -57,6 +58,18 @@ export interface MapAttributionOptions { collapsed: boolean; } +export interface MapRotateOptions { + className?: string; + label?: string | HTMLElement; + tipLabel?: string; + compassClassName?: string; + duration?: number; + autoHide?: boolean; + render?: any; + resetNorth?: (() => void) | undefined; + target?: string | HTMLElement; +} + export interface Buffer { bufferRadius?: number; bufferStroke?: [number, number, number, number]; diff --git a/packages/geo/src/lib/map/shared/map.ts b/packages/geo/src/lib/map/shared/map.ts index 9defbb81e6..a0d2cd499c 100644 --- a/packages/geo/src/lib/map/shared/map.ts +++ b/packages/geo/src/lib/map/shared/map.ts @@ -2,6 +2,7 @@ import olMap from 'ol/Map'; import olView from 'ol/View'; import olControlAttribution from 'ol/control/Attribution'; import olControlScaleLine from 'ol/control/ScaleLine'; +import olControlRotate from 'ol/control/Rotate'; import * as olproj from 'ol/proj'; import * as olproj4 from 'ol/proj/proj4'; import OlProjection from 'ol/proj/Projection'; @@ -25,7 +26,8 @@ import { MapAttributionOptions, MapScaleLineOptions, MapExtent, - MapControlsOptions + MapControlsOptions, + MapRotateOptions } from './map.interface'; import { MapViewController } from './controllers/view'; import { FeatureDataSource } from '../../datasource/shared/datasources/feature-datasource'; @@ -101,6 +103,12 @@ export class IgoMap { : this.options.controls.scaleLine) as MapScaleLineOptions; controls.push(new olControlScaleLine(scaleLineOpt)); } + if (this.options.controls.rotate) { + const rotateOpt = (this.options.controls.rotate === true + ? {} + : this.options.controls.rotate) as MapRotateOptions; + controls.push(new olControlRotate(rotateOpt)); + } } let interactions = {}; if (this.options.interactions === false) { @@ -226,6 +234,13 @@ export class IgoMap { controls.push(new olControlScaleLine(scaleLineOpt)); } + if (value.rotate) { + const rotateOpt = (value.rotate === true + ? {} + : value.rotate) as MapRotateOptions;// todo + controls.push(new olControlRotate(rotateOpt)); + } + const currentControls = Object.assign([], this.ol.getControls().getArray()); currentControls.forEach(control => { this.ol.removeControl(control); diff --git a/packages/geo/src/lib/print/shared/print.service.ts b/packages/geo/src/lib/print/shared/print.service.ts index da98f0afa5..828c73f62e 100644 --- a/packages/geo/src/lib/print/shared/print.service.ts +++ b/packages/geo/src/lib/print/shared/print.service.ts @@ -614,25 +614,50 @@ export class PrintService { if (olCollapsed) { element.classList.add('ol-collapsed'); } - } + } /** * Add Copyrigh to the map canvas * @param map - Map of the app * @param canvas Canvas of the map + * @param position String - position of the legend */ private async addCopyrightToImage( map: IgoMap, - canvas : HTMLCanvasElement + canvas : HTMLCanvasElement, + position: string ) { const context = canvas.getContext('2d'); let canvasOverlayHTML; const mapOverlayHTML = map.ol.getOverlayContainerStopEvent(); + + // this code to check the view mobile or tablet + const firstDisplay = (mapOverlayHTML as any).computedStyleMap().get('display').value; + // by defaulte display in mobile is none + // we need to show the div befor printing + // the div containes ol-attrebuts and ol-rotate arraow + if(firstDisplay === 'none') { + mapOverlayHTML.classList.remove('ol-overlaycontainer-stopevent'); + } + + const olRotate = mapOverlayHTML.getElementsByClassName('ol-rotate') as HTMLCollectionOf; + if(olRotate[0]) { + // by default the rotate arrow is none + // show rotate arrow befor printing + olRotate[0].style.display = 'block'; + // in case legend position is topright + // we change rotate btn to topleft + if(position === 'topright') { + olRotate[0].style.width = 'inherit'; + } + } // Remove the UI buttons from the nodes const OverlayHTMLButtons = mapOverlayHTML.getElementsByTagName('button'); const OverlayHTMLButtonsarr = Array.from(OverlayHTMLButtons); for (const OverlayHTMLButton of OverlayHTMLButtonsarr) { - OverlayHTMLButton.setAttribute('data-html2canvas-ignore', 'true'); + if(!OverlayHTMLButton.classList.contains('ol-rotate-reset')) { + OverlayHTMLButton.setAttribute('data-html2canvas-ignore', 'true'); + } } // Find attributions by class and delete // the collapsed class to open attribution Copyright @@ -650,10 +675,26 @@ export class PrintService { }).then( e => { canvasOverlayHTML = e; }); + + // reset canvas transform to initial + context.setTransform(1, 0, 0, 1, 0, 0); context.drawImage(canvasOverlayHTML, 0, 0); if (olCollapsed) { element.classList.add('ol-collapsed'); } + + if(firstDisplay === 'none') { + mapOverlayHTML.classList.add('ol-overlaycontainer-stopevent'); + } + // after adding rotate btn return to original style + if(olRotate[0]) { + // after changeing rotate btn to topleft + // we back to the original posiotn + if(position === 'topright') { + olRotate[0].style.width = 'initial'; + } + olRotate[0].style.display = 'none'; + } } defineNbFileToProcess(nbFileToProcess) { @@ -701,42 +742,81 @@ export class PrintService { size: Array, margins: Array ) { - const status$ = new Subject(); const mapSize = map.ol.getSize(); + const viewResolution = map.ol.getView().getResolution(); + const extent = map.ol.getView().calculateExtent(mapSize); const widthPixels = Math.round((size[0] * resolution) / 25.4); const heightPixels = Math.round((size[1] * resolution) / 25.4); + const status$ = new Subject(); let timeout; - - map.ol.once('rendercomplete', (event: any) => { - const canvases = event.target.getViewport().getElementsByTagName('canvas'); + map.ol.once('rendercomplete', async (event: any) => { + const mapResultCanvas = document.createElement('canvas'); + const mapContextResult = mapResultCanvas.getContext('2d'); + const mapCanvas = event.target.getViewport().getElementsByTagName('canvas')[0]; + + + mapResultCanvas.width = widthPixels; + mapResultCanvas.height = heightPixels; + const opacity = mapCanvas.parentNode.style.opacity; + mapContextResult.globalAlpha = opacity === '' ? 1 : Number(opacity); + let matrix; + const transform = mapCanvas.style.transform; + if (transform) { + // Get the transform parameters from the style's transform matrix + matrix = transform + .match(/^matrix\(([^\(]*)\)$/)[1] + .split(',') + .map(Number); + } else { + matrix = [ + parseFloat(mapCanvas.style.width) / mapCanvas.width, + 0, + 0, + parseFloat(mapCanvas.style.height) / mapCanvas.height, + 0, + 0, + ]; + } + // Apply the transform to the export map context + CanvasRenderingContext2D.prototype.setTransform.apply( + mapContextResult, + matrix + ); + const backgroundColor = mapCanvas.parentNode.style.backgroundColor; + if (backgroundColor) { + mapContextResult.fillStyle = backgroundColor; + mapContextResult.fillRect(0, 0, mapCanvas.width, mapCanvas.height); + } + mapContextResult.drawImage(mapCanvas, 0, 0); + mapContextResult.globalAlpha = 1; + const mapStatus$$ = map.status$.subscribe((mapStatus: SubjectStatus) => { clearTimeout(timeout); - if (mapStatus !== SubjectStatus.Done) { return; } - mapStatus$$.unsubscribe(); - let status = SubjectStatus.Done; try { - for (const canvas of canvases) { - if (canvas.width !== 0) { - this.addCanvas(doc, canvas, margins); - } + if (mapCanvas.width !== 0) { + this.addCanvas(doc, mapResultCanvas, margins); } } catch (err) { status = SubjectStatus.Error; this.messageService.error('igo.geo.printForm.corsErrorMessageBody','igo.geo.printForm.corsErrorMessageHeader'); } + // Reset original map size + map.ol.setSize(size); + map.ol.getView().setResolution(viewResolution); this.renderMap(map, mapSize, extent); + status$.next(status); }); - + // If no loading as started after 200ms, then probably no loading // is required. timeout = window.setTimeout(() => { @@ -744,23 +824,28 @@ export class PrintService { let status = SubjectStatus.Done; try { - for (const canvas of canvases) { - if (canvas.width !== 0) { - this.addCanvas(doc, canvas, margins); - } + if (mapCanvas.width !== 0) { + this.addCanvas(doc, mapResultCanvas, margins); } } catch (err) { status = SubjectStatus.Error; this.messageService.error('igo.geo.printForm.corsErrorMessageBody', 'igo.geo.printForm.corsErrorMessageHeader'); } + // Reset original map size + map.ol.setSize(size); + map.ol.getView().setResolution(viewResolution); this.renderMap(map, mapSize, extent); status$.next(status); - }, 200); + },200); }); - this.renderMap(map, [widthPixels, heightPixels], extent); - - return status$; + // Set print size + const printSize = [widthPixels, heightPixels]; + map.ol.setSize(printSize); + const scaling = Math.min(widthPixels / mapSize[0], heightPixels / mapSize[1]); + map.ol.getView().setResolution(viewResolution / scaling); + // this.renderMap(map, [widthPixels, heightPixels], extent); + return status$ } /** @@ -792,18 +877,86 @@ export class PrintService { // const resolution = map.ol.getView().getResolution(); this.activityId = this.activityService.register(); const translate = this.languageService.translate; + format = format.toLowerCase(); + // add rotation + const span: HTMLElement = document.createElement("span"); + span.innerHTML = ''; + map.updateControls({scaleLine: true, + attribution: { + collapsed: true + },rotate: {label: span}}); + map.ol.once('rendercomplete', async (event: any) => { - format = format.toLowerCase(); - const oldCanvas = event.target - .getViewport() - .getElementsByTagName('canvas')[0]; + // mapResultCanvas to save rotated map + const mapResultCanvas = document.createElement('canvas'); + const size = map.ol.getSize(); + mapResultCanvas.width = size[0]; + mapResultCanvas.height = size[1]; + const mapContextResult = mapResultCanvas.getContext('2d'); + const mapCanvas = event.target.getViewport().getElementsByTagName('canvas')[0]; + const opacity = + mapCanvas.parentNode.style.opacity || mapCanvas.style.opacity; + mapContextResult.globalAlpha = opacity === '' ? 1 : Number(opacity); + let matrix; + const transform = mapCanvas.style.transform; + if (transform) { + // Get the transform parameters from the style's transform matrix + matrix = transform + .match(/^matrix\(([^\(]*)\)$/)[1] + .split(',') + .map(Number); + } else { + matrix = [ + parseFloat(mapCanvas.style.width) / mapCanvas.width, + 0, + 0, + parseFloat(mapCanvas.style.height) / mapCanvas.height, + 0, + 0, + ]; + } + // Apply the transform to the export map context + CanvasRenderingContext2D.prototype.setTransform.apply( + mapContextResult, + matrix + ); + const backgroundColor = mapCanvas.parentNode.style.backgroundColor; + if (backgroundColor) { + mapContextResult.fillStyle = backgroundColor; + mapContextResult.fillRect(0, 0, mapCanvas.width, mapCanvas.height); + } + + mapContextResult.drawImage(mapCanvas, 0, 0); + + await this.addCopyrightToImage(map, mapResultCanvas, legendPosition); + + // Check the legendPosition + if (legendPosition !== 'none') { + if (['topleft', 'topright', 'bottomleft', 'bottomright'].indexOf(legendPosition) > -1) { + await this.addLegendToImage( + mapResultCanvas, + map, + resolution, + legendPosition, + format + ); + } else if (legendPosition === 'newpage') { + await this.getLayersLegendImage( + map, + format, + doZipFile, + resolution + ); + } + } + // add other information to final canvas before exporting const newCanvas = document.createElement('canvas'); const newContext = newCanvas.getContext('2d'); // Postion in height to set the canvas in new canvas let positionHCanvas = 0; // Get height/width of map canvas - const width = oldCanvas.width; - let height = oldCanvas.height; + const width = mapResultCanvas.width; + let height = mapResultCanvas.height; // Set Font to calculate comment width newContext.font = '20px Calibri'; const commentWidth = newContext.measureText(comment).width; @@ -822,20 +975,18 @@ export class PrintService { // Set the new canvas with the new calculated size newCanvas.width = width; newCanvas.height = height; + if (['bmp','gif', 'jpeg', 'png', 'tiff'].indexOf(format) > -1) { // Patch Jpeg default black background to white - if (format === 'jpeg') { + if ( + format === 'jpeg' || title !== '' || subtitle !== '' || + comment !== '' || projection !== false || scale !== false + ) { newContext.fillStyle = '#ffffff'; newContext.fillRect(0, 0, width, height); newContext.fillStyle = '#000000'; - } else if (title !== '' || subtitle !== '' || comment !== '' || - projection !== false || scale !== false) { - newContext.fillStyle = '#ffffff'; - newContext.fillRect(0, 0, width, height); - newContext.fillStyle = '#000000'; } } - // If a title need to be added to canvas if (title !== '') { // Set font for title @@ -845,6 +996,7 @@ export class PrintService { newContext.textAlign = 'center'; newContext.fillText(title, width / 2, 20, width * 0.9); } + if (subtitle !== '') { // Set font for subtitle // Adjust according to title length @@ -853,6 +1005,7 @@ export class PrintService { newContext.textAlign = 'center'; newContext.fillText(subtitle, width / 2, 50, width * 0.9); } + // Set font for next section newContext.font = '20px Calibri'; // If projection or/end scale need to be added to canvas @@ -916,30 +1069,7 @@ export class PrintService { } } - // Add map to new canvas - newContext.drawImage(oldCanvas, 0, positionHCanvas); - // Add copyrigh - await this.addCopyrightToImage(map, newCanvas); - - // Check the legendPosition - if (legendPosition !== 'none') { - if (['topleft', 'topright', 'bottomleft', 'bottomright'].indexOf(legendPosition) > -1) { - await this.addLegendToImage( - newCanvas, - map, - resolution, - legendPosition, - format - ); - } else if (legendPosition === 'newpage') { - await this.getLayersLegendImage( - map, - format, - doZipFile, - resolution - ); - } - } + newContext.drawImage(mapResultCanvas, 0, positionHCanvas); let status = SubjectStatus.Done; let fileNameWithExt = 'map.' + format; @@ -961,7 +1091,6 @@ export class PrintService { } catch (err) { status = SubjectStatus.Error; } - status$.next(status); if (format.toLowerCase() === 'tiff') { @@ -981,7 +1110,6 @@ export class PrintService { } }); map.ol.renderSync(); - return status$; } From 1346b9a35dc5072cb37069859b45544f29de74aa Mon Sep 17 00:00:00 2001 From: aziz Date: Tue, 16 May 2023 21:51:06 +0200 Subject: [PATCH 02/24] solve lint and optimize map updateControls function --- packages/geo/src/lib/map/shared/map.ts | 15 +++++++++------ .../geo/src/lib/print/shared/print.service.ts | 19 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/geo/src/lib/map/shared/map.ts b/packages/geo/src/lib/map/shared/map.ts index a0d2cd499c..c3956d9e73 100644 --- a/packages/geo/src/lib/map/shared/map.ts +++ b/packages/geo/src/lib/map/shared/map.ts @@ -220,31 +220,34 @@ export class IgoMap { return; } + const currentControls = Object.assign([], this.ol.getControls().getArray()); const controls = []; + if (value.attribution) { + const attribution = currentControls.find(element => element.constructor.name === 'Attribution'); + if (attribution) { this.ol.removeControl(attribution); } const attributionOpt = (value.attribution === true ? {} : value.attribution) as MapAttributionOptions; controls.push(new olControlAttribution(attributionOpt)); } if (value.scaleLine) { + const ScaleLine = currentControls.find(element => element.constructor.name === 'ScaleLine'); + if (ScaleLine) { this.ol.removeControl(ScaleLine); } const scaleLineOpt = (value.scaleLine === true ? {} : value.scaleLine) as MapScaleLineOptions; controls.push(new olControlScaleLine(scaleLineOpt)); } - if (value.rotate) { + const Rotate = currentControls.find(element => element.constructor.name === 'Rotate'); + if (Rotate) { this.ol.removeControl(Rotate); } const rotateOpt = (value.rotate === true ? {} - : value.rotate) as MapRotateOptions;// todo + : value.rotate) as MapRotateOptions; controls.push(new olControlRotate(rotateOpt)); } - const currentControls = Object.assign([], this.ol.getControls().getArray()); - currentControls.forEach(control => { - this.ol.removeControl(control); - }); controls.forEach(control => { this.ol.addControl(control); }); diff --git a/packages/geo/src/lib/print/shared/print.service.ts b/packages/geo/src/lib/print/shared/print.service.ts index 828c73f62e..9726f83119 100644 --- a/packages/geo/src/lib/print/shared/print.service.ts +++ b/packages/geo/src/lib/print/shared/print.service.ts @@ -757,7 +757,7 @@ export class PrintService { const mapResultCanvas = document.createElement('canvas'); const mapContextResult = mapResultCanvas.getContext('2d'); const mapCanvas = event.target.getViewport().getElementsByTagName('canvas')[0]; - + mapResultCanvas.width = widthPixels; mapResultCanvas.height = heightPixels; @@ -793,7 +793,7 @@ export class PrintService { } mapContextResult.drawImage(mapCanvas, 0, 0); mapContextResult.globalAlpha = 1; - + const mapStatus$$ = map.status$.subscribe((mapStatus: SubjectStatus) => { clearTimeout(timeout); if (mapStatus !== SubjectStatus.Done) { @@ -816,7 +816,7 @@ export class PrintService { status$.next(status); }); - + // If no loading as started after 200ms, then probably no loading // is required. timeout = window.setTimeout(() => { @@ -845,7 +845,7 @@ export class PrintService { const scaling = Math.min(widthPixels / mapSize[0], heightPixels / mapSize[1]); map.ol.getView().setResolution(viewResolution / scaling); // this.renderMap(map, [widthPixels, heightPixels], extent); - return status$ + return status$; } /** @@ -880,11 +880,12 @@ export class PrintService { format = format.toLowerCase(); // add rotation const span: HTMLElement = document.createElement("span"); - span.innerHTML = ''; - map.updateControls({scaleLine: true, - attribution: { - collapsed: true - },rotate: {label: span}}); + let htmlText = ''; + htmlText += ''; + htmlText += ''; + htmlText += ''; + span.innerHTML = htmlText; + map.updateControls({rotate: {label: span}}); map.ol.once('rendercomplete', async (event: any) => { // mapResultCanvas to save rotated map From 083eeaa257220846059a1e1e46f6c4392e0522fc Mon Sep 17 00:00:00 2001 From: aziz Date: Wed, 17 May 2023 20:47:20 +0200 Subject: [PATCH 03/24] integrate custom controle rotation-button component in the map --- .../rotation-button.component.html | 2 +- .../rotation-button.component.ts | 23 +++++++++- .../geo/src/lib/map/shared/map.interface.ts | 13 ------ packages/geo/src/lib/map/shared/map.ts | 18 +------- .../geo/src/lib/print/shared/print.service.ts | 46 ++++++++----------- 5 files changed, 43 insertions(+), 59 deletions(-) diff --git a/packages/geo/src/lib/map/rotation-button/rotation-button.component.html b/packages/geo/src/lib/map/rotation-button/rotation-button.component.html index d921ec2a92..3cee197090 100644 --- a/packages/geo/src/lib/map/rotation-button/rotation-button.component.html +++ b/packages/geo/src/lib/map/rotation-button/rotation-button.component.html @@ -1,7 +1,7 @@
-
-