-
-
Notifications
You must be signed in to change notification settings - Fork 90
/
export.py
381 lines (288 loc) · 12.8 KB
/
export.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# -*- coding: utf-8 -*-
# (C) 2014 Minoru Akagi
# SPDX-License-Identifier: GPL-2.0-or-later
# begin: 2014-01-16
import json
import os
from PyQt5.QtCore import QDir, QEventLoop, QFileInfo, QSize
from PyQt5.QtGui import QImage, QPainter
from .conf import DEBUG_MODE, PLUGIN_VERSION
from .build import ThreeJSBuilder
from .builddem import DEMLayerBuilder
from .buildvector import VectorLayerBuilder
from .buildpointcloud import PointCloudLayerBuilder
from .exportsettings import ExportSettings
from .q3dcontroller import Q3DController
from .q3dconst import LayerType, Script
from .q3dinterface import Q3DInterface
from .q3dview import Q3DWebPage
from .utils import hex_color
from . import utils
class ThreeJSExporter(ThreeJSBuilder):
def __init__(self, settings=None, progress=None, log=None):
ThreeJSBuilder.__init__(self, settings or ExportSettings(), progress, log)
self._index = -1
self.modelManagers = []
def loadSettings(self, filename=None):
self.settings.loadSettingsFromFile(filename)
def setMapSettings(self, settings):
self.settings.setMapSettings(settings)
def export(self, filename=None, cancelSignal=None):
if filename:
self.settings.setOutputFilename(filename)
config = self.settings.templateConfig()
# create output data directory if not exists
dataDir = self.settings.outputDataDirectory()
if not QDir(dataDir).exists():
QDir().mkpath(dataDir)
# export the scene and its layers
json_object = self.buildScene(cancelSignal=cancelSignal)
if self.canceled:
return False
# animation
if self.settings.isAnimationEnabled():
json_object["animation"] = self.settings.animationData(export=True, warning_log=self.warning_log)
if self.settings.localMode:
with open(os.path.join(dataDir, "scene.js"), "w", encoding="utf-8") as f:
f.write("app.loadJSONObject(")
json.dump(json_object, f, indent=2)
f.write('); window.setTimeout(function () { app.dispatchEvent({type: "sceneLoaded"}); }, 0);')
else:
with open(os.path.join(dataDir, "scene.json"), "w", encoding="utf-8") as f:
json.dump(json_object, f, indent=2 if DEBUG_MODE else None)
narration = self.settings.narrations(warning_log=self.warning_log)
# copy files
files = narration["files"]
if files:
self.progress(90, "Copying image files used in narrative content...")
img_dir = os.path.join(self.settings.outputDataDirectory(), "img")
QDir().mkpath(img_dir)
for f in files:
if utils.copyFile(f, os.path.join(img_dir, os.path.basename(f)), overwrite=True):
self.log("Copied {}.".format(f))
else:
self.log("Failed to copy {}.".format(f), warning=True)
self.progress(95, "Copying library files...")
utils.copyFiles(self.filesToCopy(), self.settings.outputDirectory())
# options in html file
options = []
# scene
sp = self.settings.sceneProperties()
if sp.get("radioButton_Color"):
options.append("Q3D.Config.bgColor = {0};".format(hex_color(sp.get("colorButton_Color", 0), prefix="0x")))
if not self.settings.coordDisplay():
options.append("Q3D.Config.coord.visible = false;")
if self.settings.isCoordLatLon():
options.append("Q3D.Config.coord.latlon = true;")
# camera
if self.settings.isOrthoCamera():
options.append("Q3D.Config.orthoCamera = true;")
# web export options
opts = self.settings.options()
if opts:
for key in opts:
options.append("Q3D.Config.{0} = {1};".format(key, utils.pyobj2js(self.settings.option(key))))
# North arrow
p = self.settings.widgetProperties("NorthArrow")
if p.get("visible"):
options.append("Q3D.Config.northArrow.enabled = true;")
options.append("Q3D.Config.northArrow.color = {0};".format(hex_color(p.get("color", 0), prefix="0x")))
# read html template
with open(config["path"], "r", encoding="utf-8") as f:
html = f.read()
mapping = {
"title": self.settings.title(),
"options": "\n".join(options),
"scripts": "\n".join(self.scripts()),
"scenefile": "./data/{0}/scene.{1}".format(self.settings.outputFileTitle(), "js" if self.settings.localMode else "json"),
"header": self.settings.headerLabel(),
"footer": self.settings.footerLabel(),
"narration": narration["html"],
"version": PLUGIN_VERSION
}
for key, value in mapping.items():
html = html.replace("${" + key + "}", value)
# write to html file
with open(self.settings.outputFileName(), "w", encoding="utf-8") as f:
f.write(html)
return True
def nextLayerIndex(self):
self._index += 1
return self._index
def buildLayer(self, layer, cancelSignal=None):
title = utils.abchex(self.nextLayerIndex())
if self.settings.localMode:
pathRoot = urlRoot = None
else:
pathRoot = os.path.join(self.settings.outputDataDirectory(), title)
urlRoot = "./data/{0}/{1}".format(self.settings.outputFileTitle(), title)
layer = layer.clone()
layer.opt.allMaterials = True
if layer.type == LayerType.DEM:
builder = DEMLayerBuilder(self.settings, layer, self.imageManager, pathRoot, urlRoot, log=self.log)
elif layer.type == LayerType.POINTCLOUD:
builder = PointCloudLayerBuilder(self.settings, layer, log=self.log)
else:
builder = VectorLayerBuilder(self.settings, layer, self.imageManager, pathRoot, urlRoot, log=self.log)
self.modelManagers.append(builder.modelManager)
return builder.build(True, cancelSignal)
def filesToCopy(self):
# three.js library
files = [{"dirs": ["js/threejs"]}]
# controls
files.append({"files": ["js/threejs/controls/" + self.settings.controls()], "dest": "threejs"})
if self.settings.isNavigationEnabled():
files.append({"files": ["js/threejs/editor/ViewHelper.js"], "dest": "threejs"})
# outline effect
if self.settings.useOutlineEffect():
files.append({"files": ["js/threejs/effects/OutlineEffect.js"], "dest": "threejs"})
# template specific libraries (files)
config = self.settings.templateConfig()
for f in config.get("files", "").strip().split(","):
p = f.split(">")
fs = {"files": [p[0]]}
if len(p) > 1:
fs["dest"] = p[1]
files.append(fs)
for d in config.get("dirs", "").strip().split(","):
p = d.split(">")
ds = {"dirs": [p[0]], "subdirs": True}
if len(p) > 1:
ds["dest"] = p[1]
files.append(ds)
# proj4js
if self.settings.isCoordLatLon():
files.append({"dirs": ["js/proj4js"]})
# layer-specific dependencies
wl = pc = True
for layer in [lyr for lyr in self.settings.layers() if lyr.visible]: # HACK: lyr.export
if layer.type == LayerType.LINESTRING:
if layer.properties.get("comboBox_ObjectType") == "Thick Line" and wl:
files.append({"dirs": ["js/meshline"]})
wl = False
elif layer.type == LayerType.POINTCLOUD and pc:
files.append({"dirs": ["js/potree-core"], "subdirs": True})
files.append({"files": ["js/pointcloudlayer.js"]})
pc = False
# model loades and model files
for manager in self.modelManagers:
for f in manager.filesToCopy():
if f not in files:
files.append(f)
# animation
if self.settings.isAnimationEnabled():
files.append({"dirs": ["js/tweenjs"]})
return files
def scripts(self):
files = []
# three.js and controls
files.append("./threejs/three.min.js")
files.append("./threejs/{}".format(self.settings.controls()))
if self.settings.isNavigationEnabled():
files.append("./threejs/ViewHelper.js")
# outline effect
if self.settings.useOutlineEffect():
files.append("./threejs/OutlineEffect.js")
# html template config
config = self.settings.templateConfig()
s = config.get("scripts", "").strip()
if s:
files += s.split(",")
# proj4.js
if self.settings.isCoordLatLon(): # display coordinates in latitude and longitude format
proj4 = "./proj4js/proj4.js"
if proj4 not in files:
files.append(proj4)
# animation
if self.settings.isAnimationEnabled():
files.append("./tweenjs/tween.js")
# Qgis2threejs.js
files.append("./Qgis2threejs.js")
# layer-specific dependencies
wl = pc = True
for layer in [lyr for lyr in self.settings.layers() if lyr.visible]:
if layer.type == LayerType.LINESTRING:
if layer.properties.get("comboBox_ObjectType") == "Thick Line" and wl:
files.append("./meshline/THREE.MeshLine.js")
wl = False
elif layer.type == LayerType.POINTCLOUD and pc:
files.append("./potree-core/potree.min.js")
files.append("./pointcloudlayer.js")
pc = False
# model loaders
for manager in self.modelManagers:
for f in manager.scripts():
if f not in files:
files.append(f)
return ['<script src="%s"></script>' % fn for fn in files]
def warning_log(self, msg):
self.log(msg, warning=True)
class BridgeExporterBase:
def __init__(self, settings=None):
self.settings = settings or ExportSettings()
self.settings.isPreview = True
self.page = Q3DWebPage()
self.iface = Q3DInterface(self.settings, self.page)
self.iface.statusMessage.connect(self.iface.showStatusMessage)
self.controller = Q3DController(self.settings)
self.controller.connectToIface(self.iface)
def loadSettings(self, filename=None):
self.settings.loadSettingsFromFile(filename)
def setMapSettings(self, settings):
self.settings.setMapSettings(settings)
def initWebPage(self, width, height):
loop = QEventLoop()
self.page.ready.connect(loop.quit)
self.page.setViewportSize(QSize(width, height))
if self.page.mainFrame().url().isEmpty():
self.page.setup(self.settings)
else:
self.page.reload()
loop.exec_()
def mkdir(self, filename):
dir = QFileInfo(filename).dir()
if not dir.exists():
QDir().mkpath(dir.absolutePath())
class ImageExporter(BridgeExporterBase):
def render(self, cameraState=None, cancelSignal=None):
if self.page is None:
return QImage(), "page not ready"
# set camera position and camera target
if cameraState:
self.page.setCameraState(cameraState)
# build scene
self.controller.buildScene(update_extent=False)
err = self.page.waitForSceneLoaded(cancelSignal)
# header and footer labels
self.page.runScript('setHFLabel(pyData())', data=self.settings.widgetProperties("Label"))
# render scene
size = self.page.viewportSize()
image = QImage(size.width(), size.height(), QImage.Format_ARGB32_Premultiplied)
painter = QPainter(image)
self.page.mainFrame().render(painter)
painter.end()
return image, err
def export(self, filename, cameraState=None, cancelSignal=None):
# prepare output directory
self.mkdir(filename)
image, err = self.render(cameraState, cancelSignal)
image.save(filename)
return err
class ModelExporter(BridgeExporterBase):
def __init__(self, settings=None):
super().__init__(settings)
self.settings.jsonSerializable = True
def initWebPage(self, width, height):
super().initWebPage(width, height)
self.page.loadScriptFile(Script.GLTFEXPORTER)
def export(self, filename, cancelSignal=None):
if self.page is None:
return "page not ready"
# prepare output directory
self.mkdir(filename)
# build scene
self.controller.buildScene(update_extent=False)
err = self.page.waitForSceneLoaded(cancelSignal)
# save model
self.page.runScript("saveModelAsGLTF('{0}')".format(filename.replace("\\", "\\\\")))
return err