class GraphWidget(QWidget):
    def __init__(
        self,
        parent=None,
        ts_datasources=None,
        parameter_config=[],
        name="",
        geometry_type=QgsWkbTypes.Point,
    ):
        super().__init__(parent)

        self.name = name
        self.ts_datasources = ts_datasources
        self.parent = parent
        self.geometry_type = geometry_type

        self.setup_ui()

        self.model = LocationTimeseriesModel(
            ts_datasources=self.ts_datasources)
        self.graph_plot.set_location_model(self.model)
        self.graph_plot.set_ds_model(self.ts_datasources)
        self.location_timeseries_table.setModel(self.model)

        # set listeners
        self.parameter_combo_box.currentIndexChanged.connect(
            self.parameter_change)
        self.remove_timeseries_button.clicked.connect(
            self.remove_objects_table)

        # init parameter selection
        self.set_parameter_list(parameter_config)

        if self.geometry_type == QgsWkbTypes.Point:
            self.marker = QgsVertexMarker(self.parent.iface.mapCanvas())
        else:
            self.marker = QgsRubberBand(self.parent.iface.mapCanvas())
            self.marker.setColor(Qt.red)
            self.marker.setWidth(2)

    def set_parameter_list(self, parameter_config):

        # reset
        nr_old_parameters = self.parameter_combo_box.count()

        self.parameters = dict([(p["name"], p) for p in parameter_config])

        self.parameter_combo_box.insertItems(
            0, [p["name"] for p in parameter_config])

        # todo: find best matching parameter based on previous selection
        if nr_old_parameters > 0:
            self.parameter_combo_box.setCurrentIndex(0)

        nr_parameters_tot = self.parameter_combo_box.count()
        for i in reversed(
                list(
                    range(nr_parameters_tot - nr_old_parameters,
                          nr_parameters_tot))):
            self.parameter_combo_box.removeItem(i)

        # self.graph_plot.set_parameter(self.current_parameter)

    def on_close(self):
        """
        unloading widget and remove all required stuff
        :return:
        """
        self.parameter_combo_box.currentIndexChanged.disconnect(
            self.parameter_change)
        self.remove_timeseries_button.clicked.disconnect(
            self.remove_objects_table)

    def closeEvent(self, event):
        """
        overwrite of QDockWidget class to emit signal
        :param event: QEvent
        """
        self.on_close()
        event.accept()

    def highlight_feature(self, obj_id, obj_type):

        pass
        # todo: selection generated errors and crash of Qgis. Implement method
        # with QgsRubberband and/ or QgsVertexMarker
        transform = QgsCoordinateTransform(
            QgsCoordinateReferenceSystem(4326),
            QgsProject.instance().crs(),
            QgsProject.instance(),
        )

        layers = self.parent.iface.mapCanvas().layers()
        for lyr in layers:
            # Clear other layers
            # lyr.removeSelection()
            if lyr.name() == obj_type:
                # query layer for object
                filt = u'"id" = {0}'.format(obj_id)
                request = QgsFeatureRequest().setFilterExpression(filt)
                features = lyr.getFeatures(request)
                for feature in features:
                    if self.geometry_type == QgsWkbTypes.Point:
                        geom = feature.geometry()
                        geom.transform(transform)
                        self.marker.setCenter(geom.asPoint())
                        self.marker.setVisible(True)
                    else:
                        self.marker.setToGeometry(feature.geometry(), lyr)

    def unhighlight_all_features(self):
        """Remove the highlights from all layers"""

        if self.geometry_type == QgsWkbTypes.Point:
            self.marker.setVisible(False)
        else:
            self.marker.reset()
        pass
        # todo: selection generated errors and crash of Qgis. Implement method
        # with QgsRubberband and/ or QgsVertexMarker

    def setup_ui(self):
        """
        Create Qt widgets and elements
        """

        self.setObjectName(self.name)

        self.hLayout = QHBoxLayout(self)
        self.hLayout.setObjectName("hLayout")

        # add graphplot
        self.graph_plot = GraphPlot(self)
        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(
            self.graph_plot.sizePolicy().hasHeightForWidth())
        self.graph_plot.setSizePolicy(sizePolicy)
        self.graph_plot.setMinimumSize(QSize(250, 250))
        self.hLayout.addWidget(self.graph_plot)

        # add layout for timeseries table and other controls
        self.vLayoutTable = QVBoxLayout(self)
        self.hLayout.addLayout(self.vLayoutTable)

        # add combobox for parameter selection
        self.parameter_combo_box = QComboBox(self)
        self.vLayoutTable.addWidget(self.parameter_combo_box)

        # add timeseries table
        self.location_timeseries_table = LocationTimeseriesTable(self)
        self.location_timeseries_table.hoverEnterRow.connect(
            self.highlight_feature)
        self.location_timeseries_table.hoverExitAllRows.connect(
            self.unhighlight_all_features)
        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.location_timeseries_table.sizePolicy().hasHeightForWidth())
        self.location_timeseries_table.setSizePolicy(sizePolicy)
        self.location_timeseries_table.setMinimumSize(QSize(250, 0))
        self.vLayoutTable.addWidget(self.location_timeseries_table)

        # add buttons below table
        self.hLayoutButtons = QHBoxLayout(self)
        self.vLayoutTable.addLayout(self.hLayoutButtons)

        self.remove_timeseries_button = QPushButton(self)
        sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.remove_timeseries_button.sizePolicy().hasHeightForWidth())
        self.remove_timeseries_button.setSizePolicy(sizePolicy)
        self.remove_timeseries_button.setObjectName("remove_timeseries_button")
        self.hLayoutButtons.addWidget(self.remove_timeseries_button)
        self.hLayoutButtons.addItem(
            QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))

        self.retranslateUi()

    def retranslateUi(self):
        """
        set translated widget text
        """
        self.remove_timeseries_button.setText("Delete")

    def parameter_change(self, nr):
        """
        set current selected parameter and trigger refresh of graphs
        :param nr: nr of selected option of combobox
        :return:
        """
        self.current_parameter = self.parameters[
            self.parameter_combo_box.currentText()]
        self.graph_plot.set_parameter(self.current_parameter)

    def get_feature_index(self, layer, feature):
        """
        get the id of the selected id feature
        :param layer: selected Qgis layer to be added
        :param feature: selected Qgis feature to be added
        :return: idx (integer)
        We can't do ``feature.id()``, so we have to pick something that we
        have agreed on. For now we have hardcoded the 'id' field as the
        default, but that doesn't mean it's always the case in the future
        when more layers are added!
        """
        idx = feature.id()
        if layer.dataProvider().name() in PROVIDERS_WITHOUT_PRIMARY_KEY:
            idx = feature["id"]
        return idx

    def get_object_name(self, layer, feature):
        """
        get the object_name (display_name / type)  of the selected id feature
        :param layer: selected Qgis layer to be added
        :param feature: selected Qgis feature to be added
        :return: object_name (string)
        To get a object_name we use the following logic:
        - get the '*display_name*' column if available;
        - if not: get the 'type' column if available;
        - if not: object_name = 'N/A'
        """
        object_name = None
        for column_nr, field in enumerate(layer.fields()):
            if "display_name" in field.name():
                object_name = feature[column_nr]
        if object_name is None:
            for column_nr, field in enumerate(layer.fields()):
                if field.name() == "type":
                    object_name = feature[column_nr]
                    break
                else:
                    object_name = "N/A"
                    logger.warning(
                        "Layer has no 'display_name', it's probably a result "
                        "layer, but putting a placeholder object name just "
                        "for safety.")
        return object_name

    def get_new_items(self, layer, features, filename, existing_items):
        """
        get a list of new items (that have been selected by user) to be added
        to graph (if they do not already exist in the graph items
        :param layer: selected Qgis layer to be added
        :param features: selected Qgis features to be added
        :param filename: selected Qgis features to be added
        :param existing_items: selected Qgis features to be added
        :return: new_items (list)
        """
        new_items = []
        for feature in features:
            new_idx = self.get_feature_index(layer, feature)
            new_object_name = self.get_object_name(layer, feature)
            # check if object not already exist
            if (layer.name() + "_" + str(new_idx)) not in existing_items:
                item = {
                    "object_type": layer.name(),
                    "object_id": new_idx,
                    "object_name": new_object_name,
                    "file_path": filename,
                }
                new_items.append(item)
        return new_items

    def add_objects(self, layer, features):
        """
        :param layer: layer of features
        :param features: Qgis layer features to be added
        :return: boolean: new objects are added
        """

        # Get the active database as URI, conn_info is something like:
        # u"dbname='/home/jackieleng/git/threedi-turtle/var/models/
        # DS_152_1D_totaal_bergingsbak/results/
        # DS_152_1D_totaal_bergingsbak_result.sqlite'"

        if layer.name() not in ("flowlines", "nodes", "pumplines"):
            msg = """Please select results from either the 'flowlines', 'nodes' or
            'pumplines' layer."""
            messagebar_message("Info", msg, level=0, duration=5)
            return

        conn_info = QgsDataSourceUri(
            layer.dataProvider().dataSourceUri()).connectionInfo()
        try:
            filename = conn_info.split("'")[1]
        except IndexError:
            raise RuntimeError(
                "Active database (%s) doesn't look like an sqlite filename" %
                conn_info)

        # get attribute information from selected layers
        existing_items = [
            "%s_%s" % (item.object_type.value, str(item.object_id.value))
            for item in self.model.rows
        ]
        items = self.get_new_items(layer, features, filename, existing_items)

        if len(items) > 20:
            msg = ("%i new objects selected. Adding those to the plot can "
                   "take a while. Do you want to continue?" % len(items))
            reply = QMessageBox.question(self, "Add objects", msg,
                                         QMessageBox.Yes, QMessageBox.No)

            if reply == QMessageBox.No:
                return False

        self.model.insertRows(items)
        msg = "%i new objects added to plot " % len(items)
        skipped_items = len(features) - len(items)
        if skipped_items > 0:
            msg += "(skipped %s already present objects)" % skipped_items

        statusbar_message(msg)
        return True

    def remove_objects_table(self):
        """
        removes selected objects from table
        :return:
        """
        selection_model = self.location_timeseries_table.selectionModel()
        # get unique rows in selected fields
        rows = set(
            [index.row() for index in selection_model.selectedIndexes()])
        for row in reversed(sorted(rows)):
            self.model.removeRows(row, 1)