diff --git a/build.gradle b/build.gradle index b6610d1..087fec8 100644 --- a/build.gradle +++ b/build.gradle @@ -49,6 +49,8 @@ implementation 'org.apache.xmlgraphics:batik-gvt:1.16' implementation 'org.apache.xmlgraphics:batik-svg-dom:1.16' + implementation 'guru.nidi:graphviz-java:0.18.1' + implementation 'com.eclipsesource.j2v8:j2v8_win32_x86_64:4.6.0' // Шрифты DejaVu (рекомендуемые для русского) // implementation 'com.github.olivierlemasle:java-dejavu:2.3.0' diff --git a/src/main/java/ru/mcs/genealogy/controller/TreeController.java b/src/main/java/ru/mcs/genealogy/controller/TreeController.java index 9f199c6..af21b53 100644 --- a/src/main/java/ru/mcs/genealogy/controller/TreeController.java +++ b/src/main/java/ru/mcs/genealogy/controller/TreeController.java @@ -1,36 +1,38 @@ package ru.mcs.genealogy.controller; +import com.lowagie.text.*; +import com.lowagie.text.pdf.PdfWriter; +import guru.nidi.graphviz.engine.Format; +import guru.nidi.graphviz.engine.Graphviz; +import guru.nidi.graphviz.engine.GraphvizV8Engine; import jakarta.servlet.http.HttpServletResponse; -import net.sf.jasperreports.engine.*; -import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jasperreports.engine.JasperReport; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import ru.mcs.genealogy.dto.TreeNode; import ru.mcs.genealogy.entity.Person; +import ru.mcs.genealogy.service.GraphvizTreeService; import ru.mcs.genealogy.service.PersonService; import ru.mcs.genealogy.service.TreeService; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.awt.image.BufferedImage; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +@Slf4j @Controller @RequestMapping("/tree") +@RequiredArgsConstructor public class TreeController { private final PersonService personService; private final TreeService treeService; + private final GraphvizTreeService graphvizTreeService; private final Map compiledReports = new ConcurrentHashMap<>(); - public TreeController(PersonService personService, TreeService treeService) { - this.personService = personService; - this.treeService = treeService; - } @GetMapping public String showTreePage(Model model) { @@ -41,7 +43,7 @@ @GetMapping("/{id}") public String showTreeForPerson(@PathVariable Long id, Model model) { Person selectedPerson = personService.getPersonById(id).orElseThrow(); - List tree = treeService.buildFullTree(selectedPerson); + var tree = treeService.buildFullTree(selectedPerson); model.addAttribute("selectedPerson", selectedPerson); model.addAttribute("persons", personService.getAllPersons()); @@ -49,101 +51,80 @@ return "tree"; } + @GetMapping("/image/{id}") + public void generateTreeImage(@PathVariable Long id, HttpServletResponse response) { + try { + Person selectedPerson = personService.getPersonById(id).orElseThrow(); + + // Генерируем DOT-код + String dotString = graphvizTreeService.generateDOT(selectedPerson); + + // Создаем изображение через Graphviz + Graphviz.useEngine(new GraphvizV8Engine()); + response.setContentType("image/png"); + Graphviz.fromString(dotString) + .width(1600) // Большая ширина для детализации + .render(Format.PNG) + .toOutputStream(response.getOutputStream()); + + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error("Ошибка при генерации изображения дерева", e); + } + } + @GetMapping("/pdf/{id}") public void generatePdf(@PathVariable Long id, HttpServletResponse response) { try { Person selectedPerson = personService.getPersonById(id).orElseThrow(); - List tree = treeService.buildFullTree(selectedPerson); - // Рассчитываем позиции для каждого узла дерева - List> reportData = calculateTreePositions(tree, selectedPerson.getId()); + // Генерируем DOT-код + String dotString = graphvizTreeService.generateDOT(selectedPerson); - // Загружаем шаблон отчета - InputStream reportStream = getClass().getClassLoader().getResourceAsStream("reports/family-tree2.jrxml"); - if (reportStream == null) { - throw new RuntimeException("Файл шаблона не найден"); - } + // Создаем изображение через Graphviz + Graphviz.useEngine(new GraphvizV8Engine()); + BufferedImage image = Graphviz.fromString(dotString) + .width(1200) + .render(Format.PNG) + .toImage(); - // Компилируем отчет - JasperReport jasperReport = JasperCompileManager.compileReport(reportStream); + // Создаем PDF с изображением + createPdfWithImage(response, image, selectedPerson.getFullName()); - // Создаем datasource - JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(reportData); - - // Параметры отчета - Map parameters = new HashMap<>(); - parameters.put("title", "Генеалогическое древо: " + selectedPerson.getFullName()); - parameters.put("selectedPersonId", selectedPerson.getId()); - - // Заполняем отчет - JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, dataSource); - - // Настраиваем response - response.setContentType("application/pdf"); - response.setHeader("Content-Disposition", "attachment; filename=family-tree-" + selectedPerson.getId() + ".pdf"); - - // Экспортируем в PDF - JasperExportManager.exportReportToPdfStream(jasperPrint, response.getOutputStream()); - - // Закрываем поток - reportStream.close(); } catch (Exception e) { throw new RuntimeException("Ошибка при генерации PDF", e); } } - // Метод для расчета позиций узлов дерева - private List> calculateTreePositions(List tree, Long selectedPersonId) { - List> result = new ArrayList<>(); + private void createPdfWithImage(HttpServletResponse response, + BufferedImage image, + String title) throws Exception { + // Создаем простой PDF документ + Document document = new Document(PageSize.A4.rotate()); + PdfWriter.getInstance(document, response.getOutputStream()); - // Группируем узлы по уровням - Map> levels = new HashMap<>(); - for (TreeNode node : tree) { - levels.computeIfAbsent(node.getLevel(), k -> new ArrayList<>()).add(node); - } + response.setContentType("application/pdf"); + response.setHeader("Content-Disposition", "attachment; filename=family-tree.pdf"); - // Рассчитываем позиции для каждого уровня - int yBase = 100; // Начальная Y-позиция - int levelHeight = 100; // Высота между уровнями + document.open(); - for (Map.Entry> entry : levels.entrySet()) { - int level = entry.getKey(); - List nodes = entry.getValue(); + // Добавляем заголовок + Font titleFont = new Font(Font.getFamilyIndex("Helvetica"), 18, Font.BOLD); + Paragraph titleParagraph = new Paragraph("Генеалогическое древо: " + title, titleFont); + titleParagraph.setAlignment(Element.ALIGN_CENTER); + document.add(titleParagraph); - // Рассчитываем X-позиции для узлов на этом уровне - int levelWidth = 1960; // Ширина уровня - int nodeWidth = 200; // Ширина узла - int spacing = (levelWidth - (nodes.size() * nodeWidth)) / (nodes.size() + 1); + document.add(Chunk.NEWLINE); - for (int i = 0; i < nodes.size(); i++) { - TreeNode node = nodes.get(i); - Map row = new HashMap<>(); + // Добавляем изображение + Image pdfImage = Image.getInstance(image, null); + pdfImage.setAlignment(Image.ALIGN_CENTER); - row.put("personId", node.getPerson().getId()); - row.put("fullName", node.getPerson().getFullName()); - row.put("level", node.getLevel()); - row.put("hasChildren", node.isHasChildren()); + // Масштабируем изображение под ширину страницы + float documentWidth = document.getPageSize().getWidth() - document.leftMargin() - document.rightMargin(); + pdfImage.scaleToFit(documentWidth, documentWidth * pdfImage.getHeight() / pdfImage.getWidth()); - // Определяем ID родителя - if (node.getPerson().getFather() != null) { - row.put("parentId", node.getPerson().getFather().getId()); - } else if (node.getPerson().getMother() != null) { - row.put("parentId", node.getPerson().getMother().getId()); - } else { - row.put("parentId", null); - } - - // Рассчитываем позицию - int xPosition = spacing + i * (nodeWidth + spacing); - int yPosition = yBase + level * levelHeight; - - row.put("xPosition", xPosition); - row.put("yPosition", yPosition); - - result.add(row); - } - } - - return result; + document.add(pdfImage); + document.close(); } } \ No newline at end of file diff --git a/src/main/java/ru/mcs/genealogy/service/GraphvizTreeService.java b/src/main/java/ru/mcs/genealogy/service/GraphvizTreeService.java new file mode 100644 index 0000000..aacf99f --- /dev/null +++ b/src/main/java/ru/mcs/genealogy/service/GraphvizTreeService.java @@ -0,0 +1,105 @@ +package ru.mcs.genealogy.service; + +import org.springframework.stereotype.Service; +import ru.mcs.genealogy.dto.TreeNode; +import ru.mcs.genealogy.entity.Person; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@Service +public class GraphvizTreeService { + + public String generateDOT(Person person) { + List treeNodes = buildFullTree(person); + return buildDOTString(treeNodes); + } + + private String buildDOTString(List treeNodes) { + StringBuilder dot = new StringBuilder(); + dot.append("digraph familyTree {\n"); + dot.append(" rankdir=TB;\n"); + dot.append(" node [shape=box, style=filled, fillcolor=lightblue];\n\n"); +// dot.append(" edge [dir=back];\n\n"); + + // Добавляем все узлы + for (TreeNode node : treeNodes) { + Person person = node.getPerson(); + dot.append(String.format(" %d [label=\"%s\"];\n", + person.getId(), + escapeString(person.getFullName()))); + } + + dot.append("\n"); + + // Добавляем связи + for (TreeNode node : treeNodes) { + Person person = node.getPerson(); + if (person.getFather() != null) { + dot.append(String.format(" %d -> %d [label=\"отец\"];\n", + person.getFather().getId(), + person.getId())); + } + if (person.getMother() != null) { + dot.append(String.format(" %d -> %d [label=\"мать\"];\n", + person.getMother().getId(), + person.getId())); + } + } + + dot.append("}\n"); + return dot.toString(); + } + + private String escapeString(String input) { + return input.replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r"); + } + + public List buildFullTree(Person person) { + List tree = new ArrayList<>(); + + if (person != null) { + // Добавляем предков (отрицательные уровни) + addAncestors(person, -1, tree); + + // Добавляем выбранного человека (уровень 0) + tree.add(new TreeNode(person, 0, !person.getAllChildren().isEmpty())); + + // Добавляем потомков (положительные уровни) + addDescendants(person, 1, tree); + } + + // Сортируем по уровням для правильного отображения + tree.sort(Comparator.comparingInt(TreeNode::getLevel)); + + return tree; + } + + private void addAncestors(Person person, int level, List tree) { + if (person == null) return; + + // Добавляем отца + if (person.getFather() != null) { + tree.add(0, new TreeNode(person.getFather(), level, !person.getFather().getAllChildren().isEmpty())); + addAncestors(person.getFather(), level - 1, tree); + } + + // Добавляем мать + if (person.getMother() != null) { + tree.add(0, new TreeNode(person.getMother(), level, !person.getMother().getAllChildren().isEmpty())); + addAncestors(person.getMother(), level - 1, tree); + } + } + + private void addDescendants(Person person, int level, List tree) { + if (person == null) return; + + for (Person child : person.getAllChildren()) { + tree.add(new TreeNode(child, level, !child.getAllChildren().isEmpty())); + addDescendants(child, level + 1, tree); + } + } +} \ No newline at end of file diff --git a/src/main/resources/templates/tree.html b/src/main/resources/templates/tree.html index 36472ff..47423a2 100644 --- a/src/main/resources/templates/tree.html +++ b/src/main/resources/templates/tree.html @@ -5,14 +5,21 @@ @@ -20,6 +27,9 @@