Skip to content
Snippets Groups Projects
Commit e0c9cad1 authored by dre0059's avatar dre0059
Browse files

Sofisticated graph of references

parent d1e1012a
Branches
No related merge requests found
Showing with 795 additions and 19 deletions
No preview for this file type
This diff is collapsed.
......@@ -2,7 +2,9 @@ package com.dre0059.articleprocessor.controller;
import com.dre0059.articleprocessor.dto.DocumentDto;
import com.dre0059.articleprocessor.dto.SimpleDocumentDto;
import com.dre0059.articleprocessor.model.Category;
import com.dre0059.articleprocessor.model.Dokument;
import com.dre0059.articleprocessor.repository.CategoryRepository;
import com.dre0059.articleprocessor.repository.DocumentRepository;
import com.dre0059.articleprocessor.service.DocumentService;
......@@ -23,6 +25,9 @@ public class DocumentController {
private final DocumentService documentService;
@Autowired
private CategoryRepository categoryRepository;
@Autowired
private DocumentRepository dokumentRepository;
......@@ -53,23 +58,36 @@ public class DocumentController {
var references = documentService.getReferencedDocumentsById(id);
Dokument document = dokumentRepository.findById(id).get();
if (documentService.getDocumentById(id) == null) {
throw new IllegalArgumentException("Document with ID " + id + " not found.");
}
if(!document.getStatus().equals("Referenced"))
model.addAttribute("category", document.getCategory().getName());
model.addAttribute("category", document.getCategory().getName());
model.addAttribute("author", document.authorsToString());
model.addAttribute("year", document.getYear());
model.addAttribute("doi", document.getDoi());
model.addAttribute("link", document.getTarget());
model.addAttribute("tags", document.tagsToString());
model.addAttribute("notes", document.getNotes());
model.addAttribute("status", document.getStatus());
model.addAttribute("documentId", id);
model.addAttribute("references", references);
model.addAttribute("docTitle", documentService.getDocumentById(id).getTitle());
// add category
if (document.getCategory() != null) {
model.addAttribute("category", document.getCategory().getName());
model.addAttribute("currentCategoryId", document.getCategory().getId());
} else {
model.addAttribute("category", "No category assigned");
model.addAttribute("currentCategoryId", null); // Ak nie je kategória, nastavíme na null
}
model.addAttribute("categories", categoryRepository.findAll());
return "view-pdf";
}
......@@ -104,4 +122,35 @@ public class DocumentController {
return ResponseEntity.ok(response);
}
@PostMapping("/api/documents/{documentId}/setCategory")
@ResponseBody
public ResponseEntity<Map<String, Object>> setCategory(@PathVariable Long documentId, @RequestBody Map<String, String> payload) {
Dokument document = dokumentRepository.findById(documentId)
.orElseThrow(() -> new IllegalArgumentException("Document not found"));
String categoryName = payload.get("categoryName");
// Skontrolujeme, či kategória existuje
Category category = categoryRepository.findByName(categoryName);
if (category == null) {
// Ak kategória neexistuje, môžeme buď priradiť default hodnotu, alebo poslať error
return ResponseEntity.badRequest().body(Map.of("error", "Category with name '" + categoryName + "' not found"));
}
document.setCategory(category);
dokumentRepository.save(document);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
return ResponseEntity.ok(response);
}
@GetMapping("/about")
public String aboutProject() {
return "about-project"; // názov HTML súboru bez prípony
}
}
......@@ -74,4 +74,48 @@ public class StatisticsController {
return "statistics";
}
// sofistikovaný graf
@GetMapping("/statistics/citation-timeline")
public String citationTimeline(@RequestParam("documentId") Long documentId, Model model) {
Optional<Dokument> mainDocOpt = documentRepository.findById(documentId);
if (mainDocOpt.isEmpty()) {
return "redirect:/statistics";
}
Dokument mainDoc = mainDocOpt.get();
List<Map<String, Object>> references = mainDoc.getReferences().stream()
.map(ref -> {
Dokument cited = ref.getToDocument();
Map<String, Object> refMap = new HashMap<>();
refMap.put("title", cited.getTitle());
refMap.put("year", cited.getPublicationYear());
refMap.put("category", cited.getCategory() != null ? cited.getCategory().getName() : "Unknown");
return refMap;
})
.filter(refMap -> refMap.get("year") != null)
.collect(Collectors.toList());
// 🆕 Získame všetky existujúce kategórie pre Y-os grafu
List<Dokument> allDocuments = documentRepository.findAll();
List<String> categories = allDocuments.stream()
.map(Dokument::getCategory)
.filter(Objects::nonNull)
.map(c -> c.getName())
.distinct()
.sorted()
.collect(Collectors.toList());
model.addAttribute("mainArticleTitle", mainDoc.getTitle());
model.addAttribute("mainArticleYear", mainDoc.getPublicationYear());
model.addAttribute("mainArticleCategory", mainDoc.getCategory() != null ? mainDoc.getCategory().getName() : "Unknown");
model.addAttribute("references", references);
model.addAttribute("categories", categories); // ✅ Dôležité pre JS
return "citation-timeline";
}
}
......@@ -6,5 +6,5 @@ import org.springframework.stereotype.Repository;
@Repository
public interface CategoryRepository extends JpaRepository<Category, String> {
Category findByName(String name);
}
......@@ -84,16 +84,6 @@ public class HeaderService {
System.out.println("Author list before checking duplicity: " + authorList);
System.out.println("Author last names before checking duplicity: " + authorLastNames);
// check duplicity of the document
/*if(documentRepository.existsByTitleAndAuthorsIn(title, authorLastNames)){
System.out.println("Document with this title and authors already exist");
return null;
}*/
//Dokument dok = new Dokument(title, year, doi, publisher, "PDF");
List<Author> savedAuthors = authorRepository.saveAll(authorList);
Dokument dokument = documentRepository.findByTitleAndAuthorsIn(title, authorLastNames)
......
src/main/resources/static/assets/logo_FEI_en.png

27 KiB

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About Project</title>
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700,300italic,400italic,700italic" rel="stylesheet" />
<link rel="stylesheet" th:href="@{/styles.css}" />
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
.navbar-nav .nav-link {
font-size: 1.2rem;
margin-right: 20px;
color: rgb(51, 102, 255);
}
.navbar-nav .nav-link:hover {
color: #00134d;
}
.navbar-nav .nav-link.active {
color: rgb(51, 102, 255) !important;
}
.navbar-nav .nav-link.active:hover {
color: #00134d !important;
}
.about-container {
max-width: 800px;
margin: 60px auto;
padding: 20px;
font-size: 1.2rem;
line-height: 1.8;
color: #333;
}
h2.about-heading {
text-align: center;
margin-bottom: 30px;
font-size: 2rem;
font-weight: bold;
}
</style>
</head>
<body>
<!-- navbar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light static-top">
<div class="container">
<a class="navbar-brand" href="/upload">Article Processor</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/upload">Upload</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/view">View all</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/statistics">Statistics</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/about">About project</a>
</li>
</ul>
</div>
</div>
</nav>
<header class="masthead">
<div class="container position-relative">
<div class="row justify-content-center">
<div class="col-xl-8">
<div class="text-center text-white">
<h1 class="mb-4">About the project</h1>
</div>
</div>
</div>
</div>
</header>
<!-- About Project Content -->
<div class="about-container">
<p>
<h2 class="about-heading">About the Project</h2>
This web application was developed as part of a bachelor's thesis with the goal of creating a simple and interactive
tool for processing scientific PDF documents.
</p>
<p>
The whole project was created at <strong>VŠB Technical University of Ostrava</strong>.
</p>
<p>
It utilizes the open-source GROBID system to extract metadata such as
authors, titles, abstracts, and references. Users can upload PDF files, and the application automatically analyzes them
and stores the structured results in a database.
</p>
<p>
The application follows a clean three-layer architecture (MVC) and is built using Spring Boot and Java. Extracted data
is processed and displayed in a clear format, providing users with a user-friendly way to manage and browse academic content.
</p>
<img src="/assets/logo_FEI_en.png" alt="fei" style="max-width: 100%; margin-bottom: 20px;" />
</div>
<!-- Footer -->
<footer class="footer bg-light mt-5">
<div class="container text-center">
<p class="text-muted small">&copy; Eliška Kozáčiková 2025. All Rights Reserved.</p>
</div>
</footer>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Document Statistics</title>
<script src="https://cdn.plot.ly/plotly-1.58.5.min.js"></script>
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700,300italic,400italic,700italic" rel="stylesheet" />
<link rel="stylesheet" th:href="@{/styles.css}" />
<style>
.navbar-nav .nav-link {
font-size: 1.2rem;
margin-right: 20px;
color: rgb(51, 102, 255);
}
.navbar-nav .nav-link:hover {
color: #00134d;
}
.navbar-nav .nav-link.active {
color: rgb(51, 102, 255) !important;
}
.navbar-nav .nav-link.active:hover {
color: #00134d !important;
}
h2 {
color: #ffffff;
text-align: center;
margin-top: 30px;
}
.chart-container {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 20px;
margin-top: 30px;
}
.chart-box {
flex: 1;
min-width: 400px;
height: 500px;
background-color: #6780a3;
border-radius: 12px;
padding: 15px;
overflow: hidden;
}
.chart-box:hover {
transform: scale(1.01);
transition: transform 0.2s ease-in-out;
}
.chart-box .js-plotly-plot {
position: absolute;
top: 0;
left: 0;
width: 100% !important; /* natiahne graf presne na šírku boxu */
height: 100% !important; /* natiahne graf presne na výšku boxu */
}
</style>
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light static-top">
<div class="container">
<a class="navbar-brand" href="/upload">Article Processor</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/upload">Upload</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/view">View all</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/statistics">Statistics</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/upload">About project</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Page Content -->
<header class="masthead">
<div class="container position-relative">
<div class="row justify-content-center">
<div class="col-xl-8">
<div class="text-center text-white">
<h1 class="mb-4">📊 Graph of references</h1>
</div>
</div>
</div>
</div>
</header>
<!-- sofistikovaný graf -->
<div class="container mt-5">
<div class="chart-container">
<div id="referenceTimelineChart" class="chart-box"></div>
</div>
</div>
<script th:inline="javascript">
// Predpokladám, že tieto údaje sú dostupné z backendu cez Thymeleaf.
const mainArticle = {
title: [[${mainArticleTitle}]],
year: [[${mainArticleYear}]],
category: [[${mainArticleCategory}]]
};
const references = [[${references}]];
const categories = [[${categories}]]; // Kategórie, ktoré dostaneme z databázy
console.log("Categories received:", categories);
// Predstavujeme kategórie ako mapu (indexovanie podľa názvu kategórie)
const categoryIndex = categories.reduce((acc, category, index) => {
acc[category] = index + 1; // Začneme od 1, aby sme vyhli hodnotám 0
return acc;
}, {});
// Funkcia pre získanie Y-ovej hodnoty podľa kategórie
function getCategoryY(categoryName) {
return categoryIndex[categoryName] || 0; // Ak kategória nie je v zozname, použijeme hodnotu 0
}
// Pripravíme hlavný dokument
const mainTrace = {
x: [mainArticle.year],
y: [getCategoryY(mainArticle.category)],
mode: 'markers',
type: 'scatter',
name: mainArticle.title,
hoverinfo: 'text',
marker: {
size: 14,
color: 'gold'
},
// Po kliknutí na marker sa zobrazí informácia
customdata: [{ title: mainArticle.title, year: mainArticle.year, category: mainArticle.category }]
};
// Pripravíme referencie
const referenceTrace = {
x: references.map(r => r.year),
y: references.map(r => getCategoryY(r.category)),
mode: 'markers',
type: 'scatter',
name: 'References',
text: references.map(r => r.title),
hoverinfo: 'text',
marker: {
size: 10,
color: '#66ccff'
},
customdata: references.map(r => ({ title: r.title, year: r.year, category: r.category }))
};
// Spojovacie čiary medzi referenciami a hlavným dokumentom
const connectionLines = references.map(ref => ({
type: 'line',
x0: ref.year,
y0: getCategoryY(ref.category),
x1: mainArticle.year,
y1: getCategoryY(mainArticle.category),
line: {
color: '#ffffff',
width: 1.5,
dash: 'dot'
}
}));
// Konfigurácia grafu
const layout = {
title: 'Chronological graph of categories & references',
paper_bgcolor: '#6780a3',
plot_bgcolor: '#6780a3',
font: { color: '#ffffff' },
margin: {
l: 180, // zväčší ľavý okraj pre čitateľnosť názvov kategórií
t: 150 // zväčší horný okraj pre pridanie odsadenia pod nadpis
},
xaxis: {
title: 'Publication Year',
showgrid: true,
zeroline: false,
range: [1950, 2030]
},
yaxis: {
title: 'Category',
tickmode: 'array',
tickvals: categories.map((_, index) => index + 1),
ticktext: categories,
showgrid: true,
automargin: true,
tickfont: {
size: 14
}
},
shapes: connectionLines,
legend: {
orientation: 'h', // horizontálne usporiadanie legendy
yanchor: 'bottom', // pozícia legendy
y: 1.1, // trošku nad grafom
xanchor: 'center',
x: 0.5
}
};
// Vytvorenie grafu bez pridania event listenera na kliknutie
Plotly.newPlot('referenceTimelineChart', [mainTrace, referenceTrace], layout, { responsive: true });
</script>
<footer class="footer bg-light mt-5">
<div class="container text-center">
<p class="text-muted small">&copy; Eliška Kozáčiková 2025. All Rights Reserved.</p>
</div>
</footer>
</body>
</html>
......@@ -56,7 +56,7 @@
<a class="nav-link" href="/statistics">Statistics</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/upload">About project</a>
<a class="nav-link" href="/about">About project</a>
</li>
</ul>
</div>
......
......@@ -12,7 +12,7 @@
<style>
.navbar {
background-color : #dfe2ec !important;
background-color: #dfe2ec !important;
}
.navbar-nav .nav-link {
font-size: 1.2rem;
......@@ -29,8 +29,15 @@
color: #00134d !important;
}
.italic{
font-style:italic;
.italic {
font-style: italic;
}
/* Center the graph */
#graphContainer {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>
......@@ -68,6 +75,11 @@
<div class="container mt-4">
<h2><span class="italic" th:text="${docTitle}"></span></h2>
<!-- Button to show graph of references -->
<div class="mt-4" id="graphContainer" style="display: none;">
<button id="showGraphButton" class="btn btn-secondary">SHOW GRAPH OF REFERENCES</button>
</div>
<div class="row mt-4">
<div class="col-md-6">
<h3>Information about Document</h3>
......@@ -80,7 +92,6 @@
<p><strong>Tags:</strong> <span class="italic" th:text="${tags}"></span></p>
<p><strong>Notes:</strong> <span class="italic" th:text="${notes}"></span></p>
<!-- Form for adding notes and delete document buttons -->
<div class="d-flex justify-content-between mt-4">
<!-- Add Notes Button -->
......@@ -102,6 +113,16 @@
</div>
</div>
<!-- choose Category -->
<form id="categoryForm" class="mt-3">
<label for="categorySelect" class="form-label"><strong>Category:</strong></label>
<select id="categorySelect" class="form-select" name="category">
<option th:each="category : ${categories}" th:value="${category.name}" th:text="${category.name}"
th:selected="${category.name == category}"></option>
</select>
<button type="button" class="btn btn-primary mt-2" id="saveCategoryButton">Save Category</button>
</form>
<h3 class="mt-4">References:</h3>
<div class="table-responsive">
<table class="table table-bordered table-striped">
......@@ -140,14 +161,48 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
<script th:inline="javascript">
// Check if the status is "PDF" and show the button
const status = [[${status}]]; // Get the status from the backend
if (status === "PDF") {
document.getElementById("graphContainer").style.display = "block";
}
// Save the category to the document
document.getElementById("saveCategoryButton").addEventListener("click", function () {
const selectedCategoryName = document.getElementById("categorySelect").value;
let documentId = [[${documentId}]];
// Send the selected category to the backend to update the document
fetch(`/api/documents/${documentId}/setCategory`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ categoryName: selectedCategoryName }) // Send the selected category ID
})
.then(response => {
if (response.ok) {
alert("Category updated successfully!");
} else {
alert("Error updating category.");
}
})
.catch(error => {
console.error("Error:", error);
alert("An error occurred.");
});
});
// Handle notes addition
document.getElementById("addNotesButton").addEventListener("click", function() {
document.getElementById("notesForm").style.display = "block";
});
document.getElementById("saveNotesButton").addEventListener("click", function() {
const notes = document.getElementById("notesTextarea").value;
const documentId = [[${documentId}]];
const documentId = [[${documentId}]]; // Insert the documentId here (e.g., from Thymeleaf)
fetch(`/api/documents/${documentId}/setNotes`, {
method: 'POST',
......@@ -169,6 +224,16 @@
alert("Error saving notes!");
});
});
// "SHOW GRAPH OF REFERENCES" button
document.getElementById("showGraphButton").addEventListener("click", function() {
// Get the document ID from Thymeleaf model
const documentId = [[${documentId}]];
// Redirect to the citation timeline page
window.location.href = `/statistics/citation-timeline?documentId=${documentId}`;
});
</script>
</body>
</html>
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment