1# -*- coding: utf-8 -*-
2"""QGIS Unit tests for QgsLayoutPicture.
3
4.. note:: This program is free software; you can redistribute it and/or modify
5it under the terms of the GNU General Public License as published by
6the Free Software Foundation; either version 2 of the License, or
7(at your option) any later version.
8"""
9__author__ = '(C) 2017 by Nyall Dawson'
10__date__ = '23/10/2017'
11__copyright__ = 'Copyright 2017, The QGIS Project'
12
13import qgis  # NOQA
14
15import os
16import socketserver
17import threading
18import http.server
19from qgis.PyQt.QtCore import QRectF, QDir
20from qgis.PyQt.QtTest import QSignalSpy
21from qgis.PyQt.QtXml import QDomDocument
22from qgis.core import (QgsLayoutItemPicture,
23                       QgsLayout,
24                       QgsLayoutItemMap,
25                       QgsRectangle,
26                       QgsCoordinateReferenceSystem,
27                       QgsProject,
28                       QgsReadWriteContext
29                       )
30from qgis.testing import start_app, unittest
31from utilities import unitTestDataPath
32from qgslayoutchecker import QgsLayoutChecker
33from test_qgslayoutitem import LayoutItemTestCase
34
35start_app()
36TEST_DATA_DIR = unitTestDataPath()
37
38
39class TestQgsLayoutPicture(unittest.TestCase, LayoutItemTestCase):
40
41    @classmethod
42    def setUpClass(cls):
43        cls.item_class = QgsLayoutItemPicture
44
45        # Bring up a simple HTTP server, for remote picture tests
46        os.chdir(unitTestDataPath() + '')
47        handler = http.server.SimpleHTTPRequestHandler
48
49        cls.httpd = socketserver.TCPServer(('localhost', 0), handler)
50        cls.port = cls.httpd.server_address[1]
51
52        cls.httpd_thread = threading.Thread(target=cls.httpd.serve_forever)
53        cls.httpd_thread.setDaemon(True)
54        cls.httpd_thread.start()
55
56    def __init__(self, methodName):
57        """Run once on class initialization."""
58        unittest.TestCase.__init__(self, methodName)
59
60        TEST_DATA_DIR = unitTestDataPath()
61        self.pngImage = TEST_DATA_DIR + "/sample_image.png"
62        self.svgImage = TEST_DATA_DIR + "/sample_svg.svg"
63
64        # create composition
65        self.layout = QgsLayout(QgsProject.instance())
66        self.layout.initializeDefaults()
67
68        self.picture = QgsLayoutItemPicture(self.layout)
69        self.picture.setPicturePath(self.pngImage)
70        self.picture.attemptSetSceneRect(QRectF(70, 70, 100, 100))
71        self.picture.setFrameEnabled(True)
72        self.layout.addLayoutItem(self.picture)
73
74    def setUp(self):
75        self.report = "<h1>Python QgsLayoutItemPicture Tests</h1>\n"
76
77    def tearDown(self):
78        report_file_path = "%s/qgistest.html" % QDir.tempPath()
79        with open(report_file_path, 'a') as report_file:
80            report_file.write(self.report)
81
82    def testMode(self):
83        pic = QgsLayoutItemPicture(self.layout)
84        # should default to unknown
85        self.assertEqual(pic.mode(), QgsLayoutItemPicture.FormatUnknown)
86        spy = QSignalSpy(pic.changed)
87        pic.setMode(QgsLayoutItemPicture.FormatRaster)
88        self.assertEqual(pic.mode(), QgsLayoutItemPicture.FormatRaster)
89        self.assertEqual(len(spy), 1)
90        pic.setMode(QgsLayoutItemPicture.FormatRaster)
91        self.assertEqual(len(spy), 1)
92        pic.setMode(QgsLayoutItemPicture.FormatSVG)
93        self.assertEqual(len(spy), 3)  # ideally only 2!
94        self.assertEqual(pic.mode(), QgsLayoutItemPicture.FormatSVG)
95
96        # set picture path without explicit format
97        pic.setPicturePath(self.pngImage)
98        self.assertEqual(pic.mode(), QgsLayoutItemPicture.FormatRaster)
99        pic.setPicturePath(self.svgImage)
100        self.assertEqual(pic.mode(), QgsLayoutItemPicture.FormatSVG)
101        # forced format
102        pic.setPicturePath(self.pngImage, QgsLayoutItemPicture.FormatSVG)
103        self.assertEqual(pic.mode(), QgsLayoutItemPicture.FormatSVG)
104        pic.setPicturePath(self.pngImage, QgsLayoutItemPicture.FormatRaster)
105        self.assertEqual(pic.mode(), QgsLayoutItemPicture.FormatRaster)
106        pic.setPicturePath(self.svgImage, QgsLayoutItemPicture.FormatSVG)
107        self.assertEqual(pic.mode(), QgsLayoutItemPicture.FormatSVG)
108        pic.setPicturePath(self.svgImage, QgsLayoutItemPicture.FormatRaster)
109        self.assertEqual(pic.mode(), QgsLayoutItemPicture.FormatRaster)
110
111    def testReadWriteXml(self):
112        pr = QgsProject()
113        l = QgsLayout(pr)
114
115        pic = QgsLayoutItemPicture(l)
116        # mode should be saved/restored
117        pic.setMode(QgsLayoutItemPicture.FormatRaster)
118
119        # save original item to xml
120        doc = QDomDocument("testdoc")
121        elem = doc.createElement("test")
122        self.assertTrue(pic.writeXml(elem, doc, QgsReadWriteContext()))
123
124        pic2 = QgsLayoutItemPicture(l)
125        self.assertTrue(pic2.readXml(elem.firstChildElement(), doc, QgsReadWriteContext()))
126        self.assertEqual(pic2.mode(), QgsLayoutItemPicture.FormatRaster)
127
128        pic.setMode(QgsLayoutItemPicture.FormatSVG)
129        elem = doc.createElement("test2")
130        self.assertTrue(pic.writeXml(elem, doc, QgsReadWriteContext()))
131        pic3 = QgsLayoutItemPicture(l)
132        self.assertTrue(pic3.readXml(elem.firstChildElement(), doc, QgsReadWriteContext()))
133        self.assertEqual(pic3.mode(), QgsLayoutItemPicture.FormatSVG)
134
135    def testResizeZoom(self):
136        """Test picture resize zoom mode."""
137        self.picture.setResizeMode(QgsLayoutItemPicture.Zoom)
138
139        checker = QgsLayoutChecker('composerpicture_resize_zoom', self.layout)
140        checker.setControlPathPrefix("composer_picture")
141        testResult, message = checker.testLayout()
142        self.report += checker.report()
143
144        assert testResult, message
145
146    def testRemoteImage(self):
147        """Test fetching remote picture."""
148        self.picture.setPicturePath(
149            'http://localhost:' + str(TestQgsLayoutPicture.port) + '/qgis_local_server/logo.png')
150
151        checker = QgsLayoutChecker('composerpicture_remote', self.layout)
152        checker.setControlPathPrefix("composer_picture")
153        testResult, message = checker.testLayout()
154        self.report += checker.report()
155
156        self.picture.setPicturePath(self.pngImage)
157        assert testResult, message
158
159    def testNorthArrowWithMapItemRotation(self):
160        """Test picture rotation when map item is also rotated"""
161
162        layout = QgsLayout(QgsProject.instance())
163
164        map = QgsLayoutItemMap(layout)
165        map.setExtent(QgsRectangle(0, -256, 256, 0))
166        layout.addLayoutItem(map)
167
168        picture = QgsLayoutItemPicture(layout)
169        layout.addLayoutItem(picture)
170
171        picture.setLinkedMap(map)
172        self.assertEqual(picture.linkedMap(), map)
173
174        picture.setNorthMode(QgsLayoutItemPicture.GridNorth)
175        map.setItemRotation(45)
176        self.assertEqual(picture.pictureRotation(), 45)
177        map.setMapRotation(-34)
178        self.assertEqual(picture.pictureRotation(), 11)
179
180        # add an offset
181        picture.setNorthOffset(-10)
182        self.assertEqual(picture.pictureRotation(), 1)
183
184        map.setItemRotation(55)
185        self.assertEqual(picture.pictureRotation(), 11)
186
187    def testGridNorth(self):
188        """Test syncing picture to grid north"""
189
190        layout = QgsLayout(QgsProject.instance())
191
192        map = QgsLayoutItemMap(layout)
193        map.setExtent(QgsRectangle(0, -256, 256, 0))
194        layout.addLayoutItem(map)
195
196        picture = QgsLayoutItemPicture(layout)
197        layout.addLayoutItem(picture)
198
199        picture.setLinkedMap(map)
200        self.assertEqual(picture.linkedMap(), map)
201
202        picture.setNorthMode(QgsLayoutItemPicture.GridNorth)
203        map.setMapRotation(45)
204        self.assertEqual(picture.pictureRotation(), 45)
205
206        # add an offset
207        picture.setNorthOffset(-10)
208        self.assertEqual(picture.pictureRotation(), 35)
209
210    def testTrueNorth(self):
211        """Test syncing picture to true north"""
212
213        layout = QgsLayout(QgsProject.instance())
214
215        map = QgsLayoutItemMap(layout)
216        map.attemptSetSceneRect(QRectF(0, 0, 10, 10))
217        map.setCrs(QgsCoordinateReferenceSystem.fromEpsgId(3575))
218        map.setExtent(QgsRectangle(-2126029.962, -2200807.749, -119078.102, -757031.156))
219        layout.addLayoutItem(map)
220
221        picture = QgsLayoutItemPicture(layout)
222        layout.addLayoutItem(picture)
223
224        picture.setLinkedMap(map)
225        self.assertEqual(picture.linkedMap(), map)
226
227        picture.setNorthMode(QgsLayoutItemPicture.TrueNorth)
228        self.assertAlmostEqual(picture.pictureRotation(), 37.20, 1)
229
230        # shift map
231        map.setExtent(QgsRectangle(2120672.293, -3056394.691, 2481640.226, -2796718.780))
232        self.assertAlmostEqual(picture.pictureRotation(), -38.18, 1)
233
234        # rotate map
235        map.setMapRotation(45)
236        self.assertAlmostEqual(picture.pictureRotation(), -38.18 + 45, 1)
237
238        # add an offset
239        picture.setNorthOffset(-10)
240        self.assertAlmostEqual(picture.pictureRotation(), -38.18 + 35, 1)
241
242    def testMissingImage(self):
243        layout = QgsLayout(QgsProject.instance())
244
245        picture = QgsLayoutItemPicture(layout)
246
247        # SVG
248        picture.setPicturePath("invalid_path", QgsLayoutItemPicture.FormatSVG)
249        self.assertEqual(picture.isMissingImage(), True)
250        self.assertEqual(picture.mode(), QgsLayoutItemPicture.FormatSVG)
251
252        # Raster
253        picture.setPicturePath("invalid_path", QgsLayoutItemPicture.FormatRaster)
254        self.assertEqual(picture.isMissingImage(), True)
255        self.assertEqual(picture.mode(), QgsLayoutItemPicture.FormatRaster)
256
257
258if __name__ == '__main__':
259    unittest.main()
260