/** * */ package cz.cuni.mff.peckam.java.origamist.model; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.ResourceBundle; import java.util.concurrent.Callable; import javax.xml.bind.annotation.XmlTransient; import org.apache.log4j.Level; import org.apache.log4j.Logger; import cz.cuni.mff.peckam.java.origamist.common.Author; import cz.cuni.mff.peckam.java.origamist.common.BinaryImage; import cz.cuni.mff.peckam.java.origamist.common.LangString; import cz.cuni.mff.peckam.java.origamist.common.License; import cz.cuni.mff.peckam.java.origamist.files.File; import cz.cuni.mff.peckam.java.origamist.modelstate.DefaultModelState; import cz.cuni.mff.peckam.java.origamist.utils.LangStringHashtableObserver; import cz.cuni.mff.peckam.java.origamist.utils.ObservableList; import cz.cuni.mff.peckam.java.origamist.utils.ObservablePropertyEvent; import cz.cuni.mff.peckam.java.origamist.utils.ObservablePropertyListener; /** * The origami diagram. * <p> * Provided property: src * <p> * See {@link cz.cuni.mff.peckam.java.origamist.model.jaxb.Origami} for other bound properties. * * @author Martin Pecka */ @XmlTransient public class Origami extends cz.cuni.mff.peckam.java.origamist.model.jaxb.Origami { /** * The hastable for more comfortable search in localized names. */ protected final Hashtable<Locale, String> names = new Hashtable<Locale, String>(); /** * The hastable for more comfortable search in localized short descriptions. */ protected final Hashtable<Locale, String> shortDescs = new Hashtable<Locale, String>(); /** * The hastable for more comfortable search in localized descriptions. */ protected final Hashtable<Locale, String> descriptions = new Hashtable<Locale, String>(); /** If the origami is loaded without the model, then this task will be run the first time the model is read. */ protected Callable<Model> loadModelCallable = null; /** The file in the listing containing this origami. */ protected File file = null; /** * The URL this origami was created from. Obviously this will be <code>null</code> for the just-being-created model. */ protected URL src = null; /** The cached number of pages required for this origami. */ protected Integer pages = null; /** The placement of steps in the page from key. */ protected final Map<Integer, Integer[]> stepsOnPages = new HashMap<Integer, Integer[]>(); /** The first step displayed on the page from key. */ protected final Map<Integer, Step> firstStepOnPages = new HashMap<Integer, Step>(); /** * Create a new origami diagram. */ public Origami() { ((ObservableList<LangString>) getName()).addObserver(new LangStringHashtableObserver(names)); ((ObservableList<LangString>) getShortdesc()).addObserver(new LangStringHashtableObserver(shortDescs)); ((ObservableList<LangString>) getDescription()).addObserver(new LangStringHashtableObserver(descriptions)); addObservablePropertyListener(new ObservablePropertyListener<Step>() { @Override public void changePerformed(ObservablePropertyEvent<? extends Step> evt) { pages = null; stepsOnPages.clear(); firstStepOnPages.clear(); } }, MODEL_PROPERTY, Model.STEPS_PROPERTY, Steps.STEP_PROPERTY); PropertyChangeListener pagesCacheListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { pages = null; stepsOnPages.clear(); firstStepOnPages.clear(); } }; addPropertyChangeListener(pagesCacheListener, PAPER_PROPERTY, DiagramPaper.COLS_PROPERTY); addPropertyChangeListener(pagesCacheListener, PAPER_PROPERTY, DiagramPaper.ROWS_PROPERTY); addPropertyChangeListener(pagesCacheListener, MODEL_PROPERTY, Model.STEPS_PROPERTY, Steps.STEP_PROPERTY, Step.COLSPAN_PROPERTY); addPropertyChangeListener(pagesCacheListener, MODEL_PROPERTY, Model.STEPS_PROPERTY, Steps.STEP_PROPERTY, Step.ROWSPAN_PROPERTY); } /** * Return the localized name of the model. * * @param l The locale of the name. If null or not found, returns the * content of the first <name> element defined * @return The localized note */ public String getName(Locale l) { if (names.size() == 0) { ResourceBundle b = ResourceBundle.getBundle("cz.cuni.mff.peckam.java.origamist.model.Origami", l); return b.getString("nameNotFound"); } if (l == null || !names.containsKey(l)) return names.elements().nextElement(); return names.get(l); } /** * Add a name in the given locale. * * @param l The locale of the name * @param name The name to add */ public void addName(Locale l, String name) { LangString s = (LangString) new cz.cuni.mff.peckam.java.origamist.common.jaxb.ObjectFactory() .createLangString(); s.setLang(l); s.setValue(name); this.name.add(s); } /** * Return the localized short description of the model. * * @param l The locale of the short descripton. If null or not found, * returns the content of the first <shortdesc> element * defined * @return The localized note */ public String getShortDesc(Locale l) { if (shortDescs.size() == 0) { ResourceBundle b = ResourceBundle.getBundle("cz.cuni.mff.peckam.java.origamist.model.Origami", l); return b.getString("shortDescNotFound"); } if (l == null || !shortDescs.containsKey(l)) return shortDescs.elements().nextElement(); return shortDescs.get(l); } /** * Add a short description in the given locale. * * @param l The locale of the short description * @param desc The short description to add to add */ public void addShortDesc(Locale l, String desc) { LangString s = (LangString) new cz.cuni.mff.peckam.java.origamist.common.jaxb.ObjectFactory() .createLangString(); s.setLang(l); s.setValue(desc); this.shortdesc.add(s); } /** * Return the localized description of the model. * * @param l The locale of the description. If null or not found, returns the * content of the first <description> element defined * @return The localized description */ public String getDescription(Locale l) { if (descriptions.size() == 0) { // TODO possible fallback to short description ResourceBundle b = ResourceBundle.getBundle("cz.cuni.mff.peckam.java.origamist.model.Origami", l); return b.getString("descriptionNotFound"); } if (l == null || !descriptions.containsKey(l)) return descriptions.elements().nextElement(); return descriptions.get(l); } /** * Add a description in the given locale. * * @param l The locale of the description * @param name The description to add */ public void addDescription(Locale l, String desc) { LangString s = (LangString) new cz.cuni.mff.peckam.java.origamist.common.jaxb.ObjectFactory() .createLangString(); s.setLang(l); s.setValue(desc); this.description.add(s); } @Override public Model getModel() { if (loadModelCallable != null) { try { // TODO notify the user about loading Callable<Model> callable = loadModelCallable; loadModelCallable = null; this.model = callable.call(); init(); } catch (Exception e) { this.model = (Model) new ObjectFactory().createModel(); Logger.getLogger("application").l7dlog(Level.ERROR, "modelLazyLoadException", e); } } return super.getModel(); } /** * @param loadModelCallable the loadModelCallable to set */ public void setLoadModelCallable(Callable<Model> loadModelCallable) { this.loadModelCallable = loadModelCallable; } /** * Return the number of pages needed for this origami. * <p> * This method requires the model to be completely loaded. * * @return The number of pages needed for this origami. */ public int getNumberOfPages() { if (pages == null) updatePagesCache(); return pages; } /** * Return the placement of steps on the page as a list of linearized grid positions (sorted by the ordering of steps * on the page). * <p> * This method requires the model to be completely loaded. * * @param page The number of the page (starting from 1). * @return The map of steps placement on the given page. */ public Integer[] getStepsPlacement(int page) { if (stepsOnPages.get(page) == null) updatePagesCache(); return stepsOnPages.get(page); } /** * Return the first step displayed on the given page. * * @param page The page. * @return The step that is first on that page. */ public Step getFirstStep(int page) { if (firstStepOnPages.get(page) == null) updatePagesCache(); return firstStepOnPages.get(page); } /** * Return the number of the page the given step is displayed on. * * @param step The step to search page for. * @return The page number (starting from 1). */ public int getPage(Step step) { if (!getModel().getSteps().getStep().contains(step)) return -1; Entry<Integer, Step> prev = null; for (Entry<Integer, Step> e : firstStepOnPages.entrySet()) { if (step == e.getValue()) return e.getKey(); if (prev != null && prev.getValue().getId() < step.getId() && e.getValue().getId() > step.getId()) return prev.getKey(); prev = e; } return getNumberOfPages(); } /** * Recompute the number of pages and the number of steps to be placed on every page. */ protected void updatePagesCache() { stepsOnPages.clear(); firstStepOnPages.clear(); if (getModel().getSteps().getStep().size() == 0) { pages = 0; return; } // a simple layout algoritm is implemented - try to place the step at cursor, and if it cannot be done, search // first free space - first looking to the right, then maybe going to another line, and finally mabye going to a // brand new page; the fitting is determined using a bitmap final int maxX = getPaper().getCols(), maxY = getPaper().getRows(); int cursorX = 0, cursorY = 0; int pageNr = 1; final List<Integer> stepsPlacement = new LinkedList<Integer>(); final boolean[] map = new boolean[maxX * maxY]; Arrays.fill(map, false); for (Step step : getModel().getSteps().getStep()) { int width = step.getColspan() != null ? step.getColspan() - 1 : 0, height = step.getRowspan() != null ? step .getRowspan() - 1 : 0; boolean fits; do { fits = true; if (cursorX + width > maxX) { fits = false; } else if (cursorY + height > maxY) { fits = false; } else { fit: for (int i = cursorX; i <= cursorX + width; i++) { for (int j = cursorY; j <= cursorY + height; j++) { if (map[i + j * maxX]) { fits = false; break fit; } } } } if (!fits) { if (cursorX + width < maxX - 1) { cursorX++; } else if (cursorY + height < maxY - 1) { cursorY++; cursorX = 0; } else { cursorX = 0; cursorY = 0; stepsOnPages.put(pageNr, stepsPlacement.toArray(new Integer[] {})); stepsPlacement.clear(); stepsPlacement.add(0); pageNr++; Arrays.fill(map, false); for (int i = 0; i <= width; i++) { for (int j = 0; j <= height; j++) { map[i + j * maxX] = true; } } firstStepOnPages.put(pageNr, step); break; } continue; } stepsPlacement.add(cursorX + cursorY * maxX); for (int i = cursorX; i <= cursorX + width; i++) { for (int j = cursorY; j <= cursorY + height; j++) { map[i + j * maxX] = true; } } if (firstStepOnPages.get(pageNr) == null) firstStepOnPages.put(pageNr, step); break; } while (!fits); } if (stepsOnPages.get(pageNr) == null) stepsOnPages.put(pageNr, stepsPlacement.toArray(new Integer[] {})); pages = stepsOnPages.size(); } /** * @return the file */ @XmlTransient public File getFile() { return file; } /** * @param file the file to set */ public void setFile(File file) { this.file = file; } /** * @return the src */ @XmlTransient public URL getSrc() { return src; } /** * Free all the memory held by model state information. */ public void unloadModelStates() { for (Step s : getModel().getSteps().getStep()) s.unloadModelState(); } /** * @param value the new src */ public void setSrc(URL value) { URL oldValue = this.src; this.src = value; if ((oldValue == null && value != null) || (oldValue != null && value == null) || (oldValue != null && value != null && !oldValue.equals(value))) support.firePropertyChange("src", oldValue, value); } /** * Initializes the substructures, so that no structure that may contain other structures will be <code>null</code>. */ public void initStructure(boolean preserveExisting) { ObjectFactory of = new ObjectFactory(); cz.cuni.mff.peckam.java.origamist.common.jaxb.ObjectFactory cof = new cz.cuni.mff.peckam.java.origamist.common.jaxb.ObjectFactory(); if (!preserveExisting || getAuthor() == null) this.setAuthor((Author) cof.createAuthor()); if (!preserveExisting || getLicense() == null) this.setLicense((License) cof.createLicense()); if (!preserveExisting || getThumbnail() == null) this.setThumbnail(cof.createThumbnail()); if (!preserveExisting || getThumbnail().getImage() == null) this.getThumbnail().setImage((BinaryImage) cof.createBinaryImage()); if (!preserveExisting || getModel() == null) this.setModel((Model) of.createModel()); if (!preserveExisting || getModel().getPaper() == null) this.getModel().setPaper((ModelPaper) of.createModelPaper()); if (!preserveExisting || getModel().getPaper().getSize() == null) this.getModel().getPaper().setSize((UnitDimension) of.createUnitDimension()); if (!preserveExisting || getModel().getPaper().getColors() == null) this.getModel().getPaper().setColors(of.createModelColors()); if (!preserveExisting || getModel().getSteps() == null) this.getModel().setSteps((cz.cuni.mff.peckam.java.origamist.model.Steps) of.createSteps()); if (!preserveExisting || getPaper() == null) this.setPaper((DiagramPaper) of.createDiagramPaper()); if (!preserveExisting || getPaper().getColor() == null) this.getPaper().setColor(of.createDiagramColors()); if (!preserveExisting || getPaper().getSize() == null) this.getPaper().setSize((UnitDimension) of.createUnitDimension()); } /** * Set the given origami's metadata to be this origami's metadata. The new metadata are another instance of the * <code>from</code>'s metadata, so it doesn't reflect any further changes to the <code>from</code>'s metadata. * * @param from The origami the metadata should be loaded from. */ public void getMetadataFrom(Origami from) { // reset the metadata to the empty ones initStructure(true); if (!getName().equals(from.getName())) { getName().clear(); for (LangString name : from.getName()) getName().add(name.clone()); } setCreationDate((Date) from.getCreationDate().clone()); if (!getShortdesc().equals(from.getShortdesc())) { getShortdesc().clear(); for (LangString shortDesc : from.getShortdesc()) getShortdesc().add(shortDesc.clone()); } if (!getDescription().equals(from.getDescription())) { getDescription().clear(); for (LangString desc : from.getDescription()) getDescription().add(desc.clone()); } try { setOriginal(new URI(from.getOriginal().toString())); } catch (URISyntaxException e) {} catch (NullPointerException e) {} getAuthor().setName(from.getAuthor().getName()); try { getAuthor().setHomepage(new URI(from.getAuthor().getHomepage().toString())); } catch (URISyntaxException e) {} catch (NullPointerException e) {} getLicense().setContent(from.getLicense().getContent()); try { getLicense().setHomepage(new URI(from.getLicense().getHomepage().toString())); } catch (URISyntaxException e) {} catch (NullPointerException e) {} getLicense().setName(from.getLicense().getName()); if (!getLicense().getPermission().equals(from.getLicense().getPermission())) { getLicense().getPermission().clear(); getLicense().getPermission().addAll(from.getLicense().getPermission()); } getThumbnail().getImage().setType(from.getThumbnail().getImage().getType()); getThumbnail().setGenerated(from.getThumbnail().isGenerated()); if (from.getThumbnail().getImage().getValue() != null && from.getThumbnail().getImage().getValue().length > 0) { byte[] newImage = Arrays.copyOf(from.getThumbnail().getImage().getValue(), from.getThumbnail().getImage() .getValue().length); getThumbnail().getImage().setValue(newImage); } else { getThumbnail().getImage().setValue(null); } getModel().getPaper().setWeight(from.getModel().getPaper().getWeight()); getModel().getPaper().getColors().setBackground(from.getModel().getPaper().getColors().getBackground()); getModel().getPaper().getColors().setForeground(from.getModel().getPaper().getColors().getForeground()); getModel().getPaper().getSize().setWidth(from.getModel().getPaper().getSize().getWidth()); getModel().getPaper().getSize().setHeight(from.getModel().getPaper().getSize().getHeight()); getModel().getPaper().getSize().setUnit(from.getModel().getPaper().getSize().getUnit()); getModel() .getPaper() .getSize() .setReference(from.getModel().getPaper().getSize().getReferenceUnit(), from.getModel().getPaper().getSize().getReferenceLength()); if (!getModel().getPaper().getNote().equals(from.getModel().getPaper().getNote())) { getModel().getPaper().getNote().clear(); for (LangString s : from.getModel().getPaper().getNote()) getModel().getPaper().addNote(s.getLang(), s.getValue()); } getPaper().getColor().setBackground(from.getPaper().getColor().getBackground()); getPaper().setCols(from.getPaper().getCols()); getPaper().setRows(from.getPaper().getRows()); getPaper().getSize().setWidth(from.getPaper().getSize().getWidth()); getPaper().getSize().setHeight(from.getPaper().getSize().getHeight()); getPaper().getSize().setUnit(from.getPaper().getSize().getUnit()); getPaper().getSize().setReference(from.getPaper().getSize().getReferenceUnit(), from.getPaper().getSize().getReferenceLength()); } @Override protected void init() { super.init(); initListeners(); } /** * Create pointers to previous/next steps and setup model state invalidation callbacks. */ public void initListeners() { PropertyChangeListener invalidateListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (getModel() != null && getModel().getSteps() != null) getModel().getSteps().invalidateSteps(); } }; addPrefixedPropertyChangeListener(invalidateListener, MODEL_PROPERTY, Model.PAPER_PROPERTY, ModelPaper.COLORS_PROPERTY); addPrefixedPropertyChangeListener(invalidateListener, MODEL_PROPERTY, Model.PAPER_PROPERTY, ModelPaper.SIZE_PROPERTY); ObservablePropertyListener<Step> defaultStateListener = new ObservablePropertyListener<Step>() { @Override public void changePerformed(ObservablePropertyEvent<? extends Step> evt) { if (evt.getEvent().getItem().getPrevious() == null && (evt.getEvent().getItem().defaultModelState == null || evt.getEvent().getItem().defaultModelState .getOrigami() != Origami.this)) evt.getEvent().getItem().setDefaultModelState(new DefaultModelState(Origami.this)); } }; addObservablePropertyListener(defaultStateListener, MODEL_PROPERTY, Model.STEPS_PROPERTY, Steps.STEP_PROPERTY); PropertyChangeListener defaultStatePropertyListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (getModel() != null && getModel().getSteps() != null && getModel().getSteps().getStep().size() > 0) { Step firstStep = getModel().getSteps().getStep().get(0); if (firstStep.defaultModelState == null || firstStep.defaultModelState.getOrigami() != Origami.this) firstStep.setDefaultModelState(new DefaultModelState(Origami.this)); } } }; addPrefixedPropertyChangeListener(defaultStatePropertyListener, MODEL_PROPERTY); } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((src == null) ? 0 : src.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!super.equals(obj)) return false; if (getClass() != obj.getClass()) return false; Origami other = (Origami) obj; if (src == null) { if (other.src != null) return false; } else if (!src.equals(other.src)) return false; return true; } }