/*
* This file is part of LaTeXDraw.
* Copyright (c) 2005-2017 Arnaud BLOUIN
* LaTeXDraw 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 2 of the License, or (at your option) any later version.
* LaTeXDraw is distributed 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.
*/
package net.sf.latexdraw.view.svg;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.transform.Scale;
import javax.imageio.ImageIO;
import net.sf.latexdraw.LaTeXDraw;
import net.sf.latexdraw.actions.ExportFormat;
import net.sf.latexdraw.badaboom.BadaboomCollector;
import net.sf.latexdraw.instruments.ExceptionsManager;
import net.sf.latexdraw.models.ShapeFactory;
import net.sf.latexdraw.models.interfaces.shape.IDrawing;
import net.sf.latexdraw.models.interfaces.shape.IGroup;
import net.sf.latexdraw.models.interfaces.shape.IPoint;
import net.sf.latexdraw.models.interfaces.shape.IShape;
import net.sf.latexdraw.parsers.svg.SVGAttributes;
import net.sf.latexdraw.parsers.svg.SVGDefsElement;
import net.sf.latexdraw.parsers.svg.SVGDocument;
import net.sf.latexdraw.parsers.svg.SVGElement;
import net.sf.latexdraw.parsers.svg.SVGElements;
import net.sf.latexdraw.parsers.svg.SVGGElement;
import net.sf.latexdraw.parsers.svg.SVGMetadataElement;
import net.sf.latexdraw.parsers.svg.SVGSVGElement;
import net.sf.latexdraw.util.LNamespace;
import net.sf.latexdraw.util.LPath;
import net.sf.latexdraw.util.LangTool;
import net.sf.latexdraw.view.jfx.Canvas;
import net.sf.latexdraw.view.jfx.ViewFactory;
import org.malai.javafx.instrument.JfxInstrument;
import org.malai.javafx.ui.OpenSaver;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* The SVG document generator of the app.
* @author Arnaud BLOUIN
*/
public final class SVGDocumentGenerator implements OpenSaver<Label> {
/** The singleton for saving and loading latexdraw SVG documents. */
public static final SVGDocumentGenerator INSTANCE = new SVGDocumentGenerator();
private SVGDocumentGenerator() {
super();
}
@Override
public Task<Boolean> save(final String path, final ProgressBar progressBar, final Label statusBar) {
final SaveWorker sw = new SaveWorker(path, statusBar, true, false, progressBar);
progressBar.progressProperty().bind(sw.progressProperty());
new Thread(sw).start();
return sw;
}
@Override
public Task<Boolean> open(final String path, final ProgressBar progressBar, final Label statusBar) {
final LoadWorker lw = new LoadWorker(path, statusBar, progressBar);
progressBar.progressProperty().bind(lw.progressProperty());
new Thread(lw).start();
return lw;
}
/**
* Exports the selected shapes as a template.
* @param path The path where the template will be saved.
* @param progressBar The progress bar.
* @param statusBar The status bar.
* @param templatePane The menu that contains the template menu items.
*/
public void saveTemplate(final String path, final ProgressBar progressBar, final Label statusBar, final Pane templatePane) {
final SaveTemplateWorker stw = new SaveTemplateWorker(path, statusBar, templatePane, progressBar);
progressBar.progressProperty().bind(stw.progressProperty());
new Thread(stw).start();
}
/**
* Inserts a set of shapes into the drawing.
* @param path The file of the SVG document to load.
* @param position The position where the shapes will be inserted. Can be null.
*/
public IShape insert(final String path, final IPoint position) {
final InsertWorker worker = new InsertWorker(path, position);
new Thread(worker).start();
try {
if(worker.get()) {
return worker.getInsertedShapes();
}
}catch(final InterruptedException | ExecutionException e) {
BadaboomCollector.INSTANCE.add(e);
}
return null;
}
/**
* Updates the templates.
* @param templatesPane The pane that contains the templates.
* @param updatesThumbnails True: the thumbnails of the template will be updated.
*/
public void updateTemplates(final Pane templatesPane, final boolean updatesThumbnails) {
new Thread(new UpdateTemplatesWorker(templatesPane, updatesThumbnails)).start();
}
/**
* The abstract worker that factorises the code of loading and saving workers.
*/
private abstract static class IOWorker extends Task<Boolean> {
protected final Label statusBar;
protected final String path;
protected final ProgressBar progressBar;
private final Map<JfxInstrument, Boolean> instrumentsState;
private final List<JfxInstrument> instruments;
/** set the ui as modified after the work? */
protected boolean setModified;
IOWorker(final String ioPath, final Label status, final ProgressBar bar) {
super();
path = ioPath;
statusBar = status;
setModified = false;
progressBar = bar;
instrumentsState = new HashMap<>();
instruments = new ArrayList<>();
}
/**
* @return The name of the SVG document.
*/
protected String getDocumentName() {
String name;
if(path == null) {
name = ""; //$NON-NLS-1$
}else {
name = new File(path).getName();
final int indexSVG = name.lastIndexOf(".svg");
if(indexSVG != -1) {
name = name.substring(0, indexSVG);
}
}
return name;
}
@Override
protected Boolean call() throws Exception {
if(progressBar!=null) {
progressBar.setVisible(true);
}
Platform.runLater(() -> LaTeXDraw.getINSTANCE().getInstruments().stream().filter(ins -> !(ins instanceof ExceptionsManager))
.forEach(ins -> {
instrumentsState.put(ins, ins.isActivated());
instruments.add(ins);
ins.setActivated(false, true);
}));
return true;
}
@Override
protected void done() {
super.done();
Platform.runLater(() -> instruments.forEach(ins -> ins.setActivated(instrumentsState.getOrDefault(ins, false))));
if(progressBar != null) {
progressBar.progressProperty().unbind();
progressBar.setVisible(false);
}
LaTeXDraw.getINSTANCE().setModified(setModified);
}
}
/** This worker inserts the given set of shapes into the drawing. */
private static class InsertWorker extends LoadShapesWorker {
private IShape insertedShapes;
private IPoint position;
InsertWorker(final String path, final IPoint positionTemplate) {
super(path, null, null);
setModified = true;
insertedShapes = null;
position = positionTemplate;
}
@Override
protected Boolean call() throws Exception {
try {
final SVGDocument svgDoc = new SVGDocument(new File(path).toURI());
final IDrawing drawing = LaTeXDraw.getINSTANCE().getInjector().getInstance(IDrawing.class);
Platform.runLater(() -> {
final List<IShape> shapes = toLatexdraw(svgDoc, 0);
if(shapes.size() == 1) {
insertedShapes = shapes.get(0);
}else {
final IGroup gp = ShapeFactory.INST.createGroup();
shapes.forEach(sh -> gp.addShape(sh));
insertedShapes = gp;
}
if(position != null) {
final IPoint tp = insertedShapes.getTopLeftPoint();
insertedShapes.translate(position.getX() - tp.getX(), position.getY() - tp.getY());
}
drawing.addShape(insertedShapes);
// Updating the possible widgets of the instruments.
LaTeXDraw.getINSTANCE().getInstruments().forEach(ins -> ins.interimFeedback());
// ui.updatePresentations();
});
return true;
}catch(final Exception e) {
BadaboomCollector.INSTANCE.add(e);
return false;
}
}
protected IShape getInsertedShapes() {
return insertedShapes;
}
}
/** This worker updates the templates. */
private static class UpdateTemplatesWorker extends LoadShapesWorker {
private final Pane templatesPane;
private final boolean updateThumbnails;
UpdateTemplatesWorker(final Pane pane, final boolean updateThumbs) {
super(null, null, null);
templatesPane = pane;
updateThumbnails = updateThumbs;
}
@Override
protected Boolean call() throws Exception {
if(updateThumbnails) {
updateTemplates(LPath.PATH_TEMPLATES_DIR_USER, LPath.PATH_CACHE_DIR);
updateTemplates(LPath.PATH_TEMPLATES_SHARED, LPath.PATH_CACHE_SHARE_DIR);
}
Platform.runLater(() -> {
templatesPane.getChildren().clear();
fillTemplatePane(LPath.PATH_TEMPLATES_DIR_USER, LPath.PATH_CACHE_DIR, true);
fillTemplatePane(LPath.PATH_TEMPLATES_SHARED, LPath.PATH_CACHE_SHARE_DIR, true);
});
return true;
}
/**
* Creates an image view from the template
* @param nameThumb The name of the thumbnail.
* @param pathPic The path of the thumbnail of the template.
* @return The created image view or nothing.
*/
private Optional<ImageView> createTemplateItem(final String svgPath, final String nameThumb, final String pathPic) {
try {
final ImageView view = new ImageView(new Image("file:"+pathPic + File.separator + nameThumb));
view.setUserData(svgPath);
final int id = nameThumb.lastIndexOf(".svg" + ExportFormat.PNG.getFileExtension());
if(id != -1) {
Tooltip.install(view, new Tooltip(nameThumb.substring(0, id)));
}
return Optional.of(view);
}catch(final Exception ex) {
return Optional.empty();
}
}
/**
* fills the template pane with image views gathered from the given directory of templates.
* @param pathTemplate The path of the folder that contains the templates.
* @param pathCache The path of the folder that contains the cache of the templates.
* @param sharedTemplates True: the templates are shared templates (in the shared directory).
*/
private void fillTemplatePane(final String pathTemplate, final String pathCache, final boolean sharedTemplates) {
try {
Files.newDirectoryStream(Paths.get(pathTemplate), elt -> Files.isRegularFile(elt) && elt.toString().endsWith(".svg")).
forEach(entry -> createTemplateItem(entry.toFile().getPath(),
entry.getFileName() + ExportFormat.PNG.getFileExtension(), pathCache).
ifPresent(item -> templatesPane.getChildren().add(item)));
}catch(final IOException ex) {
// No matter.
}
}
/**
* Updates the templates from the given path, in the given cache path.
* @param pathTemplate The path of the templates to update.
* @param pathCache The path where the cache of the thumbnails of the templates will be stored.
*/
private void updateTemplates(final String pathTemplate, final String pathCache) {
final File templateDir = new File(pathTemplate);
if(!templateDir.isDirectory()) return;
try {
Files.newDirectoryStream(Paths.get(pathTemplate), elt -> Files.isRegularFile(elt) && elt.toString().endsWith(".svg")).
forEach(file -> Platform.runLater(() -> {
try {
final Group template = new Group();
final List<IShape> shapes = toLatexdraw(new SVGDocument(file.toUri()), 0);
template.getChildren().setAll(shapes.stream().map(sh -> ViewFactory.INSTANCE.createView(sh)).
filter(opt -> opt.isPresent()).map(opt -> opt.get()).collect(Collectors.toList()));
final File thumb = new File(pathCache + File.separator + file.getFileName() + ExportFormat.PNG.getFileExtension());
createTemplateThumbnail(thumb, template);
}catch(final Exception ex) {
BadaboomCollector.INSTANCE.add(ex);
}
}));
}catch(final IOException ex) {
ex.printStackTrace();
}
}
/**
* Creates a thumbnail from the given selection in the given file.
* @param templateFile The file of the future thumbnail.
* @param selection The set of shapes composing the template.
*/
private void createTemplateThumbnail(final File templateFile, final Group selection) {
final Bounds bounds = selection.getBoundsInParent();
final double scale = 70d / Math.max(bounds.getWidth(), bounds.getHeight());
final WritableImage img = new WritableImage((int) (bounds.getWidth() * scale), (int) (bounds.getHeight() * scale));
final SnapshotParameters snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.WHITE);
snapshotParameters.setTransform(new Scale(scale, scale));
selection.snapshot(snapshotParameters, img);
while(img.isBackgroundLoading()) {
try {
Thread.sleep(100);
}catch(final InterruptedException ex) {
BadaboomCollector.INSTANCE.add(ex);
}
}
final BufferedImage bufferedImage = SwingFXUtils.fromFXImage(img, null);
try {
ImageIO.write(bufferedImage, "png", templateFile); //$NON-NLS-1$
}catch(final IOException ex) {
BadaboomCollector.INSTANCE.add(ex);
}
bufferedImage.flush();
}
}
private static class SaveTemplateWorker extends SaveWorker {
private final Pane templatePane;
SaveTemplateWorker(final String path, final Label statusBar, final Pane templates, final ProgressBar bar) {
super(path, statusBar, false, true, bar);
templatePane = templates;
}
@Override
protected void done() {
super.done();
INSTANCE.updateTemplates(templatePane, true);
if(statusBar != null) {
Platform.runLater(() -> statusBar.setText(LangTool.INSTANCE.getBundle().getString("LaTeXDrawFrame.169"))); //$NON-NLS-1$
}
}
}
/** This worker saves the given document. */
private static class SaveWorker extends IOWorker {
/** Defines if the parameters of the drawing (instruments, presentations, etc.) must be saved. */
private final boolean saveParameters;
/** Specifies if only the selected shapes must be saved. */
private final boolean onlySelection;
SaveWorker(final String path, final Label statusBar, final boolean saveParams, final boolean onlySelected, final ProgressBar bar) {
super(path, statusBar, bar);
saveParameters = saveParams;
onlySelection = onlySelected;
}
/**
* Creates an SVG document from a drawing.
* @param drawing The drawing to convert in SVG.
* @return The created SVG document or null.
*/
private SVGDocument toSVG(final IDrawing drawing, final double incr) {
// Creation of the SVG document.
final List<IShape> shapes = onlySelection ? drawing.getSelection().getShapes() : drawing.getShapes();
final SVGDocument doc = new SVGDocument();
final SVGSVGElement root = doc.getFirstChild();
final SVGGElement g = new SVGGElement(doc);
root.appendChild(g);
root.setAttribute("xmlns:" + LNamespace.LATEXDRAW_NAMESPACE, LNamespace.LATEXDRAW_NAMESPACE_URI);//$NON-NLS-1$
root.appendChild(new SVGDefsElement(doc));
try {
shapes.forEach(sh -> {
// For each shape an SVG element is created.
SVGElement elt = SVGShapesFactory.INSTANCE.createSVGElement(sh, doc);
if(elt != null) {
g.appendChild(elt);
}
Platform.runLater(() -> updateProgress(getProgress() + incr, 100d));
});
}catch(final Exception ex) {
BadaboomCollector.INSTANCE.add(ex);
}
// Setting SVG attributes to the created document.
root.setAttribute(SVGAttributes.SVG_VERSION, "1.1");//$NON-NLS-1$
root.setAttribute(SVGAttributes.SVG_BASE_PROFILE, "full");//$NON-NLS-1$
return doc;
}
@Override
protected Boolean call() throws Exception {
super.call();
final IDrawing drawing = LaTeXDraw.getINSTANCE().getInjector().getInstance(IDrawing.class);
final Canvas canvas = LaTeXDraw.getINSTANCE().getInjector().getInstance(Canvas.class);
// Creation of the SVG document.
final Set<JfxInstrument> instruments = LaTeXDraw.getINSTANCE().getInstruments();
final double incr = 100d / (drawing.size() + instruments.size() + 1d);
final SVGDocument doc = toSVG(drawing, incr);
final SVGMetadataElement meta = new SVGMetadataElement(doc);
final SVGSVGElement root = doc.getFirstChild();
final SVGElement metaLTD = (SVGElement) doc.createElement(LNamespace.LATEXDRAW_NAMESPACE + ':' + SVGElements.SVG_METADATA);
// Creation of the SVG meta data tag.
meta.appendChild(metaLTD);
root.appendChild(meta);
if(saveParameters) {
Platform.runLater(() -> {
// The parameters of the instruments are now saved.
instruments.forEach(ins -> {
ins.save(false, LNamespace.LATEXDRAW_NAMESPACE, doc, metaLTD);
updateProgress(getProgress() + incr, 100d);
});
canvas.save(false, LNamespace.LATEXDRAW_NAMESPACE, doc, metaLTD);
updateProgress(getProgress() + incr, 100d);
LaTeXDraw.getINSTANCE().save(false, LNamespace.LATEXDRAW_NAMESPACE, doc, metaLTD);
LaTeXDraw.getINSTANCE().getMainStage().setTitle(getDocumentName());
});
}
return doc.saveSVGDocument(path);
}
@Override
protected void done() {
super.done();
// Showing a message in the status bar.
if(statusBar != null) {
Platform.runLater(() -> statusBar.setText(LangTool.INSTANCE.getBundle().getString("SVG.1"))); //$NON-NLS-1$
}
}
}
private abstract static class LoadShapesWorker extends IOWorker {
LoadShapesWorker(final String path, final Label statusBar, final ProgressBar bar) {
super(path, statusBar, bar);
}
/**
* Converts an SVG document into a set of shapes.
* @param doc The SVG document.
* @param incrProgressBar The increment that will be used by the progress bar.
* @return The created shapes or null.
*/
protected List<IShape> toLatexdraw(final SVGDocument doc, final double incrProgressBar) {
final NodeList elts = doc.getDocumentElement().getChildNodes();
final List<IShape> shapes = IntStream.range(0, elts.getLength()).mapToObj(i -> {
updateProgress(getProgress() + incrProgressBar, 100d);
return elts.item(i);
}).filter(node -> node instanceof SVGElement).map(node -> IShapeSVGFactory.INSTANCE.createShape((SVGElement) node)).
filter(sh -> sh != null).collect(Collectors.toList());
if(shapes.size() == 1 && shapes.get(0) instanceof IGroup) {
return ((IGroup) shapes.get(0)).getShapes();
}
return shapes;
}
}
/**
* The worker that loads SVG documents.
*/
private static class LoadWorker extends LoadShapesWorker {
LoadWorker(final String path, final Label statusBar, final ProgressBar bar) {
super(path, statusBar, bar);
}
/**
* Loads the instruments of the systems.
* @param meta The meta-data that contains the data of the instruments.
* @param instruments The instruments to set.
*/
private void loadInstruments(final Element meta, final Set<JfxInstrument> instruments) {
final NodeList nl = meta.getChildNodes();
Platform.runLater(() -> {
for(int i = 0, size = nl.getLength(); i < size; i++) {
final Node n = nl.item(i);
if(n instanceof Element && LNamespace.LATEXDRAW_NAMESPACE_URI.equals(n.getNamespaceURI())) {
try {
final Element elt = (Element) n;
instruments.forEach(ins -> ins.load(false, LNamespace.LATEXDRAW_NAMESPACE_URI, elt));
}catch(final Exception e) {
BadaboomCollector.INSTANCE.add(e);
}
}
}
});
}
@Override
protected Boolean call() throws Exception {
super.call();
try {
final SVGDocument svgDoc = new SVGDocument(new File(path).toURI());
final Element meta = svgDoc.getDocumentElement().getMeta();
final Set<JfxInstrument> instruments = LaTeXDraw.getINSTANCE().getInstruments();
final IDrawing drawing = LaTeXDraw.getINSTANCE().getInjector().getInstance(IDrawing.class);
final Canvas canvas = LaTeXDraw.getINSTANCE().getInjector().getInstance(Canvas.class);
final Element ldMeta;
if(meta == null) {
ldMeta = null;
}else {
final NodeList nl = meta.getElementsByTagNameNS(LNamespace.LATEXDRAW_NAMESPACE_URI, SVGElements.SVG_METADATA);
final Node node = nl.getLength() == 0 ? null : nl.item(0);
ldMeta = node instanceof Element ? (Element) node : null;
}
// Adding loaded shapes.
final double incrProgressBar = Math.max(50d / (svgDoc.getDocumentElement().getChildNodes().getLength() + 1d), 1d);
Platform.runLater(() -> {
toLatexdraw(svgDoc, incrProgressBar).forEach(s -> drawing.addShape(s));
updateProgress(getProgress() + 50d, 100d);
// Loads the canvas' data.
canvas.load(false, LNamespace.LATEXDRAW_NAMESPACE_URI, ldMeta);
updateProgress(getProgress() + incrProgressBar, 100d);
// The parameters of the instruments are loaded.
if(ldMeta != null) {
loadInstruments(ldMeta, instruments);
}
// Updating the possible widgets of the instruments.
instruments.forEach(ins -> {
ins.interimFeedback();
if(ldMeta != null) {
LaTeXDraw.getINSTANCE().load(false, LNamespace.LATEXDRAW_NAMESPACE_URI, ldMeta);
}
LaTeXDraw.getINSTANCE().getMainStage().setTitle(getDocumentName());
});
});
return true;
}catch(final Exception e) {
BadaboomCollector.INSTANCE.add(e);
return false;
}
}
}
}