diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..26d33521af10bcc7fd8cea344038eaaeb78d0ef5
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..105ce2da2d6447d11dfe32bfb846c3d5b199fc99
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000000000000000000000000000000000000..94c069ce408206d6c87cf00b7f89a3b4d8512b68
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Black">
+    <option name="sdkName" value="Python 3.10 (pamhyr)" />
+  </component>
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (pamhyr)" project-jdk-type="Python SDK" />
+</project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8ad567a589205c549c5c5fd2c87d7b60fb8e2c7b
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/pamhyr.iml" filepath="$PROJECT_DIR$/.idea/pamhyr.iml" />
+    </modules>
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/pamhyr.iml b/.idea/pamhyr.iml
new file mode 100644
index 0000000000000000000000000000000000000000..c422cdfc5a6269c23ac82259efc8958bc893c067
--- /dev/null
+++ b/.idea/pamhyr.iml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/venv" />
+    </content>
+    <orderEntry type="jdk" jdkName="Python 3.10 (pamhyr)" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+  <component name="PyDocumentationSettings">
+    <option name="format" value="PLAIN" />
+    <option name="myDocStringFormat" value="Plain" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..35eb1ddfbbc029bcab630581847471d7f238ec53
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/mage8/mage b/mage8/mage
new file mode 100755
index 0000000000000000000000000000000000000000..dc109d6501bed63680fc49d3ff39bfe0fcadd0f5
Binary files /dev/null and b/mage8/mage differ
diff --git a/mage8/mage_as7 b/mage8/mage_as7
new file mode 100755
index 0000000000000000000000000000000000000000..74f79fc89126373f12b6a3439125a1d53ebe0a56
Binary files /dev/null and b/mage8/mage_as7 differ
diff --git a/mage8/mage_extraire b/mage8/mage_extraire
new file mode 100755
index 0000000000000000000000000000000000000000..5526370e8ef023fe7b011791a7f39fa567e7e1fc
Binary files /dev/null and b/mage8/mage_extraire differ
diff --git a/mage8/mage_extraire_gra b/mage8/mage_extraire_gra
new file mode 100755
index 0000000000000000000000000000000000000000..07c594dc62cfe6295ea20645bbfcea3a3ffa5c39
Binary files /dev/null and b/mage8/mage_extraire_gra differ
diff --git a/mage8/mailleurTT b/mage8/mailleurTT
new file mode 100755
index 0000000000000000000000000000000000000000..5ea642d7f6c9c4a483123778461c7b8186ad4895
Binary files /dev/null and b/mage8/mailleurTT differ
diff --git a/src/.pamhyr b/src/.pamhyr
new file mode 100644
index 0000000000000000000000000000000000000000..f84c9cfc61d1518be825460524be475ce771ef7a
Binary files /dev/null and b/src/.pamhyr differ
diff --git a/src/Model/OutputKpAdists/OutputKpAdists.py b/src/Model/OutputKpAdists/OutputKpAdists.py
index bb3cc185c4cfe2bc6239cf73ffaaa21dd0c51194..f27332b108bb99e464abc18ca1dbea3db08c6014 100644
--- a/src/Model/OutputKpAdists/OutputKpAdists.py
+++ b/src/Model/OutputKpAdists/OutputKpAdists.py
@@ -101,8 +101,8 @@ class OutputKpAdists(SQLSubModel):
     def _db_load(cls, execute, data=None):
         new = []
 
-        reach   = data["reach"]
-        profile = data["profile"]
+        #reach   = data["reach"]
+        #profile = data["profile"]
         status  = data["status"]
 
         table = execute(
diff --git a/src/Model/OutputKpAdists/OutputKpListAdists.py b/src/Model/OutputKpAdists/OutputKpListAdists.py
index 331f08374b7a131870922664d3c50ab1f7736461..95886137ce62990f07ab0d2cc507483ab2003ec9 100644
--- a/src/Model/OutputKpAdists/OutputKpListAdists.py
+++ b/src/Model/OutputKpAdists/OutputKpListAdists.py
@@ -24,7 +24,7 @@ from Model.OutputKpAdists.OutputKpAdists import OutputKpAdists
 
 
 class OutputKpAdistsList(PamhyrModelList):
-    _sub_classes = [OutputKpAdistsList]
+    _sub_classes = [OutputKpAdists]
 
     @classmethod
     def _db_load(cls, execute, data=None):
diff --git a/src/Model/River.py b/src/Model/River.py
index 55c17c634b72808e3c0a4e3ad3a5d173ae8783d7..cb4141797d6f57ba701fc754b56f45e0c597e35d 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -45,6 +45,8 @@ from Model.REPLine.REPLineList import REPLineList
 
 from Solver.Solvers import solver_type_list
 
+from Model.OutputKpAdists.OutputKpListAdists import OutputKpAdistsList
+
 
 class RiverNode(Node, SQLSubModel):
     _sub_classes = []
@@ -228,6 +230,7 @@ class River(Graph, SQLSubModel):
         HydraulicStructureList,
         AddFileList,
         REPLineList,
+        OutputKpAdistsList,
     ]
 
     def __init__(self, status=None):
@@ -251,6 +254,7 @@ class River(Graph, SQLSubModel):
         )
         self._additional_files = AddFileList(status=self._status)
         self._rep_lines = REPLineList(status=self._status)
+        self._Output_kp_adists = OutputKpAdistsList(status=self._status)
 
     @classmethod
     def _db_create(cls, execute):
@@ -325,6 +329,10 @@ class River(Graph, SQLSubModel):
         )
         new._rep_lines = REPLineList._db_load(execute, data)
 
+        new._Output_kp_adists = OutputKpAdistsList._db_load(
+            execute, data
+        )
+
         return new
 
     def _db_save(self, execute, data=None):
@@ -344,6 +352,8 @@ class River(Graph, SQLSubModel):
         for solver in self._parameters:
             objs.append(self._parameters[solver])
 
+        objs.append(self._Output_kp_adists)
+
         self._save_submodel(execute, objs, data)
         return True
 
@@ -466,6 +476,10 @@ Last export at: @date."""
     def parameters(self):
         return self._parameters
 
+    @property
+    def Output_kp_adists(self):
+        return self._Output_kp_adists
+
     def get_params(self, solver):
         if solver in self._parameters:
             return self._parameters[solver]
diff --git a/src/Solver/AdisTS.py b/src/Solver/AdisTS.py
index 573df17233ca1666863894f949e7e05874c53641..364f2be40a4695d2b3b57012a13a07deb4a3182a 100644
--- a/src/Solver/AdisTS.py
+++ b/src/Solver/AdisTS.py
@@ -81,6 +81,10 @@ class AdisTS(CommandLineSolver):
         name = self._study.name
         return f"{name}.REP"
 
+    def log_file(self):
+        name = self._study.name
+        return f"{name}.TRA"
+
     def _export_REP_additional_lines(self, study, rep_file):
         lines = filter(
             lambda line: line.is_enabled(),
@@ -117,6 +121,25 @@ class AdisTS(CommandLineSolver):
 
         return True
 
+    ###########
+    # RESULTS #
+    ###########
+
+    def read_bin(self, study, repertory, results, qlog=None, name="0"):
+        return
+
+    @timer
+    def results(self, study, repertory, qlog=None, name="0"):
+        results = Results(
+            study=study,
+            solver=self,
+            repertory=repertory,
+            name=name,
+        )
+        self.read_bin(study, repertory, results, qlog, name=name)
+
+        return results
+
 ################################
 # Adis-TS in low coupling mode #
 ################################
diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py
index e1ab44f49d5e4f126659c65f8de4105d9efecccd..448756591c2a10a8bc145a00170ddf7a9990dd2e 100644
--- a/src/View/MainWindow.py
+++ b/src/View/MainWindow.py
@@ -75,6 +75,7 @@ from View.CheckList.Window import CheckListWindow
 from View.Results.Window import ResultsWindow
 from View.Results.ReadingResultsDialog import ReadingResultsDialog
 from View.Debug.Window import ReplWindow
+from View.OutputKpAdisTS.Window import OutputKpAdisTSWindow
 
 # Optional internal display of documentation for make the application
 # package lighter...
@@ -118,7 +119,7 @@ define_model_action = [
     "action_menu_edit_hydraulic_structures", "action_menu_additional_file",
     "action_menu_results_last", "action_open_results_from_file",
     "action_menu_boundary_conditions_sediment",
-    "action_menu_rep_additional_lines",
+    "action_menu_rep_additional_lines", "action_menu_output_kp",
 ]
 
 action = (
@@ -232,6 +233,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
         """
         actions = {
             # Menu action
+            "action_menu_output_kp": self.open_output_kp_adists,
             "action_menu_config": self.open_configure,
             "action_menu_new": self.open_new_study,
             "action_menu_edit": self.open_edit_study,
@@ -860,6 +862,19 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
     # SUB WINDOWS #
     ###############
 
+    def open_output_kp_adists(self):
+        if self.sub_window_exists(
+            OutputKpAdisTSWindow,
+            data=[self._study, None]
+        ):
+            return
+
+        Output_Kp_AdisTS = OutputKpAdisTSWindow(
+            study=self._study,
+            parent=self
+        )
+        Output_Kp_AdisTS.show()
+
     def open_configure(self):
         """Open configure window
 
diff --git a/src/View/OutputKpAdisTS/BasicHydraulicStructures/Table.py b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Table.py
new file mode 100644
index 0000000000000000000000000000000000000000..f69a4099f458accc233412f1cfbc4fa6fcf361d4
--- /dev/null
+++ b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Table.py
@@ -0,0 +1,277 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+import traceback
+
+from tools import trace, timer
+
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel,
+    QCoreApplication, QModelIndex, pyqtSlot,
+    QRect,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QComboBox, QMessageBox,
+)
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from View.HydraulicStructures.BasicHydraulicStructures.UndoCommand import (
+    SetNameCommand, SetTypeCommand,
+    SetEnabledCommand, AddCommand, DelCommand,
+    SetValueCommand,
+)
+from Model.HydraulicStructures.Basic.Types import BHS_types
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class ComboBoxDelegate(QItemDelegate):
+    def __init__(self, data=None, trad=None, parent=None):
+        super(ComboBoxDelegate, self).__init__(parent)
+
+        self._data = data
+        self._trad = trad
+
+        self._long_types = {}
+        if self._trad is not None:
+            self._long_types = self._trad.get_dict("long_types")
+
+    def createEditor(self, parent, option, index):
+        self.editor = QComboBox(parent)
+
+        lst = list(
+            map(
+                lambda k: self._long_types[k],
+                BHS_types.keys()
+            )
+        )
+        self.editor.addItems(lst)
+
+        self.editor.setCurrentText(index.data(Qt.DisplayRole))
+        return self.editor
+
+    def setEditorData(self, editor, index):
+        value = index.data(Qt.DisplayRole)
+        self.editor.currentTextChanged.connect(self.currentItemChanged)
+
+    def setModelData(self, editor, model, index):
+        text = str(editor.currentText())
+        model.setData(index, text)
+        editor.close()
+        editor.deleteLater()
+
+    def updateEditorGeometry(self, editor, option, index):
+        r = QRect(option.rect)
+        if self.editor.windowFlags() & Qt.Popup:
+            if editor.parent() is not None:
+                r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
+        editor.setGeometry(r)
+
+    @pyqtSlot()
+    def currentItemChanged(self):
+        self.commitData.emit(self.sender())
+
+
+class TableModel(PamhyrTableModel):
+    def __init__(self, trad=None, **kwargs):
+        self._trad = trad
+        self._long_types = {}
+        if self._trad is not None:
+            self._long_types = self._trad.get_dict("long_types")
+
+        super(TableModel, self).__init__(trad=trad, **kwargs)
+
+    def rowCount(self, parent):
+        return len(self._lst)
+
+    def data(self, index, role):
+        if role != Qt.ItemDataRole.DisplayRole:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        if self._headers[column] == "name":
+            return self._data.basic_structure(row).name
+        elif self._headers[column] == "type":
+            return self._long_types[self._data.basic_structure(row).type]
+
+        return QVariant()
+
+    def setData(self, index, value, role=Qt.EditRole):
+        if not index.isValid() or role != Qt.EditRole:
+            return False
+
+        row = index.row()
+        column = index.column()
+
+        try:
+            if self._headers[column] == "name":
+                self._undo.push(
+                    SetNameCommand(
+                        self._data, row, value
+                    )
+                )
+            elif self._headers[column] == "type":
+                old_type = self._data.basic_structure(row).type
+
+                if old_type == "ND" or self._question_set_type():
+                    key = next(
+                        k for k, v in self._long_types.items()
+                        if v == value
+                    )
+
+                    self._undo.push(
+                        SetTypeCommand(
+                            self._data, row, BHS_types[key]
+                        )
+                    )
+        except Exception as e:
+            logger.error(e)
+            logger.debug(traceback.format_exc())
+
+        self.dataChanged.emit(index, index)
+        return True
+
+    def _question_set_type(self):
+        question = QMessageBox(self._parent)
+
+        question.setWindowTitle(self._trad['msg_type_change_title'])
+        question.setText(self._trad['msg_type_change_text'])
+        question.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok)
+        question.setIcon(QMessageBox.Question)
+
+        res = question.exec()
+        return res == QMessageBox.Ok
+
+    def add(self, row, parent=QModelIndex()):
+        self.beginInsertRows(parent, row, row - 1)
+
+        self._undo.push(
+            AddCommand(
+                self._data, row
+            )
+        )
+
+        self.endInsertRows()
+        self.layoutChanged.emit()
+
+    def delete(self, rows, parent=QModelIndex()):
+        self.beginRemoveRows(parent, rows[0], rows[-1])
+
+        self._undo.push(
+            DelCommand(
+                self._data, rows
+            )
+        )
+
+        self.endRemoveRows()
+        self.layoutChanged.emit()
+
+    def enabled(self, row, enabled, parent=QModelIndex()):
+        self._undo.push(
+            SetEnabledCommand(
+                self._lst, row, enabled
+            )
+        )
+        self.layoutChanged.emit()
+
+    def undo(self):
+        self._undo.undo()
+        self.layoutChanged.emit()
+
+    def redo(self):
+        self._undo.redo()
+        self.layoutChanged.emit()
+
+
+class ParametersTableModel(PamhyrTableModel):
+    def __init__(self, trad=None, **kwargs):
+        self._trad = trad
+        self._long_types = {}
+
+        if self._trad is not None:
+            self._long_types = self._trad.get_dict("long_types")
+
+        self._hs_index = None
+
+        super(ParametersTableModel, self).__init__(trad=trad, **kwargs)
+
+    def rowCount(self, parent):
+        if self._hs_index is None:
+            return 0
+
+        return len(
+            self._data.basic_structure(self._hs_index)
+        )
+
+    def data(self, index, role):
+        if role != Qt.ItemDataRole.DisplayRole:
+            return QVariant()
+
+        if self._hs_index is None:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        hs = self._data.basic_structure(self._hs_index)
+
+        if self._headers[column] == "name":
+            return self._trad[hs.parameters[row].name]
+        elif self._headers[column] == "value":
+            return str(hs.parameters[row].value)
+
+        return QVariant()
+
+    def setData(self, index, value, role=Qt.EditRole):
+        if not index.isValid() or role != Qt.EditRole:
+            return False
+
+        if self._hs_index is None:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        try:
+            if self._headers[column] == "value":
+                self._undo.push(
+                    SetValueCommand(
+                        self._data.basic_structure(self._hs_index),
+                        row, value
+                    )
+                )
+        except Exception as e:
+            logger.error(e)
+            logger.debug(traceback.format_exc())
+
+        self.dataChanged.emit(index, index)
+        return True
+
+    def update_hs_index(self, index):
+        self._hs_index = index
+        self.layoutChanged.emit()
diff --git a/src/View/OutputKpAdisTS/BasicHydraulicStructures/Translate.py b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Translate.py
new file mode 100644
index 0000000000000000000000000000000000000000..f55fb2d778e212588520e94dafd133c101c38413
--- /dev/null
+++ b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Translate.py
@@ -0,0 +1,155 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+from View.Translate import MainTranslate
+
+_translate = QCoreApplication.translate
+
+
+class BasicHydraulicStructuresTranslate(MainTranslate):
+    def __init__(self):
+        super(BasicHydraulicStructuresTranslate, self).__init__()
+
+        self._dict["Basic Hydraulic Structures"] = _translate(
+            "BasicHydraulicStructures", "Basic Hydraulic Structures"
+        )
+
+        self._dict['msg_type_change_title'] = _translate(
+            "BasicHydraulicStructures",
+            "Change hydraulic structure type"
+        )
+
+        self._dict['msg_type_change_text'] = _translate(
+            "BasicHydraulicStructures",
+            "Do you want to change the hydraulic structure type and reset \
+hydraulic structure values?"
+        )
+
+        # BHSValues translation
+
+        self._dict['width'] = self._dict["unit_width"]
+        self._dict['height'] = self._dict["unit_thickness"]
+        self._dict['elevation'] = self._dict["unit_elevation"]
+        self._dict['diameter'] = self._dict["unit_diameter"]
+        self._dict['discharge_coefficient'] = _translate(
+            "BasicHydraulicStructures", "Discharge coefficient"
+        )
+        self._dict['loading_elevation'] = _translate(
+            "BasicHydraulicStructures", "Upper elevation (m)"
+        )
+        self._dict['half-angle_tangent'] = _translate(
+            "BasicHydraulicStructures", "Half-angle tangent"
+        )
+        self._dict['maximal_loading_elevation'] = _translate(
+            "BasicHydraulicStructures", "Maximal loading elevation"
+        )
+        self._dict['siltation_height'] = _translate(
+            "BasicHydraulicStructures", "Siltation height (m)"
+        )
+        self._dict['top_of_the_vault'] = _translate(
+            "BasicHydraulicStructures", "Top of the vault (m)"
+        )
+        self._dict['bottom_of_the_vault'] = _translate(
+            "BasicHydraulicStructures", "Bottom of the vault (m)"
+        )
+        self._dict['opening'] = _translate(
+            "BasicHydraulicStructures", "Opening"
+        )
+        self._dict['maximal_opening'] = _translate(
+            "BasicHydraulicStructures", "Maximal opening"
+        )
+        self._dict['step_space'] = _translate(
+            "BasicHydraulicStructures", "Step space"
+        )
+        self._dict['weir'] = _translate(
+            "BasicHydraulicStructures", "Weir"
+        )
+        self._dict['coefficient'] = _translate(
+            "BasicHydraulicStructures", "Coefficient"
+        )
+
+        # Dummy parameters
+
+        self._dict['parameter_1'] = _translate(
+            "BasicHydraulicStructures", "Parameter 1"
+        )
+        self._dict['parameter_2'] = _translate(
+            "BasicHydraulicStructures", "Parameter 2"
+        )
+        self._dict['parameter_3'] = _translate(
+            "BasicHydraulicStructures", "Parameter 3"
+        )
+        self._dict['parameter_4'] = _translate(
+            "BasicHydraulicStructures", "Parameter 4"
+        )
+        self._dict['parameter_5'] = _translate(
+            "BasicHydraulicStructures", "Parameter 5"
+        )
+
+        # BHS types long names
+
+        self._sub_dict["long_types"] = {
+            "ND": self._dict["not_defined"],
+            "S1": _translate(
+                "BasicHydraulicStructures", "Discharge weir"
+            ),
+            "S2": _translate(
+                "BasicHydraulicStructures", "Trapezoidal weir"
+            ),
+            "S3": _translate(
+                "BasicHydraulicStructures", "Triangular weir"
+            ),
+            "OR": _translate(
+                "BasicHydraulicStructures", "Rectangular orifice"
+            ),
+            "OC": _translate(
+                "BasicHydraulicStructures", "Circular orifice"
+            ),
+            "OV": _translate(
+                "BasicHydraulicStructures", "Vaulted orifice"
+            ),
+            "V1": _translate(
+                "BasicHydraulicStructures", "Rectangular gate"
+            ),
+            "V2": _translate(
+                "BasicHydraulicStructures", "Simplified rectangular gate"
+            ),
+            "BO": _translate(
+                "BasicHydraulicStructures", "Borda-type head loss"
+            ),
+            "CV": _translate(
+                "BasicHydraulicStructures", "Check valve"
+            ),
+            "UD": _translate(
+                "BasicHydraulicStructures", "User defined"
+            ),
+        }
+
+        # Tables
+
+        self._sub_dict["table_headers"] = {
+            "name": self._dict["name"],
+            "type": self._dict["type"],
+        }
+
+        self._sub_dict["table_headers_parameters"] = {
+            "name": self._dict["name"],
+            "value": self._dict["value"],
+        }
diff --git a/src/View/OutputKpAdisTS/BasicHydraulicStructures/UndoCommand.py b/src/View/OutputKpAdisTS/BasicHydraulicStructures/UndoCommand.py
new file mode 100644
index 0000000000000000000000000000000000000000..78fa3d62ea3f06c45c7d11d4c79b8b7c8b81e6d8
--- /dev/null
+++ b/src/View/OutputKpAdisTS/BasicHydraulicStructures/UndoCommand.py
@@ -0,0 +1,153 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+
+class SetNameCommand(QUndoCommand):
+    def __init__(self, hs, index, new_value):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+        self._index = index
+        self._old = self._hs.basic_structure(self._index).name
+        self._new = str(new_value)
+
+    def undo(self):
+        self._hs.basic_structure(self._index).name = self._old
+
+    def redo(self):
+        self._hs.basic_structure(self._index).name = self._new
+
+
+class SetTypeCommand(QUndoCommand):
+    def __init__(self, hs, index, new_type):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+        self._index = index
+        self._type = new_type
+        self._old = self._hs.basic_structure(self._index)
+        self._new = self._hs.basic_structure(self._index)\
+            .convert(self._type)
+
+    def undo(self):
+        self._hs.delete_i([self._index])
+        self._hs.insert(self._index, self._old)
+
+    def redo(self):
+        self._hs.delete_i([self._index])
+        self._hs.insert(self._index, self._new)
+
+
+class SetEnabledCommand(QUndoCommand):
+    def __init__(self, hs, index, enabled):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+        self._index = index
+        self._old = not enabled
+        self._new = enabled
+
+    def undo(self):
+        self._hs.basic_structure(self._index).enabled = self._old
+
+    def redo(self):
+        self._hs.basic_structure(self._index).enabled = self._new
+
+
+class AddCommand(QUndoCommand):
+    def __init__(self, hs, index):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+
+        self._index = index
+        self._new = None
+
+    def undo(self):
+        self._hs.delete_i([self._index])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._hs.add(self._index)
+        else:
+            self._hs.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+    def __init__(self, hs, rows):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+
+        self._rows = rows
+
+        self._bhs = []
+        for row in rows:
+            self._bhs.append((row, self._hs.basic_structure(row)))
+
+    def undo(self):
+        for row, el in self._bhs:
+            self._hs.insert(row, el)
+
+    def redo(self):
+        self._hs.delete_i(self._rows)
+
+
+class PasteCommand(QUndoCommand):
+    def __init__(self, hs, row, h_s):
+        QUndoCommand.__init__(self)
+
+        self._hs = hs
+
+        self._row = row
+        self._bhs = deepcopy(h_s)
+        self._bhs.reverse()
+
+    def undo(self):
+        self._hs.delete_i(range(self._row, self._row + len(self._bhs)))
+
+    def redo(self):
+        for r in self._bhs:
+            self._hs.insert(self._row, r)
+
+####################################
+# Basic hydraulic structure values #
+####################################
+
+
+class SetValueCommand(QUndoCommand):
+    def __init__(self, bhs, index, value):
+        QUndoCommand.__init__(self)
+
+        self._bhs = bhs
+        self._index = index
+        self._old = self._bhs.parameters[self._index].value
+        self._new = self._bhs.parameters[self._index].type(value)
+
+    def undo(self):
+        self._bhs.parameters[self._index].value = self._old
+
+    def redo(self):
+        self._bhs.parameters[self._index].value = self._new
diff --git a/src/View/OutputKpAdisTS/BasicHydraulicStructures/Window.py b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Window.py
new file mode 100644
index 0000000000000000000000000000000000000000..9a39f49c64749b995b3ac3733038ee0e720003b5
--- /dev/null
+++ b/src/View/OutputKpAdisTS/BasicHydraulicStructures/Window.py
@@ -0,0 +1,270 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import timer, trace
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+
+from PyQt5 import QtCore
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel, QCoreApplication,
+    pyqtSlot, pyqtSignal, QItemSelectionModel,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox
+)
+
+from View.Tools.Plot.PamhyrCanvas import MplCanvas
+from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
+
+from View.HydraulicStructures.PlotAC import PlotAC
+
+from View.HydraulicStructures.BasicHydraulicStructures.Table import (
+    ComboBoxDelegate, TableModel, ParametersTableModel,
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.HydraulicStructures.BasicHydraulicStructures.Translate import (
+    BasicHydraulicStructuresTranslate
+)
+
+_translate = QCoreApplication.translate
+
+logger = logging.getLogger()
+
+
+class BasicHydraulicStructuresWindow(PamhyrWindow):
+    _pamhyr_ui = "BasicHydraulicStructures"
+    _pamhyr_name = "Basic Hydraulic Structures"
+
+    def __init__(self, data=None, study=None, config=None, parent=None):
+        trad = BasicHydraulicStructuresTranslate()
+        name = " - ".join([
+            trad[self._pamhyr_name], data.name, study.name
+        ])
+
+        super(BasicHydraulicStructuresWindow, self).__init__(
+            title=name,
+            study=study,
+            config=config,
+            trad=trad,
+            parent=parent
+        )
+
+        self._hash_data.append(data)
+
+        self._hs = data
+
+        self.setup_table()
+        self.setup_checkbox()
+        self.setup_plot()
+        self.setup_connections()
+
+        self.update()
+
+    def setup_table(self):
+        self.setup_table_bhs()
+        self.setup_table_bhs_parameters()
+
+    def setup_table_bhs(self):
+        self._table = None
+
+        self._delegate_type = ComboBoxDelegate(
+            trad=self._trad,
+            parent=self
+        )
+
+        table = self.find(QTableView, f"tableView")
+        self._table = TableModel(
+            table_view=table,
+            table_headers=self._trad.get_dict("table_headers"),
+            editable_headers=["name", "type"],
+            delegates={
+                "type": self._delegate_type,
+            },
+            trad=self._trad,
+            data=self._hs,
+            undo=self._undo_stack,
+            parent=self,
+        )
+
+        selectionModel = table.selectionModel()
+        index = table.model().index(0, 0)
+
+        selectionModel.select(
+            index,
+            QItemSelectionModel.Rows |
+            QItemSelectionModel.ClearAndSelect |
+            QItemSelectionModel.Select
+        )
+        table.scrollTo(index)
+
+    def setup_table_bhs_parameters(self):
+        self._table_parameters = None
+
+        table = self.find(QTableView, f"tableView_2")
+        self._table_parameters = ParametersTableModel(
+            table_view=table,
+            table_headers=self._trad.get_dict("table_headers_parameters"),
+            editable_headers=["value"],
+            delegates={},
+            trad=self._trad,
+            data=self._hs,
+            undo=self._undo_stack,
+            parent=self,
+        )
+
+    def setup_checkbox(self):
+        self._checkbox = self.find(QCheckBox, f"checkBox")
+        self._set_checkbox_state()
+
+    def setup_plot(self):
+        self.canvas = MplCanvas(width=5, height=4, dpi=100)
+        self.canvas.setObjectName("canvas")
+        self.toolbar = PamhyrPlotToolbar(
+            self.canvas, self
+        )
+        self.plot_layout = self.find(QVBoxLayout, "verticalLayout")
+        self.plot_layout.addWidget(self.toolbar)
+        self.plot_layout.addWidget(self.canvas)
+
+        reach = self._hs.input_reach
+        profile_kp = self._hs.input_kp
+        if profile_kp is not None:
+            profiles = reach.reach.get_profiles_from_kp(float(profile_kp))
+        else:
+            profiles = None
+        if profiles is not None:
+            profile = profiles[0]
+        else:
+            profile = None
+
+        self.plot_ac = PlotAC(
+            canvas=self.canvas,
+            river=self._study.river,
+            reach=self._hs.input_reach,
+            profile=profile,
+            trad=self._trad,
+            toolbar=self.toolbar
+        )
+        self.plot_ac.draw()
+
+    def setup_connections(self):
+        self.find(QAction, "action_add").triggered.connect(self.add)
+        self.find(QAction, "action_delete").triggered.connect(self.delete)
+        self._checkbox.clicked.connect(self._set_basic_structure_state)
+
+        table = self.find(QTableView, "tableView")
+        table.selectionModel()\
+             .selectionChanged\
+             .connect(self.update)
+
+        self._table.dataChanged.connect(self.update)
+        self._table.layoutChanged.connect(self.update)
+
+    def index_selected(self):
+        table = self.find(QTableView, "tableView")
+        r = table.selectionModel().selectedRows()
+
+        if len(r) > 0:
+            return r[0]
+        else:
+            return None
+
+    def index_selected_row(self):
+        table = self.find(QTableView, "tableView")
+        r = table.selectionModel().selectedRows()
+
+        if len(r) > 0:
+            return r[0].row()
+        else:
+            return None
+
+    def index_selected_rows(self):
+        table = self.find(QTableView, "tableView")
+        return list(
+            # Delete duplicate
+            set(
+                map(
+                    lambda i: i.row(),
+                    table.selectedIndexes()
+                )
+            )
+        )
+
+    def add(self):
+        rows = self.index_selected_rows()
+
+        if len(self._hs) == 0 or len(rows) == 0:
+            self._table.add(0)
+        else:
+            self._table.add(rows[0])
+
+    def delete(self):
+        rows = self.index_selected_rows()
+
+        if len(rows) == 0:
+            return
+
+        self._table.delete(rows)
+
+    def _copy(self):
+        logger.info("TODO: copy")
+
+    def _paste(self):
+        logger.info("TODO: paste")
+
+    def _undo(self):
+        self._table.undo()
+
+    def _redo(self):
+        self._table.redo()
+
+    def _set_checkbox_state(self):
+        row = self.index_selected_row()
+
+        if row is None:
+            self._checkbox.setEnabled(False)
+            self._checkbox.setChecked(True)
+        else:
+            self._checkbox.setEnabled(True)
+            self._checkbox.setChecked(self._hs.basic_structure(row).enabled)
+
+    def _set_basic_structure_state(self):
+        rows = self.index_selected_rows()
+        if len(rows) != 0:
+            for row in rows:
+                if row is not None:
+                    self._table.enabled(
+                        row,
+                        self._checkbox.isChecked()
+                    )
+
+    def update(self):
+        self._set_checkbox_state()
+        self._update_parameters_table()
+
+    def _update_parameters_table(self):
+        row = self.index_selected_row()
+        self._table_parameters.update_hs_index(row)
diff --git a/src/View/OutputKpAdisTS/PlotAC.py b/src/View/OutputKpAdisTS/PlotAC.py
new file mode 100644
index 0000000000000000000000000000000000000000..c63dc9bfdef5fb2e311b54eb90cf85e4937a4b1c
--- /dev/null
+++ b/src/View/OutputKpAdisTS/PlotAC.py
@@ -0,0 +1,120 @@
+# PlotAC.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from tools import timer
+from View.Tools.PamhyrPlot import PamhyrPlot
+from matplotlib import pyplot as plt
+
+
+class PlotAC(PamhyrPlot):
+    def __init__(self, canvas=None, trad=None, toolbar=None,
+                 river=None, reach=None, profile=None,
+                 parent=None):
+        super(PlotAC, self).__init__(
+            canvas=canvas,
+            trad=trad,
+            data=river,
+            toolbar=toolbar,
+            parent=parent
+        )
+
+        self._current_reach = reach
+        self._current_profile = profile
+
+        self.label_x = self._trad["x"]
+        self.label_y = self._trad["unit_elevation"]
+
+        self._isometric_axis = False
+
+        self._auto_relim_update = True
+        self._autoscale_update = True
+
+    @property
+    def river(self):
+        return self.data
+
+    @river.setter
+    def river(self, river):
+        self.data = river
+
+    @timer
+    def draw(self):
+        self.init_axes()
+
+        if self.data is None:
+            self.line_kp = None
+            return
+
+        if self._current_reach is None:
+            self.line_kp = None
+            return
+
+        self.draw_data()
+
+        self.idle()
+        self._init = True
+
+    def draw_data(self):
+        reach = self._current_reach
+
+        if self._current_profile is None:
+            self.line_kp = None
+        else:
+            profile = self._current_profile
+            x = profile.get_station()
+            z = profile.z()
+
+            self.line_kp, = self.canvas.axes.plot(
+                x, z,
+                color=self.color_plot_river_bottom,
+                **self.plot_default_kargs
+            )
+
+    def set_reach(self, reach):
+        self._current_reach = reach
+        self.update()
+
+    def set_profile(self, profile):
+        self._current_profile = profile
+        self.update()
+
+    def update(self):
+        if self.line_kp is None:
+            self.draw()
+            return
+
+        if self._current_reach is None or self._current_profile is None:
+            self.update_clear()
+        else:
+            self.update_data()
+
+        self.update_idle()
+
+    def update_data(self):
+        profile = self._current_profile
+        x = profile.get_station()
+        z = profile.z()
+
+        self.line_kp.set_data(x, z)
+
+    def clear(self):
+        self.update_clear()
+
+    def update_clear(self):
+        if self.line_kp is not None:
+            self.line_kp.set_data([], [])
diff --git a/src/View/OutputKpAdisTS/PlotKPC.py b/src/View/OutputKpAdisTS/PlotKPC.py
new file mode 100644
index 0000000000000000000000000000000000000000..3327f6e627114ac8513dda1ca57670b0cdf722e8
--- /dev/null
+++ b/src/View/OutputKpAdisTS/PlotKPC.py
@@ -0,0 +1,165 @@
+# PlotKPC.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from tools import timer
+from View.Tools.PamhyrPlot import PamhyrPlot
+
+from PyQt5.QtCore import (
+    QCoreApplication
+)
+
+from matplotlib.collections import LineCollection
+
+_translate = QCoreApplication.translate
+
+
+class PlotKPC(PamhyrPlot):
+    def __init__(self, canvas=None, trad=None, toolbar=None,
+                 river=None, reach=None, profile=None,
+                 parent=None):
+        super(PlotKPC, self).__init__(
+            canvas=canvas,
+            trad=trad,
+            data=river,
+            toolbar=toolbar,
+            parent=parent
+        )
+
+        self._current_reach = reach
+        self._current_profile = profile
+
+        self.label_x = self._trad["unit_kp"]
+        self.label_y = self._trad["unit_elevation"]
+
+        self._isometric_axis = False
+
+        self._auto_relim_update = True
+        self._autoscale_update = True
+
+    @property
+    def river(self):
+        return self.data
+
+    @river.setter
+    def river(self, river):
+        self.data = river
+
+    @timer
+    def draw(self, highlight=None):
+        self.init_axes()
+
+        if self.data is None:
+            self.profile = None
+            self.line_kp_zmin_zmax = None
+            self.line_kp_zmin = None
+            return
+
+        if self._current_reach is None:
+            self.profile = None
+            self.line_kp_zmin_zmax = None
+            self.line_kp_zmin = None
+            return
+
+        self.draw_data()
+        self.draw_current()
+
+        self.idle()
+        self._init = True
+
+    def draw_data(self):
+        reach = self._current_reach
+
+        kp = reach.reach.get_kp()
+        z_min = reach.reach.get_z_min()
+        z_max = reach.reach.get_z_max()
+
+        self.line_kp_zmin, = self.canvas.axes.plot(
+            kp, z_min,
+            color=self.color_plot_river_bottom,
+            lw=1.
+        )
+
+        if len(kp) != 0:
+            self.line_kp_zmin_zmax = self.canvas.axes.vlines(
+                x=kp,
+                ymin=z_min, ymax=z_max,
+                color=self.color_plot,
+                lw=1.
+            )
+
+    def draw_current(self):
+        if self._current_profile is None:
+            self.profile = None
+        else:
+            kp = [self._current_profile.kp,
+                  self._current_profile.kp]
+            min_max = [self._current_profile.z_min(),
+                       self._current_profile.z_max()]
+
+            self.profile = self.canvas.axes.plot(
+                kp, min_max,
+                color=self.color_plot_current,
+                lw=1.
+            )
+
+    def set_reach(self, reach):
+        self._current_reach = reach
+        self._current_profile = None
+        self.update()
+
+    def set_profile(self, profile):
+        self._current_profile = profile
+        self.update_current_profile()
+
+    def update(self):
+        self.draw()
+
+    def update_current_profile(self):
+        reach = self._current_reach
+        kp = reach.reach.get_kp()
+        z_min = reach.reach.get_z_min()
+        z_max = reach.reach.get_z_max()
+
+        if self.profile is None:
+            self.draw()
+        else:
+            self.profile.set_data(
+                [self._current_profile.kp, self._current_profile.kp],
+                [self._current_profile.z_min(), self._current_profile.z_max()],
+            )
+
+            self.update_idle()
+
+    def clear(self):
+        if self.profile is not None:
+            self.profile[0].set_data([], [])
+
+        if self.line_kp_zmin_zmax is not None:
+            self.line_kp_zmin_zmax.remove()
+            self.line_kp_zmin_zmax = None
+
+        if self.line_kp_zmin is not None:
+            self.line_kp_zmin.set_data([], [])
+
+        self.canvas.figure.canvas.draw_idle()
+
+    def clear_profile(self):
+        if self.profile is not None:
+            self.profile.set_data([], [])
+
+        self.canvas.figure.canvas.draw_idle()
diff --git a/src/View/OutputKpAdisTS/Table.py b/src/View/OutputKpAdisTS/Table.py
new file mode 100644
index 0000000000000000000000000000000000000000..a794f0a0d2ffc9bf14cf14071086d361e042dbfc
--- /dev/null
+++ b/src/View/OutputKpAdisTS/Table.py
@@ -0,0 +1,215 @@
+# Table.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+import traceback
+
+from tools import trace, timer
+
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel,
+    QCoreApplication, QModelIndex, pyqtSlot,
+    QRect,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QComboBox,
+)
+
+from View.Tools.PamhyrTable import PamhyrTableModel
+
+from View.HydraulicStructures.UndoCommand import (
+    SetNameCommand, SetReachCommand, SetKpCommand,
+    SetEnabledCommand, AddCommand, DelCommand,
+)
+
+logger = logging.getLogger()
+
+_translate = QCoreApplication.translate
+
+
+class ComboBoxDelegate(QItemDelegate):
+    def __init__(self, data=None, trad=None, parent=None, mode="reaches"):
+        super(ComboBoxDelegate, self).__init__(parent)
+
+        self._data = data
+        self._trad = trad
+        self._mode = mode
+
+    def createEditor(self, parent, option, index):
+        self.editor = QComboBox(parent)
+
+        val = []
+        if self._mode == "kp":
+            reach = self._data.hydraulic_structures\
+                              .get(index.row())\
+                              .input_reach
+            if reach is not None:
+                val = list(
+                    map(
+                        lambda kp: str(kp), reach.reach.get_kp()
+                    )
+                )
+        else:
+            val = list(
+                map(
+                    lambda n: n.name, self._data.edges()
+                )
+            )
+
+        self.editor.addItems(
+            [self._trad['not_associated']] +
+            val
+        )
+
+        self.editor.setCurrentText(str(index.data(Qt.DisplayRole)))
+        return self.editor
+
+    def setEditorData(self, editor, index):
+        value = index.data(Qt.DisplayRole)
+        self.editor.currentTextChanged.connect(self.currentItemChanged)
+
+    def setModelData(self, editor, model, index):
+        text = str(editor.currentText())
+        model.setData(index, text)
+        editor.close()
+        editor.deleteLater()
+
+    def updateEditorGeometry(self, editor, option, index):
+        r = QRect(option.rect)
+        if self.editor.windowFlags() & Qt.Popup:
+            if editor.parent() is not None:
+                r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
+        editor.setGeometry(r)
+
+    @pyqtSlot()
+    def currentItemChanged(self):
+        self.commitData.emit(self.sender())
+
+
+class TableModel(PamhyrTableModel):
+    def _setup_lst(self):
+        self._lst = self._data._hydraulic_structures
+
+    def rowCount(self, parent):
+        return len(self._lst)
+
+    def data(self, index, role):
+        if role != Qt.ItemDataRole.DisplayRole:
+            return QVariant()
+
+        row = index.row()
+        column = index.column()
+
+        if self._headers[column] == "name":
+            return self._lst.get(row).name
+        elif self._headers[column] == "reach":
+            n = self._lst.get(row).input_reach
+            if n is None:
+                return self._trad['not_associated']
+            return n.name
+        elif self._headers[column] == "kp":
+            n = self._lst.get(row).input_kp
+            if n is None:
+                return self._trad['not_associated']
+            return n
+
+        return QVariant()
+
+    def setData(self, index, value, role=Qt.EditRole):
+        if not index.isValid() or role != Qt.EditRole:
+            return False
+
+        row = index.row()
+        column = index.column()
+        na = self._trad['not_associated']
+
+        try:
+            if self._headers[column] == "name":
+                self._undo.push(
+                    SetNameCommand(
+                        self._lst, row, value
+                    )
+                )
+            elif self._headers[column] == "reach":
+                if value == na:
+                    value = None
+
+                self._undo.push(
+                    SetReachCommand(
+                        self._lst, row, self._data.edge(value)
+                    )
+                )
+            elif self._headers[column] == "kp":
+                if value == na:
+                    value = None
+
+                self._undo.push(
+                    SetKpCommand(
+                        self._lst, row, value
+                    )
+                )
+        except Exception as e:
+            logger.info(e)
+            logger.debug(traceback.format_exc())
+
+        self.dataChanged.emit(index, index)
+        return True
+
+    def add(self, row, parent=QModelIndex()):
+        self.beginInsertRows(parent, row, row - 1)
+
+        self._undo.push(
+            AddCommand(
+                self._lst, row
+            )
+        )
+
+        self.endInsertRows()
+        self.layoutChanged.emit()
+
+    def delete(self, rows, parent=QModelIndex()):
+        self.beginRemoveRows(parent, rows[0], rows[-1])
+
+        self._undo.push(
+            DelCommand(
+                self._lst, rows
+            )
+        )
+
+        self.endRemoveRows()
+        self.layoutChanged.emit()
+
+    def enabled(self, row, enabled, parent=QModelIndex()):
+        self._undo.push(
+            SetEnabledCommand(
+                self._lst, row, enabled
+            )
+        )
+        self.layoutChanged.emit()
+
+    def undo(self):
+        self._undo.undo()
+        self.layoutChanged.emit()
+
+    def redo(self):
+        self._undo.redo()
+        self.layoutChanged.emit()
diff --git a/src/View/OutputKpAdisTS/Translate.py b/src/View/OutputKpAdisTS/Translate.py
new file mode 100644
index 0000000000000000000000000000000000000000..b57fb116242da3b1948e8c092878835d139511d7
--- /dev/null
+++ b/src/View/OutputKpAdisTS/Translate.py
@@ -0,0 +1,40 @@
+# translate.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+from View.Translate import MainTranslate
+
+_translate = QCoreApplication.translate
+
+
+class OutputKpAdisTSTranslate(MainTranslate):
+    def __init__(self):
+        super(OutputKpAdisTSTranslate, self).__init__()
+
+        self._dict["Output Kp"] = _translate(
+            "OutputKpAdisTS", "Output Kp"
+        )
+
+        self._dict["x"] = _translate("OutputKpAdisTS", "X (m)")
+
+        self._sub_dict["table_headers"] = {
+            "name": self._dict["name"],
+            "reach": self._dict["reach"],
+            "kp": self._dict["unit_kp"],
+        }
diff --git a/src/View/OutputKpAdisTS/UndoCommand.py b/src/View/OutputKpAdisTS/UndoCommand.py
new file mode 100644
index 0000000000000000000000000000000000000000..cbb8a2e7927237c3b1a2d97f689c0a547270794d
--- /dev/null
+++ b/src/View/OutputKpAdisTS/UndoCommand.py
@@ -0,0 +1,159 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+logger = logging.getLogger()
+
+
+class SetNameCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index, new_value):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+        self._index = index
+        self._old = self._h_s_lst.get(self._index).name
+        self._new = str(new_value)
+
+    def undo(self):
+        self._h_s_lst.get(self._index).name = self._old
+
+    def redo(self):
+        self._h_s_lst.get(self._index).name = self._new
+
+
+class SetReachCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index, reach):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+        self._index = index
+        self._old = self._h_s_lst.get(self._index).input_reach
+        self._new = reach
+        self._old_kp = self._h_s_lst.get(self._index).input_kp
+        self._new_kp = None
+
+    def undo(self):
+        i = self._h_s_lst.get(self._index)
+        i.input_reach = self._old
+        i.input_kp = self._old_kp
+
+    def redo(self):
+        i = self._h_s_lst.get(self._index)
+        i.input_reach = self._new
+        i.input_kp = self._new_kp
+
+
+class SetKpCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index, kp):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+        self._index = index
+        self._old = self._h_s_lst.get(self._index).input_kp
+        self._new = kp
+
+    def undo(self):
+        self._h_s_lst.get(self._index).input_kp = self._old
+
+    def redo(self):
+        self._h_s_lst.get(self._index).input_kp = self._new
+
+
+class SetEnabledCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index, enabled):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+        self._index = index
+        self._old = not enabled
+        self._new = enabled
+
+    def undo(self):
+        self._h_s_lst.get(self._index).enabled = self._old
+
+    def redo(self):
+        self._h_s_lst.get(self._index).enabled = self._new
+
+
+class AddCommand(QUndoCommand):
+    def __init__(self, h_s_lst, index):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+
+        self._index = index
+        self._new = None
+
+    def undo(self):
+        self._h_s_lst.delete_i([self._index])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._h_s_lst.new(self._h_s_lst, self._index)
+        else:
+            self._h_s_lst.insert(self._index, self._new)
+
+
+class DelCommand(QUndoCommand):
+    def __init__(self, h_s_lst, rows):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+
+        self._rows = rows
+
+        self._h_s = []
+        for row in rows:
+            self._h_s.append((row, self._h_s_lst.get(row)))
+        self._h_s.sort()
+
+    def undo(self):
+        for row, el in self._h_s:
+            self._h_s_lst.insert(row, el)
+
+    def redo(self):
+        self._h_s_lst.delete_i(self._rows)
+
+
+class PasteCommand(QUndoCommand):
+    def __init__(self, h_s_lst, row, h_s):
+        QUndoCommand.__init__(self)
+
+        self._h_s_lst = h_s_lst
+
+        self._row = row
+        self._h_s = deepcopy(h_s)
+        self._h_s.reverse()
+
+    def undo(self):
+        self._h_s_lst.delete_i(
+            self._tab,
+            range(self._row, self._row + len(self._h_s))
+        )
+
+    def redo(self):
+        for r in self._h_s:
+            self._h_s_lst.insert(self._row, r)
diff --git a/src/View/OutputKpAdisTS/Window.py b/src/View/OutputKpAdisTS/Window.py
new file mode 100644
index 0000000000000000000000000000000000000000..3951f52af2fb979a117025fa0ff6f0a99221bc85
--- /dev/null
+++ b/src/View/OutputKpAdisTS/Window.py
@@ -0,0 +1,315 @@
+# Window.py -- Pamhyr
+# Copyright (C) 2023-2024  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import timer, trace
+
+from View.Tools.PamhyrWindow import PamhyrWindow
+
+from PyQt5 import QtCore
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel, QCoreApplication,
+    pyqtSlot, pyqtSignal, QItemSelectionModel,
+)
+
+from PyQt5.QtWidgets import (
+    QDialogButtonBox, QPushButton, QLineEdit,
+    QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
+    QHeaderView, QDoubleSpinBox, QVBoxLayout, QCheckBox
+)
+
+from View.Tools.Plot.PamhyrCanvas import MplCanvas
+from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
+
+from View.OutputKpAdisTS.PlotAC import PlotAC
+from View.OutputKpAdisTS.PlotKPC import PlotKPC
+
+from View.OutputKpAdisTS.Table import (
+    TableModel, ComboBoxDelegate
+)
+
+from View.Network.GraphWidget import GraphWidget
+from View.OutputKpAdisTS.Translate import OutputKpAdisTSTranslate
+
+from View.OutputKpAdisTS.BasicHydraulicStructures.Window import (
+    BasicHydraulicStructuresWindow
+)
+
+logger = logging.getLogger()
+
+
+class OutputKpAdisTSWindow(PamhyrWindow):
+    _pamhyr_ui = "OutputKpAdisTS"
+    _pamhyr_name = "Output Kp"
+
+    def __init__(self, study=None, config=None, parent=None):
+        trad = OutputKpAdisTSTranslate()
+        name = trad[self._pamhyr_name] + " - " + study.name
+
+        super(OutputKpAdisTSWindow, self).__init__(
+            title=name,
+            study=study,
+            config=config,
+            trad=trad,
+            parent=parent
+        )
+
+        self._hs_lst = self._study.river._hydraulic_structures
+
+        self.setup_table()
+        #self.setup_checkbox()
+        #self.setup_plots()
+        self.setup_connections()
+
+        #self.update()
+
+    def setup_table(self):
+        self._table = None
+
+        self._delegate_reach = ComboBoxDelegate(
+            trad=self._trad,
+            data=self._study.river,
+            parent=self,
+            mode="reaches"
+        )
+        self._delegate_kp = ComboBoxDelegate(
+            trad=self._trad,
+            data=self._study.river,
+            parent=self,
+            mode="kp"
+        )
+
+        table = self.find(QTableView, f"tableView")
+        self._table = TableModel(
+            table_view=table,
+            table_headers=self._trad.get_dict("table_headers"),
+            editable_headers=["name", "reach", "kp"],
+            delegates={
+                "reach": self._delegate_reach,
+                "kp": self._delegate_kp,
+            },
+            trad=self._trad,
+            data=self._study.river,
+            undo=self._undo_stack,
+        )
+
+        selectionModel = table.selectionModel()
+        index = table.model().index(0, 0)
+
+        selectionModel.select(
+            index,
+            QItemSelectionModel.Rows |
+            QItemSelectionModel.ClearAndSelect |
+            QItemSelectionModel.Select
+        )
+        table.scrollTo(index)
+
+    def setup_checkbox(self):
+        self._checkbox = self.find(QCheckBox, f"checkBox")
+        self._set_checkbox_state()
+
+    def setup_plots(self):
+        self.canvas = MplCanvas(width=5, height=4, dpi=100)
+        self.canvas.setObjectName("canvas")
+        self.toolbar = PamhyrPlotToolbar(
+            self.canvas, self
+        )
+        self.plot_layout = self.find(QVBoxLayout, "verticalLayout")
+        self.plot_layout.addWidget(self.toolbar)
+        self.plot_layout.addWidget(self.canvas)
+
+        self.plot_kpc = PlotKPC(
+            canvas=self.canvas,
+            river=self._study.river,
+            reach=None,
+            profile=None,
+            trad=self._trad,
+            toolbar=self.toolbar
+        )
+        self.plot_kpc.draw()
+
+        self.canvas_2 = MplCanvas(width=5, height=4, dpi=100)
+        self.canvas_2.setObjectName("canvas_2")
+        self.toolbar_2 = PamhyrPlotToolbar(
+            self.canvas_2, self
+        )
+        self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2")
+        self.plot_layout_2.addWidget(self.toolbar_2)
+        self.plot_layout_2.addWidget(self.canvas_2)
+
+        self.plot_ac = PlotAC(
+            canvas=self.canvas_2,
+            river=self._study.river,
+            reach=None,
+            profile=None,
+            trad=self._trad,
+            toolbar=self.toolbar_2
+        )
+        self.plot_ac.draw()
+
+    def setup_connections(self):
+        self.find(QAction, "action_add").triggered.connect(self.add)
+        self.find(QAction, "action_delete").triggered.connect(self.delete)
+        self.find(QAction, "action_edit").triggered.connect(self.edit)
+        self._checkbox.clicked.connect(self._set_structure_state)
+
+        table = self.find(QTableView, "tableView")
+        table.selectionModel()\
+             .selectionChanged\
+             .connect(self.update)
+
+        self._table.dataChanged.connect(self.update)
+        self._table.layoutChanged.connect(self.update)
+
+    def index_selected(self):
+        table = self.find(QTableView, "tableView")
+        r = table.selectionModel().selectedRows()
+
+        if len(r) > 0:
+            return r[0]
+        else:
+            return None
+
+    def index_selected_row(self):
+        table = self.find(QTableView, "tableView")
+        r = table.selectionModel().selectedRows()
+
+        if len(r) > 0:
+            return r[0].row()
+        else:
+            return None
+
+    def index_selected_rows(self):
+        table = self.find(QTableView, "tableView")
+        return list(
+            # Delete duplicate
+            set(
+                map(
+                    lambda i: i.row(),
+                    table.selectedIndexes()
+                )
+            )
+        )
+
+    def add(self):
+        rows = self.index_selected_rows()
+        if len(self._hs_lst) == 0 or len(rows) == 0:
+            self._table.add(0)
+        else:
+            self._table.add(rows[0])
+
+    def delete(self):
+        rows = self.index_selected_rows()
+        if len(rows) == 0:
+            return
+
+        self._table.delete(rows)
+
+    def _copy(self):
+        logger.info("TODO: copy")
+
+    def _paste(self):
+        logger.info("TODO: paste")
+
+    def _undo(self):
+        self._table.undo()
+
+    def _redo(self):
+        self._table.redo()
+
+    def edit(self):
+        rows = self.index_selected_rows()
+        for row in rows:
+            data = self._hs_lst.get(row)
+
+            if self.sub_window_exists(
+                BasicHydraulicStructuresWindow,
+                data=[self._study, None, data]
+            ):
+                continue
+
+            win = BasicHydraulicStructuresWindow(
+                data=data,
+                study=self._study,
+                parent=self
+            )
+            win.show()
+
+    def _set_checkbox_state(self):
+        row = self.index_selected_row()
+        if row is None:
+            self._checkbox.setEnabled(False)
+            self._checkbox.setChecked(True)
+        else:
+            self._checkbox.setEnabled(True)
+            self._checkbox.setChecked(self._hs_lst.get(row).enabled)
+
+    def _set_structure_state(self):
+        rows = self.index_selected_rows()
+        if len(rows) != 0:
+            for row in rows:
+                if row is not None:
+                    self._table.enabled(
+                        row,
+                        self._checkbox.isChecked()
+                    )
+
+    def update(self):
+        self._set_checkbox_state()
+        self._update_clear_plot()
+
+    def _update_clear_plot(self):
+        rows = self.index_selected_rows()
+
+        if len(rows) == 0 or len(self._hs_lst) == 0:
+            self._update_clear_all()
+            return
+
+        reach = self._hs_lst.get(rows[0]).input_reach
+        if reach is not None:
+            self.plot_kpc.set_reach(reach)
+            self.plot_ac.set_reach(reach)
+
+            profile_kp = self._hs_lst.get(rows[0]).input_kp
+            if profile_kp is not None:
+                profiles = reach.reach\
+                                .get_profiles_from_kp(
+                                    float(profile_kp)
+                                )
+
+                if len(profiles) != 0 and profiles is not None:
+                    profile = profiles[0]
+
+                    self.plot_kpc.set_profile(profile)
+                    self.plot_ac.set_profile(profile)
+                else:
+                    self._update_clear_profile()
+            else:
+                self._update_clear_profile()
+        else:
+            self._update_clear_all()
+
+    def _update_clear_all(self):
+        self.plot_kpc.clear()
+        self.plot_ac.clear()
+
+    def _update_clear_profile(self):
+        self.plot_ac.clear()
+        self.plot_kpc.clear_profile()
diff --git a/tests_cases/Enlargement/Enlargement.pamhyr b/tests_cases/Enlargement/Enlargement.pamhyr
index 12b3751da32b437e218fa5932fe18c75df6d416f..275fad25b44b7bc23ef14b467d0a1211b777f704 100644
Binary files a/tests_cases/Enlargement/Enlargement.pamhyr and b/tests_cases/Enlargement/Enlargement.pamhyr differ