def get_shape_type(input_filename):
def get_lines(input_filename, shape_type):
def get_open_polylines(input_filename):
def get_polygons(input_filename):
def get_all_fields(input_filename):
def get_attribute_names(input_filename):
def get_numeric_attribute_names(input_filename):
def get_points(input_filename, indices=None, with_z=False):
def write_shp_points_z(output_filename, z_name, points):
def write_shp_lines(output_filename, shape_type, lines, attribute_name): class FluxPlotViewer(TemporalPlotViewer):
    def __init__(self):
        super().__init__('section')
        self.setWindowTitle('Visualize the temporal evolution of flux')
        self.csv_separator = ''
        self.flux_title = ''
        self.var_IDs = []
        self.cumulative = False
        self.cumulative_flux_act = QAction('Show\ncumulative flux', self, checkable=True,
                                           icon=self.style().standardIcon(QStyle.SP_DialogApplyButton))
        self.cumulative_flux_act.toggled.connect(self.changeFluxType)

        self.current_columns = tuple()  # Has to be initialized manually
        self.poly_menu = QMenu('&Sections', self)
        self.poly_menu.addAction(self.selectColumnsAct_short)
        self.poly_menu.addAction(self.editColumnNamesAct_short)
        self.poly_menu.addAction(self.editColumColorAct_short)

    def changeFluxType(self):
        self.cumulative = not self.cumulative
        self.current_ylabel = 'Cumulative ' + self.flux_title
        self.replot()

    def replot(self):
        self.canvas.axes.clear()
        for column in self.current_columns:
            if not self.cumulative:
                self.canvas.axes.plot(self.time[self.timeFormat], self.data[column], '-',
                                      color=self.column_colors[column],
                                      linewidth=2, label=self.column_labels[column])
            else:
                self.canvas.axes.plot(self.time[self.timeFormat], np.cumsum(self.data[column]), '-',
                                      color=self.column_colors[column],
                                      linewidth=2, label=self.column_labels[column])

        self.canvas.axes.legend()
        self.canvas.axes.grid(linestyle='dotted')
        self.canvas.axes.set_xlabel(self.current_xlabel)
        self.canvas.axes.set_ylabel(self.current_ylabel)
        self.canvas.axes.set_title(self.current_title)
        if self.timeFormat in [1, 2]:
            self.canvas.axes.set_xticklabels(self.str_datetime if self.timeFormat == 1 else self.str_datetime_bis)
            for label in self.canvas.axes.get_xticklabels():
                label.set_rotation(45)
                label.set_fontsize(8)
        self.canvas.draw() class MapCanvas(FigureCanvas):
    def __init__(self, width=settings.MAP_SIZE[0], height=settings.MAP_SIZE[1], dpi=settings.MAP_OUT_DPI):
        self.BLACK = '#a9a9a9'

        self.fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = self.fig.add_subplot(111)

        FigureCanvas.__init__(self, self.fig)
        self.setParent(None)

        FigureCanvas.setSizePolicy(self,
                                   QSizePolicy.Expanding,
                                   QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)

    def initFigure(self, mesh):
        self.axes.clear()
        self.axes.set_xlabel(settings.X_AXIS_LABEL)
        self.axes.set_ylabel(settings.Y_AXIS_LABEL)
        self.axes.triplot(mesh.x, mesh.y, mesh.ikle, '--', color=self.BLACK, alpha=0.5, lw=0.3)
        self.axes.set_aspect('equal', adjustable='box')
        self.draw() class MultiFolderDialog(QFileDialog):
    """
    Dialog to select one or more folders
    """
    def __init__(self, title, multi_selection=True):
        super().__init__()
        self.setWindowTitle(title)
        self.setFileMode(QFileDialog.DirectoryOnly)
        self.setOption(QFileDialog.DontUseNativeDialog, True)
        if multi_selection:
            file_view = self.findChild(QListView, 'listView')
            if file_view:
                file_view.setSelectionMode(QAbstractItemView.MultiSelection)
        self.tree = self.findChild(QTreeView)
        if multi_selection and self.tree:
            self.tree.setSelectionMode(QAbstractItemView.MultiSelection) class PointLabelEditor(QDialog):
    def __init__(self, column_labels, name, points, is_inside, fields, all_attributes):
        super().__init__()
        attribute_table = PointAttributeTable()
        attribute_table.getData(points, is_inside, fields, all_attributes)
        self.attribute_dialog = PointAttributeDialog(attribute_table)

        self.btnAttribute = QPushButton('Use attributes')
        self.btnAttribute.setFixedSize(105, 50)
        self.btnAttribute.clicked.connect(self.btnAttributeEvent)

        self.btnDefault = QPushButton('Default')
        self.btnDefault.setFixedSize(105, 50)
        self.btnDefault.clicked.connect(self.btnDefaultEvent)

        self.column_labels = column_labels
        self.table = QTableWidget()
        self.table .setColumnCount(2)
        self.table .setHorizontalHeaderLabels([name, 'Label'])
        row = 0
        for column, label in column_labels.items():
            label = column_labels[column]
            self.table.insertRow(row)
            c = QTableWidgetItem(column)
            l = QTableWidgetItem(label)
            self.table.setItem(row, 0, c)
            self.table.setItem(row, 1, l)
            self.table.item(row, 0).setFlags(Qt.ItemIsEditable)
            row += 1

        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
                                   Qt.Horizontal, self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        vlayout = QVBoxLayout()
        hlayout = QHBoxLayout()
        hlayout.addWidget(self.btnAttribute)
        hlayout.addWidget(self.btnDefault)
        hlayout.setSpacing(10)
        vlayout.addLayout(hlayout, Qt.AlignHCenter)
        vlayout.addItem(QSpacerItem(1, 15))
        vlayout.addWidget(QLabel('Click on the label to modify'))
        vlayout.addWidget(self.table)
        vlayout.addWidget(buttons)
        self.setLayout(vlayout)

        self.setWindowTitle('Change %s labels' % name)
        self.resize(self.sizeHint())
        self.setMinimumWidth(300)

    def btnDefaultEvent(self):
        self.table.setRowCount(0)
        row = 0
        for column in self.column_labels.keys():
            self.table.insertRow(row)
            c = QTableWidgetItem(column)
            l = QTableWidgetItem(column)
            self.table.setItem(row, 0, c)
            self.table.setItem(row, 1, l)
            self.table.item(row, 0).setFlags(Qt.ItemIsEditable)
            row += 1

    def btnAttributeEvent(self):
        value = self.attribute_dialog.exec_()
        if value == QDialog.Rejected:
            return
        selected_labels = self.attribute_dialog.selection
        self.table.setRowCount(0)
        row = 0
        for column, label in zip(self.column_labels.keys(), selected_labels):
            self.table.insertRow(row)
            c = QTableWidgetItem(column)
            l = QTableWidgetItem(label)
            self.table.setItem(row, 0, c)
            self.table.setItem(row, 1, l)
            self.table.item(row, 0).setFlags(Qt.ItemIsEditable)
            row += 1

    def getLabels(self, old_labels):
        for row in range(self.table.rowCount()):
            column = self.table.item(row, 0).text()
            label = self.table.item(row, 1).text()
            old_labels[column] = label class PointPlotViewer(TemporalPlotViewer):
    def __init__(self):
        super().__init__('point')
        self.setWindowTitle('Visualize the temporal evolution of values on points')
        self.var_IDs = []
        self.current_var = ''
        self.points = None
        self.select_variable = QAction('Select\nvariable', self, triggered=self.selectVariableEvent,
                                       icon=self.style().standardIcon(QStyle.SP_FileDialogDetailedView))
        self.select_variable_short = QAction('Select\nvariable', self, triggered=self.selectVariableEvent,
                                             icon=self.style().standardIcon(QStyle.SP_FileDialogDetailedView))

        self.current_columns = ('Point 1',)
        self.poly_menu = QMenu('&Sections', self)
        self.poly_menu.addAction(self.selectColumnsAct_short)
        self.poly_menu.addAction(self.editColumnNamesAct_short)
        self.poly_menu.addAction(self.editColumColorAct_short)

    def _to_column(self, point):
        point_index = int(point.split()[1]) - 1
        x, y = self.points.points[point_index]
        return 'Point %d %s (%s|%s)' % (point_index+1, self.current_var, settings.FMT_COORD.format(x),
                                        settings.FMT_COORD.format(y))

    def _defaultYLabel(self):
        word = {'fr': 'de', 'en': 'of'}[self.language]
        return 'Values %s %s' % (word, self.current_var)

    def selectVariableEvent(self):
        msg = QDialog()
        combo = QComboBox()
        for var in self.var_IDs:
            combo.addItem(var)
        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
                                   Qt.Horizontal, msg)
        buttons.accepted.connect(msg.accept)
        buttons.rejected.connect(msg.reject)
        vlayout = QVBoxLayout()
        vlayout.addWidget(combo)
        vlayout.addWidget(buttons)
        msg.setLayout(vlayout)
        msg.setWindowTitle('Select a variable to plot')
        msg.resize(300, 150)
        msg.exec_()
        self.current_var = combo.currentText()
        self.current_ylabel = self._defaultYLabel()
        self.replot()

    def replot(self):
        self.canvas.axes.clear()
        for point in self.current_columns:
            self.canvas.axes.plot(self.time[self.timeFormat], self.data[self._to_column(point)], '-',
                                  color=self.column_colors[point],
                                  linewidth=2, label=self.column_labels[point])
        self.canvas.axes.legend()
        self.canvas.axes.grid(linestyle='dotted')
        self.canvas.axes.set_xlabel(self.current_xlabel)
        self.canvas.axes.set_ylabel(self.current_ylabel)
        self.canvas.axes.set_title(self.current_title)
        if self.timeFormat in [1, 2]:
            self.canvas.axes.set_xticklabels(self.str_datetime if self.timeFormat == 1 else self.str_datetime_bis)
            for label in self.canvas.axes.get_xticklabels():
                label.set_rotation(45)
                label.set_fontsize(8)
        self.canvas.draw() class PlotViewer(QWidget):
    def __init__(self):
        super().__init__()
        self.defaultColors = [c for n, c in settings.DEFAULT_COLORS.items()]
        self.colorToName = {c: n for n, c in settings.DEFAULT_COLORS.items()}
        self.nameToColor = {n: c for n, c in settings.DEFAULT_COLORS.items()}

        self.canvas = PlotCanvas(self)
        self.current_xlabel = settings.X_AXIS_LABEL
        self.current_ylabel = settings.Y_AXIS_LABEL
        self.current_title = 'Default plot'

        # add a default plot
        self.defaultPlot()

        # add the menu bar, the tool bar and the status bar
        self.menuBar = QMenuBar()
        self.toolBar = QToolBar()
        self.toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)

        self.scrollArea = QScrollArea()
        self.scrollArea.setBackgroundRole(QPalette.Dark)
        self.scrollArea.setWidget(self.canvas)

        self.statusbar = QStatusBar()

        self.createActions()
        self.createMenus()
        self.createTools()

        vlayout = QVBoxLayout()
        vlayout.addWidget(self.menuBar)
        vlayout.addWidget(self.toolBar)
        vlayout.addWidget(self.scrollArea)
        vlayout.addWidget(self.statusbar)
        vlayout.setSpacing(0)
        vlayout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(vlayout)

        self.setWindowTitle('Plot Viewer')
        self.resize(self.sizeHint())

    def save(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        filename, _ = QFileDialog.getSaveFileName(self, 'Save image', '',
                                                  'PNG Files (*.png)', options=options)

        # check the file name consistency
        if not filename:
            return
        if len(filename) < 5 or not filename.endswith('.png'):
            filename += '.png'

        self.canvas.print_png(filename)

    def createActions(self):
        icons = self.style().standardIcon
        self.saveAct = QAction('Save', self, shortcut='Ctrl+S',
                               triggered=self.save, icon=icons(QStyle.SP_DialogSaveButton))
        self.exitAct = QAction('Exit', self,
                               triggered=self.close, icon=icons(QStyle.SP_DialogCloseButton))
        self.titleAct = QAction('Modify title', self, triggered=self.changeTitle)
        self.xLabelAct = QAction('Modify X label', self, triggered=self.changeXLabel)
        self.yLabelAct = QAction('Modify Y label', self, triggered=self.changeYLabel)

    def changeTitle(self):
        value, ok = QInputDialog.getText(self, 'Change title',
                                         'Enter a new title', text=self.canvas.axes.get_title())
        if not ok:
            return
        self.canvas.axes.set_title(value)
        self.canvas.draw()
        self.current_title = value

    def changeXLabel(self):
        value, ok = QInputDialog.getText(self, 'Change X label',
                                         'Enter a new X label', text=self.canvas.axes.get_xlabel())
        if not ok:
            return
        self.canvas.axes.set_xlabel(value)
        self.canvas.draw()
        self.current_xlabel = value

    def changeYLabel(self):
        value, ok = QInputDialog.getText(self, 'Change X label',
                                         'Enter a new X label', text=self.canvas.axes.get_ylabel())
        if not ok:
            return
        self.canvas.axes.set_ylabel(value)
        self.canvas.draw()
        self.current_ylabel = value

    def createMenus(self):
        self.fileMenu = QMenu('&File', self)
        self.fileMenu.addAction(self.saveAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.exitAct)

        self.editMenu = QMenu('&Edit', self)
        self.editMenu.addAction(self.titleAct)
        self.editMenu.addAction(self.xLabelAct)
        self.editMenu.addAction(self.yLabelAct)

        self.menuBar.addMenu(self.fileMenu)
        self.menuBar.addMenu(self.editMenu)

    def createTools(self):
        self.toolBar.addAction(self.saveAct)
        self.toolBar.addSeparator()

    def defaultPlot(self):
        x = [0]
        y = [0]
        self.current_xlabel = settings.X_AXIS_LABEL
        self.current_ylabel = settings.Y_AXIS_LABEL
        self.current_title = 'Default plot'
        self.plot(x, y)

    def mouseMove(self, event):
        if event.xdata is None or event.ydata is None:
            self.statusbar.clearMessage()
        msg = 'Time: %s \t Value: %s' % (str(event.xdata), str(event.ydata))
        self.statusbar.showMessage(msg)

    def plot(self, x, y):
        """!
        Default plotting behaviour
        """
        self.canvas.axes.clear()
        self.canvas.axes.plot(x, y, 'b-', linewidth=2)
        self.canvas.axes.grid(linestyle='dotted')
        self.canvas.axes.set_xlabel(self.current_xlabel)
        self.canvas.axes.set_ylabel(self.current_ylabel)
        self.canvas.axes.set_title(self.current_title)
        self.canvas.draw() def read_csv(filename, separator):
    data = {}
    with open(filename, 'r') as f:
        headers = f.readline().rstrip().split(separator)
        for header in headers:
            data[header] = []
        for line in f.readlines():
            items = line.rstrip().split(separator)
            for header, item in zip(headers, items):
                data[header].append(float(item))
        for header in headers:
            data[header] = np.array(data[header])
    return data, headers class SimpleTimeDateSelection(QWidget):
    def __init__(self):
        super().__init__()

        self.frames = []
        self.dates = []

        self.index = QLineEdit('', self)
        self.slider = TimeSlider(self)
        self.value = QLineEdit('', self)
        self.date = QLineEdit('', self)

        self.value.setReadOnly(True)
        self.date.setReadOnly(True)

        self.index.setMaximumWidth(50)
        self.value.setMaximumWidth(100)
        self.date.setMaximumWidth(150)

        mainLayout = QVBoxLayout()
        mainLayout.addWidget(self.slider)
        hlayout = QHBoxLayout()
        hlayout.addWidget(QLabel('Frame index'))
        hlayout.addWidget(self.index)
        hlayout.addWidget(QLabel('value'))
        hlayout.addWidget(self.value)
        hlayout.addWidget(QLabel('date'))
        hlayout.addWidget(self.date)
        hlayout.addStretch()
        hlayout.setAlignment(Qt.AlignLeft)
        hlayout.setSpacing(10)
        mainLayout.addLayout(hlayout)
        self.setLayout(mainLayout)
        self.index.editingFinished.connect(self.slider.enterIndexEvent)

    def updateSelection(self):
        index = int(self.index.text()) - 1
        self.value.setText(str(self.frames[index]))
        self.date.setText(str(self.dates[index]))

    def initTime(self, frames, dates):
        self.frames = frames
        self.dates = dates
        self.slider.reinit(len(frames), 0)
        self.index.setText(str(1))
        self.updateSelection()

    def clearText(self):
        self.index.clear()
        self.value.clear()
        self.date.clear() class TemporalPlotViewer(PlotViewer):
    def __init__(self, column_name='Column'):
        super().__init__()
        self.data = {}
        self.columns = []
        self.setMinimumWidth(600)
        self.canvas.figure.canvas.mpl_connect('motion_notify_event', self.mouseMove)
        self.column_name = column_name
        self.language = 'fr'

        # initialize graphical parameters
        self.time_seconds = []
        self.time = []
        self.current_columns = []
        self.column_labels = {}
        self.column_colors = {}
        self.start_time = None
        self.datetime = []
        self.str_datetime = []
        self.str_datetime_bis = []
        self.timeFormat = 0   # 0: second, 1: date, 2: date (alternative), 3: minutes, 4: hours, 5: days

        self.selectColumnsAct = QAction('Select\n%s' % self.column_name,
                                        self, icon=self.style().standardIcon(QStyle.SP_FileDialogDetailedView),
                                        triggered=self.selectColumns)
        self.editColumnNamesAct = QAction('Edit %s\nlabels' % self.column_name, self, icon=self.style().standardIcon(QStyle.SP_FileDialogDetailedView),
                                          triggered=self.editColumns)
        self.editColumColorAct = QAction('Edit %s\ncolors' % self.column_name, self, icon=self.style().standardIcon(QStyle.SP_FileDialogDetailedView),
                                         triggered=self.editColor)
        self.selectColumnsAct_short = QAction('Select %s' % self.column_name,
                                              self, icon=self.style().standardIcon(QStyle.SP_FileDialogDetailedView),
                                              triggered=self.selectColumns)
        self.editColumnNamesAct_short = QAction('Edit %s labels' % self.column_name, self,
                                                icon=self.style().standardIcon(QStyle.SP_FileDialogDetailedView),
                                                triggered=self.editColumns)
        self.editColumColorAct_short = QAction('Edit %s colors' % self.column_name, self,
                                               icon=self.style().standardIcon(QStyle.SP_FileDialogDetailedView),
                                               triggered=self.editColor)

        self.convertTimeAct = QAction('Toggle date/time\nformat', self, checkable=True,
                                      icon=self.style().standardIcon(QStyle.SP_DialogApplyButton))
        convertTimeAct_short = QAction('Toggle date/time format', self, checkable=True,
                                       icon=self.style().standardIcon(QStyle.SP_DialogApplyButton))
        self.changeDateAct = QAction('Edit\nstart date', self, triggered=self.changeDate,
                                     icon=self.style().standardIcon(QStyle.SP_DialogApplyButton))
        changeDateAct_short = QAction('Edit start date', self, triggered=self.changeDate,
                                      icon=self.style().standardIcon(QStyle.SP_DialogApplyButton))
        self.convertTimeAct.toggled.connect(self.convertTime)
        convertTimeAct_short.toggled.connect(self.convertTime)

        timeMenu = QMenu('&Date/&Time', self)
        timeMenu.addAction(convertTimeAct_short)
        timeMenu.addAction(changeDateAct_short)
        self.menuBar.addMenu(timeMenu)

    def _defaultXLabel(self):
        if self.language == 'fr':
            return 'Temps ({})'.format(['seconde', '', '', 'minute', 'heure', 'jour'][self.timeFormat])
        return 'Time ({})'.format(['second', '', '', 'minute', 'hour', 'day'][self.timeFormat])

    def selectColumns(self, unique_selection=False):
        msg = PlotColumnsSelector(self.columns, self.current_columns, self.column_name, unique_selection)
        value = msg.exec_()
        if value == QDialog.Rejected:
            return
        self.current_columns = msg.selection
        self.replot()

    def editColumns(self):
        msg = ColumnLabelEditor(self.column_labels, self.current_columns, self.column_name)
        value = msg.exec_()
        if value == QDialog.Rejected:
            return
        msg.getLabels(self.column_labels)
        self.replot()

    def editColor(self):
        msg = ColumnColorEditor(self.column_name, self.current_columns, self.column_labels, self.column_colors,
                                self.defaultColors, self.colorToName)
        value = msg.exec_()
        if value == QDialog.Rejected:
            return
        msg.getColors(self.column_colors, self.column_labels, self.nameToColor)
        self.replot()

    def replot(self):
        pass

    def reset(self):
        # reinitialize old graphical parameters and clear data
        self.time = []
        self.timeFormat = 0
        self.current_title = ''
        self.column_labels = {}
        self.column_colors = {}

    def mouseMove(self, event):
        current_time = event.xdata
        if current_time is None:
            self.statusbar.clearMessage()
            return
        if self.timeFormat == 1:
            current_time = self.start_time + datetime.timedelta(seconds=current_time)
            current_time = current_time.strftime('%Y/%m/%d %H:%M')
        elif self.timeFormat == 2:
            current_time = self.start_time + datetime.timedelta(seconds=current_time)
            current_time = current_time.strftime('%d/%m/%y %H:%M')
        elif self.timeFormat == 3:
            current_time /= 60
        elif self.timeFormat == 4:
            current_time /= 3600
        elif self.timeFormat == 5:
            current_time /= 86400
        current_time = str(current_time)
        msg = 'Time: %s \t Value: %s' % (current_time, str(event.ydata))
        self.statusbar.showMessage(msg)

    def changeDate(self):
        value, ok = QInputDialog.getText(self, 'Change start date',
                                         'Enter the start date',
                                         text=self.start_time.strftime('%Y-%m-%d %X'))
        if not ok:
            return
        try:
            self.start_time = datetime.datetime.strptime(value, '%Y-%m-%d %X')
        except ValueError:
            QMessageBox.critical(self, 'Error', 'Invalid input.',
                                 QMessageBox.Ok)
            return
        self.datetime = list(map(lambda x: self.start_time + datetime.timedelta(seconds=x), self.time_seconds))
        self.str_datetime = list(map(lambda x: x.strftime('%Y/%m/%d\n%H:%M'), self.datetime))
        self.str_datetime_bis = list(map(lambda x: x.strftime('%d/%m/%y\n%H:%M'), self.datetime))
        self.replot()

    def convertTime(self):
        self.timeFormat = (1 + self.timeFormat) % 6
        self.current_xlabel = self._defaultXLabel()
        self.xLabelAct.setEnabled(self.timeFormat not in [1, 2])
        self.replot() class VolumePlotViewer(TemporalPlotViewer):
    def __init__(self):
        super().__init__('polygon')
        self.csv_separator = ''
        self.var_ID = None
        self.second_var_ID = None
        self.language = 'fr'

        self.setWindowTitle('Visualize the temporal evolution of volumes')

        self.poly_menu = QMenu('&Polygons', self)
        self.poly_menu.addAction(self.selectColumnsAct_short)
        self.poly_menu.addAction(self.editColumnNamesAct_short)
        self.poly_menu.addAction(self.editColumColorAct_short)

    def _defaultYLabel(self):
        word = {'fr': 'de', 'en': 'of'}[self.language]
        if self.second_var_ID == VolumeCalculator.INIT_VALUE:
            return 'Volume %s (%s - %s$_0$)' % (word, self.var_ID, self.var_ID)
        elif self.second_var_ID is None:
            return 'Volume %s %s' % (word, self.var_ID)
        return 'Volume %s (%s - %s)' % (word, self.var_ID, self.second_var_ID)

    def replot(self):
        self.canvas.axes.clear()
        for column in self.current_columns:
            self.canvas.axes.plot(self.time[self.timeFormat], self.data[column], '-', color=self.column_colors[column],
                                  linewidth=2, label=self.column_labels[column])
        self.canvas.axes.legend()
        self.canvas.axes.grid(linestyle='dotted')
        self.canvas.axes.set_xlabel(self.current_xlabel)
        self.canvas.axes.set_ylabel(self.current_ylabel)
        self.canvas.axes.set_title(self.current_title)
        if self.timeFormat in [1, 2]:
            self.canvas.axes.set_xticklabels(self.str_datetime if self.timeFormat == 1 else self.str_datetime_bis)
            for label in self.canvas.axes.get_xticklabels():
                label.set_rotation(45)
                label.set_fontsize(8)
        self.canvas.draw() class SerafinData:
    def __init__(self, job_id, filename, language):
        self.job_id = job_id
        self.language = language
        self.filename = filename
        self.index = None
        self.triangles = {}
        self.header = None
        self.time = []  # <[float]>
        self.time_second = []  # <[datetime.timedelta]>  FIXME: should be renamed differently!
        self.start_time = None

        self.selected_vars = []
        self.selected_vars_names = {}
        self.selected_time_indices = []
        self.equations = []
        self.us_equation = None
        self.to_single = False

        self.operator = None
        self.metadata = {}
        # * select single layer: 'layer_selection': an integer
        # * vertical aggregation: 'vertical_operator': 'Min', 'Max' or 'Mean'

    def read(self):
        try:
            with Serafin.Read(self.filename, self.language) as input_stream:
                input_stream.read_header()
                input_stream.get_time()

                self.header = input_stream.header.copy()
                self.time = input_stream.time[:]
        except PermissionError:
            raise Serafin.SerafinRequestError('Permission denied (Is the file opened by another application?).')

        if self.header.date is not None:
            try:
                year, month, day, hour, minute, second = self.header.date
                self.start_time = datetime.datetime(year, month, day, hour, minute, second)
            except ValueError:
                logger.warning('Date seems invalid, replaced by default date.')
        if self.start_time is None:
            self.start_time = datetime.datetime(1900, 1, 1, 0, 0, 0)
        self.time_second = list(map(lambda x: datetime.timedelta(seconds=x), self.time))
        self.selected_vars = self.header.var_IDs[:]
        self.selected_vars_names = {var_id: (var_name, var_unit) for (var_id, var_name, var_unit)
                                    in zip(self.header.var_IDs, self.header.var_names, self.header.var_units)}
        self.selected_time_indices = list(range(len(self.time)))
        return self.header.is_2d

    def copy(self):
        copy_data = SerafinData(self.job_id, self.filename, self.language)
        copy_data.index = self.index
        copy_data.triangles = self.triangles
        copy_data.header = self.header
        copy_data.time = self.time
        copy_data.start_time = self.start_time
        copy_data.time_second = self.time_second
        copy_data.metadata = self.metadata

        copy_data.selected_vars = self.selected_vars[:]
        copy_data.selected_vars_names = deepcopy(self.selected_vars_names)
        copy_data.selected_time_indices = self.selected_time_indices[:]
        copy_data.equations = self.equations[:]
        copy_data.us_equation = self.us_equation
        copy_data.to_single = self.to_single
        copy_data.operator = self.operator
        return copy_data

    def default_output_header(self):
        output_header = self.header.copy()
        output_header.empty_variables()
        for var_ID in self.selected_vars:
            var_name, var_unit = self.selected_vars_names[var_ID]
            output_header.add_variable(var_ID, var_name, var_unit)
        if self.to_single:
            output_header.to_single_precision()
        return output_header

    def build_2d_output_header(self):
        output_header = self.header.copy_as_2d()
        output_header.empty_variables()
        for var_ID in self.selected_vars:
            var_name, var_unit = self.selected_vars_names[var_ID]
            output_header.add_variable(var_ID, var_name, var_unit)
        if self.to_single:
            output_header.to_single_precision()
        return output_header

    def transform_mesh(self, transformations):
        """!
        @brief Apply transformations on mesh nodes (only in 2D)
        @param transformations <[geom.transformation.Transformation]>: list of successive transformations
        """
        self.header.transform_mesh(transformations)
        self.index = None
        self.triangles = {} class MeshInterpolator(Mesh2D):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def get_point_interpolators(self, points):
        nb_points = len(points)
        is_inside = [False] * nb_points
        point_interpolators = [None] * nb_points

        for index, (x, y) in enumerate(points):
            potential_elements = self.get_intersecting_elements((x, y, x, y))
            if not potential_elements:
                continue
            for i, j, k in potential_elements:
                t = self.triangles[i, j, k]
                is_in, point_interpolator = Interpolator(t).is_in_triangle(x, y)
                if is_in:
                    is_inside[index] = True
                    point_interpolators[index] = ((i, j, k), point_interpolator)
                    break

        return is_inside, point_interpolators

    def _get_line_interpolators(self, line):
        intersections = []
        internal_points = []  # line interpolators without intersections

        # record the distance offset before the first intersection point
        offset = 0
        found_intersection = False

        for right, up, segment in line.segments():  # for every segment, sort intersection points
            segment_intersections = []
            potential_elements = self.get_intersecting_elements(segment.bounds())
            for i, j, k in potential_elements:
                t = self.triangles[i, j, k]
                is_intersected, t_intersections = segment.linestring_intersection(t)
                if is_intersected:
                    interpolator = Interpolator(t)
                    for intersection in t_intersections:
                        for x, y in intersection.coords:
                            segment_intersections.append((x, y, (i, j, k), interpolator.get_interpolator_at(x, y)))

            # first sort by y, then sort by x
            if up:
                segment_intersections.sort(key=lambda x: x[1])
            else:
                segment_intersections.sort(key=lambda x: x[1], reverse=True)
            if right:
                segment_intersections.sort()
            else:
                segment_intersections.sort(reverse=True)

            intersections.extend(segment_intersections)
            if not segment_intersections:
                continue

            internal_points.append(segment_intersections[0])
            internal_points.append(segment_intersections[-1])

            if not found_intersection:
                first_point, second_point = list(segment.coords())
                if not segment_intersections:
                    offset += np.linalg.norm(np.array(second_point) - np.array(first_point))
                else:
                    found_intersection = True
                    first_intersection = np.array(segment_intersections[0][:2])
                    offset += np.linalg.norm(first_intersection - np.array(first_point))

        # if the intersection is continuous, every internal point or turning point has at least two duplicates
        prev_x, prev_y = None, None
        duplicates = 0
        to_remove = [False] * len(intersections)
        for i, (x, y, _, __) in enumerate(intersections):
            if i == 0:  # the start and end points are not duplicated
                continue
            if prev_x is None:
                prev_x, prev_y = x, y
                continue
            if x == prev_x and y == prev_y:
                to_remove[i] = True
                duplicates += 1
            else:
                if duplicates == 0:  # no duplicate found, the intersection is discontinuous
                    return [], [], [], []
                duplicates = 0
                prev_x, prev_y = x, y

        intersections = [intersections[i] for i in range(len(intersections)) if not to_remove[i]]

        # trim internal points from 2n+2 to n+1
        if internal_points:
            internal_points = internal_points[0:-1:2] + [internal_points[-1]]

        # compute cumulative distance
        distance = offset
        distances = [offset]
        for i in range(len(intersections)-1):
            first, second = intersections[i+1], intersections[i]
            distance += np.linalg.norm([second[0] - first[0], second[1] - first[1]])
            distances.append(distance)

        distance = offset
        distances_internal = [offset]
        for i in range(len(internal_points)-1):
            first, second = internal_points[i+1], internal_points[i]
            distance += np.linalg.norm([second[0] - first[0], second[1] - first[1]])
            distances_internal.append(distance)

        return intersections, distances, internal_points, distances_internal

    def get_line_interpolators(self, lines):
        nb_nonempty = 0
        indices_nonempty = []
        line_interpolators = []
        line_interpolators_internal = []

        for i, line in enumerate(lines):
            line_interpolator, distance, line_interpolator_internal, distance_internal = self._get_line_interpolators(line)

            if line_interpolator:
                nb_nonempty += 1
                indices_nonempty.append(i)

            line_interpolators.append((line_interpolator, distance))
            line_interpolators_internal.append((line_interpolator_internal, distance_internal))

        return nb_nonempty, indices_nonempty, line_interpolators, line_interpolators_internal

    @staticmethod
    def interpolate_along_lines(input_stream, selected_vars, selected_time_indices, indices_nonempty,
                                line_interpolators, fmt_float):
        for u, id_line in enumerate(indices_nonempty):
            line_interpolator, distances = line_interpolators[id_line]

            for v, time_index in enumerate(selected_time_indices):
                time_value = input_stream.time[time_index]
                var_values = []
                for var in selected_vars:
                    var_values.append(input_stream.read_var_in_frame(time_index, var))

                for (x, y, (i, j, k), interpolator), distance in zip(line_interpolator, distances):
                    row = [str(id_line+1), str(time_value), fmt_float.format(x), fmt_float.format(y),
                           fmt_float.format(distance)]

                    for i_var, var in enumerate(selected_vars):
                        values = var_values[i_var]
                        row.append(fmt_float.format(interpolator.dot(values[[i, j, k]])))
                    yield u, v, row

    @staticmethod
    def project_lines(input_stream, selected_vars, time_index, indices_nonempty, max_distance,
                      reference, line_interpolators, fmt_float):
        var_values = []
        for var in selected_vars:
            var_values.append(input_stream.read_var_in_frame(time_index, var))

        for u, id_line in enumerate(indices_nonempty):
            line_interpolator, _ = line_interpolators[id_line]

            for x, y, (i, j, k), interpolator in line_interpolator:
                d = reference.project(x, y)
                if d <= 0 or d >= max_distance:
                    continue
                row = [str(id_line+1), fmt_float.format(x), fmt_float.format(y), fmt_float.format(d)]
                for i_var, var in enumerate(selected_vars):
                    values = var_values[i_var]
                    row.append(fmt_float.format(interpolator.dot(values[[i, j, k]])))
                yield u, row class Serafin:
    """!
    @brief A Serafin object corresponds to a single Serafin in file IO stream

    # Attributes:
    - language <str>: language for variables ('fr' or 'en')
    - mode <str>: file open mode
    - file <_io.BufferedReader>: input Serafin stream
    - filename <str>: path to file
    - file_size <int>: size of file
    """
    def __init__(self, filename, mode, language):
        self.language = language
        self.mode = mode

        self.file = None  # will be opened when called using `with` statement
        self.filename = filename
        self.file_size = None

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()
        return False def new_logger(name):
    """!
    Get a new logger
    @param name <str>: logger name
    """
    logger = logging.getLogger(name)
    if settings.COLOR_LOGS:
        coloredlogs.install(logger=logger, level=settings.LOGGING_LEVEL, fmt=settings.LOGGING_FMT_CLI,
                            level_styles=LEVEL_STYLES, field_styles=FIELD_STYLES)
    else:
        handler = logging.StreamHandler()
        handler.setFormatter(logging.Formatter(settings.LOGGING_FMT_CLI))
        logger.addHandler(handler)
        logger.setLevel(settings.LOGGING_LEVEL)
    return logger
import datetime
import numpy as np
import os
import matplotlib.pyplot as plt
import matplotlib.tri as mtri
import warnings
from PyQt5.QtCore import (QDir, Qt)
from PyQt5.QtGui import (QColor, QPalette)
from PyQt5.QtWidgets import (QAbstractItemView, QAction, QApplication, QCheckBox, QComboBox,
                             QDialog, QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFormLayout, QGroupBox,
                             QHBoxLayout, QInputDialog, QLabel, QLineEdit, QMenu, QMessageBox,
                             QPlainTextEdit, QPushButton, QRadioButton, QScrollArea, QStackedLayout, QStyle,
                             QTableWidget, QTableWidgetItem, QToolBar, QTreeView, QVBoxLayout, QWidget)
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT
from matplotlib import cm
from mpl_toolkits.axes_grid1 import make_axes_locatable
from shapefile import ShapefileException
from pyteltools.conf import settings
from pyteltools.geom import Shapefile
from pyteltools.gui.util import FluxPlotViewer, MapCanvas, MultiFolderDialog, PointLabelEditor, PointPlotViewer,\
    PlotViewer, read_csv, SimpleTimeDateSelection, TemporalPlotViewer, VolumePlotViewer
from pyteltools.slf.datatypes import SerafinData
from pyteltools.slf.interpolation import MeshInterpolator
from pyteltools.slf import Serafin
from pyteltools.utils.log import new_logger
        self.triang = mtri.Triangulation(point_x.flatten(), point_y.flatten(), triangles)
        self.values = point_values.flatten()
        self.values_ut = point_ut.flatten()
        self.values_w = point_w.flatten()

    def _get_var_list(self):
        variables = [var for var in self.data.header.var_IDs if var != 'Z']
        names = [name.decode(Serafin.SLF_EIT).strip()
                 for var, name in zip(self.data.header.var_IDs, self.data.header.var_names) if var in variables]
        # Add normal velocity
        if {'U', 'V'}.issubset(self.data.header.var_IDs):
            variables.insert(0, 'Un')
            names.insert(0, 'Computed Normal Velocity')
        return variables, names

    def read_var(self):
        """Read current variable values"""
        self.vars_values = {}
        with Serafin.Read(self.data.filename, self.data.language) as input_stream:
            input_stream.header = self.data.header
            input_stream.time = self.data.time
            self.z_values = input_stream.read_var_in_frame_as_3d(self.time_index, 'Z').T
            for varID in self.current_vars2read:
                self.vars_values[varID] = input_stream.read_var_in_frame_as_3d(self.time_index, varID).T

    def replot(self, read_var=True, compute=True):
        if not self.current_vars2read:
            self._set_vars2read()
        if read_var:
            self.read_var()
        if compute or not self.vars_values:
            self.compute()

        self._update_next_prev()
        self.canvas.figure.clear()  # remove the old color bar
        self.canvas.axes = self.canvas.figure.add_subplot(111)

        if self.color_limits is not None:
            levels = np.linspace(self.color_limits[0], self.color_limits[1], settings.NB_COLOR_LEVELS)
            self.canvas.axes.tricontourf(self.triang, self.values, cmap=self.current_style, levels=levels,
                                         extend='both', vmin=self.color_limits[0], vmax=self.color_limits[1])
        else:
            levels = build_levels_from_minmax(np.nanmin(self.values), np.nanmax(self.values))
            self.canvas.axes.tricontourf(self.triang, self.values, cmap=self.current_style, levels=levels,
                                         extend='both')
        if self.display_mesh:
            self.canvas.axes.triplot(self.triang, 'ko-')

        if self.tangential_vel_scales:
            kx, ky = self.tangential_vel_scales

            if self.tangential_vel_grid:
                dx, dy = self.tangential_vel_grid
                x = np.arange(np.amin(self.triang.x), np.amax(self.triang.x), dx)
                y = np.arange(np.amin(self.triang.y), np.amax(self.triang.y), dy)
                xv, yv = np.meshgrid(x, y)

                # Add mask to ignore zero-area triangles and avoid an invalid triangulation
                # Solution taken from http://matplotlib.1069221.n5.nabble.com/Matplotlib-with-invalid-triangulations-td47627.html
                xy = np.dstack((self.triang.x[self.triang.triangles], self.triang.y[self.triang.triangles]))  # shape (ntri,3,2)
                twice_area = np.cross(xy[:, 1, :] - xy[:, 0, :], xy[:, 2, :] - xy[:, 0, :])  # shape (ntri)
                mask = twice_area < 1e-3  # shape (ntri). Hardcoded threshold of 0.001 m²
                if np.any(mask):
                    self.triang.set_mask(mask)
                try:
                    ut = mtri.LinearTriInterpolator(self.triang, self.values_ut)(xv, yv)
                    w = mtri.LinearTriInterpolator(self.triang, self.values_w)(xv, yv)
                    self.canvas.axes.quiver(xv, yv, kx*ut, ky*w, angles='xy', scale_units='xy', scale=1)
                except RuntimeError as e:
                    QMessageBox.critical(self, 'Error', 'Error with the grid interpolation: %s\n'
                                         'Velocities at nodes will be displayed.' % e, QMessageBox.Ok)
                    self.tangential_vel_grid = None
            else:
                self.canvas.axes.quiver(self.triang.x, self.triang.y, kx*self.values_ut, ky*self.values_w,
                                        angles='xy', scale_units='xy', scale=1)

        self.canvas.axes.set_aspect(aspect=self.aspect_ratio_value, adjustable='datalim')
        self.canvas.figure.subplots_adjust(right=0.80)  # left side (80% of figure width) for plot
        cax = self.canvas.figure.add_axes([0.84, 0.1, 0.03, 0.8])  # right side (20% of figure width) for colorbar
        cax.set_title(self.current_var)
        self.cmap = cm.ScalarMappable(cmap=self.current_style)
        self.cmap.set_array(levels)
        self.canvas.figure.colorbar(self.cmap, cax=cax)

        self.canvas.axes.set_xlabel(self.current_xlabel)
        self.canvas.axes.set_ylabel(self.current_ylabel)
        self.current_title = self._defaultTitle()
        self.canvas.axes.set_title(self.current_title)
        self.canvas.draw()

    def _update_next_prev(self):
        """Enable/disable next and previous action depending on position"""
        enable_prev = False if self.current_section_index == 0 else True
        enable_next = False if self.current_section_index == len(self.section_names) - 1 else True
        self.select_section_prev_act.setEnabled(enable_prev)
        self.select_section_next_act.setEnabled(enable_next)

    def get_data(self, data, sections, line_interpolators, section_indices):
        self.data = data
        self.sections = sections
        self.line_interpolators = line_interpolators
        self.section_indices = section_indices

        self.current_var = self._get_var_list()[0][0]  # first possible variable by default
        self.section_names = [self.sections[i].id for i in self.section_indices]
        self.current_section_index = 0  # default value
        self.current_section = self.section_names[self.current_section_index]

        # initialize the plot
        self.language = self.data.language
        self.current_xlabel = self._defaultXLabel()
        self.current_ylabel = self._defaultYLabel()
        self.current_title = self._defaultTitle()

        self.nplan = self.data.header.nb_planes

        self.slider.initTime(self.data.time, list(map(lambda x: x + self.data.start_time, self.data.time_second)))
        self.replot()



class VerticalProfilePlotViewer(TemporalPlotViewer):