This commit is contained in:
cjw
2026-02-12 23:22:11 +08:00
parent 7b09eb3d89
commit 89660bba4e
5988 changed files with 2517516 additions and 0 deletions
@@ -0,0 +1,62 @@
import hashlib, base64
arrayTypesMapping = [
" ", # VTK_VOID 0
" ", # VTK_BIT 1
"b", # VTK_CHAR 2
"B", # VTK_UNSIGNED_CHAR 3
"h", # VTK_SHORT 4
"H", # VTK_UNSIGNED_SHORT 5
"i", # VTK_INT 6
"I", # VTK_UNSIGNED_INT 7
"l", # VTK_LONG 8
"L", # VTK_UNSIGNED_LONG 9
"f", # VTK_FLOAT 10
"d", # VTK_DOUBLE 11
"L", # VTK_ID_TYPE 12
" ", # unspecified 13
" ", # unspecified 14
"b", # signed_char 15
]
javascriptMapping = {
"b": "Int8Array",
"B": "Uint8Array",
"h": "Int16Array",
"H": "Int16Array",
"i": "Int32Array",
"I": "Uint32Array",
"l": "Int32Array",
"L": "Uint32Array",
"f": "Float32Array",
"d": "Float64Array",
}
def iteritems(d, **kwargs):
return iter(d.items(**kwargs))
def base64Encode(x):
return base64.b64encode(x).decode("utf-8")
def hashDataArray(dataArray):
hashedBit = hashlib.md5(memoryview(dataArray)).hexdigest()
typeCode = arrayTypesMapping[dataArray.GetDataType()]
return "%s_%d%s" % (hashedBit, dataArray.GetSize(), typeCode)
def getJSArrayType(dataArray):
return javascriptMapping[arrayTypesMapping[dataArray.GetDataType()]]
def getReferenceId(ref):
if ref:
try:
return ref.__this__[1:17]
except:
idStr = str(ref)[-12:-1]
# print('====> fallback ID %s for %s' % (idStr, ref))
return idStr
return "0x0"
@@ -0,0 +1,640 @@
from math import *
# -----------------------------------------------------------------------------
# Set of helper functions
# -----------------------------------------------------------------------------
def normalize(vect, tolerance=0.00001):
mag2 = sum(n * n for n in vect)
if abs(mag2 - 1.0) > tolerance:
mag = sqrt(mag2)
vect = tuple(n / mag for n in vect)
return vect
def q_mult(q1, q2):
w1, x1, y1, z1 = q1
w2, x2, y2, z2 = q2
w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2
z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2
return w, x, y, z
def q_conjugate(q):
w, x, y, z = q
return (w, -x, -y, -z)
def qv_mult(q1, v1):
q2 = (0.0,) + v1
return q_mult(q_mult(q1, q2), q_conjugate(q1))[1:]
def axisangle_to_q(v, theta):
v = normalize(v)
x, y, z = v
theta /= 2
w = cos(theta)
x = x * sin(theta)
y = y * sin(theta)
z = z * sin(theta)
return w, x, y, z
def vectProduct(axisA, axisB):
xa, ya, za = axisA
xb, yb, zb = axisB
normalVect = (ya * zb - za * yb, za * xb - xa * zb, xa * yb - ya * xb)
normalVect = normalize(normalVect)
return normalVect
def dotProduct(vecA, vecB):
return (vecA[0] * vecB[0]) + (vecA[1] * vecB[1]) + (vecA[2] * vecB[2])
def rotate(axis, angle, center, point):
angleInRad = 3.141592654 * angle / 180.0
rotation = axisangle_to_q(axis, angleInRad)
tPoint = tuple((point[i] - center[i]) for i in range(3))
rtPoint = qv_mult(rotation, tPoint)
rPoint = tuple((rtPoint[i] + center[i]) for i in range(3))
return rPoint
# -----------------------------------------------------------------------------
# Spherical Camera
# -----------------------------------------------------------------------------
class SphericalCamera(object):
def __init__(
self, dataHandler, focalPoint, position, phiAxis, phiAngles, thetaAngles
):
self.dataHandler = dataHandler
self.cameraSettings = []
self.thetaBind = {
"mouse": {
"drag": {"modifier": 0, "coordinate": 1, "step": 30, "orientation": 1}
}
}
self.phiBind = {
"mouse": {
"drag": {"modifier": 0, "coordinate": 0, "step": 30, "orientation": 1}
}
}
# Convert to serializable type
fp = tuple(i for i in focalPoint)
# Register arguments to the data handler
if len(phiAngles) > 1 and phiAngles[-1] + phiAngles[1] == 360:
self.dataHandler.registerArgument(
priority=0,
name="phi",
values=phiAngles,
ui="slider",
loop="modulo",
bind=self.phiBind,
)
else:
self.dataHandler.registerArgument(
priority=0, name="phi", values=phiAngles, ui="slider", bind=self.phiBind
)
if thetaAngles[0] < 0 and thetaAngles[0] >= -90:
idx = 0
for theta in thetaAngles:
if theta < 0:
idx += 1
self.dataHandler.registerArgument(
priority=0,
name="theta",
values=[(x + 90) for x in thetaAngles],
ui="slider",
default=idx,
bind=self.thetaBind,
)
else:
self.dataHandler.registerArgument(
priority=0,
name="theta",
values=thetaAngles,
ui="slider",
bind=self.thetaBind,
)
# Compute all camera settings
for theta in thetaAngles:
for phi in phiAngles:
phiPos = rotate(phiAxis, -phi, fp, position)
thetaAxis = vectProduct(
phiAxis, tuple(fp[i] - phiPos[i] for i in range(3))
)
thetaPhiPos = rotate(thetaAxis, theta, fp, phiPos)
viewUp = rotate(thetaAxis, theta, (0, 0, 0), phiAxis)
self.cameraSettings.append(
{
"theta": theta,
"thetaIdx": thetaAngles.index(theta),
"phi": phi,
"phiIdx": phiAngles.index(phi),
"focalPoint": fp,
"position": thetaPhiPos,
"viewUp": viewUp,
}
)
self.dataHandler.updateBasePattern()
def updatePriority(self, priorityList):
keyList = ["theta", "phi"]
for idx in range(min(len(priorityList), len(keyList))):
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
def __iter__(self):
for cameraData in self.cameraSettings:
self.dataHandler.setArguments(
phi=cameraData["phiIdx"], theta=cameraData["thetaIdx"]
)
yield cameraData
# -----------------------------------------------------------------------------
# Cylindrical Camera
# -----------------------------------------------------------------------------
class CylindricalCamera(object):
def __init__(
self,
dataHandler,
focalPoint,
position,
rotationAxis,
phiAngles,
translationValues,
):
self.dataHandler = dataHandler
self.cameraSettings = []
# Register arguments to the data handler
self.dataHandler.registerArgument(
priority=0, name="phi", values=phiAngles, ui="slider", loop="modulo"
)
self.dataHandler.registerArgument(
priority=0, name="n_pos", values=translationValues, ui="slider"
)
# Compute all camera settings
for translation in translationValues:
for phi in phiAngles:
phiPos = rotate(rotationAxis, phi, focalPoint, position)
newfocalPoint = tuple(
focalPoint[i] + (translation * rotationAxis[i]) for i in range(3)
)
transPhiPoint = tuple(
phiPos[i] + (translation * rotationAxis[i]) for i in range(3)
)
self.cameraSettings.append(
{
"n_pos": translation,
"n_posIdx": translationValues.index(translation),
"phi": phi,
"phiIdx": phiAngles.index(phi),
"focalPoint": newfocalPoint,
"position": transPhiPoint,
"viewUp": rotationAxis,
}
)
self.dataHandler.updateBasePattern()
def updatePriority(self, priorityList):
keyList = ["n_pos", "phi"]
for idx in range(min(len(priorityList), len(keyList))):
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
def __iter__(self):
for cameraData in self.cameraSettings:
self.dataHandler.setArguments(
phi=cameraData["phiIdx"], n_pos=cameraData["n_posIdx"]
)
yield cameraData
# -----------------------------------------------------------------------------
# MultiView Cube Camera
# -----------------------------------------------------------------------------
class CubeCamera(object):
# positions = [ { position: [x,y,z], args: { i: 1, j: 0, k: 7 } }, ... ]
def __init__(self, dataHandler, viewForward, viewUp, positions):
self.dataHandler = dataHandler
self.cameraSettings = []
self.viewForward = viewForward
self.viewUp = viewUp
self.rightDirection = vectProduct(viewForward, viewUp)
self.positions = positions
# Register arguments to the data handler
self.dataHandler.registerArgument(
priority=0, name="orientation", values=["f", "b", "r", "l", "u", "d"]
)
# Register arguments to id position
self.args = {}
for pos in positions:
for key in pos["args"]:
if key not in self.args:
self.args[key] = {}
self.args[key][pos["args"][key]] = True
for key in self.args:
self.args[key] = sorted(self.args[key], key=lambda k: int(k))
self.keyList = self.args.keys()
for key in self.args:
self.dataHandler.registerArgument(
priority=1, name=key, values=self.args[key]
)
self.dataHandler.updateBasePattern()
def updatePriority(self, priorityList):
keyList = ["orientation"]
for idx in range(min(len(priorityList), len(keyList))):
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
def __iter__(self):
for pos in self.positions:
cameraData = {
"position": pos["position"],
}
print("=" * 80)
for key in pos["args"]:
idx = self.args[key].index(pos["args"][key])
self.dataHandler.setArguments(**{key: idx})
print(key, idx)
print("position", cameraData["position"])
# front
cameraData["focalPoint"] = [
(cameraData["position"][i] + self.viewForward[i]) for i in range(3)
]
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["orientation"] = "front"
self.dataHandler.setArguments(orientation=0)
yield cameraData
# back
cameraData["focalPoint"] = [
(cameraData["position"][i] - self.viewForward[i]) for i in range(3)
]
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["orientation"] = "back"
self.dataHandler.setArguments(orientation=1)
yield cameraData
# right
self.dataHandler.setArguments(orientation=2)
cameraData["focalPoint"] = [
(cameraData["position"][i] + self.rightDirection[i]) for i in range(3)
]
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["orientation"] = "right"
yield cameraData
# left
self.dataHandler.setArguments(orientation=3)
cameraData["focalPoint"] = [
(cameraData["position"][i] - self.rightDirection[i]) for i in range(3)
]
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["orientation"] = "left"
yield cameraData
# up
self.dataHandler.setArguments(orientation=4)
cameraData["focalPoint"] = [
(cameraData["position"][i] + self.viewUp[i]) for i in range(3)
]
cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)]
cameraData["orientation"] = "up"
yield cameraData
# doww
self.dataHandler.setArguments(orientation=5)
cameraData["focalPoint"] = [
(cameraData["position"][i] - self.viewUp[i]) for i in range(3)
]
cameraData["viewUp"] = [self.viewForward[i] for i in range(3)]
cameraData["orientation"] = "down"
yield cameraData
# -----------------------------------------------------------------------------
# MultiView Cube Camera
# -----------------------------------------------------------------------------
class StereoCubeCamera(object):
# positions = [ { position: [x,y,z], args: { i: 1, j: 0, k: 7 } }, ... ]
def __init__(self, dataHandler, viewForward, viewUp, positions, eyeSpacing):
self.dataHandler = dataHandler
self.cameraSettings = []
self.viewForward = viewForward
self.viewUp = viewUp
self.rightDirection = vectProduct(viewForward, viewUp)
self.positions = positions
self.eyeSpacing = eyeSpacing
# Register arguments to the data handler
self.dataHandler.registerArgument(
priority=0, name="orientation", values=["f", "b", "r", "l", "u", "d"]
)
self.dataHandler.registerArgument(
priority=0, name="eye", values=["left", "right"]
)
# Register arguments to id position
self.args = {}
for pos in positions:
for key in pos["args"]:
if key not in self.args:
self.args[key] = {}
self.args[key][pos["args"][key]] = True
for key in self.args:
self.args[key] = sorted(self.args[key], key=lambda k: int(k))
self.keyList = self.args.keys()
for key in self.args:
self.dataHandler.registerArgument(
priority=1, name=key, values=self.args[key]
)
self.dataHandler.updateBasePattern()
def updatePriority(self, priorityList):
keyList = ["orientation"]
for idx in range(min(len(priorityList), len(keyList))):
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
def __iter__(self):
for pos in self.positions:
cameraData = {}
for key in pos["args"]:
idx = self.args[key].index(pos["args"][key])
self.dataHandler.setArguments(**{key: idx})
# front
cameraData["orientation"] = "front"
self.dataHandler.setArguments(orientation=0)
deltaVect = [
(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
]
## Left-Eye
self.dataHandler.setArguments(eye=0)
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] + self.viewForward[i] - deltaVect[i])
for i in range(3)
]
yield cameraData
## Right-Eye
self.dataHandler.setArguments(eye=1)
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] + self.viewForward[i] + deltaVect[i])
for i in range(3)
]
yield cameraData
# back
cameraData["orientation"] = "back"
self.dataHandler.setArguments(orientation=1)
deltaVect = [
-(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
]
## Left-Eye
self.dataHandler.setArguments(eye=0)
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] - self.viewForward[i] - deltaVect[i])
for i in range(3)
]
yield cameraData
## Right-Eye
self.dataHandler.setArguments(eye=1)
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] - self.viewForward[i] + deltaVect[i])
for i in range(3)
]
yield cameraData
# right
self.dataHandler.setArguments(orientation=2)
cameraData["orientation"] = "right"
deltaVect = [-(v * float(self.eyeSpacing) * 0.5) for v in self.viewForward]
## Left-Eye
self.dataHandler.setArguments(eye=0)
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] + self.rightDirection[i] - deltaVect[i])
for i in range(3)
]
yield cameraData
## Right-Eye
self.dataHandler.setArguments(eye=1)
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] + self.rightDirection[i] + deltaVect[i])
for i in range(3)
]
yield cameraData
# left
self.dataHandler.setArguments(orientation=3)
cameraData["orientation"] = "left"
deltaVect = [(v * float(self.eyeSpacing) * 0.5) for v in self.viewForward]
## Left-Eye
self.dataHandler.setArguments(eye=0)
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] - self.rightDirection[i] - deltaVect[i])
for i in range(3)
]
yield cameraData
## Right-Eye
self.dataHandler.setArguments(eye=1)
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] - self.rightDirection[i] + deltaVect[i])
for i in range(3)
]
yield cameraData
# up
self.dataHandler.setArguments(orientation=4)
cameraData["orientation"] = "up"
deltaVect = [
(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
]
## Left-Eye
self.dataHandler.setArguments(eye=0)
cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] + self.viewUp[i] - deltaVect[i]) for i in range(3)
]
yield cameraData
## Right-Eye
self.dataHandler.setArguments(eye=1)
cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] + self.viewUp[i] + deltaVect[i]) for i in range(3)
]
yield cameraData
# doww
self.dataHandler.setArguments(orientation=5)
cameraData["orientation"] = "down"
deltaVect = [
(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
]
## Left-Eye
self.dataHandler.setArguments(eye=0)
cameraData["viewUp"] = [self.viewForward[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] - self.viewUp[i] - deltaVect[i]) for i in range(3)
]
yield cameraData
## Right-Eye
self.dataHandler.setArguments(eye=1)
cameraData["viewUp"] = [self.viewForward[i] for i in range(3)]
cameraData["position"] = [
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
]
cameraData["focalPoint"] = [
(pos["position"][i] - self.viewUp[i] + deltaVect[i]) for i in range(3)
]
yield cameraData
# -----------------------------------------------------------------------------
# MultiView Camera
# -----------------------------------------------------------------------------
class MultiViewCamera(object):
def __init__(self, dataHandler):
self.dataHandler = dataHandler
self.cameraSettings = []
self.positionNames = []
def registerViewPoint(self, name, focalPoint, position, viewUp):
self.cameraSettings.append(
{
"name": name,
"nameIdx": len(self.positionNames),
"focalPoint": focalPoint,
"position": position,
"viewUp": viewUp,
}
)
self.positionNames.append(name)
self.dataHandler.registerArgument(
priority=0, name="multiView", values=self.positionNames
)
self.dataHandler.updateBasePattern()
def updatePriority(self, priorityList):
keyList = ["multiView"]
for idx in range(min(len(priorityList), len(keyList))):
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
def __iter__(self):
for cameraData in self.cameraSettings:
self.dataHandler.setArguments(multiView=cameraData["nameIdx"])
yield cameraData
# -----------------------------------------------------------------------------
# Helper methods
# -----------------------------------------------------------------------------
def update_camera(renderer, cameraData):
camera = renderer.GetActiveCamera()
camera.SetPosition(cameraData["position"])
camera.SetFocalPoint(cameraData["focalPoint"])
camera.SetViewUp(cameraData["viewUp"])
def create_spherical_camera(renderer, dataHandler, phiValues, thetaValues):
camera = renderer.GetActiveCamera()
return SphericalCamera(
dataHandler,
camera.GetFocalPoint(),
camera.GetPosition(),
camera.GetViewUp(),
phiValues,
thetaValues,
)
def create_cylindrical_camera(renderer, dataHandler, phiValues, translationValues):
camera = renderer.GetActiveCamera()
return CylindricalCamera(
dataHandler,
camera.GetFocalPoint(),
camera.GetPosition(),
camera.GetViewUp(),
phiValues,
translationValues,
)
@@ -0,0 +1,620 @@
import json, os, gzip, shutil
from vtkmodules.vtkRenderingCore import vtkWindowToImageFilter
from vtkmodules.vtkIOImage import vtkPNGReader, vtkPNGWriter, vtkJPEGWriter
from vtkmodules.vtkCommonDataModel import vtkImageData
from vtkmodules.vtkCommonCore import vtkUnsignedCharArray
from vtkmodules.vtkFiltersParallel import vtkPResampleFilter
from vtkmodules.web import iteritems, getJSArrayType
from vtkmodules.web.camera import (
update_camera,
create_spherical_camera,
create_cylindrical_camera,
)
from vtkmodules.web.query_data_model import DataHandler
# Global helper variables
encode_codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# -----------------------------------------------------------------------------
# Capture image from render window
# -----------------------------------------------------------------------------
class CaptureRenderWindow(object):
def __init__(self, magnification=1):
self.windowToImage = vtkWindowToImageFilter()
self.windowToImage.SetScale(magnification)
self.windowToImage.SetInputBufferTypeToRGB()
self.windowToImage.ReadFrontBufferOn()
self.writer = None
def SetRenderWindow(self, renderWindow):
self.windowToImage.SetInput(renderWindow)
def SetFormat(self, mimeType):
if mimeType == "image/png":
self.writer = vtkPNGWriter()
self.writer.SetInputConnection(self.windowToImage.GetOutputPort())
elif mimeType == "image/jpg":
self.writer = vtkJPEGWriter()
self.writer.SetInputConnection(self.windowToImage.GetOutputPort())
def writeImage(self, path):
if self.writer:
self.windowToImage.Modified()
self.windowToImage.Update()
self.writer.SetFileName(path)
self.writer.Write()
# -----------------------------------------------------------------------------
# Basic Dataset Builder
# -----------------------------------------------------------------------------
class DataSetBuilder(object):
def __init__(self, location, camera_data, metadata={}, sections={}):
self.dataHandler = DataHandler(location)
self.cameraDescription = camera_data
self.camera = None
self.imageCapture = CaptureRenderWindow()
for key, value in iteritems(metadata):
self.dataHandler.addMetaData(key, value)
for key, value in iteritems(sections):
self.dataHandler.addSection(key, value)
def getDataHandler(self):
return self.dataHandler
def getCamera(self):
return self.camera
def updateCamera(self, camera):
update_camera(self.renderer, camera)
self.renderWindow.Render()
def start(self, renderWindow=None, renderer=None):
if renderWindow:
# Keep track of renderWindow and renderer
self.renderWindow = renderWindow
self.renderer = renderer
# Initialize image capture
self.imageCapture.SetRenderWindow(renderWindow)
# Handle camera if any
if self.cameraDescription:
if self.cameraDescription["type"] == "spherical":
self.camera = create_spherical_camera(
renderer,
self.dataHandler,
self.cameraDescription["phi"],
self.cameraDescription["theta"],
)
elif self.cameraDescription["type"] == "cylindrical":
self.camera = create_cylindrical_camera(
renderer,
self.dataHandler,
self.cameraDescription["phi"],
self.cameraDescription["translation"],
)
# Update background color
bgColor = renderer.GetBackground()
bgColorString = "rgb(%d, %d, %d)" % tuple(
int(bgColor[i] * 255) for i in range(3)
)
self.dataHandler.addMetaData("backgroundColor", bgColorString)
# Update file patterns
self.dataHandler.updateBasePattern()
def stop(self):
self.dataHandler.writeDataDescriptor()
# -----------------------------------------------------------------------------
# Image Dataset Builder
# -----------------------------------------------------------------------------
class ImageDataSetBuilder(DataSetBuilder):
def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}):
DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections)
imageExtenstion = "." + imageMimeType.split("/")[1]
self.dataHandler.registerData(
name="image", type="blob", mimeType=imageMimeType, fileName=imageExtenstion
)
self.imageCapture.SetFormat(imageMimeType)
def writeImage(self):
self.imageCapture.writeImage(self.dataHandler.getDataAbsoluteFilePath("image"))
def writeImages(self):
for cam in self.camera:
update_camera(self.renderer, cam)
self.renderWindow.Render()
self.imageCapture.writeImage(
self.dataHandler.getDataAbsoluteFilePath("image")
)
# -----------------------------------------------------------------------------
# Volume Composite Dataset Builder
# -----------------------------------------------------------------------------
class VolumeCompositeDataSetBuilder(DataSetBuilder):
def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}):
DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections)
self.dataHandler.addTypes("volume-composite", "rgba+depth")
self.imageMimeType = imageMimeType
self.imageExtenstion = "." + imageMimeType.split("/")[1]
if imageMimeType == "image/png":
self.imageWriter = vtkPNGWriter()
if imageMimeType == "image/jpg":
self.imageWriter = vtkJPEGWriter()
self.imageDataColor = vtkImageData()
self.imageWriter.SetInputData(self.imageDataColor)
self.imageDataDepth = vtkImageData()
self.depthToWrite = None
self.layerInfo = {}
self.colorByMapping = {}
self.compositePipeline = {
"layers": [],
"dimensions": [],
"fields": {},
"layer_fields": {},
"pipeline": [],
}
self.activeDepthKey = ""
self.activeRGBKey = ""
self.nodeWithChildren = {}
def _getColorCode(self, colorBy):
if colorBy in self.colorByMapping:
# The color code exist
return self.colorByMapping[colorBy]
else:
# No color code assigned yet
colorCode = encode_codes[len(self.colorByMapping)]
# Assign color code
self.colorByMapping[colorBy] = colorCode
# Register color code with color by value
self.compositePipeline["fields"][colorCode] = colorBy
# Return the color code
return colorCode
def _getLayerCode(self, parent, layerName):
if layerName in self.layerInfo:
# Layer already exist
return (self.layerInfo[layerName]["code"], False)
else:
layerCode = encode_codes[len(self.layerInfo)]
self.layerInfo[layerName] = {
"code": layerCode,
"name": layerName,
"parent": parent,
}
self.compositePipeline["layers"].append(layerCode)
self.compositePipeline["layer_fields"][layerCode] = []
# Let's register it in the pipeline
if parent:
if parent not in self.nodeWithChildren:
# Need to create parent
rootNode = {"name": parent, "ids": [], "children": []}
self.nodeWithChildren[parent] = rootNode
self.compositePipeline["pipeline"].append(rootNode)
# Add node to its parent
self.nodeWithChildren[parent]["children"].append(
{"name": layerName, "ids": [layerCode]}
)
self.nodeWithChildren[parent]["ids"].append(layerCode)
else:
self.compositePipeline["pipeline"].append(
{"name": layerName, "ids": [layerCode]}
)
return (layerCode, True)
def _needToRegisterColor(self, layerCode, colorCode):
if colorCode in self.compositePipeline["layer_fields"][layerCode]:
return False
else:
self.compositePipeline["layer_fields"][layerCode].append(colorCode)
return True
def activateLayer(self, parent, name, colorBy):
layerCode, needToRegisterDepth = self._getLayerCode(parent, name)
colorCode = self._getColorCode(colorBy)
needToRegisterColor = self._needToRegisterColor(layerCode, colorCode)
# Update active keys
self.activeDepthKey = "%s_depth" % layerCode
self.activeRGBKey = "%s%s_rgb" % (layerCode, colorCode)
# Need to register data
if needToRegisterDepth:
self.dataHandler.registerData(
name=self.activeDepthKey,
type="array",
fileName="/%s_depth.uint8" % layerCode,
categories=[layerCode],
)
if needToRegisterColor:
self.dataHandler.registerData(
name=self.activeRGBKey,
type="blob",
fileName="/%s%s_rgb%s" % (layerCode, colorCode, self.imageExtenstion),
categories=["%s%s" % (layerCode, colorCode)],
mimeType=self.imageMimeType,
)
def writeData(self, mapper):
width = self.renderWindow.GetSize()[0]
height = self.renderWindow.GetSize()[1]
if not self.depthToWrite:
self.depthToWrite = bytearray(width * height)
for cam in self.camera:
self.updateCamera(cam)
imagePath = self.dataHandler.getDataAbsoluteFilePath(self.activeRGBKey)
depthPath = self.dataHandler.getDataAbsoluteFilePath(self.activeDepthKey)
# -----------------------------------------------------------------
# Write Image
# -----------------------------------------------------------------
mapper.GetColorImage(self.imageDataColor)
self.imageWriter.SetFileName(imagePath)
self.imageWriter.Write()
# -----------------------------------------------------------------
# Write Depth
# -----------------------------------------------------------------
mapper.GetDepthImage(self.imageDataDepth)
inputArray = self.imageDataDepth.GetPointData().GetArray(0)
size = inputArray.GetNumberOfTuples()
for idx in range(size):
self.depthToWrite[idx] = int(inputArray.GetValue(idx))
with open(depthPath, "wb") as f:
f.write(self.depthToWrite)
def start(self, renderWindow, renderer):
DataSetBuilder.start(self, renderWindow, renderer)
self.camera.updatePriority([2, 1])
def stop(self, compress=True):
# Push metadata
self.compositePipeline["dimensions"] = self.renderWindow.GetSize()
self.compositePipeline["default_pipeline"] = (
"A".join(self.compositePipeline["layers"]) + "A"
)
self.dataHandler.addSection("CompositePipeline", self.compositePipeline)
# Write metadata
DataSetBuilder.stop(self)
if compress:
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
print("Compress", root)
for name in files:
if ".uint8" in name and ".gz" not in name:
with open(os.path.join(root, name), "rb") as f_in:
with gzip.open(
os.path.join(root, name + ".gz"), "wb"
) as f_out:
shutil.copyfileobj(f_in, f_out)
os.remove(os.path.join(root, name))
# -----------------------------------------------------------------------------
# Data Prober Dataset Builder
# -----------------------------------------------------------------------------
class DataProberDataSetBuilder(DataSetBuilder):
def __init__(
self,
location,
sampling_dimesions,
fields_to_keep,
custom_probing_bounds=None,
metadata={},
):
DataSetBuilder.__init__(self, location, None, metadata)
self.fieldsToWrite = fields_to_keep
self.resamplerFilter = vtkPResampleFilter()
self.resamplerFilter.SetSamplingDimension(sampling_dimesions)
if custom_probing_bounds:
self.resamplerFilter.SetUseInputBounds(0)
self.resamplerFilter.SetCustomSamplingBounds(custom_probing_bounds)
else:
self.resamplerFilter.SetUseInputBounds(1)
# Register all fields
self.dataHandler.addTypes("data-prober", "binary")
self.DataProber = {
"types": {},
"dimensions": sampling_dimesions,
"ranges": {},
"spacing": [1, 1, 1],
}
for field in self.fieldsToWrite:
self.dataHandler.registerData(
name=field, type="array", fileName="/%s.array" % field
)
def setDataToProbe(self, dataset):
self.resamplerFilter.SetInputData(dataset)
def setSourceToProbe(self, source):
self.resamplerFilter.SetInputConnection(source.GetOutputPort())
def writeData(self):
self.resamplerFilter.Update()
arrays = self.resamplerFilter.GetOutput().GetPointData()
for field in self.fieldsToWrite:
array = arrays.GetArray(field)
if array:
b = memoryview(array)
with open(self.dataHandler.getDataAbsoluteFilePath(field), "wb") as f:
f.write(b)
self.DataProber["types"][field] = getJSArrayType(array)
if field in self.DataProber["ranges"]:
dataRange = array.GetRange()
if dataRange[0] < self.DataProber["ranges"][field][0]:
self.DataProber["ranges"][field][0] = dataRange[0]
if dataRange[1] > self.DataProber["ranges"][field][1]:
self.DataProber["ranges"][field][1] = dataRange[1]
else:
self.DataProber["ranges"][field] = [
array.GetRange()[0],
array.GetRange()[1],
]
else:
print("No array for", field)
print(self.resamplerFilter.GetOutput())
def stop(self, compress=True):
# Push metadata
self.dataHandler.addSection("DataProber", self.DataProber)
# Write metadata
DataSetBuilder.stop(self)
if compress:
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
print("Compress", root)
for name in files:
if ".array" in name and ".gz" not in name:
with open(os.path.join(root, name), "rb") as f_in:
with gzip.open(
os.path.join(root, name + ".gz"), "wb"
) as f_out:
shutil.copyfileobj(f_in, f_out)
os.remove(os.path.join(root, name))
# -----------------------------------------------------------------------------
# Sorted Composite Dataset Builder
# -----------------------------------------------------------------------------
class ConvertVolumeStackToSortedStack(object):
def __init__(self, width, height):
self.width = width
self.height = height
self.layers = 0
def convert(self, directory):
imagePaths = {}
depthPaths = {}
layerNames = []
for fileName in os.listdir(directory):
if "_rgb" in fileName or "_depth" in fileName:
fileId = fileName.split("_")[0][0]
if "_rgb" in fileName:
imagePaths[fileId] = os.path.join(directory, fileName)
else:
layerNames.append(fileId)
depthPaths[fileId] = os.path.join(directory, fileName)
layerNames.sort()
if len(layerNames) == 0:
return
# Load data in Memory
depthArrays = []
imageReader = vtkPNGReader()
numberOfValues = self.width * self.height * len(layerNames)
imageSize = self.width * self.height
self.layers = len(layerNames)
# Write all images as single memoryview
opacity = vtkUnsignedCharArray()
opacity.SetNumberOfComponents(1)
opacity.SetNumberOfTuples(numberOfValues)
intensity = vtkUnsignedCharArray()
intensity.SetNumberOfComponents(1)
intensity.SetNumberOfTuples(numberOfValues)
for layer in range(self.layers):
imageReader.SetFileName(imagePaths[layerNames[layer]])
imageReader.Update()
rgbaArray = imageReader.GetOutput().GetPointData().GetArray(0)
for idx in range(imageSize):
intensity.SetValue(
(layer * imageSize) + idx, rgbaArray.GetValue(idx * 4)
)
opacity.SetValue(
(layer * imageSize) + idx, rgbaArray.GetValue(idx * 4 + 3)
)
with open(depthPaths[layerNames[layer]], "rb") as depthFile:
depthArrays.append(depthFile.read())
# Apply pixel sorting
destOrder = vtkUnsignedCharArray()
destOrder.SetNumberOfComponents(1)
destOrder.SetNumberOfTuples(numberOfValues)
opacityOrder = vtkUnsignedCharArray()
opacityOrder.SetNumberOfComponents(1)
opacityOrder.SetNumberOfTuples(numberOfValues)
intensityOrder = vtkUnsignedCharArray()
intensityOrder.SetNumberOfComponents(1)
intensityOrder.SetNumberOfTuples(numberOfValues)
for pixelIdx in range(imageSize):
depthStack = []
for depthArray in depthArrays:
depthStack.append((depthArray[pixelIdx], len(depthStack)))
depthStack.sort(key=lambda tup: tup[0])
for destLayerIdx in range(len(depthStack)):
sourceLayerIdx = depthStack[destLayerIdx][1]
# Copy Idx
destOrder.SetValue(
(imageSize * destLayerIdx) + pixelIdx, sourceLayerIdx
)
opacityOrder.SetValue(
(imageSize * destLayerIdx) + pixelIdx,
opacity.GetValue((imageSize * sourceLayerIdx) + pixelIdx),
)
intensityOrder.SetValue(
(imageSize * destLayerIdx) + pixelIdx,
intensity.GetValue((imageSize * sourceLayerIdx) + pixelIdx),
)
with open(os.path.join(directory, "alpha.uint8"), "wb") as f:
f.write(memoryview(opacityOrder))
with open(os.path.join(directory, "intensity.uint8"), "wb") as f:
f.write(memoryview(intensityOrder))
with open(os.path.join(directory, "order.uint8"), "wb") as f:
f.write(memoryview(destOrder))
class SortedCompositeDataSetBuilder(VolumeCompositeDataSetBuilder):
def __init__(self, location, cameraInfo, metadata={}, sections={}):
VolumeCompositeDataSetBuilder.__init__(
self, location, "image/png", cameraInfo, metadata, sections
)
self.dataHandler.addTypes("sorted-composite", "rgba")
# Register order and color textures
self.layerScalars = []
self.dataHandler.registerData(
name="order", type="array", fileName="/order.uint8"
)
self.dataHandler.registerData(
name="alpha", type="array", fileName="/alpha.uint8"
)
self.dataHandler.registerData(
name="intensity",
type="array",
fileName="/intensity.uint8",
categories=["intensity"],
)
def start(self, renderWindow, renderer):
VolumeCompositeDataSetBuilder.start(self, renderWindow, renderer)
imageSize = self.renderWindow.GetSize()
self.dataConverter = ConvertVolumeStackToSortedStack(imageSize[0], imageSize[1])
def activateLayer(self, colorBy, scalar):
VolumeCompositeDataSetBuilder.activateLayer(
self, "root", "%s" % scalar, colorBy
)
self.layerScalars.append(scalar)
def writeData(self, mapper):
VolumeCompositeDataSetBuilder.writeData(self, mapper)
# Fill data pattern
self.dataHandler.getDataAbsoluteFilePath("order")
self.dataHandler.getDataAbsoluteFilePath("alpha")
self.dataHandler.getDataAbsoluteFilePath("intensity")
def stop(self, clean=True, compress=True):
VolumeCompositeDataSetBuilder.stop(self, compress=False)
# Go through all directories and convert them
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
for name in dirs:
print("Process", os.path.join(root, name))
self.dataConverter.convert(os.path.join(root, name))
# Rename index.json to info_origin.json
os.rename(
os.path.join(self.dataHandler.getBasePath(), "index.json"),
os.path.join(self.dataHandler.getBasePath(), "index_origin.json"),
)
# Update index.json
with open(
os.path.join(self.dataHandler.getBasePath(), "index_origin.json"), "r"
) as infoFile:
metadata = json.load(infoFile)
metadata["SortedComposite"] = {
"dimensions": metadata["CompositePipeline"]["dimensions"],
"layers": self.dataConverter.layers,
"scalars": self.layerScalars[0 : self.dataConverter.layers],
}
# Clean metadata
dataToKeep = []
del metadata["CompositePipeline"]
for item in metadata["data"]:
if item["name"] in ["order", "alpha", "intensity"]:
dataToKeep.append(item)
metadata["data"] = dataToKeep
metadata["type"] = ["tonic-query-data-model", "sorted-composite", "alpha"]
# Override index.json
with open(
os.path.join(self.dataHandler.getBasePath(), "index.json"), "w"
) as newMetaFile:
newMetaFile.write(json.dumps(metadata))
# Clean temporary data
if clean:
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
print("Clean", root)
for name in files:
if (
"_rgb.png" in name
or "_depth.uint8" in name
or name == "index_origin.json"
):
os.remove(os.path.join(root, name))
if compress:
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
print("Compress", root)
for name in files:
if ".uint8" in name and ".gz" not in name:
with open(os.path.join(root, name), "rb") as f_in:
with gzip.open(
os.path.join(root, name + ".gz"), "wb"
) as f_out:
shutil.copyfileobj(f_in, f_out)
os.remove(os.path.join(root, name))
@@ -0,0 +1,12 @@
WEB_DEPENDENCY_MISSING_MESSAGE = """Please install VTK's Web module dependencies.
These include `wslink` and can be easily installed with vtk by using the
`web` extra requirements option. For example:
pip install vtk[web]
"""
class WebDependencyMissingError(ImportError):
def __init__(self, message=WEB_DEPENDENCY_MISSING_MESSAGE):
super().__init__(message)
@@ -0,0 +1,842 @@
r"""protocols is a module that contains a set of VTK Web related
protocols that can be combined together to provide a flexible way to define
very specific web application.
"""
from __future__ import absolute_import, division, print_function
import os, sys, logging, types, inspect, traceback, re, base64, time
from vtkmodules.vtkWebCore import vtkWebInteractionEvent
from vtkmodules.web.errors import WebDependencyMissingError
from vtkmodules.web.render_window_serializer import (
serializeInstance,
SynchronizationContext,
getReferenceId,
initializeSerializers,
)
try:
from wslink import schedule_callback
from wslink import register as exportRpc
from wslink.websocket import LinkProtocol
except ImportError:
raise WebDependencyMissingError()
# =============================================================================
#
# Base class for any VTK Web based protocol
#
# =============================================================================
class vtkWebProtocol(LinkProtocol):
def getApplication(self):
return self.getSharedObject("app")
# no need for a setApplication anymore, but keep for compatibility
def setApplication(self, app):
pass
def mapIdToObject(self, id):
"""
Maps global-id for a vtkObject to the vtkObject instance. May return None if the
id is not valid.
"""
id = int(id)
if id <= 0:
return None
return self.getApplication().GetObjectIdMap().GetVTKObject(id)
def getGlobalId(self, obj):
"""
Return the id for a given vtkObject
"""
return self.getApplication().GetObjectIdMap().GetGlobalId(obj)
def freeObject(self, obj):
"""
Delete the given vtkObject from the objectIdMap. Returns true if delete succeeded.
"""
return self.getApplication().GetObjectIdMap().FreeObject(obj)
def freeObjectById(self, id):
"""
Delete the vtkObject corresponding to the given objectId from the objectIdMap.
Returns true if delete succeeded.
"""
return self.getApplication().GetObjectIdMap().FreeObjectById(id)
def getView(self, vid):
"""
Returns the view for a given view ID, if vid is None then return the
current active view.
:param vid: The view ID
:type vid: str
"""
v = self.mapIdToObject(vid)
if not v:
# Use active view is none provided.
v = self.getApplication().GetObjectIdMap().GetActiveObject("VIEW")
if not v:
raise Exception("no view provided: %s" % vid)
return v
def setActiveView(self, view):
"""
Set a vtkRenderWindow to be the active one
"""
self.getApplication().GetObjectIdMap().SetActiveObject("VIEW", view)
# =============================================================================
#
# Handle Mouse interaction on any type of view
#
# =============================================================================
class vtkWebMouseHandler(vtkWebProtocol):
@exportRpc("viewport.mouse.interaction")
def mouseInteraction(self, event):
"""
RPC Callback for mouse interactions.
"""
view = self.getView(event["view"])
buttons = 0
if event["buttonLeft"]:
buttons |= vtkWebInteractionEvent.LEFT_BUTTON
if event["buttonMiddle"]:
buttons |= vtkWebInteractionEvent.MIDDLE_BUTTON
if event["buttonRight"]:
buttons |= vtkWebInteractionEvent.RIGHT_BUTTON
modifiers = 0
if event["shiftKey"]:
modifiers |= vtkWebInteractionEvent.SHIFT_KEY
if event["ctrlKey"]:
modifiers |= vtkWebInteractionEvent.CTRL_KEY
if event["altKey"]:
modifiers |= vtkWebInteractionEvent.ALT_KEY
if event["metaKey"]:
modifiers |= vtkWebInteractionEvent.META_KEY
pvevent = vtkWebInteractionEvent()
pvevent.SetButtons(buttons)
pvevent.SetModifiers(modifiers)
if "x" in event:
pvevent.SetX(event["x"])
if "y" in event:
pvevent.SetY(event["y"])
if "scroll" in event:
pvevent.SetScroll(event["scroll"])
if event["action"] == "dblclick":
pvevent.SetRepeatCount(2)
# pvevent.SetKeyCode(event["charCode"])
retVal = self.getApplication().HandleInteractionEvent(view, pvevent)
del pvevent
if event["action"] == "down":
self.getApplication().InvokeEvent("StartInteractionEvent")
if event["action"] == "up":
self.getApplication().InvokeEvent("EndInteractionEvent")
if retVal:
self.getApplication().InvokeEvent("UpdateEvent")
return retVal
@exportRpc("viewport.mouse.zoom.wheel")
def updateZoomFromWheel(self, event):
if "Start" in event["type"]:
self.getApplication().InvokeEvent("StartInteractionEvent")
renderWindow = self.getView(event["view"])
if renderWindow and "spinY" in event:
zoomFactor = 1.0 - event["spinY"] / 10.0
camera = renderWindow.GetRenderers().GetFirstRenderer().GetActiveCamera()
fp = camera.GetFocalPoint()
pos = camera.GetPosition()
delta = [fp[i] - pos[i] for i in range(3)]
camera.Zoom(zoomFactor)
pos2 = camera.GetPosition()
camera.SetFocalPoint([pos2[i] + delta[i] for i in range(3)])
renderWindow.Modified()
if "End" in event["type"]:
self.getApplication().InvokeEvent("EndInteractionEvent")
# =============================================================================
#
# Basic 3D Viewport API (Camera + Orientation + CenterOfRotation
#
# =============================================================================
class vtkWebViewPort(vtkWebProtocol):
@exportRpc("viewport.camera.reset")
def resetCamera(self, viewId):
"""
RPC callback to reset camera.
"""
view = self.getView(viewId)
renderer = view.GetRenderers().GetFirstRenderer()
renderer.ResetCamera()
self.getApplication().InvalidateCache(view)
self.getApplication().InvokeEvent("UpdateEvent")
return str(self.getGlobalId(view))
@exportRpc("viewport.axes.orientation.visibility.update")
def updateOrientationAxesVisibility(self, viewId, showAxis):
"""
RPC callback to show/hide OrientationAxis.
"""
view = self.getView(viewId)
# FIXME seb: view.OrientationAxesVisibility = (showAxis if 1 else 0);
self.getApplication().InvalidateCache(view)
self.getApplication().InvokeEvent("UpdateEvent")
return str(self.getGlobalId(view))
@exportRpc("viewport.axes.center.visibility.update")
def updateCenterAxesVisibility(self, viewId, showAxis):
"""
RPC callback to show/hide CenterAxesVisibility.
"""
view = self.getView(viewId)
# FIXME seb: view.CenterAxesVisibility = (showAxis if 1 else 0);
self.getApplication().InvalidateCache(view)
self.getApplication().InvokeEvent("UpdateEvent")
return str(self.getGlobalId(view))
@exportRpc("viewport.camera.update")
def updateCamera(self, view_id, focal_point, view_up, position, forceUpdate=True):
view = self.getView(view_id)
camera = view.GetRenderers().GetFirstRenderer().GetActiveCamera()
camera.SetFocalPoint(focal_point)
camera.SetViewUp(view_up)
camera.SetPosition(position)
if forceUpdate:
self.getApplication().InvalidateCache(view)
self.getApplication().InvokeEvent("UpdateEvent")
# =============================================================================
#
# Provide Image delivery mechanism (deprecated - will be removed in VTK 10+)
#
# =============================================================================
class vtkWebViewPortImageDelivery(vtkWebProtocol):
@exportRpc("viewport.image.render")
def stillRender(self, options):
"""
RPC Callback to render a view and obtain the rendered image.
"""
beginTime = int(round(time.time() * 1000))
view = self.getView(options["view"])
size = [view.GetSize()[0], view.GetSize()[1]]
# use existing size, overridden only if options["size"] is set.
resize = size != options.get("size", size)
if resize:
size = options["size"]
if size[0] > 0 and size[1] > 0:
view.SetSize(size)
t = 0
if options and "mtime" in options:
t = options["mtime"]
quality = 100
if options and "quality" in options:
quality = options["quality"]
localTime = 0
if options and "localTime" in options:
localTime = options["localTime"]
reply = {}
app = self.getApplication()
if t == 0:
app.InvalidateCache(view)
reply["image"] = app.StillRenderToString(view, t, quality)
# Check that we are getting image size we have set. If not, wait until we
# do. The render call will set the actual window size.
tries = 10
while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0:
app.InvalidateCache(view)
reply["image"] = app.StillRenderToString(view, t, quality)
tries -= 1
reply["stale"] = app.GetHasImagesBeingProcessed(view)
reply["mtime"] = app.GetLastStillRenderToMTime()
reply["size"] = [view.GetSize()[0], view.GetSize()[1]]
reply["format"] = "jpeg;base64"
reply["global_id"] = str(self.getGlobalId(view))
reply["localTime"] = localTime
endTime = int(round(time.time() * 1000))
reply["workTime"] = endTime - beginTime
return reply
# =============================================================================
#
# Provide publish-based Image delivery mechanism
#
# =============================================================================
class vtkWebPublishImageDelivery(vtkWebProtocol):
def __init__(self, decode=True):
super(vtkWebPublishImageDelivery, self).__init__()
self.trackingViews = {}
self.lastStaleTime = 0
self.staleHandlerCount = 0
self.deltaStaleTimeBeforeRender = 0.5 # 0.5s
self.decode = decode
self.viewsInAnimations = []
self.targetFrameRate = 30.0
self.minFrameRate = 12.0
self.maxFrameRate = 30.0
def pushRender(self, vId, ignoreAnimation=False):
if vId not in self.trackingViews:
return
if not self.trackingViews[vId]["enabled"]:
return
if not ignoreAnimation and len(self.viewsInAnimations) > 0:
return
if "originalSize" not in self.trackingViews[vId]:
view = self.getView(vId)
self.trackingViews[vId]["originalSize"] = list(view.GetSize())
if "ratio" not in self.trackingViews[vId]:
self.trackingViews[vId]["ratio"] = 1
ratio = self.trackingViews[vId]["ratio"]
mtime = self.trackingViews[vId]["mtime"]
quality = self.trackingViews[vId]["quality"]
size = [int(s * ratio) for s in self.trackingViews[vId]["originalSize"]]
reply = self.stillRender(
{"view": vId, "mtime": mtime, "quality": quality, "size": size}
)
stale = reply["stale"]
if reply["image"]:
# depending on whether the app has encoding enabled:
if self.decode:
reply["image"] = base64.standard_b64decode(reply["image"])
reply["image"] = self.addAttachment(reply["image"])
reply["format"] = "jpeg"
# save mtime for next call.
self.trackingViews[vId]["mtime"] = reply["mtime"]
# echo back real ID, instead of -1 for 'active'
reply["id"] = vId
self.publish("viewport.image.push.subscription", reply)
if stale:
self.lastStaleTime = time.time()
if self.staleHandlerCount == 0:
self.staleHandlerCount += 1
schedule_callback(
self.deltaStaleTimeBeforeRender, lambda: self.renderStaleImage(vId)
)
else:
self.lastStaleTime = 0
def renderStaleImage(self, vId):
self.staleHandlerCount -= 1
if self.lastStaleTime != 0:
delta = time.time() - self.lastStaleTime
if delta >= self.deltaStaleTimeBeforeRender:
self.pushRender(vId)
else:
self.staleHandlerCount += 1
schedule_callback(
self.deltaStaleTimeBeforeRender - delta + 0.001,
lambda: self.renderStaleImage(vId),
)
def animate(self):
if len(self.viewsInAnimations) == 0:
return
nextAnimateTime = time.time() + 1.0 / self.targetFrameRate
for vId in self.viewsInAnimations:
self.pushRender(vId, True)
nextAnimateTime -= time.time()
if self.targetFrameRate > self.maxFrameRate:
self.targetFrameRate = self.maxFrameRate
if nextAnimateTime < 0:
if nextAnimateTime < -1.0:
self.targetFrameRate = 1
if self.targetFrameRate > self.minFrameRate:
self.targetFrameRate -= 1.0
schedule_callback(0.001, lambda: self.animate())
else:
if self.targetFrameRate < self.maxFrameRate and nextAnimateTime > 0.005:
self.targetFrameRate += 1.0
schedule_callback(nextAnimateTime, lambda: self.animate())
@exportRpc("viewport.image.animation.fps.max")
def setMaxFrameRate(self, fps=30):
self.maxFrameRate = fps
@exportRpc("viewport.image.animation.fps.get")
def getCurrentFrameRate(self):
return self.targetFrameRate
@exportRpc("viewport.image.animation.start")
def startViewAnimation(self, viewId="-1"):
sView = self.getView(viewId)
realViewId = str(self.getGlobalId(sView))
self.viewsInAnimations.append(realViewId)
if len(self.viewsInAnimations) == 1:
self.animate()
@exportRpc("viewport.image.animation.stop")
def stopViewAnimation(self, viewId="-1"):
sView = self.getView(viewId)
realViewId = str(self.getGlobalId(sView))
if realViewId in self.viewsInAnimations:
self.viewsInAnimations.remove(realViewId)
@exportRpc("viewport.image.push")
def imagePush(self, options):
sView = self.getView(options["view"])
realViewId = str(self.getGlobalId(sView))
# Make sure an image is pushed
self.getApplication().InvalidateCache(sView)
self.pushRender(realViewId)
# Internal function since the reply[image] is not
# JSON(serializable) it can not be an RPC one
def stillRender(self, options):
"""
RPC Callback to render a view and obtain the rendered image.
"""
beginTime = int(round(time.time() * 1000))
view = self.getView(options["view"])
size = view.GetSize()[0:2]
resize = size != options.get("size", size)
if resize:
size = options["size"]
if size[0] > 10 and size[1] > 10:
view.SetSize(size)
t = 0
if options and "mtime" in options:
t = options["mtime"]
quality = 100
if options and "quality" in options:
quality = options["quality"]
localTime = 0
if options and "localTime" in options:
localTime = options["localTime"]
reply = {}
app = self.getApplication()
if t == 0:
app.InvalidateCache(view)
if self.decode:
stillRender = app.StillRenderToString
else:
stillRender = app.StillRenderToBuffer
reply_image = stillRender(view, t, quality)
# Check that we are getting image size we have set if not wait until we
# do. The render call will set the actual window size.
tries = 10
while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0:
app.InvalidateCache(view)
reply_image = stillRender(view, t, quality)
tries -= 1
if (
not resize
and options
and ("clearCache" in options)
and options["clearCache"]
):
app.InvalidateCache(view)
reply_image = stillRender(view, t, quality)
reply["stale"] = app.GetHasImagesBeingProcessed(view)
reply["mtime"] = app.GetLastStillRenderToMTime()
reply["size"] = view.GetSize()[0:2]
reply["memsize"] = reply_image.GetDataSize() if reply_image else 0
reply["format"] = "jpeg;base64" if self.decode else "jpeg"
reply["global_id"] = str(self.getGlobalId(view))
reply["localTime"] = localTime
if self.decode:
reply["image"] = reply_image
else:
# Convert the vtkUnsignedCharArray into a bytes object, required by Autobahn websockets
reply["image"] = memoryview(reply_image).tobytes() if reply_image else None
endTime = int(round(time.time() * 1000))
reply["workTime"] = endTime - beginTime
return reply
@exportRpc("viewport.image.push.observer.add")
def addRenderObserver(self, viewId):
sView = self.getView(viewId)
if not sView:
return {"error": "Unable to get view with id %s" % viewId}
realViewId = str(self.getGlobalId(sView))
if not realViewId in self.trackingViews:
observerCallback = lambda *args, **kwargs: self.pushRender(realViewId)
startCallback = lambda *args, **kwargs: self.startViewAnimation(realViewId)
stopCallback = lambda *args, **kwargs: self.stopViewAnimation(realViewId)
tag = self.getApplication().AddObserver("UpdateEvent", observerCallback)
tagStart = self.getApplication().AddObserver(
"StartInteractionEvent", startCallback
)
tagStop = self.getApplication().AddObserver(
"EndInteractionEvent", stopCallback
)
# TODO do we need self.getApplication().AddObserver('ResetActiveView', resetActiveView())
self.trackingViews[realViewId] = {
"tags": [tag, tagStart, tagStop],
"observerCount": 1,
"mtime": 0,
"enabled": True,
"quality": 100,
}
else:
# There is an observer on this view already
self.trackingViews[realViewId]["observerCount"] += 1
self.pushRender(realViewId)
return {"success": True, "viewId": realViewId}
@exportRpc("viewport.image.push.observer.remove")
def removeRenderObserver(self, viewId):
sView = self.getView(viewId)
if not sView:
return {"error": "Unable to get view with id %s" % viewId}
realViewId = str(self.getGlobalId(sView))
observerInfo = None
if realViewId in self.trackingViews:
observerInfo = self.trackingViews[realViewId]
if not observerInfo:
return {"error": "Unable to find subscription for view %s" % realViewId}
observerInfo["observerCount"] -= 1
if observerInfo["observerCount"] <= 0:
for tag in observerInfo["tags"]:
self.getApplication().RemoveObserver(tag)
del self.trackingViews[realViewId]
return {"result": "success"}
@exportRpc("viewport.image.push.quality")
def setViewQuality(self, viewId, quality, ratio=1):
sView = self.getView(viewId)
if not sView:
return {"error": "Unable to get view with id %s" % viewId}
realViewId = str(self.getGlobalId(sView))
observerInfo = None
if realViewId in self.trackingViews:
observerInfo = self.trackingViews[realViewId]
if not observerInfo:
return {"error": "Unable to find subscription for view %s" % realViewId}
observerInfo["quality"] = quality
observerInfo["ratio"] = ratio
# Update image size right now!
if "originalSize" in self.trackingViews[realViewId]:
size = [
int(s * ratio) for s in self.trackingViews[realViewId]["originalSize"]
]
if hasattr(sView, "SetSize"):
sView.SetSize(size)
else:
sView.ViewSize = size
return {"result": "success"}
@exportRpc("viewport.image.push.original.size")
def setViewSize(self, viewId, width, height):
sView = self.getView(viewId)
if not sView:
return {"error": "Unable to get view with id %s" % viewId}
realViewId = str(self.getGlobalId(sView))
observerInfo = None
if realViewId in self.trackingViews:
observerInfo = self.trackingViews[realViewId]
if not observerInfo:
return {"error": "Unable to find subscription for view %s" % realViewId}
observerInfo["originalSize"] = [width, height]
return {"result": "success"}
@exportRpc("viewport.image.push.enabled")
def enableView(self, viewId, enabled):
sView = self.getView(viewId)
if not sView:
return {"error": "Unable to get view with id %s" % viewId}
realViewId = str(self.getGlobalId(sView))
observerInfo = None
if realViewId in self.trackingViews:
observerInfo = self.trackingViews[realViewId]
if not observerInfo:
return {"error": "Unable to find subscription for view %s" % realViewId}
observerInfo["enabled"] = enabled
return {"result": "success"}
@exportRpc("viewport.image.push.invalidate.cache")
def invalidateCache(self, viewId):
sView = self.getView(viewId)
if not sView:
return {"error": "Unable to get view with id %s" % viewId}
self.getApplication().InvalidateCache(sView)
self.getApplication().InvokeEvent("UpdateEvent")
return {"result": "success"}
# =============================================================================
#
# Provide Geometry delivery mechanism (WebGL) (deprecated - will be removed in VTK 10+)
#
# =============================================================================
class vtkWebViewPortGeometryDelivery(vtkWebProtocol):
@exportRpc("viewport.webgl.metadata")
def getSceneMetaData(self, view_id):
view = self.getView(view_id)
data = self.getApplication().GetWebGLSceneMetaData(view)
return data
@exportRpc("viewport.webgl.data")
def getWebGLData(self, view_id, object_id, part):
view = self.getView(view_id)
data = self.getApplication().GetWebGLBinaryData(view, str(object_id), part - 1)
return data
# =============================================================================
#
# Provide File/Directory listing
#
# =============================================================================
class vtkWebFileBrowser(vtkWebProtocol):
def __init__(
self, basePath, name, excludeRegex=r"^\.|~$|^\$", groupRegex=r"[0-9]+\."
):
"""
Configure the way the WebFile browser will expose the server content.
- basePath: specify the base directory that we should start with
- name: Name of that base directory that will show up on the web
- excludeRegex: Regular expression of what should be excluded from the list of files/directories
"""
self.baseDirectory = basePath
self.rootName = name
self.pattern = re.compile(excludeRegex)
self.gPattern = re.compile(groupRegex)
@exportRpc("file.server.directory.list")
def listServerDirectory(self, relativeDir="."):
"""
RPC Callback to list a server directory relative to the basePath
provided at start-up.
"""
path = [self.rootName]
if len(relativeDir) > len(self.rootName):
relativeDir = relativeDir[len(self.rootName) + 1 :]
path += relativeDir.replace("\\", "/").split("/")
currentPath = os.path.join(self.baseDirectory, relativeDir)
result = {
"label": relativeDir,
"files": [],
"dirs": [],
"groups": [],
"path": path,
}
if relativeDir == ".":
result["label"] = self.rootName
for file in os.listdir(currentPath):
if os.path.isfile(os.path.join(currentPath, file)) and not re.search(
self.pattern, file
):
result["files"].append({"label": file, "size": -1})
elif os.path.isdir(os.path.join(currentPath, file)) and not re.search(
self.pattern, file
):
result["dirs"].append(file)
# Filter files to create groups
files = result["files"]
files.sort()
groups = result["groups"]
groupIdx = {}
filesToRemove = []
for file in files:
fileSplit = re.split(self.gPattern, file["label"])
if len(fileSplit) == 2:
filesToRemove.append(file)
gName = "*.".join(fileSplit)
if gName in groupIdx:
groupIdx[gName]["files"].append(file["label"])
else:
groupIdx[gName] = {"files": [file["label"]], "label": gName}
groups.append(groupIdx[gName])
for file in filesToRemove:
gName = "*.".join(re.split(self.gPattern, file["label"]))
if len(groupIdx[gName]["files"]) > 1:
files.remove(file)
else:
groups.remove(groupIdx[gName])
return result
# =============================================================================
#
# Provide an updated geometry delivery mechanism which better matches the
# client-side rendering capability we have in vtk.js
#
# =============================================================================
class vtkWebLocalRendering(vtkWebProtocol):
def __init__(self, **kwargs):
super(vtkWebLocalRendering, self).__init__()
initializeSerializers()
self.context = SynchronizationContext()
self.trackingViews = {}
self.mtime = 0
# RpcName: getArray => viewport.geometry.array.get
@exportRpc("viewport.geometry.array.get")
def getArray(self, dataHash, binary=False):
if binary:
return self.addAttachment(self.context.getCachedDataArray(dataHash, binary))
return self.context.getCachedDataArray(dataHash, binary)
# RpcName: addViewObserver => viewport.geometry.view.observer.add
@exportRpc("viewport.geometry.view.observer.add")
def addViewObserver(self, viewId):
sView = self.getView(viewId)
if not sView:
return {"error": "Unable to get view with id %s" % viewId}
realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
def pushGeometry(newSubscription=False):
stateToReturn = self.getViewState(realViewId, newSubscription)
stateToReturn["mtime"] = 0 if newSubscription else self.mtime
self.mtime += 1
return stateToReturn
if not realViewId in self.trackingViews:
observerCallback = lambda *args, **kwargs: self.publish(
"viewport.geometry.view.subscription", pushGeometry()
)
tag = self.getApplication().AddObserver("UpdateEvent", observerCallback)
self.trackingViews[realViewId] = {"tags": [tag], "observerCount": 1}
else:
# There is an observer on this view already
self.trackingViews[realViewId]["observerCount"] += 1
self.publish("viewport.geometry.view.subscription", pushGeometry(True))
return {"success": True, "viewId": realViewId}
# RpcName: removeViewObserver => viewport.geometry.view.observer.remove
@exportRpc("viewport.geometry.view.observer.remove")
def removeViewObserver(self, viewId):
sView = self.getView(viewId)
if not sView:
return {"error": "Unable to get view with id %s" % viewId}
realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
observerInfo = None
if realViewId in self.trackingViews:
observerInfo = self.trackingViews[realViewId]
if not observerInfo:
return {"error": "Unable to find subscription for view %s" % realViewId}
observerInfo["observerCount"] -= 1
if observerInfo["observerCount"] <= 0:
for tag in observerInfo["tags"]:
self.getApplication().RemoveObserver(tag)
del self.trackingViews[realViewId]
return {"result": "success"}
# RpcName: getViewState => viewport.geometry.view.get.state
@exportRpc("viewport.geometry.view.get.state")
def getViewState(self, viewId, newSubscription=False):
sView = self.getView(viewId)
if not sView:
return {"error": "Unable to get view with id %s" % viewId}
self.context.setIgnoreLastDependencies(newSubscription)
# Get the active view and render window, use it to iterate over renderers
renderWindow = sView
renderer = renderWindow.GetRenderers().GetFirstRenderer()
camera = renderer.GetActiveCamera()
renderWindowId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
viewInstance = serializeInstance(
None, renderWindow, renderWindowId, self.context, 1
)
viewInstance["extra"] = {
"vtkRefId": getReferenceId(renderWindow),
"centerOfRotation": camera.GetFocalPoint(),
"camera": getReferenceId(camera),
}
self.context.setIgnoreLastDependencies(False)
self.context.checkForArraysToRelease()
if viewInstance:
return viewInstance
return None
@@ -0,0 +1,182 @@
"""
Core Module for Web Base Data Generation
"""
import sys, os, json
from vtkmodules.web import iteritems
class DataHandler(object):
def __init__(self, basePath):
self.__root = basePath
self.types = ["tonic-query-data-model"]
self.metadata = {}
self.data = {}
self.arguments = {}
self.current = {}
self.sections = {}
self.basePattern = None
self.priority = []
self.argOrder = []
self.realValues = {}
self.can_write = True
def getBasePath(self):
return self.__root
def updateBasePattern(self):
self.priority.sort(key=lambda item: item[1])
self.basePattern = ""
patternSeparator = ""
currentPriority = -1
for item in self.priority:
if currentPriority != -1:
if currentPriority == item[1]:
patternSeparator = "_"
else:
patternSeparator = "/"
currentPriority = item[1]
self.basePattern = "{%s}%s%s" % (
item[0],
patternSeparator,
self.basePattern,
)
def registerArgument(self, **kwargs):
"""
We expect the following set of arguments
- priority
- name
- label (optional)
- values
- uiType
- defaultIdx
"""
newArgument = {}
argName = kwargs["name"]
self.argOrder.append(argName)
for key, value in iteritems(kwargs):
if key == "priority":
self.priority.append([argName, value])
elif key == "values":
self.realValues[argName] = value
newArgument[key] = ["{value}".format(value=x) for x in value]
else:
newArgument[key] = value
self.arguments[argName] = newArgument
def updatePriority(self, argumentName, newPriority):
for item in self.priority:
if item[0] == argumentName:
item[1] = newPriority
def setArguments(self, **kwargs):
"""
Update the arguments index
"""
for key, value in iteritems(kwargs):
self.current[key] = value
def removeData(self, name):
del self.data[name]
def registerData(self, **kwargs):
"""
name, type, mimeType, fileName, dependencies
"""
newData = {"metadata": {}}
argName = kwargs["name"]
for key, value in iteritems(kwargs):
if key == "fileName":
if "rootFile" in kwargs and kwargs["rootFile"]:
newData["pattern"] = "{pattern}/%s" % value
else:
newData["pattern"] = "{pattern}%s" % value
else:
newData[key] = value
self.data[argName] = newData
def addDataMetaData(self, name, key, value):
self.data[name]["metadata"][key] = value
def getDataAbsoluteFilePath(self, name, createDirectories=True):
dataPattern = self.data[name]["pattern"]
if "{pattern}" in dataPattern:
if len(self.basePattern) == 0:
dataPattern = dataPattern.replace(
"{pattern}/", self.basePattern
).replace("{pattern}", self.basePattern)
self.data[name]["pattern"] = dataPattern
else:
dataPattern = dataPattern.replace("{pattern}", self.basePattern)
self.data[name]["pattern"] = dataPattern
keyValuePair = {}
for key, value in iteritems(self.current):
keyValuePair[key] = self.arguments[key]["values"][value]
fullpath = os.path.join(self.__root, dataPattern.format(**keyValuePair))
if createDirectories and self.can_write:
if not os.path.exists(os.path.dirname(fullpath)):
os.makedirs(os.path.dirname(fullpath))
return fullpath
def addTypes(self, *args):
for arg in args:
self.types.append(arg)
def addMetaData(self, key, value):
self.metadata[key] = value
def addSection(self, key, value):
self.sections[key] = value
def computeDataPatterns(self):
if self.basePattern == None:
self.updateBasePattern()
for name in self.data:
dataPattern = self.data[name]["pattern"]
if "{pattern}" in dataPattern:
dataPattern = dataPattern.replace("{pattern}", self.basePattern)
self.data[name]["pattern"] = dataPattern
def __getattr__(self, name):
if self.basePattern == None:
self.updateBasePattern()
for i in range(len(self.arguments[name]["values"])):
self.current[name] = i
yield self.realValues[name][i]
def writeDataDescriptor(self):
if not self.can_write:
return
self.computeDataPatterns()
jsonData = {
"arguments_order": self.argOrder,
"type": self.types,
"arguments": self.arguments,
"metadata": self.metadata,
"data": [],
}
# Add sections
for key, value in iteritems(self.sections):
jsonData[key] = value
# Add data
for key, value in iteritems(self.data):
jsonData["data"].append(value)
filePathToWrite = os.path.join(self.__root, "index.json")
with open(filePathToWrite, "w") as fileToWrite:
fileToWrite.write(json.dumps(jsonData))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,788 @@
r"""
This module provides some testing functionality for paraview and
vtk web applications. It provides the ability to run an arbitrary
test script in a separate thread and communicate the results back
to the service so that the CTest framework can be notified of the
success or failure of the test.
This test harness will notice when the test script has finished
running and will notify the service to stop. At this point, the
test results will be checked in the main thread which ran the
service, and in the case of failure an exception will be raised
to notify CTest of the failure.
Test scripts need to follow some simple rules in order to work
within the test harness framework:
1) implement a function called "runTest(args)", where the args
parameter contains all the arguments given to the web application
upon starting. Among other important items, args will contain the
port number where the web application is listening.
2) import the testing module so that the script has access to
the functions which indicate success and failure. Also the
testing module contains convenience functions that might be of
use to the test scripts.
from vtk.web import testing
3) Call the "testPass(testName)" or "testFail(testName)" functions
from within the runTest(args) function to indicate to the framework
whether the test passed or failed.
"""
import_warning_info = ""
test_module_comm_queue = None
from vtkmodules.vtkTestingRendering import vtkTesting
# Try standard Python imports
try:
import os, re, time, datetime, threading, imp, inspect, Queue, types, io
except:
import_warning_info += "\nUnable to load at least one basic Python module"
# Image comparison imports
try:
try:
from PIL import Image
except ImportError:
import Image
except:
raise
import base64
import itertools
except:
import_warning_info += (
"\nUnable to load at least one modules necessary for image comparison"
)
# Browser testing imports
try:
import selenium
from selenium import webdriver
except:
import_warning_info += (
"\nUnable to load at least one module necessary for browser tests"
)
# HTTP imports
try:
import requests
except:
import_warning_info += (
"\nUnable to load at least one module necessary for HTTP tests"
)
# Define some infrastructure to support different (or no) browsers
test_module_browsers = ["firefox", "chrome", "internet_explorer", "safari", "nobrowser"]
class TestModuleBrowsers:
firefox, chrome, internet_explorer, safari, nobrowser = range(5)
# =============================================================================
# We can use this exception type to indicate that the test shouldn't actually
# "fail", rather that it was unable to run because some dependencies were not
# met.
# =============================================================================
class DependencyError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
# =============================================================================
# This class allows usage as a dictionary and an object with named property
# access.
# =============================================================================
class Dictionary(dict):
def __getattribute__(self, attrName):
return self[attrName]
def __setattr__(self, attrName, attrValue):
self[attrName] = attrValue
# =============================================================================
# Checks whether test script supplied, if so, safely imports needed modules
# =============================================================================
def initialize(opts, reactor=None, cleanupMethod=None):
"""
This function should be called to initialize the testing module. The first
important thing it does is to store the options for later, since the
startTestThread function will need them. Then it checks the arguments that
were passed into the server to see if a test was actually requested, making
a note of this fact. Then, if a test was required, this function then
checks if all the necessary testing modules were safely imported, printing
a warning if not. If tests were requested and all modules were present,
then this function sets "test_module_do_testing" to True and sets up the
startTestThread function to be called after the reactor is running.
opts: Parsed arguments from the server
reactor: This argument is optional, but is used by server.py to
cause the test thread to be started only after the server itself
has started. If it is not provided, the test thread is launched
immediately.
cleanupMethod: A callback method you would like the test thread
to execute when the test has finished. This is used by server.py
as a way to have the server terminated after the test has finished,
but could be used for other cleanup purposes. This argument is
also optional.
"""
global import_warning_info
global testModuleOptions
testModuleOptions = Dictionary()
# Copy the testing options into something we can easily extend
for arg in vars(opts):
optValue = getattr(opts, arg)
testModuleOptions[arg] = optValue
# If we got one, add the cleanup method to the testing options
if cleanupMethod:
testModuleOptions["cleanupMethod"] = cleanupMethod
# Check if a test was actually requested
if (
testModuleOptions.testScriptPath != ""
and testModuleOptions.testScriptPath is not None
):
# Check if we ran into trouble with any of the testing imports
if import_warning_info != "":
print("WARNING: Some tests may have unmet dependencies")
print(import_warning_info)
if reactor is not None:
# Add startTest callback to the reactor callback queue, so that
# the test thread gets started after the reactor is running. Of
# course this should only happen if everything is good for tests.
reactor.callWhenRunning(_start_test_thread)
else:
# Otherwise, our aim is to start the thread from another process
# so just call the start method.
_start_test_thread()
# =============================================================================
# Grab out the command-line arguments needed for by the testing module.
# =============================================================================
def add_arguments(parser):
"""
This function retrieves any command-line arguments that the client-side
tester needs. In order to run a test, you will typically just need the
following:
--run-test-script => This should be the full path to the test script to
be run.
--baseline-img-dir => This should be the 'Baseline' directory where the
baseline images for this test are located.
--test-use-browser => This should be one of the supported browser types,
or else 'nobrowser'. The choices are 'chrome', 'firefox', 'internet_explorer',
'safari', or 'nobrowser'.
"""
parser.add_argument(
"--run-test-script",
default="",
help="The path to a test script to run",
dest="testScriptPath",
)
parser.add_argument(
"--baseline-img-dir",
default="",
help="The path to the directory containing the web test baseline images",
dest="baselineImgDir",
)
parser.add_argument(
"--test-use-browser",
default="nobrowser",
help="One of 'chrome', 'firefox', 'internet_explorer', 'safari', or 'nobrowser'.",
dest="useBrowser",
)
parser.add_argument(
"--temporary-directory",
default=".",
help="A temporary directory for storing test images and diffs",
dest="tmpDirectory",
)
parser.add_argument(
"--test-image-file-name",
default="",
help="Name of file in which to store generated test image",
dest="testImgFile",
)
# =============================================================================
# Initialize the test client
# =============================================================================
def _start_test_thread():
"""
This function checks whether testing is required and if so, sets up a Queue
for the purpose of communicating with the thread. then it starts the
after waiting 5 seconds for the server to have a chance to start up.
"""
global test_module_comm_queue
test_module_comm_queue = Queue.Queue()
t = threading.Thread(
target=launch_web_test,
args=[],
kwargs={
"serverOpts": testModuleOptions,
"commQueue": test_module_comm_queue,
"testScript": testModuleOptions.testScriptPath,
},
)
t.start()
# =============================================================================
# Test scripts call this function to indicate passage of their test
# =============================================================================
def test_pass(testName):
"""
Test scripts should call this function to indicate that the test passed. A
note is recorded that the test succeeded, and is checked later on from the
main thread so that CTest can be notified of this result.
"""
global test_module_comm_queue
resultObj = {testName: "pass"}
test_module_comm_queue.put(resultObj)
# =============================================================================
# Test scripts call this function to indicate failure of their test
# =============================================================================
def test_fail(testName):
"""
Test scripts should call this function to indicate that the test failed. A
note is recorded that the test did not succeed, and this note is checked
later from the main thread so that CTest can be notified of the result.
The main thread is the only one that can signal test failure in
CTest framework, and the main thread won't have a chance to check for
passage or failure of the test until the main loop has terminated. So
here we just record the failure result, then we check this result in the
processTestResults() function, throwing an exception at that point to
indicate to CTest that the test failed.
"""
global test_module_comm_queue
resultObj = {testName: "fail"}
test_module_comm_queue.put(resultObj)
# =============================================================================
# Concatenate any number of strings into a single path string.
# =============================================================================
def concat_paths(*pathElts):
"""
A very simple convenience function so that test scripts can build platform
independent paths out of a list of elements, without having to import the
os module.
pathElts: Any number of strings which should be concatenated together
in a platform independent manner.
"""
return os.path.join(*pathElts)
# =============================================================================
# So we can change our time format in a single place, this function is
# provided.
# =============================================================================
def get_current_time_string():
"""
This function returns the current time as a string, using ISO 8601 format.
"""
return datetime.datetime.now().isoformat(" ")
# =============================================================================
# Uses vtkTesting to compare images. According to comments in the vtkTesting
# C++ code (and this seems to work), if there are multiple baseline images in
# the same directory as the baseline_img, and they follow the naming pattern:
# 'img.png', 'img_1.png', ... , 'img_N.png', then all of these images will be
# tried for a match.
# =============================================================================
def compare_images(test_img, baseline_img, tmp_dir="."):
"""
This function creates a vtkTesting object, and specifies the name of the
baseline image file, using a fully qualified path (baseline_img must be
fully qualified). Then it calls the vtkTesting method which compares the
image (test_img, specified only with a relative path) against the baseline
image as well as any other images in the same directory as the baseline
image which follow the naming pattern: 'img.png', 'img_1.png', ... , 'img_N.png'
test_img: File name of output image to be compared against baseline.
baseline_img: Fully qualified path to first of the baseline images.
tmp_dir: Fully qualified path to a temporary directory for storing images.
"""
# Create a vtkTesting object and specify a baseline image
t = vtkTesting()
t.AddArgument("-T")
t.AddArgument(tmp_dir)
t.AddArgument("-V")
t.AddArgument(baseline_img)
# Perform the image comparison test and print out the result.
return t.RegressionTest(test_img, 0.05)
# =============================================================================
# Provide a wait function
# =============================================================================
def wait_with_timeout(delay=None, limit=0, criterion=None):
"""
This function provides the ability to wait for a certain number of seconds,
or else to wait for a specific criterion to be met.
"""
for i in itertools.count():
if criterion is not None and criterion():
return True
elif delay * i > limit:
return False
else:
time.sleep(delay)
# =============================================================================
# Define a WebTest class with five stages of testing: initialization, setup,
# capture, postprocess, and cleanup.
# =============================================================================
class WebTest(object):
"""
This is the base class for all automated web-based tests. It defines five
stages that any test must run through, and allows any or all of these
stages to be overridden by subclasses. This class defines the run_test
method to invoke the five stages overridden by subclasses, one at a time:
1) initialize, 2) setup, 3) capture, 4) postprocess, and 5) cleanup.
"""
class Abort:
pass
def __init__(self, url=None, testname=None, **kwargs):
self.url = url
self.testname = testname
def run_test(self):
try:
self.checkdependencies()
self.initialize()
self.setup()
self.capture()
self.postprocess()
except WebTest.Abort:
# Placeholder for future option to return failure result
pass
except:
self.cleanup()
raise
self.cleanup()
def checkdependencies(self):
pass
def initialize(self):
pass
def setup(self):
pass
def capture(self):
pass
def postprocess(self):
pass
def cleanup(self):
pass
# =============================================================================
# Define a WebTest subclass designed specifically for browser-based tests.
# =============================================================================
class BrowserBasedWebTest(WebTest):
"""
This class can be used as a base for any browser-based web tests. It
introduces the notion of a selenium browser and overrides phases (1) and
(3), initialization and cleanup, of the test phases introduced in the base
class. Initialization involves selecting the browser type, setting the
browser window size, and asking the browser to load the url. Cleanup
involves closing the browser window.
"""
def __init__(self, size=None, browser=None, **kwargs):
self.size = size
self.browser = browser
self.window = None
WebTest.__init__(self, **kwargs)
def initialize(self):
try:
if self.browser is None or self.browser == TestModuleBrowsers.chrome:
self.window = webdriver.Chrome()
elif self.browser == TestModuleBrowsers.firefox:
self.window = webdriver.Firefox()
elif self.browser == TestModuleBrowsers.internet_explorer:
self.window = webdriver.Ie()
else:
raise DependencyError(
"self.browser argument has illegal value %r" % (self.browser)
)
except DependencyError as dErr:
raise
except Exception as inst:
raise DependencyError(inst)
if self.size is not None:
self.window.set_window_size(self.size[0], self.size[1])
if self.url is not None:
self.window.get(self.url)
def cleanup(self):
try:
self.window.quit()
except:
print(
"Unable to call window.quit, perhaps this is expected because of unmet browser dependency."
)
# =============================================================================
# Extend BrowserBasedWebTest to handle vtk-style image comparison
# =============================================================================
class ImageComparatorWebTest(BrowserBasedWebTest):
"""
This class extends browser based web tests to include image comparison. It
overrides the capture phase of testing with some functionality to simply
grab a screenshot of the entire browser window. It overrides the
postprocess phase with a call to vtk image comparison functionality.
Derived classes can then simply override the setup function with a series
of selenium-based browser interactions to create a complete test. Derived
classes may also prefer to override the capture phase to capture only
certain portions of the browser window for image comparison.
"""
def __init__(self, filename=None, baseline=None, temporaryDir=None, **kwargs):
if filename is None:
raise TypeError("missing argument 'filename'")
if baseline is None:
raise TypeError("missing argument 'baseline'")
BrowserBasedWebTest.__init__(self, **kwargs)
self.filename = filename
self.baseline = baseline
self.tmpDir = temporaryDir
def capture(self):
self.window.save_screenshot(self.filename)
def postprocess(self):
result = compare_images(self.filename, self.baseline, self.tmpDir)
if result == 1:
test_pass(self.testname)
else:
test_fail(self.testname)
# =============================================================================
# Given a css selector to use in finding the image element, get the element,
# then base64 decode the "src" attribute and return it.
# =============================================================================
def get_image_data(browser, cssSelector):
"""
This function takes a selenium browser and a css selector string and uses
them to find the target HTML image element. The desired image element
should contain it's image data as a Base64 encoded JPEG image string.
The 'src' attribute of the image is read, Base64-decoded, and then
returned.
browser: A selenium browser instance, as created by webdriver.Chrome(),
for example.
cssSelector: A string containing a CSS selector which will be used to
find the HTML image element of interest.
"""
# Here's maybe a better way to get at that image element
imageElt = browser.find_element_by_css_selector(cssSelector)
# Now get the Base64 image string and decode it into image data
base64String = imageElt.get_attribute("src")
b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)")
b64Matcher = b64RegEx.match(base64String)
imgdata = base64.b64decode(b64Matcher.group(1))
return imgdata
# =============================================================================
# Combines a variation on above function with the write_image_to_disk function.
# converting jpg to png in the process, if necessary.
# =============================================================================
def save_image_data_as_png(browser, cssSelector, imgfilename):
"""
This function takes a selenium browser instance, a css selector string,
and a file name. It uses the css selector string to finds the target HTML
Image element, which should contain a Base64 encoded JPEG image string,
it decodes the string to image data, and then saves the data to the file.
The image type of the written file is determined from the extension of the
provided filename.
browser: A selenium browser instance as created by webdriver.Chrome(),
for example.
cssSelector: A string containing a CSS selector which will be used to
find the HTML image element of interest.
imgFilename: The filename to which to save the image. The extension is
used to determine the type of image which should be saved.
"""
imageElt = browser.find_element_by_css_selector(cssSelector)
base64String = imageElt.get_attribute("src")
b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)")
b64Matcher = b64RegEx.match(base64String)
img = Image.open(io.BytesIO(base64.b64decode(b64Matcher.group(1))))
img.save(imgfilename)
# =============================================================================
# Given a decoded image and the full path to a file, write the image to the
# file.
# =============================================================================
def write_image_to_disk(imgData, filePath):
"""
This function takes an image data, as returned by this module's
get_image_data() function for example, and writes it out to the file given by
the filePath parameter.
imgData: An image data object
filePath: The full path, including the file name and extension, where
the image should be written.
"""
with open(filePath, "wb") as f:
f.write(imgData)
# =============================================================================
# There could be problems if the script file has more than one class defn which
# is a subclass of vtk.web.testing.WebTest, so we should write some
# documentation to help people avoid that.
# =============================================================================
def instantiate_test_subclass(pathToScript, **kwargs):
"""
This function takes the fully qualified path to a test file, along with
any needed keyword arguments, then dynamically loads the file as a module
and finds the test class defined inside of it via inspection. It then
uses the keyword arguments to instantiate the test class and return the
instance.
pathToScript: Fully qualified path to python file containing defined
subclass of one of the test base classes.
kwargs: Keyword arguments to be passed to the constructor of the
testing subclass.
"""
# Load the file as a module
moduleName = imp.load_source("dynamicTestModule", pathToScript)
instance = None
# Inspect dynamically loaded module members
for name, obj in inspect.getmembers(moduleName):
# Looking for classes only
if inspect.isclass(obj):
instance = obj.__new__(obj)
# And only classes defined in the dynamically loaded module
if instance.__module__ == "dynamicTestModule":
try:
instance.__init__(**kwargs)
break
except Exception as inst:
print("Caught exception: " + str(type(inst)))
print(inst)
raise
return instance
# =============================================================================
# For testing purposes, define a function which can interact with a running
# paraview or vtk web application service.
# =============================================================================
def launch_web_test(*args, **kwargs):
"""
This function loads a python file as a module (with no package), and then
instantiates the class it must contain, and finally executes the run_test()
method of the class (which the class may override, but which is defined in
both of the testing base classes, WebTest and ImageComparatorBaseClass).
After the run_test() method finishes, this function will stop the web
server if required. This function expects some keyword arguments will be
present in order for it to complete it's task:
kwargs['serverOpts']: An object containing all the parameters used
to start the web service. Some of them will be used in the test script
in order perform the test. For example, the port on which the server
was started will be required in order to connect to the server.
kwargs['testScript']: The full path to the python file containing the
testing subclass.
"""
serverOpts = None
testScriptFile = None
# This is really the thing all test scripts will need: access to all
# the options used to start the server process.
if "serverOpts" in kwargs:
serverOpts = kwargs["serverOpts"]
# print 'These are the serverOpts we got: '
# print serverOpts
# Get the full path to the test script
if "testScript" in kwargs:
testScriptFile = kwargs["testScript"]
testName = "unknown"
# Check for a test file (python file)
if testScriptFile is None:
print("No test script file found, no test script will be run.")
test_fail(testName)
# The test name will be generated from the python script name, so
# match and capture a bunch of contiguous characters which are
# not '.', '\', or '/', followed immediately by the string '.py'.
fnamePattern = re.compile("([^\.\/\\\]+)\.py")
fmatch = re.search(fnamePattern, testScriptFile)
if fmatch:
testName = fmatch.group(1)
else:
print(
"Unable to parse testScriptFile ("
+ str(testScriptfile)
+ "), no test will be run"
)
test_fail(testName)
# If we successfully got a test name, we are ready to try and run the test
if testName != "unknown":
# Output file and baseline file names are generated from the test name
imgFileName = testName + ".png"
knownGoodFileName = concat_paths(serverOpts.baselineImgDir, imgFileName)
tempDir = serverOpts.tmpDirectory
testImgFileName = serverOpts.testImgFile
testBrowser = test_module_browsers.index(serverOpts.useBrowser)
# Now try to instantiate and run the test
try:
testInstance = instantiate_test_subclass(
testScriptFile,
testname=testName,
host=serverOpts.host,
port=serverOpts.port,
browser=testBrowser,
filename=testImgFileName,
baseline=knownGoodFileName,
temporaryDir=tempDir,
)
# If we were able to instantiate the test, run it, otherwise we
# consider it a failure.
if testInstance is not None:
try:
testInstance.run_test()
except DependencyError as derr:
# TODO: trigger return SKIP_RETURN_CODE when CMake 3 is required
print(
"Some dependency of this test was not met, allowing it to pass"
)
test_pass(testName)
else:
print("Unable to instantiate test instance, failing test")
test_fail(testName)
return
except Exception as inst:
import sys, traceback
tb = sys.exc_info()[2]
print("Caught an exception while running test script:")
print(" " + str(type(inst)))
print(" " + str(inst))
print(" " + "".join(traceback.format_tb(tb)))
test_fail(testName)
# If we were passed a cleanup method to run after testing, invoke it now
if "cleanupMethod" in serverOpts:
serverOpts["cleanupMethod"]()
# =============================================================================
# To keep the service module clean, we'll process the test results here, given
# the test result object we generated in "launch_web_test". It is
# passed back to this function after the service has completed. Failure of
# of the test is indicated by raising an exception in here.
# =============================================================================
def finalize():
"""
This function checks the module's global test_module_comm_queue variable for a
test result. If one is found and the result is 'fail', then this function
raises an exception to communicate the failure to the CTest framework.
In order for a test result to be found in the test_module_comm_queue variable,
the test script must have called either the testPass or testFail functions
provided by this test module before returning.
"""
global test_module_comm_queue
if test_module_comm_queue is not None:
resultObject = test_module_comm_queue.get()
failedATest = False
for testName in resultObject:
testResult = resultObject[testName]
if testResult == "fail":
print(" Test -> " + testName + ": " + testResult)
failedATest = True
if failedATest is True:
raise Exception(
"At least one of the requested tests failed. "
+ "See detailed output, above, for more information"
)
@@ -0,0 +1,211 @@
try:
import numpy as np
except ImportError:
raise ImportError(
"This module depends on the numpy module. Please make\
sure that it is installed properly."
)
import base64
from vtkmodules.util.numpy_support import vtk_to_numpy
from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter
# Numpy to JS TypedArray
to_js_type = {
"int8": "Int8Array",
"uint8": "Uint8Array",
"int16": "Int16Array",
"uint16": "Uint16Array",
"int32": "Int32Array",
"uint32": "Uint32Array",
"int64": "Int32Array",
"uint64": "Uint32Array",
"float32": "Float32Array",
"float64": "Float64Array",
}
def b64_encode_numpy(obj):
# Convert 1D numpy arrays with numeric types to memoryviews with
# datatype and shape metadata.
if len(obj) == 0:
return obj.tolist()
dtype = obj.dtype
if dtype.kind == "f":
return np_encode(obj)
elif dtype.kind == "b":
return np_encode(obj, np.uint8)
elif dtype.kind in ["u", "i"]:
# Try to see if we can downsize the array
max_value = np.amax(obj)
min_value = np.amin(obj)
signed = min_value < 0
test_value = max(max_value, -min_value)
if signed:
if test_value < np.iinfo(np.int8):
return np_encode(obj, np.int8)
if test_value < np.iinfo(np.int16).max:
return np_encode(obj, np.int16)
if test_value < np.iinfo(np.int32).max:
return np_encode(obj, np.int32)
else:
if test_value < np.iinfo(np.uint8).max:
return np_encode(obj, np.uint8)
if test_value < np.iinfo(np.uint16).max:
return np_encode(obj, np.uint16)
if test_value < np.iinfo(np.uint32).max:
return np_encode(obj, np.uint32)
# Convert all other numpy arrays to lists
return obj.tolist()
def np_encode(array, np_type=None):
if np_type:
n_array = array.astype(np_type).ravel(order="C")
return {
"bvals": base64.b64encode(memoryview(n_array)).decode("utf-8"),
"dtype": str(n_array.dtype),
"shape": list(array.shape),
}
return {
"bvals": base64.b64encode(memoryview(array.ravel(order="C"))).decode("utf-8"),
"dtype": str(array.dtype),
"shape": list(array.shape),
}
def mesh_array(array):
if array:
return b64_encode_numpy(vtk_to_numpy(array.GetData()))
def data_array(data_array, location="PointData", name=None):
if data_array:
dataRange = data_array.GetRange(-1)
nb_comp = data_array.GetNumberOfComponents()
values = vtk_to_numpy(data_array)
js_types = to_js_type[str(values.dtype)]
return {
"name": name if name else data_array.GetName(),
"values": b64_encode_numpy(values),
"numberOfComponents": nb_comp,
"type": js_types,
"location": location,
"dataRange": dataRange,
}
def field_data(field_data, names, location="PointData"):
fields = []
for name in names:
array = field_data.GetArray(name)
js_array = data_array(array, location, name)
if js_array:
fields.append(js_array)
return fields
def mesh(dataset, field_to_keep=None, point_arrays=None, cell_arrays=None):
"""Expect any dataset and extract its surface into a dash_vtk.Mesh state property"""
if dataset is None:
return None
# Make sure we have a polydata to export
polydata = None
if dataset.IsA("vtkPolyData"):
polydata = dataset
else:
extractSkinFilter = vtkDataSetSurfaceFilter()
extractSkinFilter.SetInputData(dataset)
extractSkinFilter.Update()
polydata = extractSkinFilter.GetOutput()
if polydata.GetPoints() is None:
return None
# Extract mesh
state = {"mesh": {}}
points = mesh_array(polydata.GetPoints())
if points:
state["mesh"]["points"] = points
verts = mesh_array(polydata.GetVerts())
if verts:
state["mesh"]["verts"] = verts
lines = mesh_array(polydata.GetLines())
if lines:
state["mesh"]["lines"] = lines
polys = mesh_array(polydata.GetPolys())
if polys:
state["mesh"]["polys"] = polys
strips = mesh_array(polydata.GetStrips())
if strips:
state["mesh"]["strips"] = strips
# Scalars
if field_to_keep is not None:
field = None
p_array = polydata.GetPointData().GetArray(field_to_keep)
c_array = polydata.GetCellData().GetArray(field_to_keep)
if c_array:
field = data_array(c_array, location="CellData", name=field_to_keep)
if p_array:
field = data_array(p_array, location="PointData", name=field_to_keep)
if field:
state.update({"field": field})
# PointData Fields
if point_arrays:
point_data = field_data(polydata.GetPointData(), point_arrays, "PointData")
if len(point_data):
state.update({"pointArrays": point_data})
# CellData Fields
if cell_arrays:
cell_data = field_data(polydata.GetCellData(), cell_arrays, "CellData")
if len(cell_data):
state.update({"cellArrays": cell_data})
return state
def volume(dataset):
"""Expect a vtkImageData and extract its setting for the dash_vtk.Volume state"""
if dataset is None or not dataset.IsA("vtkImageData"):
return None
state = {
"image": {
"dimensions": dataset.GetDimensions(),
"spacing": dataset.GetSpacing(),
"origin": dataset.GetOrigin(),
},
}
# Capture image orientation if any
if hasattr(dataset, "GetDirectionMatrix"):
matrix = dataset.GetDirectionMatrix()
js_mat = []
for j in range(3):
for i in range(3):
js_mat.append(matrix.GetElement(i, j))
state["image"]["direction"] = js_mat
scalars = dataset.GetPointData().GetScalars()
field = data_array(scalars, location="PointData")
if field:
state["field"] = field
return state
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
"""Activate venv for current interpreter:
Use `from vtk.web import venv` along one of the following
- `--venv /path/to/venv/base` argument
- environment variable `VTK_VENV=/path/to/venv/base`
This can be used when you must use an existing Python interpreter, not the venv bin/python.
"""
import os
import site
import sys
VENV_BASE = None
VENV_LOADED = False
if "--venv" in sys.argv:
VENV_BASE = os.path.abspath(sys.argv[sys.argv.index("--venv") + 1])
if os.environ.get("VTK_VENV"):
VENV_BASE = os.path.abspath(os.environ.get("VTK_VENV"))
if not VENV_LOADED and VENV_BASE and os.path.exists(VENV_BASE):
VENV_LOADED = True
# Code inspired by virutal-env::bin/active_this.py
bin_dir = os.path.join(VENV_BASE, "bin")
os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep))
os.environ["VIRTUAL_ENV"] = VENV_BASE
prev_length = len(sys.path)
python_libs = os.path.join(VENV_BASE, f"lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages")
site.addsitedir(python_libs)
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
sys.real_prefix = sys.prefix
sys.prefix = VENV_BASE
#
print(f"VTK is using venv: {VENV_BASE}")
@@ -0,0 +1,283 @@
import base64
import json
import re
import os
import shutil
import sys
import zipfile
try:
import zlib
compression = zipfile.ZIP_DEFLATED
except:
compression = zipfile.ZIP_STORED
# -----------------------------------------------------------------------------
def convertDirectoryToZipFile(directoryPath):
if os.path.isfile(directoryPath):
return
zipFilePath = "%s.zip" % directoryPath
zf = zipfile.ZipFile(zipFilePath, mode="w")
try:
for dirName, subdirList, fileList in os.walk(directoryPath):
for fname in fileList:
fullPath = os.path.join(dirName, fname)
relPath = "%s" % (os.path.relpath(fullPath, directoryPath))
zf.write(fullPath, arcname=relPath, compress_type=compression)
finally:
zf.close()
shutil.rmtree(directoryPath)
shutil.move(zipFilePath, directoryPath)
# -----------------------------------------------------------------------------
def addDataToViewer(dataPath, srcHtmlPath, disableGirder=False):
if os.path.isfile(dataPath) and os.path.exists(srcHtmlPath):
dstDir = os.path.dirname(dataPath)
dstHtmlPath = os.path.join(dstDir, "%s.html" % os.path.basename(dataPath)[:-6])
# Extract data as base64
with open(dataPath, "rb") as data:
dataContent = data.read()
base64Content = base64.b64encode(dataContent)
base64Content = base64Content.decode().replace("\n", "")
# Create new output file
with open(srcHtmlPath, mode="r", encoding="utf-8") as srcHtml:
with open(dstHtmlPath, mode="w", encoding="utf-8") as dstHtml:
for line in srcHtml:
if disableGirder and "</title>" in line:
dstHtml.write(
"""
<script>
// Force reloading the page if we want to disable girder before anything else.
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('noGirder') != 'true') {
urlParams.set('noGirder', 'true');
window.location.search = urlParams;
}
</script>
"""
)
if "</body>" in line:
dstHtml.write("<script>\n")
dstHtml.write('var contentToLoad = "%s";\n\n' % base64Content)
dstHtml.write(
'Glance.importBase64Dataset("%s" , contentToLoad, glanceInstance.proxyManager);\n'
% os.path.basename(dataPath)
)
dstHtml.write("glanceInstance.showApp();\n")
dstHtml.write("</script>\n")
dstHtml.write(line)
# -----------------------------------------------------------------------------
def numericSorted(l):
"""Numerically sort a list of strings."""
# pattern to split name into numeric and non-numeric parts
splitter_pattern = re.compile('([0-9]+|[^0-9]+)')
def keyfunc(name):
"""Sorting key for numeric sorting."""
split_name = re.findall(splitter_pattern, name)
# one-liner to convert numeric parts into integers
split_name = list(map(lambda x: int(x) if x.isdigit() else x, split_name))
# ensure that list begins with a string to avoid string<->int compare
if split_name and isinstance(split_name[0], int):
split_name.insert(0, '')
return split_name
# return the numerically sorted list
return sorted(l, key=keyfunc)
# -----------------------------------------------------------------------------
def zipAllTimeSteps(directoryPath):
if os.path.isfile(directoryPath):
return
class UrlCounterDict(dict):
Counter = 0
def GetUrlName(self, name):
if name not in self.keys():
self[name] = str(objNameToUrls.Counter)
self.Counter = self.Counter + 1
return self[name]
def InitIndex(sourcePath, destObj):
with open(sourcePath, "r") as sourceFile:
sourceData = sourceFile.read()
sourceObj = json.loads(sourceData)
for key in sourceObj:
destObj[key] = sourceObj[key]
# remove vtkHttpDataSetReader information
for obj in destObj["scene"]:
obj.pop(obj["type"])
obj.pop("type")
def getUrlToNameDictionary(indexObj):
urls = {}
for obj in indexObj["scene"]:
urls[obj[obj["type"]]["url"]] = obj["name"]
return urls
def addDirectoryToZip(
dirname, zipobj, storedData, rootIdx, timeStep, objNameToUrls
):
# Update root index.json file from index.json of this timestep
with open(os.path.join(dirname, "index.json"), "r") as currentIdxFile:
currentIdx = json.loads(currentIdxFile.read())
urlToName = getUrlToNameDictionary(currentIdx)
rootTimeStepSection = rootIdx["animation"]["timeSteps"][timeStep]
for key in currentIdx:
if key == "scene" or key == "version":
continue
rootTimeStepSection[key] = currentIdx[key]
for obj in currentIdx["scene"]:
objName = obj["name"]
rootTimeStepSection[objName] = {}
rootTimeStepSection[objName]["actor"] = obj["actor"]
rootTimeStepSection[objName]["actorRotation"] = obj["actorRotation"]
rootTimeStepSection[objName]["mapper"] = obj["mapper"]
rootTimeStepSection[objName]["property"] = obj["property"]
# For every object in the current timestep
for folder in sorted(os.listdir(dirname)):
currentItem = os.path.join(dirname, folder)
if os.path.isdir(currentItem) is False:
continue
# Write all data array of the current timestep in the archive
for filename in os.listdir(os.path.join(currentItem, "data")):
fullpath = os.path.join(currentItem, "data", filename)
if os.path.isfile(fullpath) and filename not in storedData:
storedData.add(filename)
relPath = os.path.join("data", filename)
zipobj.write(fullpath, arcname=relPath, compress_type=compression)
# Write the index.json containing pointers to these data arrays
# while replacing every basepath as '../../data'
objIndexFilePath = os.path.join(dirname, folder, "index.json")
with open(objIndexFilePath, "r") as objIndexFile:
objIndexObjData = json.loads(objIndexFile.read())
for elm in objIndexObjData.keys():
try:
if "ref" in objIndexObjData[elm].keys():
objIndexObjData[elm]["ref"]["basepath"] = "../../data"
if "arrays" in objIndexObjData[elm].keys():
for array in objIndexObjData[elm]["arrays"]:
array["data"]["ref"]["basepath"] = "../../data"
except AttributeError:
continue
currentObjName = urlToName[folder]
objIndexRelPath = os.path.join(
objNameToUrls.GetUrlName(currentObjName), str(timeStep), "index.json"
)
zipobj.writestr(
objIndexRelPath,
json.dumps(objIndexObjData, indent=2),
compress_type=compression,
)
# ---
zipFilePath = "%s.zip" % directoryPath
currentDirectory = os.path.abspath(os.path.join(directoryPath, os.pardir))
rootIndexPath = os.path.join(currentDirectory, "index.json")
rootIndexFile = open(rootIndexPath, "r")
rootIndexObj = json.loads(rootIndexFile.read())
zf = zipfile.ZipFile(zipFilePath, mode="w")
try:
# We copy the scene from an index of a specific timestep to the root index
# Scenes should all have the same objects so only do it for the first one
isSceneInitialized = False
# currentlyAddedData set stores hashes of every data we already added to the
# vtkjs archive to prevent data duplication
currentlyAddedData = set()
# Regex that folders storing timestep data from paraview should follow
reg = re.compile(r"^" + os.path.basename(directoryPath) + r"\.[0-9]+$")
# We assume an object will not be deleted from a timestep to another so we create a generic index.json for each object
genericIndexObj = {}
genericIndexObj["series"] = []
timeStep = 0
for item in rootIndexObj["animation"]["timeSteps"]:
genericIndexObj["series"].append({})
genericIndexObj["series"][timeStep]["url"] = str(timeStep)
genericIndexObj["series"][timeStep]["timeStep"] = float(item["time"])
timeStep = timeStep + 1
# Keep track of the url for every object
objNameToUrls = UrlCounterDict()
timeStep = 0
# zip all timestep directories
for folder in numericSorted(os.listdir(currentDirectory)):
fullPath = os.path.join(currentDirectory, folder)
if os.path.isdir(fullPath) and reg.match(folder):
if not isSceneInitialized:
InitIndex(os.path.join(fullPath, "index.json"), rootIndexObj)
isSceneInitialized = True
addDirectoryToZip(
fullPath,
zf,
currentlyAddedData,
rootIndexObj,
timeStep,
objNameToUrls,
)
shutil.rmtree(fullPath)
timeStep = timeStep + 1
# Write every index.json holding time information for each object
for name in objNameToUrls:
zf.writestr(
os.path.join(objNameToUrls[name], "index.json"),
json.dumps(genericIndexObj, indent=2),
compress_type=compression,
)
# Update root index.json urls and write it in the archive
for obj in rootIndexObj["scene"]:
obj["id"] = obj["name"]
obj["type"] = "vtkHttpDataSetSeriesReader"
obj["vtkHttpDataSetSeriesReader"] = {}
obj["vtkHttpDataSetSeriesReader"]["url"] = objNameToUrls[obj["name"]]
zf.writestr(
"index.json", json.dumps(rootIndexObj, indent=2), compress_type=compression
)
os.remove(rootIndexPath)
finally:
zf.close()
shutil.move(zipFilePath, directoryPath)
# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------
if __name__ == "__main__":
if len(sys.argv) < 2:
print(
"Usage: directoryToFile /path/to/directory.vtkjs [/path/to/ParaViewGlance.html]"
)
else:
fileName = sys.argv[1]
convertDirectoryToZipFile(fileName)
if len(sys.argv) == 3:
addDataToViewer(fileName, sys.argv[2])
@@ -0,0 +1,67 @@
r"""wslink is a module that extends any
wslink related classes for the purposes of vtkWeb.
"""
from __future__ import absolute_import, division, print_function
# import inspect, types, string, random, logging, six, json, re, base64
import json, base64, logging, time
from vtkmodules.web.errors import WebDependencyMissingError
try:
from wslink import websocket
from wslink import register as exportRpc
except ImportError:
raise WebDependencyMissingError()
from vtkmodules.web import protocols
from vtkmodules.vtkWebCore import vtkWebApplication
# =============================================================================
application = None
# =============================================================================
#
# Base class for vtkWeb ServerProtocol
#
# =============================================================================
class ServerProtocol(websocket.ServerProtocol):
"""
Defines the core server protocol for vtkWeb. Adds support to
marshall/unmarshall RPC callbacks that involve ServerManager proxies as
arguments or return values.
Applications typically don't use this class directly, but instead
sub-class it and call self.registerVtkWebProtocol() with useful vtkWebProtocols.
"""
def __init__(self):
logging.info("Creating SP")
self.setSharedObject("app", self.initApplication())
websocket.ServerProtocol.__init__(self)
def initApplication(self):
"""
Let subclass optionally initialize a custom application in lieu
of the default vtkWebApplication.
"""
global application
if not application:
application = vtkWebApplication()
return application
def setApplication(self, application):
self.setSharedObject("app", application)
def getApplication(self):
return self.getSharedObject("app")
def registerVtkWebProtocol(self, protocol):
self.registerLinkProtocol(protocol)
def getVtkWebProtocols(self):
return self.getLinkProtocols()