/*---------------- FILE HEADER ------------------------------------------
This file is part of deegree.
Copyright (C) 2001-2006 by:
EXSE, Department of Geography, University of Bonn
http://www.giub.uni-bonn.de/deegree/
lat/lon GmbH
http://www.lat-lon.de
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Contact:
Andreas Poth
lat/lon GmbH
Aennchenstr. 19
53115 Bonn
Germany
E-Mail: poth@lat-lon.de
Prof. Dr. Klaus Greve
Department of Geography
University of Bonn
Meckenheimer Allee 166
53115 Bonn
Germany
E-Mail: greve@giub.uni-bonn.de
---------------------------------------------------------------------------*/
package org.deegree.graphics.optimizers;
import java.awt.Graphics2D;
import java.util.ArrayList;
import org.deegree.framework.log.ILogger;
import org.deegree.framework.log.LoggerFactory;
import org.deegree.graphics.Theme;
import org.deegree.graphics.displayelements.Label;
import org.deegree.graphics.displayelements.LabelDisplayElement;
import org.deegree.graphics.sld.TextSymbolizer;
import org.deegree.graphics.transformation.GeoTransform;
/**
* Selects optimized <tt>Label</tt>s (graphical representations generated
* from <tt>LabelDisplayElements</tt>) that have a minimimal amount of
* overlapping.
* <p>
* The labeling and optimization approach uses ideas from papers by
* Ingo Petzold on automated label placement.
* <p>
* TODO: The handling of rotated labels is currently broken. Don't use
* rotated <tt>LabelDisplayElement</tt>s with this optimizer at the
* moment!
* <p>
* @author <a href="mailto:mschneider@lat-lon.de">Markus Schneider</a>
* @version $Revision: 1.9 $ $Date: 2006/07/04 18:31:05 $
*/
public class LabelOptimizer extends AbstractOptimizer {
private static final ILogger LOG = LoggerFactory.getLogger( LabelOptimizer.class );
// contains the LabelDisplayElements that are to be optimized
private ArrayList displayElements = new ArrayList (1000);
// contains the LabelChoices
private ArrayList choices = new ArrayList (1000);
// collision matrix of LabelChoices that may collide
private boolean [] [] candidates;
/**
* Creates a new instance of LabelOptimizer.
*/
public LabelOptimizer () {}
/**
* Creates a new instance of LabelOptimizer for the given <tt>Themes</tt>.
* @param themes
*/
public LabelOptimizer (Theme [] themes) {
// collect all LabelDisplayElements from all Themes
for (int i = 0; i < themes.length; i++) {
addTheme (themes [i]);
}
}
/**
* Adds a <tt>Theme</tt> that the <tt>Optimizer</tt> should consider.
* @param theme
*/
public void addTheme (Theme theme) {
if (!themes.contains (theme)) {
ArrayList themeElements = theme.getDisplayElements ();
for (int i = 0; i < themeElements.size(); i++) {
Object o = themeElements.get(i);
if (o instanceof LabelDisplayElement) {
LabelDisplayElement element = (LabelDisplayElement) o;
TextSymbolizer symbolizer = (TextSymbolizer) element.getSymbolizer ();
// only add element if "auto" is set
if ( symbolizer.getLabelPlacement() != null ) {
if (symbolizer.getLabelPlacement().getPointPlacement() != null &&
symbolizer.getLabelPlacement().getPointPlacement().isAuto()) {
displayElements.add (o);
// } else if (symbolizer.getLabelPlacement().getLinePlacement() != null) {
// displayElements.add (o);
}
}
}
}
themes.add (theme);
}
}
/**
* Finds optimized <tt>Label</tt> representations for the registered
* <tt>LabelDisplayElement</tt>s.
* <p>
* @param g
*/
public void optimize (Graphics2D g) throws Exception {
choices.clear ();
double scale = mapView.getScale( g );
GeoTransform projection = mapView.getProjection();
// used to signal the LabelDisplayElement that it should
// not create Labels itself (in any case)
Label [] dummyLabels = new Label [0];
// collect LabelChoices for all LabelDisplayElements
for (int i = 0; i < displayElements.size (); i++) {
LabelDisplayElement element = (LabelDisplayElement) displayElements.get (i);
if (!element.doesScaleConstraintApply( scale ) ) continue;
element.setLabels (dummyLabels);
choices.addAll (LabelChoiceFactory.createLabelChoices(element, g, projection));
}
buildCollisionMatrix ();
// do the magic
try {
anneal ();
} catch (Exception e) {
e.printStackTrace();
}
// sets the optimized labels to the LabelDisplayElements, so
// they are considered when the DisplayElements are painted the next
// time
for (int i = 0; i < choices.size(); i++) {
LabelChoice choice = (LabelChoice)choices.get( i );
choice.getElement ().addLabel (choice.getSelectedLabel ());
}
}
/**
* Builds the collision matrix for all <tt>LabelChoice</tt>s.
*/
private void buildCollisionMatrix () {
long now = System.currentTimeMillis();
candidates = new boolean [choices.size ()] [choices.size ()];
for (int i = 0; i < choices.size (); i++) {
LabelChoice choice1 = (LabelChoice) choices.get (i);
for (int j = i + 1; j < choices.size (); j++) {
LabelChoice choice2 = (LabelChoice) choices.get (j);
if (choice1.intersects (choice2)) {
candidates [i] [j] = true;
}
}
}
LOG.logDebug( "Building of collision matrix took: " + (System.currentTimeMillis() - now) +
" millis.");
}
/**
* Performs the "Simulated Annealing" for the <tt>LabelChoices</tt>.
*/
private void anneal () {
double currentValue = objectiveFunction ();
double temperature = 1.0;
int counter = 0;
int successCounter = 0;
int failCounter = 0;
int n = choices.size ();
LOG.logDebug( "Starting Annealing with value: " + currentValue );
long now = System.currentTimeMillis();
while (counter <= 2500 && currentValue > (n + 0.8 * 40) ) {
counter++;
if (successCounter % 5 == 0) {
temperature *= 0.9;
}
// choose one Label from one LabelChoice randomly
int choiceIndex = (int) (Math.random () * (n - 1) + 0.5);
LabelChoice choice = (LabelChoice) choices.get (choiceIndex);
int oldPos = choice.getSelected ();
choice.selectLabelRandomly ();
double value = objectiveFunction ();
// does the new placement imply an improvement?
if (value < currentValue) {
// yes -> keep it
currentValue = value;
successCounter++;
failCounter = 0;
} else {
// no -> only keep it with a certain probability
if (Math.random () < temperature) {
currentValue = value;
failCounter = 0;
} else {
// change it back to the old placement
choice.setSelected (oldPos);
failCounter++;
}
}
}
LOG.logDebug( "Final value: " + currentValue);
LOG.logDebug( "Annealing took: " + (System.currentTimeMillis() - now) + " millis.");
}
/**
* Calculates a quality value for the currently selected combination
* of <tt>Label</tt>s.
* <p>
* @return
*/
private double objectiveFunction () {
float value = 0.0f;
for (int i = 0; i < choices.size (); i++) {
LabelChoice choice1 = (LabelChoice) choices.get (i);
Label label1 = choice1.getSelectedLabel ();
value += choice1.getQuality () + 1.0f;
for (int j = i + 1; j < choices.size (); j++) {
if (candidates [i] [j]) {
LabelChoice choice2 = (LabelChoice) choices.get (j);
Label label2 = choice2.getSelectedLabel ();
if (label1.intersects (label2)) {
value += 40.0f;
}
}
}
}
return value;
}
}
/* ********************************************************************
Changes to this class. What the people have been up to:
$Log: LabelOptimizer.java,v $
Revision 1.9 2006/07/04 18:31:05 poth
avoid NPEs if no centroid can be calculated
********************************************************************** */