@osfarm/itineraire-technique 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +32 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/css/styles-editor.css +1 -0
- package/css/styles-editor.css.map +1 -0
- package/css/styles-rendering.css +1 -0
- package/css/styles-rendering.css.map +1 -0
- package/editor.html +725 -0
- package/images/rendu_frise.png +0 -0
- package/images/rendu_rotation.png +0 -0
- package/js/chart-render.js +853 -0
- package/js/editor-attributes.js +96 -0
- package/js/editor-crops.js +78 -0
- package/js/editor-export.js +118 -0
- package/js/editor-interventions.js +117 -0
- package/package.json +16 -0
- package/rendu_statique_1.html +664 -0
- package/scss/styles-editor.scss +106 -0
- package/scss/styles-rendering.scss +101 -0
- package/test/itk-templates/export-itk-1.json +153 -0
- package/test/test.json +324 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
function addOrUpdateAttributeClickEvent() {
|
|
2
|
+
let id = getInputValue("attributeId");
|
|
3
|
+
let key = getInputValue("attributeName");
|
|
4
|
+
let value = getInputValue("attributeValue");
|
|
5
|
+
|
|
6
|
+
if (id != "") {
|
|
7
|
+
selectedStep.updateAttribute(id, key, value);
|
|
8
|
+
} else {
|
|
9
|
+
selectedStep.addAttribute(key, value);
|
|
10
|
+
|
|
11
|
+
document.getElementById("newAttributeButton").classList.remove("d-none");
|
|
12
|
+
getAndCleanElement("newAttributeContainer");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
refreshAllTables();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function refreshAttributesTable() {
|
|
19
|
+
let attributesContainer = document.getElementById("attributesContainer");
|
|
20
|
+
attributesContainer.innerHTML = "";
|
|
21
|
+
|
|
22
|
+
if (selectedStep && selectedStep.getStep().attributes) {
|
|
23
|
+
selectedStep.getStep().attributes.forEach((attribute) => {
|
|
24
|
+
const rowDiv = createAttributeRow(attribute);
|
|
25
|
+
attributesContainer.appendChild(rowDiv);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createAttributeRow(attribute) {
|
|
31
|
+
let rowDiv = document.createElement("div");
|
|
32
|
+
rowDiv.className = "row mb-2 attribute-row editable-row position-relative";
|
|
33
|
+
|
|
34
|
+
let nameValueDiv = createAttributeNameAndValueColumn(attribute);
|
|
35
|
+
rowDiv.appendChild(nameValueDiv);
|
|
36
|
+
|
|
37
|
+
addEditAndRemoveButtons(
|
|
38
|
+
rowDiv,
|
|
39
|
+
attribute.id,
|
|
40
|
+
function() {
|
|
41
|
+
createAttributForm(attribute.id, attribute.name, attribute.value, rowDiv);
|
|
42
|
+
},
|
|
43
|
+
function(id) {
|
|
44
|
+
selectedStep.removeAttribute(id);
|
|
45
|
+
refreshAllTables();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return rowDiv;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createAttributeNameAndValueColumn(attribute) {
|
|
52
|
+
let nameValueDiv = document.createElement("div");
|
|
53
|
+
nameValueDiv.className = "col";
|
|
54
|
+
nameValueDiv.innerHTML = `<strong>${attribute.name}</strong></br> ${attribute.value}`;
|
|
55
|
+
|
|
56
|
+
return nameValueDiv;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createAttributForm(id, name, value, row) {
|
|
60
|
+
id = id || "";
|
|
61
|
+
name = name || "";
|
|
62
|
+
value = value || "";
|
|
63
|
+
|
|
64
|
+
const formContainer = document.createElement("div");
|
|
65
|
+
formContainer.innerHTML = `
|
|
66
|
+
<form>
|
|
67
|
+
<div class="row card-white edit-attribute-view mb-2">
|
|
68
|
+
<input type="hidden" id="attributeId" value="${id}">
|
|
69
|
+
<div class="col-12 mb-2">
|
|
70
|
+
<label for="attributeName" class="form-label">Nom</label>
|
|
71
|
+
<input type="text" id="attributeName" class="form-control" placeholder="Nom" value="${name}">
|
|
72
|
+
</div>
|
|
73
|
+
<div class="col-12 mb-2">
|
|
74
|
+
<input type="text" id="attributeValue" class="form-control" placeholder="Description" value="${value}">
|
|
75
|
+
</div>
|
|
76
|
+
<div class="col-12 mb-2">
|
|
77
|
+
<button type="button" onclick="addOrUpdateAttributeClickEvent()"
|
|
78
|
+
class="w-100 btn btn-outline-primary primary-button">Valider</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</form>
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
if (row) { //we are editing an attribute
|
|
85
|
+
row.replaceWith(formContainer);
|
|
86
|
+
} else { //we are adding an attribute
|
|
87
|
+
document.getElementById("newAttributeContainer").appendChild(formContainer);
|
|
88
|
+
document.getElementById("newAttributeButton").classList.add("d-none");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (name === "") {
|
|
92
|
+
document.getElementById("attributeName").focus();
|
|
93
|
+
} else {
|
|
94
|
+
document.getElementById("attributeValue").focus();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
function addNewStepClickEvent() {
|
|
2
|
+
createAndSelectEmptyCrop();
|
|
3
|
+
loadSelectedStepToEditor(selectedStep);
|
|
4
|
+
displayCropDetailView();
|
|
5
|
+
refreshAllTables();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createAndSelectEmptyCrop() {
|
|
9
|
+
let crop = new StepModel({
|
|
10
|
+
startDate: getRotationEndDate(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
crop.setDurationInMonths(2)
|
|
14
|
+
crops.steps.push(crop.getStep());
|
|
15
|
+
|
|
16
|
+
crop.addAttribute("Pré-semis", "");
|
|
17
|
+
crop.addAttribute("Travail du sol", "");
|
|
18
|
+
crop.addAttribute("Type de semoir", " ");
|
|
19
|
+
crop.addAttribute("Date des semis", "");
|
|
20
|
+
|
|
21
|
+
//select last created crop to be editable
|
|
22
|
+
selectedStep = crop;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadSelectedStepToEditor(aStep) {
|
|
26
|
+
setInputValue("cropName", aStep.getStep().name);
|
|
27
|
+
setInputValue("cropColor", aStep.getStep().color);
|
|
28
|
+
setInputValue("cropStartDate", aStep.getStep().startDate.toISOString().split('T')[0]);
|
|
29
|
+
setInputValue("cropEndDate", aStep.getStep().endDate.toISOString().split('T')[0]);
|
|
30
|
+
setInputValue("cropDescription", aStep.getStep().description);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function refreshStepsButtonList() {
|
|
34
|
+
let cropsContainer = $("#cropsContainer");
|
|
35
|
+
cropsContainer.html('');
|
|
36
|
+
|
|
37
|
+
crops.steps.forEach((crop) => {
|
|
38
|
+
const rowDiv = createCropRow(crop);
|
|
39
|
+
cropsContainer.append(rowDiv);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
cropsContainer.sortable("refresh");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createCropRow(crop) {
|
|
46
|
+
let step = new StepModel(crop); // in case crop is a plain object, convert to Crop instance
|
|
47
|
+
|
|
48
|
+
let rowDiv = $('<div class="row mb-2 step-row editable-row position-relative" data-id="'+step.getStep().id +'"></div>');
|
|
49
|
+
|
|
50
|
+
rowDiv.append($('<div class="col"></div>')
|
|
51
|
+
.append($('<i class="fa fa-bars drag-handle" aria-hidden="true"></i>'))
|
|
52
|
+
.append($('<strong>' + step.getStep().name + '</strong>')));
|
|
53
|
+
|
|
54
|
+
addEditAndRemoveButtons(rowDiv,
|
|
55
|
+
step.getStep().id,
|
|
56
|
+
function () {
|
|
57
|
+
console.log("Selected step:", step.getStep().name);
|
|
58
|
+
SelectStep(step);
|
|
59
|
+
},
|
|
60
|
+
function(id) {
|
|
61
|
+
crops.steps = crops.steps.filter(function (crop) { return crop.id != id })
|
|
62
|
+
|
|
63
|
+
refreshAllTables();
|
|
64
|
+
displayCropListView();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
rowDiv.click();
|
|
68
|
+
|
|
69
|
+
return rowDiv;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
function SelectStep(crop) {
|
|
74
|
+
selectedStep = crop;
|
|
75
|
+
loadSelectedStepToEditor(selectedStep);
|
|
76
|
+
displayCropDetailView();
|
|
77
|
+
refreshAllTables();
|
|
78
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
function exportToJsonFile(data, fileName = 'export-itk.json') {
|
|
2
|
+
const jsonData = JSON.stringify(data, null, 2);
|
|
3
|
+
const blob = new Blob([jsonData], { type: 'application/json' });
|
|
4
|
+
const url = URL.createObjectURL(blob);
|
|
5
|
+
|
|
6
|
+
const a = document.createElement('a');
|
|
7
|
+
a.href = url;
|
|
8
|
+
a.download = fileName;
|
|
9
|
+
a.click();
|
|
10
|
+
|
|
11
|
+
URL.revokeObjectURL(url);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function importFromJsonFile() {
|
|
15
|
+
if (crops.steps && crops.steps.length > 0) {
|
|
16
|
+
showConfirmationModal(() => {
|
|
17
|
+
openFileInput();
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
openFileInput();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function importFromTestJson() {
|
|
25
|
+
if (crops.steps && crops.steps.length > 0) {
|
|
26
|
+
showConfirmationModal(() => {
|
|
27
|
+
importTestJSON();
|
|
28
|
+
});
|
|
29
|
+
} else {
|
|
30
|
+
importTestJSON();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function importTestJSON() {
|
|
35
|
+
fetch('test/test.json')
|
|
36
|
+
.then(response => {
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error("Erreur HTTP " + response.status);
|
|
39
|
+
}
|
|
40
|
+
return response.json();
|
|
41
|
+
})
|
|
42
|
+
.then(data => {
|
|
43
|
+
console.log("Données JSON :", data);
|
|
44
|
+
reloadCropsFromJson(data);
|
|
45
|
+
})
|
|
46
|
+
.catch(error => {
|
|
47
|
+
console.error("Impossible de charger le JSON :", error);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function wipe() {
|
|
52
|
+
let crops = {
|
|
53
|
+
"title": DEFAULT_TITLE,
|
|
54
|
+
"options": {
|
|
55
|
+
"view": "horizontal",
|
|
56
|
+
"show_transcript": true,
|
|
57
|
+
"title_top_interventions": "Contrôle adventices",
|
|
58
|
+
"title_bottom_interventions": "Autres interventions",
|
|
59
|
+
"title_steps": "Étapes de la rotation dans la parcelle",
|
|
60
|
+
},
|
|
61
|
+
"steps": []
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (crops.steps && crops.steps.length > 0) {
|
|
65
|
+
showConfirmationModal(() => {
|
|
66
|
+
reloadCropsFromJson(crops);
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
reloadCropsFromJson(crops);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function openFileInput() {
|
|
74
|
+
const input = document.createElement('input');
|
|
75
|
+
input.type = 'file';
|
|
76
|
+
input.accept = '.json';
|
|
77
|
+
|
|
78
|
+
input.addEventListener('change', (event) => {
|
|
79
|
+
const file = event.target.files[0];
|
|
80
|
+
if (file) {
|
|
81
|
+
const reader = new FileReader();
|
|
82
|
+
reader.onload = () => {
|
|
83
|
+
try {
|
|
84
|
+
const jsonData = JSON.parse(reader.result);
|
|
85
|
+
reloadCropsFromJson(jsonData);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error("Error parsing JSON file:", error);
|
|
88
|
+
showJsonErrorModal(error.message);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
reader.readAsText(file);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
input.click();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function showJsonErrorModal(errorMessage) {
|
|
99
|
+
const errorModal = new bootstrap.Modal(document.getElementById('jsonErrorModal'));
|
|
100
|
+
document.getElementById('jsonErrorMessage').textContent = errorMessage;
|
|
101
|
+
errorModal.show();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function showConfirmationModal(onConfirm) {
|
|
105
|
+
const confirmationModal = new bootstrap.Modal(document.getElementById('confirmationModal'));
|
|
106
|
+
const confirmButton = document.getElementById('confirmImport');
|
|
107
|
+
|
|
108
|
+
// Avoid multiple event listeners
|
|
109
|
+
const newConfirmButton = confirmButton.cloneNode(true);
|
|
110
|
+
confirmButton.replaceWith(newConfirmButton);
|
|
111
|
+
|
|
112
|
+
newConfirmButton.addEventListener('click', () => {
|
|
113
|
+
confirmationModal.hide();
|
|
114
|
+
onConfirm();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
confirmationModal.show();
|
|
118
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
function addInterventionClickEvent() {
|
|
2
|
+
let id = getInputValue("interventionId");
|
|
3
|
+
let name = getInputValue("interventionName");
|
|
4
|
+
let day = getInputValue("interventionDay");
|
|
5
|
+
let type = getInputValue("interventionType");
|
|
6
|
+
let description = getInputValue("interventionDescription");
|
|
7
|
+
|
|
8
|
+
if (id != "") {
|
|
9
|
+
selectedStep.updateIntervention(id, day, name, type, description);
|
|
10
|
+
} else {
|
|
11
|
+
selectedStep.addIntervention(day, name, type, description);
|
|
12
|
+
|
|
13
|
+
document.getElementById("newInterventionButton").classList.remove("d-none");
|
|
14
|
+
getAndCleanElement("newInterventionContainer");
|
|
15
|
+
}
|
|
16
|
+
refreshAllTables();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function refreshInterventionsTable() {
|
|
20
|
+
let interventionsTopContainer = getAndCleanElement("interventionsTopContainer");
|
|
21
|
+
let interventionsBottomContainer = getAndCleanElement("interventionsBottomContainer");
|
|
22
|
+
|
|
23
|
+
if (selectedStep && selectedStep.getStep().interventions) {
|
|
24
|
+
selectedStep.getStep().interventions.forEach((intervention) => {
|
|
25
|
+
const rowDiv = createInterventionRow(intervention);
|
|
26
|
+
|
|
27
|
+
if (intervention.type === crops.options.title_top_interventions) {
|
|
28
|
+
interventionsTopContainer.appendChild(rowDiv);
|
|
29
|
+
} else {
|
|
30
|
+
interventionsBottomContainer.appendChild(rowDiv);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createInterventionRow(intervention) {
|
|
38
|
+
let rowDiv = document.createElement("div");
|
|
39
|
+
rowDiv.className = "row mb-2 intervention-row editable-row position-relative";
|
|
40
|
+
|
|
41
|
+
let nameValueDiv = createInterventionNameAndValueColumn(intervention);
|
|
42
|
+
rowDiv.appendChild(nameValueDiv);
|
|
43
|
+
|
|
44
|
+
addEditAndRemoveButtons(
|
|
45
|
+
rowDiv,
|
|
46
|
+
intervention.id,
|
|
47
|
+
function () {
|
|
48
|
+
createInterventionForm(intervention.id, intervention.day, intervention.name, intervention.type, intervention.description, rowDiv);
|
|
49
|
+
},
|
|
50
|
+
function(id) {
|
|
51
|
+
selectedStep.removeIntervention(id);
|
|
52
|
+
refreshAllTables();
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return rowDiv;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createInterventionNameAndValueColumn(intervention) {
|
|
60
|
+
let nameValueDiv = document.createElement("div");
|
|
61
|
+
nameValueDiv.className = "col";
|
|
62
|
+
nameValueDiv.innerHTML = `<strong>${intervention.name}</strong></br> ${intervention.description}`;
|
|
63
|
+
|
|
64
|
+
return nameValueDiv;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createInterventionForm(id, day, name, type, description, row) {
|
|
68
|
+
id = id || "";
|
|
69
|
+
day = day || "";
|
|
70
|
+
name = name || "";
|
|
71
|
+
type = type || "";
|
|
72
|
+
description = description || "";
|
|
73
|
+
|
|
74
|
+
const formContainer = document.createElement("div");
|
|
75
|
+
formContainer.innerHTML =
|
|
76
|
+
`<form id="interventionForm">
|
|
77
|
+
<div class="row card-white mb-2">
|
|
78
|
+
<input type="hidden" id="interventionId" value="${id}">
|
|
79
|
+
<div class="col-12 mb-2">
|
|
80
|
+
<label for="interventionName" class="form-label">Nom</label>
|
|
81
|
+
<input type="text" id="interventionName" class="form-control" placeholder="Nom" value="${name}">
|
|
82
|
+
</div>
|
|
83
|
+
<div class="col-12 mb-2">
|
|
84
|
+
<textarea id="interventionDescription" class="form-control"
|
|
85
|
+
placeholder="Ajouter une description">${description}</textarea>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="col-12 mb-2">
|
|
88
|
+
<label for="interventionDay" class="form-label">Quel jour après la plantation ?</label>
|
|
89
|
+
<input type="number" id="interventionDay" class="form-control" placeholder="Jour"
|
|
90
|
+
value="${day}">
|
|
91
|
+
</div>
|
|
92
|
+
<div class="col-12 mb-2">
|
|
93
|
+
<select id="interventionType" class="form-select" aria-label="Type">
|
|
94
|
+
<option value="intervention_top"
|
|
95
|
+
${type === "intervention_top" ? "selected" : ""}>${crops.options.title_top_interventions}</option>
|
|
96
|
+
<option value="intervention_bottom"
|
|
97
|
+
${type === "intervention_bottom" ? "selected" : ""}>${crops.options.title_bottom_interventions}</option>
|
|
98
|
+
</select>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="col">
|
|
102
|
+
<button type="button" onclick="addInterventionClickEvent()"
|
|
103
|
+
class="w-100 btn btn-outline-primary primary-button">Valider</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</form>
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
if (row) { //we are editing an intervention
|
|
110
|
+
row.replaceWith(formContainer);
|
|
111
|
+
} else { //we are adding an intervention
|
|
112
|
+
document.getElementById("newInterventionContainer").appendChild(formContainer);
|
|
113
|
+
document.getElementById("newInterventionButton").classList.add("d-none");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
$("#interventionName").focus();
|
|
117
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@osfarm/itineraire-technique",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A visualisation tool to show agricultural technical itineraries based on Echarts",
|
|
5
|
+
"main": "editor.html",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build:scss": "sass --style=compressed scss/styles-editor.scss css/styles-editor.css && sass --style=compressed scss/styles-rendering.scss css/styles-rendering.css",
|
|
8
|
+
"dev:scss": "sass scss/styles-editor.scss css/styles-editor.css && sass scss/styles-rendering.scss css/styles-rendering.css",
|
|
9
|
+
"watch:scss": "sass --watch scss/styles-editor.scss:css/styles-editor.css scss/styles-rendering.scss:css/styles-rendering.css",
|
|
10
|
+
"build": "npm run build:scss",
|
|
11
|
+
"start": "npm run watch:scss"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"sass": "^1.85.1"
|
|
15
|
+
}
|
|
16
|
+
}
|