Repository: silx-kit/silx
Issue: opengl: bound error

Exception while playing with arc ROIs (whne i have tried to close an arc to a circle)

```
Traceback (most recent call last):
  File "/users/valls/workspace/silx.git/build/lib.linux-x86_64-3.7/silx/gui/_glutils/OpenGLWidget.py", line 224, in paintGL
    parent.paintGL()
  File "/users/valls/workspace/silx.git/build/lib.linux-x86_64-3.7/silx/gui/plot/backends/BackendOpenGL.py", line 405, in paintGL
    self._paintFBOGL()
  File "/users/valls/workspace/silx.git/build/lib.linux-x86_64-3.7/silx/gui/plot/backends/BackendOpenGL.py", line 386, in _paintFBOGL
    self._renderOverlayGL()
  File "/users/valls/workspace/silx.git/build/lib.linux-x86_64-3.7/silx/gui/plot/backends/BackendOpenGL.py", line 640, in _renderOverlayGL
    self._renderItems(overlay=True)
  File "/users/valls/workspace/silx.git/build/lib.linux-x86_64-3.7/silx/gui/plot/backends/BackendOpenGL.py", line 476, in _renderItems
    for (x, y) in zip(item['x'], item['y'])])
  File "/users/valls/workspace/silx.git/build/lib.linux-x86_64-3.7/silx/gui/plot/backends/BackendOpenGL.py", line 476, in <listcomp>
    for (x, y) in zip(item['x'], item['y'])])
  File "/users/valls/workspace/silx.git/build/lib.linux-x86_64-3.7/silx/gui/plot/backends/glutils/GLPlotFrame.py", line 1029, in dataToPixel
    (trBounds.x[1] - trBounds.x[0]))
ValueError: cannot convert float NaN to integer

```

Changed files:
silx/gui/plot/test/testPlotWidget.py, silx/gui/plot/backends/glutils/GLPlotCurve.py, silx/gui/plot/backends/BackendOpenGL.py

diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py
index 2b41ae166..f4845eeec 100755
--- a/silx/gui/plot/backends/BackendOpenGL.py
+++ b/silx/gui/plot/backends/BackendOpenGL.py
@@ -458,8 +458,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
                         0.5 * sum(self._plotFrame.dataRanges[0]),
                         item['y'],
                         axis='left')
-                    points = numpy.array(((0., yPixel), (width, yPixel)),
-                                         dtype=numpy.float32)
+                    subShapes = [numpy.array(((0., yPixel), (width, yPixel)),
+                                             dtype=numpy.float32)]
 
                 elif item['shape'] == 'vline':
                     xPixel, _ = self._plotFrame.dataToPixel(
@@ -467,46 +467,54 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
                         0.5 * sum(self._plotFrame.dataRanges[1]),
                         axis='left')
                     height = self._plotFrame.size[1]
-                    points = numpy.array(((xPixel, 0), (xPixel, height)),
-                                         dtype=numpy.float32)
+                    subShapes = [numpy.array(((xPixel, 0), (xPixel, height)),
+                                             dtype=numpy.float32)]
 
                 else:
-                    points = numpy.array([
-                        self._plotFrame.dataToPixel(x, y, axis='left')
-                        for (x, y) in zip(item['x'], item['y'])])
-
-                # Draw the fill
-                if (item['fill'] is not None and
-                        item['shape'] not in ('hline', 'vline')):
-                    self._progBase.use()
-                    gl.glUniformMatrix4fv(
-                        self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
-                        self.matScreenProj.astype(numpy.float32))
-                    gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
-                    gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
-
-                    shape2D = glutils.FilledShape2D(
-                        points, style=item['fill'], color=item['color'])
-                    shape2D.render(
-                        posAttrib=self._progBase.attributes['position'],
-                        colorUnif=self._progBase.uniforms['color'],
-                        hatchStepUnif=self._progBase.uniforms['hatchStep'])
-
-                # Draw the stroke
-                if item['linestyle'] not in ('', ' ', None):
-                    if item['shape'] != 'polylines':
-                        # close the polyline
-                        points = numpy.append(points,
-                                              numpy.atleast_2d(points[0]), axis=0)
-
-                    lines = glutils.GLLines2D(
-                        points[:, 0], points[:, 1],
-                        style=item['linestyle'],
-                        color=item['color'],
-                        dash2ndColor=item['linebgcolor'],
-                        width=item['linewidth'])
-                    context.matrix = self.matScreenProj
-                    lines.render(context)
+                    # Split sub-shapes at not finite values
+                    splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
+                        numpy.isfinite(item['x']), numpy.isfinite(item['y']))))[0]
+                    splits = numpy.concatenate(([-1], splits, [len(item['x'])]))
+                    subShapes = []
+                    for begin, end in zip(splits[:-1] + 1, splits[1:]):
+                        if end > begin:
+                            subShapes.append(numpy.array([
+                                self._plotFrame.dataToPixel(x, y, axis='left')
+                                for (x, y) in zip(item['x'][begin:end], item['y'][begin:end])]))
+
+                for points in subShapes:  # Draw each sub-shape
+                    # Draw the fill
+                    if (item['fill'] is not None and
+                            item['shape'] not in ('hline', 'vline')):
+                        self._progBase.use()
+                        gl.glUniformMatrix4fv(
+                            self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
+                            self.matScreenProj.astype(numpy.float32))
+                        gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
+                        gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
+
+                        shape2D = glutils.FilledShape2D(
+                            points, style=item['fill'], color=item['color'])
+                        shape2D.render(
+                            posAttrib=self._progBase.attributes['position'],
+                            colorUnif=self._progBase.uniforms['color'],
+                            hatchStepUnif=self._progBase.uniforms['hatchStep'])
+
+                    # Draw the stroke
+                    if item['linestyle'] not in ('', ' ', None):
+                        if item['shape'] != 'polylines':
+                            # close the polyline
+                            points = numpy.append(points,
+                                                  numpy.atleast_2d(points[0]), axis=0)
+
+                        lines = glutils.GLLines2D(
+                            points[:, 0], points[:, 1],
+                            style=item['linestyle'],
+                            color=item['color'],
+                            dash2ndColor=item['linebgcolor'],
+                            width=item['linewidth'])
+                        context.matrix = self.matScreenProj
+                        lines.render(context)
 
             elif isinstance(item, _MarkerItem):
                 gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py
index 5cafe3bc9..c4e2c1e2c 100644
--- a/silx/gui/plot/backends/glutils/GLPlotCurve.py
+++ b/silx/gui/plot/backends/glutils/GLPlotCurve.py
@@ -519,10 +519,26 @@ def distancesFromArrays(xData, yData):
     :param numpy.ndarray yData: Y coordinate of points
     :rtype: numpy.ndarray
     """
-    deltas = numpy.dstack((
-        numpy.ediff1d(xData, to_begin=numpy.float32(0.)),
-        numpy.ediff1d(yData, to_begin=numpy.float32(0.))))[0]
-    return numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1)))
+    # Split array into sub-shapes at not finite points
+    splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
+        numpy.isfinite(xData), numpy.isfinite(yData))))[0]
+    splits = numpy.concatenate(([-1], splits, [len(xData) - 1]))
+
+    # Compute distance independently for each sub-shapes,
+    # putting not finite points as last points of sub-shapes
+    distances = []
+    for begin, end in zip(splits[:-1] + 1, splits[1:] + 1):
+        if begin == end:  # Empty shape
+            continue
+        elif end - begin == 1: # Single element
+            distances.append([0])
+        else:
+            deltas = numpy.dstack((
+                numpy.ediff1d(xData[begin:end], to_begin=numpy.float32(0.)),
+                numpy.ediff1d(yData[begin:end], to_begin=numpy.float32(0.))))[0]
+            distances.append(
+                numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1))))
+    return numpy.concatenate(distances)
 
 
 # points ######################################################################
diff --git a/silx/gui/plot/test/testPlotWidget.py b/silx/gui/plot/test/testPlotWidget.py
index a250c3f39..7a4752301 100755
--- a/silx/gui/plot/test/testPlotWidget.py
+++ b/silx/gui/plot/test/testPlotWidget.py
@@ -43,7 +43,7 @@ from silx.test.utils import test_options
 from silx.gui import qt
 from silx.gui.plot import PlotWidget
 from silx.gui.plot.items.curve import CurveStyle
-from silx.gui.plot.items import BoundingRect, XAxisExtent, YAxisExtent
+from silx.gui.plot.items import BoundingRect, XAxisExtent, YAxisExtent, Axis
 from silx.gui.colors import Colormap
 
 from .utils import PlotWidgetTestCase
@@ -831,17 +831,25 @@ class TestPlotItem(PlotWidgetTestCase):
     """Basic tests for addItem."""
 
     # Polygon coordinates and color
-    polygons = [  # legend, x coords, y coords, color
+    POLYGONS = [  # legend, x coords, y coords, color
         ('triangle', numpy.array((10, 30, 50)),
          numpy.array((55, 70, 55)), 'red'),
         ('square', numpy.array((10, 10, 50, 50)),
          numpy.array((10, 50, 50, 10)), 'green'),
         ('star', numpy.array((60, 70, 80, 60, 80)),
          numpy.array((25, 50, 25, 40, 40)), 'blue'),
+        ('2 triangles-simple',
+         numpy.array((90., 95., 100., numpy.nan, 90., 95., 100.)),
+         numpy.array((25., 5., 25., numpy.nan, 30., 50., 30.)),
+         'pink'),
+        ('2 triangles-extra NaN',
+         numpy.array((numpy.nan, 90., 95., 100., numpy.nan, 0., 90., 95., 100., numpy.nan)),
+         numpy.array((0., 55., 70., 55., numpy.nan, numpy.nan, 75., 90., 75., numpy.nan)),
+         'black'),
     ]
 
     # Rectangle coordinantes and color
-    rectangles = [  # legend, x coords, y coords, color
+    RECTANGLES = [  # legend, x coords, y coords, color
         ('square 1', numpy.array((1., 10.)),
          numpy.array((1., 10.)), 'red'),
         ('square 2', numpy.array((10., 20.)),
@@ -854,6 +862,8 @@ class TestPlotItem(PlotWidgetTestCase):
          numpy.array((45., 45.)), 'darkRed'),
     ]
 
+    SCALES = Axis.LINEAR, Axis.LOGARITHMIC
+
     def setUp(self):
         super(TestPlotItem, self).setUp()
 
@@ -865,40 +875,60 @@ class TestPlotItem(PlotWidgetTestCase):
         self.plot.setLimits(0., 100., -100., 100.)
 
     def testPlotItemPolygonFill(self):
-        self.plot.setGraphTitle('Item Fill')
-
-        for legend, xList, yList, color in self.polygons:
-            self.plot.addShape(xList, yList, legend=legend,
-                               replace=False,
-                               shape="polygon", fill=True, color=color)
-        self.plot.resetZoom()
+        for scale in self.SCALES:
+            with self.subTest(scale=scale):
+                self.plot.clear()
+                self.plot.getXAxis().setScale(scale)
+                self.plot.getYAxis().setScale(scale)
+                self.plot.setGraphTitle('Item Fill %s' % scale)
+
+                for legend, xList, yList, color in self.POLYGONS:
+                    self.plot.addShape(xList, yList, legend=legend,
+                                       replace=False, linestyle='--',
+                                       shape="polygon", fill=True, color=color)
+                self.plot.resetZoom()
 
     def testPlotItemPolygonNoFill(self):
-        self.plot.setGraphTitle('Item No Fill')
-
-        for legend, xList, yList, color in self.polygons:
-            self.plot.addShape(xList, yList, legend=legend,
-                               replace=False,
-                               shape="polygon", fill=False, color=color)
-        self.plot.resetZoom()
+        for scale in self.SCALES:
+            with self.subTest(scale=scale):
+                self.plot.clear()
+                self.plot.getXAxis().setScale(scale)
+                self.plot.getYAxis().setScale(scale)
+                self.plot.setGraphTitle('Item No Fill %s' % scale)
+
+                for legend, xList, yList, color in self.POLYGONS:
+                    self.plot.addShape(xList, yList, legend=legend,
+                                       replace=False, linestyle='--',
+                                       shape="polygon", fill=False, color=color)
+                self.plot.resetZoom()
 
     def testPlotItemRectangleFill(self):
-        self.plot.setGraphTitle('Rectangle Fill')
-
-        for legend, xList, yList, color in self.rectangles:
-            self.plot.addShape(xList, yList, legend=legend,
-                               replace=False,
-                               shape="rectangle", fill=True, color=color)
-        self.plot.resetZoom()
+        for scale in self.SCALES:
+            with self.subTest(scale=scale):
+                self.plot.clear()
+                self.plot.getXAxis().setScale(scale)
+                self.plot.getYAxis().setScale(scale)
+                self.plot.setGraphTitle('Rectangle Fill %s' % scale)
+
+                for legend, xList, yList, color in self.RECTANGLES:
+                    self.plot.addShape(xList, yList, legend=legend,
+                                       replace=False,
+                                       shape="rectangle", fill=True, color=color)
+                self.plot.resetZoom()
 
     def testPlotItemRectangleNoFill(self):
-        self.plot.setGraphTitle('Rectangle No Fill')
-
-        for legend, xList, yList, color in self.rectangles:
-            self.plot.addShape(xList, yList, legend=legend,
-                               replace=False,
-                               shape="rectangle", fill=False, color=color)
-        self.plot.resetZoom()
+        for scale in self.SCALES:
+            with self.subTest(scale=scale):
+                self.plot.clear()
+                self.plot.getXAxis().setScale(scale)
+                self.plot.getYAxis().setScale(scale)
+                self.plot.setGraphTitle('Rectangle No Fill %s' % scale)
+
+                for legend, xList, yList, color in self.RECTANGLES:
+                    self.plot.addShape(xList, yList, legend=legend,
+                                       replace=False,
+                                       shape="rectangle", fill=False, color=color)
+                self.plot.resetZoom()
 
 
 class TestPlotActiveCurveImage(PlotWidgetTestCase):
@@ -1798,82 +1828,6 @@ class TestPlotMarkerLog(PlotWidgetTestCase):
         self.plot.resetZoom()
 
 
-class TestPlotItemLog(PlotWidgetTestCase):
-    """Basic tests for items with log scale axes"""
-
-    # Polygon coordinates and color
-    polygons = [  # legend, x coords, y coords, color
-        ('triangle', numpy.array((10, 30, 50)),
-         numpy.array((55, 70, 55)), 'red'),
-        ('square', numpy.array((10, 10, 50, 50)),
-         numpy.array((10, 50, 50, 10)), 'green'),
-        ('star', numpy.array((60, 70, 80, 60, 80)),
-         numpy.array((25, 50, 25, 40, 40)), 'blue'),
-    ]
-
-    # Rectangle coordinantes and color
-    rectangles = [  # legend, x coords, y coords, color
-        ('square 1', numpy.array((1., 10.)),
-         numpy.array((1., 10.)), 'red'),
-        ('square 2', numpy.array((10., 20.)),
-         numpy.array((10., 20.)), 'green'),
-        ('square 3', numpy.array((20., 30.)),
-         numpy.array((20., 30.)), 'blue'),
-        ('rect 1', numpy.array((1., 30.)),
-         numpy.array((35., 40.)), 'black'),
-        ('line h', numpy.array((1., 30.)),
-         numpy.array((45., 45.)), 'darkRed'),
-    ]
-
-    def setUp(self):
-        super(TestPlotItemLog, self).setUp()
-
-        self.plot.getYAxis().setLabel('Rows')
-        self.plot.getXAxis().setLabel('Columns')
-        self.plot.getXAxis().setAutoScale(False)
-        self.plot.getYAxis().setAutoScale(False)
-        self.plot.setKeepDataAspectRatio(False)
-        self.plot.setLimits(1., 100., 1., 100.)
-        self.plot.getXAxis()._setLogarithmic(True)
-        self.plot.getYAxis()._setLogarithmic(True)
-
-    def testPlotItemPolygonLogFill(self):
-        self.plot.setGraphTitle('Item Fill Log')
-
-        for legend, xList, yList, color in self.polygons:
-            self.plot.addShape(xList, yList, legend=legend,
-                               replace=False,
-                               shape="polygon", fill=True, color=color)
-        self.plot.resetZoom()
-
-    def testPlotItemPolygonLogNoFill(self):
-        self.plot.setGraphTitle('Item No Fill Log')
-
-        for legend, xList, yList, color in self.polygons:
-            self.plot.addShape(xList, yList, legend=legend,
-                               replace=False,
-                               shape="polygon", fill=False, color=color)
-        self.plot.resetZoom()
-
-    def testPlotItemRectangleLogFill(self):
-        self.plot.setGraphTitle('Rectangle Fill Log')
-
-        for legend, xList, yList, color in self.rectangles:
-            self.plot.addShape(xList, yList, legend=legend,
-                               replace=False,
-                               shape="rectangle", fill=True, color=color)
-        self.plot.resetZoom()
-
-    def testPlotItemRectangleLogNoFill(self):
-        self.plot.setGraphTitle('Rectangle No Fill Log')
-
-        for legend, xList, yList, color in self.rectangles:
-            self.plot.addShape(xList, yList, legend=legend,
-                               replace=False,
-                               shape="rectangle", fill=False, color=color)
-        self.plot.resetZoom()
-
-
 class TestPlotWidgetSwitchBackend(PlotWidgetTestCase):
     """Test [get|set]Backend to switch backend"""
 
@@ -1916,8 +1870,7 @@ def suite():
                    TestPlotEmptyLog,
                    TestPlotCurveLog,
                    TestPlotImageLog,
-                   TestPlotMarkerLog,
-                   TestPlotItemLog)
+                   TestPlotMarkerLog)
 
     test_suite = unittest.TestSuite()