/* * Copyright (c) 2009, 2010, 2011 Daniel Rendall * This file is part of FractDim. * * FractDim 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. * * FractDim 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 FractDim. If not, see <http://www.gnu.org/licenses/> */ package uk.co.danielrendall.fractdim.app.controller; import org.apache.batik.dom.svg.SAXSVGDocumentFactory; import org.apache.batik.swing.JSVGCanvas; import org.w3c.dom.Element; import org.w3c.dom.svg.SVGDocument; import uk.co.danielrendall.fractdim.app.FractDim; import uk.co.danielrendall.fractdim.app.gui.GridSelectedEvent; import uk.co.danielrendall.fractdim.app.gui.ResultPanelListener; import uk.co.danielrendall.fractdim.app.model.*; import uk.co.danielrendall.fractdim.app.gui.FractalPanel; import uk.co.danielrendall.fractdim.app.gui.actions.ActionRepository; import uk.co.danielrendall.fractdim.app.workers.ExcelExportWorker; import uk.co.danielrendall.fractdim.app.workers.Notifiable; import uk.co.danielrendall.fractdim.app.workers.SquareCountingWorker; import uk.co.danielrendall.fractdim.calculation.FractalMetadataUtil; import uk.co.danielrendall.fractdim.calculation.SquareCountingResult; import uk.co.danielrendall.fractdim.calculation.grids.Grid; import uk.co.danielrendall.fractdim.calculation.iterators.*; import uk.co.danielrendall.fractdim.logging.Log; import uk.co.danielrendall.fractdim.logging.PrettyPrinter; import uk.co.danielrendall.fractdim.svg.SVGContentGenerator; import uk.co.danielrendall.fractdim.svg.SVGElementCreator; import uk.co.danielrendall.fractdim.svg.Utilities; import uk.co.danielrendall.mathlib.geom2d.BoundingBox; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import java.awt.event.ActionEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.io.*; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicReference; /** * Created by IntelliJ IDEA. * User: daniel * Date: 03-Apr-2010 * Time: 21:02:09 * To change this template use File | Settings | File Templates. */ public class FractalController { private final ControllerThread controllerThread = new ControllerThread(this); private enum Status {NEW, DOC_LOADED, READY_FOR_COUNT, COUNTING_SQUARES, SQUARES_COUNTED, EXPORTING}; private final static String CALC_STATS = "CalcStats"; private final static String GENERATE_METADATA = "GenerateMetadata"; private final static String COUNT_SQUARES = "CountSquares"; private final static String UPDATE_RESOLUTION_LIST = "UpdateResolutionList"; private final static String RESULT_GRID = "ResultGrid"; private final static int RESULT_GRID_Z_INDEX = -1; private final static String MIN_MAX_GRIDS = "MinMaxGrids"; private final static String INDICATIVE_RESOLUTION_GRID = "IndicativeResolutionGrids"; private final static int INDICATIVE_RESOLUTION_GRID_Z_INDEX = -12; private final static String MIN_GRID = "MinGrid"; private final static int MIN_GRID_Z_INDEX = -15; private final static String MAX_GRID = "MaxGrid"; private final static int MAX_GRID_Z_INDEX = -10; private final static String BOUNDING_BOX = "BoundingBox"; private final static int BOUNDING_BOX_Z_INDEX = -5; private final FractalDocument document; private final FractalPanel panel; private final BoundedRangeModel minimumSquareSizeModel; private final BoundedRangeModel maximumSquareSizeModel; private final BoundedRangeModel resolutionModel; private final BoundedRangeModel angleModel; private final BoundedRangeModel displacementModel; private boolean indicativeResolutionGridIsDisplayed = false; private SquareCountingResult result = null; private AtomicReference<ResolutionIteratorFactory> resolutionIteratorFactory = new AtomicReference<ResolutionIteratorFactory>(ResolutionIteratorFactory.factories[0]); private final Action actionCalculateFractalDimension = new AbstractAction() { public void actionPerformed(ActionEvent e) { actionCalculateFractalDimension(); } }; private final Action actionFileExport = new AbstractAction() { public void actionPerformed(ActionEvent e) { actionFileExport(); } }; private final Action actionCloseFile = new AbstractAction() { public void actionPerformed(ActionEvent e) { actionCloseFile(); } }; private volatile Status status = Status.NEW; public static FractalController fromFile(File file) throws IOException { SAXSVGDocumentFactory factory = Utilities.getDocumentFactory(); SVGDocument doc = factory.createSVGDocument(file.toURI().toString()); return fromDocument(doc, file.getName()); } public static FractalController fromInputStream(InputStream inputStream) throws IOException { SAXSVGDocumentFactory factory = Utilities.getDocumentFactory(); SVGDocument doc = factory.createSVGDocument("SomeURI", inputStream); return fromDocument(doc, "Inputstream " + new Date().getTime()); } public static FractalController fromDocument(SVGDocument doc, String name) { FractalDocument document = new FractalDocument(doc, name); return new FractalController(document); } private FractalController(FractalDocument document) { this.document = document; panel = new FractalPanel(); minimumSquareSizeModel = new DefaultBoundedRangeModel(10, 0, 1, 1000); panel.getMinimumSquareSizeSlider().setModel(minimumSquareSizeModel); maximumSquareSizeModel = new DefaultBoundedRangeModel(990, 0, 1, 1000); panel.getMaximumSquareSizeSlider().setModel(maximumSquareSizeModel); angleModel = new DefaultBoundedRangeModel(5,0,1,10); panel.getAngleSlider().setModel(angleModel); resolutionModel = new DefaultBoundedRangeModel(10, 0, 2, 20); panel.getResolutionSlider().setModel(resolutionModel); panel.getResolutionSlider().addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { controllerThread.addToQueue(UPDATE_RESOLUTION_LIST, new Runnable() { public void run() { removeIndicativeResolutionGrid(); updateResolutionList(); } }); } }); displacementModel = new DefaultBoundedRangeModel(2,0,1,5); panel.getDisplacementSlider().setModel(displacementModel); panel.getResolutionIteratorList().setModel(new DefaultComboBoxModel(ResolutionIteratorFactory.factories)); panel.getResolutionIteratorList().addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { final ResolutionIteratorFactory chosenFactory = (ResolutionIteratorFactory) e.getItem(); resolutionIteratorFactory.set(chosenFactory); if (chosenFactory == ResolutionIteratorFactory.factories[2]) { FractalController.this.panel.getResolutionSlider().setEnabled(false); } else { FractalController.this.panel.getResolutionSlider().setEnabled(true); } controllerThread.addToQueue(UPDATE_RESOLUTION_LIST, new Runnable() { public void run() { removeIndicativeResolutionGrid(); updateResolutionList(); } }); } } }); panel.addResultPanelListener(new ResultPanelListener() { public void gridSelected(final GridSelectedEvent e) { controllerThread.addToQueue(RESULT_GRID, new Runnable() { public void run() { Log.thread.debug("Grid selected: " + e.getGrid().toString()); updateResultGrid(e.getGrid()); } }); } }); final JList jList = panel.getResolutionList(); jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); jList.addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { if (!e.getValueIsAdjusting()) { int selectedIndex = e.getFirstIndex(); final Double selectedResolution = (Double) jList.getModel().getElementAt(selectedIndex); controllerThread.addToQueue(INDICATIVE_RESOLUTION_GRID, new Runnable() { public void run() { Log.thread.debug("Updating indicative resolution grid"); updateIndicativeResolutionGrid(selectedResolution); } }); } } }); } public void notifyAdded() { String threadName = "Controller: " + document.getName(); controllerThread.setName(threadName); controllerThread.start(); panel.updateProgressBar(0); panel.showProgressBar(); controllerThread.addToQueue(GENERATE_METADATA, new Runnable() { public void run() { generateMetaData(); } }); } public void notifyRemoving() { // nothing to do } public void notifyRemoved() { controllerThread.quit(); } private void generateMetaData() { controllerThread.checkControllerThread(); Log.thread.debug("Generating metadata"); panel.updateProgressBar(33); FractalDocumentMetadata metadata = FractalMetadataUtil.getMetadata(document.getSvgDoc()); document.setMetadata(metadata); BoundingBox box = metadata.getBoundingBox(); double maximumBoxSize = Math.min(box.getWidth(), box.getHeight()); this.status = Status.DOC_LOADED; Log.thread.debug("Populating settings panel"); maximumSquareSizeModel.setValue((int)maximumBoxSize + 1); minimumSquareSizeModel.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { final int minValue = minimumSquareSizeModel.getValue(); final int maxValue = maximumSquareSizeModel.getValue(); if (!minimumSquareSizeModel.getValueIsAdjusting()) { controllerThread.addToQueue(MIN_MAX_GRIDS, new Runnable() { public void run() { Log.thread.debug("Updating grids in response to minimum square size change"); updateMinimumAndMaximumGrids(); } }); if (minValue > maxValue) { maximumSquareSizeModel.setValue(minValue); } } controllerThread.addToQueue(UPDATE_RESOLUTION_LIST, new Runnable() { public void run() { removeIndicativeResolutionGrid(); updateResolutionList(); } }); } }); maximumSquareSizeModel.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { final int minValue = minimumSquareSizeModel.getValue(); final int maxValue = maximumSquareSizeModel.getValue(); if (!maximumSquareSizeModel.getValueIsAdjusting()) { controllerThread.addToQueue(MIN_MAX_GRIDS, new Runnable() { public void run() { Log.thread.debug("Updating grids in response to maximum square size change"); updateMinimumAndMaximumGrids(); } }); if (maxValue < minValue) { minimumSquareSizeModel.setValue(maxValue); } } controllerThread.addToQueue(UPDATE_RESOLUTION_LIST, new Runnable() { public void run() { removeIndicativeResolutionGrid(); updateResolutionList(); } }); } }); panel.updateProgressBar(66); panel.updateDocument(document); updateMinimumAndMaximumGrids(); updateResolutionList(); panel.updateProgressBar(100); panel.hideProgressBar(); setStatus(Status.READY_FOR_COUNT); // All the setting up of the panel etc. will be done in the controller thread. } public FractalDocument getDocument() { return document; } public FractalPanel getPanel() { return panel; } // called when our panel becomes active public void enableMenuItems() { Log.gui.info("enableMenuItems called"); ActionMap actionMap = panel.getActionMap(); ActionRepository repository = ActionRepository.instance(); repository.getFileClose().setDelegate(actionCloseFile); repository.getDiagramZoomIn().setDelegate(actionMap.get(JSVGCanvas.ZOOM_IN_ACTION)); repository.getDiagramZoomOut().setDelegate(actionMap.get(JSVGCanvas.ZOOM_OUT_ACTION)); if (isCapableOfCalculation()) { repository.getFileCalculate().setDelegate(actionCalculateFractalDimension); } else { repository.getFileCalculate().removeDelegate(); } if (isCapableOfExport()) { repository.getFileExport().setDelegate(actionFileExport); } else { repository.getFileExport().removeDelegate(); } } private boolean isCapableOfCalculation() { return (status == Status.READY_FOR_COUNT || status == Status.SQUARES_COUNTED); } private boolean isCapableOfExport() { return (status == Status.SQUARES_COUNTED); } private void setStatus(Status newStatus) { status = newStatus; FractDim.instance().updateMeIfCurrent(this); } public void actionFileExport() { if (result == null) { Log.app.warn("Shouldn't be able to export if the result is null"); } File exportFile = FractDim.instance().getExportFile(document.getName()); if (exportFile != null) { panel.disableAllControls(); panel.updateProgressBar(0); panel.showProgressBar(); Log.app.debug("Exporting to " + exportFile.getAbsolutePath()); ExcelExportWorker eew = new ExcelExportWorker(document.getName(), result, exportFile, new Notifiable<ExcelExportWorker>() { public void notifyComplete(ExcelExportWorker worker) { panel.enableAllControls(); panel.hideProgressBar(); setStatus(Status.SQUARES_COUNTED); } public void updateProgress(int progress) { panel.updateProgressBar(progress); } }); setStatus(Status.EXPORTING); eew.execute(); Log.app.info("Started excel export worker"); } } public void actionCloseFile() { // TODO - check we're in a fit state to close FractDim.instance().remove(this); } public void actionCalculateFractalDimension() { panel.disableAllControls(); panel.updateProgressBar(0); panel.showProgressBar(); AngleIterator angleIterator = new UniformAngleIterator(angleModel.getValue()); ResolutionIterator resolutionIterator = resolutionIteratorFactory.get().create(minimumSquareSizeModel.getValue(), maximumSquareSizeModel.getValue(), resolutionModel.getValue()); DisplacementIterator displacementIterator = new UniformDisplacementIterator(displacementModel.getValue()); SquareCountingWorker scw = new SquareCountingWorker(document, angleIterator, resolutionIterator, displacementIterator, new Notifiable<SquareCountingWorker>() { public void notifyComplete(SquareCountingWorker worker) { Log.calc.info("Square counting worker reported"); try { result = worker.get(); panel.enableAllControls(); panel.update(result); panel.hideProgressBar(); setStatus(Status.SQUARES_COUNTED); } catch (InterruptedException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } catch (ExecutionException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } } public void updateProgress(int progress) { panel.updateProgressBar(progress); } }); setStatus(Status.COUNTING_SQUARES); scw.execute(); Log.calc.info("Started square counting worker"); } private void updateResolutionList() { controllerThread.checkControllerThread(); final ResolutionIteratorFactory iteratorFactory = resolutionIteratorFactory.get(); if (iteratorFactory != null) { JList resolutionList = this.panel.getResolutionList(); Vector<Double> resolutions = new Vector<Double>(); for (ResolutionIterator it = iteratorFactory.create(minimumSquareSizeModel.getValue(), maximumSquareSizeModel.getValue(), resolutionModel.getValue()); it.hasNext();) { resolutions.add(it.next()); } resolutionList.setListData(resolutions); } } private void updateMinimumAndMaximumGrids() { controllerThread.checkControllerThread(); final Grid minGrid = new Grid(minimumSquareSizeModel.getValue()); final Grid maxGrid = new Grid(maximumSquareSizeModel.getValue()); final BoundingBox boundingBox = document.getMetadata().getBoundingBox(); Log.gui.info("Bounding box is " + boundingBox); panel.updateOverlay(BOUNDING_BOX, BOUNDING_BOX_Z_INDEX, new SVGContentGenerator() { public BoundingBox generateContent(Element rootElement, SVGElementCreator creator) { Element path = creator.createPath("#999999"); path.setAttributeNS(null, "d", String.format("M %s,%s L %s,%s L %s,%s L %s,%s z", boundingBox.getMinX(), boundingBox.getMinY(), boundingBox.getMaxX(), boundingBox.getMinY(), boundingBox.getMaxX(), boundingBox.getMaxY(), boundingBox.getMinX(), boundingBox.getMaxY())); rootElement.appendChild(path); return boundingBox; } }); panel.updateOverlay(MIN_GRID, MIN_GRID_Z_INDEX, new SVGContentGenerator() { public BoundingBox generateContent(Element rootElement, SVGElementCreator creator) { return minGrid.writeToSVG(rootElement, creator, boundingBox, "#99ff99"); } }); panel.updateOverlay(MAX_GRID, MAX_GRID_Z_INDEX, new SVGContentGenerator() { public BoundingBox generateContent(Element rootElement, SVGElementCreator creator) { return maxGrid.writeToSVG(rootElement, creator, boundingBox, "#9999ff"); } }); } private void updateIndicativeResolutionGrid(double resolution) { controllerThread.checkControllerThread(); final Grid grid = new Grid(resolution); final BoundingBox boundingBox = document.getMetadata().getBoundingBox(); panel.updateOverlay(INDICATIVE_RESOLUTION_GRID, INDICATIVE_RESOLUTION_GRID_Z_INDEX, new SVGContentGenerator() { public BoundingBox generateContent(Element rootElement, SVGElementCreator creator) { return grid.writeToSVG(rootElement, creator, boundingBox, "#cccc00"); } }); indicativeResolutionGridIsDisplayed = true; } private void removeIndicativeResolutionGrid() { controllerThread.checkControllerThread(); if (indicativeResolutionGridIsDisplayed) { final BoundingBox boundingBox = document.getMetadata().getBoundingBox(); panel.removeOverlay(INDICATIVE_RESOLUTION_GRID); } indicativeResolutionGridIsDisplayed = false; } private void updateResultGrid(final Grid theGrid) { controllerThread.checkControllerThread(); final BoundingBox boundingBox = document.getMetadata().getBoundingBox(); panel.updateOverlay(RESULT_GRID, RESULT_GRID_Z_INDEX, new SVGContentGenerator() { public BoundingBox generateContent(Element rootElement, SVGElementCreator creator) { return theGrid.writeFilledToSVG(rootElement, creator, boundingBox, "#ff9999"); } }); } private void prettyPrint(String message, SVGDocument doc) { if (Log.gui.isDebugEnabled()) { Log.gui.debug(message); StringWriter sw = new StringWriter(); PrettyPrinter pp = new PrettyPrinter(doc); pp.prettyPrint(sw); Log.gui.debug(sw.toString()); } } private void checkControllerThread() { controllerThread.checkControllerThread(); } }