001 //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/graphics/optimizers/LabelOptimizer.java $ 002 /*---------------- FILE HEADER ------------------------------------------ 003 004 This file is part of deegree. 005 Copyright (C) 2001-2008 by: 006 EXSE, Department of Geography, University of Bonn 007 http://www.giub.uni-bonn.de/deegree/ 008 lat/lon GmbH 009 http://www.lat-lon.de 010 011 This library is free software; you can redistribute it and/or 012 modify it under the terms of the GNU Lesser General Public 013 License as published by the Free Software Foundation; either 014 version 2.1 of the License, or (at your option) any later version. 015 016 This library is distributed in the hope that it will be useful, 017 but WITHOUT ANY WARRANTY; without even the implied warranty of 018 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 019 Lesser General Public License for more details. 020 021 You should have received a copy of the GNU Lesser General Public 022 License along with this library; if not, write to the Free Software 023 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 024 025 Contact: 026 027 Andreas Poth 028 lat/lon GmbH 029 Aennchenstr. 19 030 53115 Bonn 031 Germany 032 E-Mail: poth@lat-lon.de 033 034 Prof. Dr. Klaus Greve 035 Department of Geography 036 University of Bonn 037 Meckenheimer Allee 166 038 53115 Bonn 039 Germany 040 E-Mail: greve@giub.uni-bonn.de 041 042 ---------------------------------------------------------------------------*/ 043 package org.deegree.graphics.optimizers; 044 045 import java.awt.Graphics2D; 046 import java.util.ArrayList; 047 import java.util.List; 048 049 import org.deegree.framework.log.ILogger; 050 import org.deegree.framework.log.LoggerFactory; 051 import org.deegree.graphics.Theme; 052 import org.deegree.graphics.displayelements.DisplayElement; 053 import org.deegree.graphics.displayelements.Label; 054 import org.deegree.graphics.displayelements.LabelDisplayElement; 055 import org.deegree.graphics.displayelements.ScaledFeature; 056 import org.deegree.graphics.sld.TextSymbolizer; 057 import org.deegree.graphics.transformation.GeoTransform; 058 059 /** 060 * Selects optimized <tt>Label</tt>s (graphical representations generated from 061 * <tt>LabelDisplayElements</tt>) that have a minimimal amount of overlapping. 062 * <p> 063 * The labeling and optimization approach uses ideas from papers by Ingo Petzold on automated label 064 * placement. 065 * <p> 066 * TODO: The handling of rotated labels is currently broken. Don't use rotated 067 * <tt>LabelDisplayElement</tt>s with this optimizer at the moment! 068 * <p> 069 * 070 * @author <a href="mailto:mschneider@lat-lon.de">Markus Schneider</a> 071 * @version $Revision: 9340 $ $Date: 2007-12-27 13:32:12 +0100 (Do, 27 Dez 2007) $ 072 */ 073 public class LabelOptimizer extends AbstractOptimizer { 074 075 private static final ILogger LOG = LoggerFactory.getLogger( LabelOptimizer.class ); 076 077 // contains the LabelDisplayElements that are to be optimized 078 private ArrayList<DisplayElement> displayElements = new ArrayList<DisplayElement>( 1000 ); 079 080 // contains the LabelChoices 081 private ArrayList<LabelChoice> choices = new ArrayList<LabelChoice>( 1000 ); 082 083 // collision matrix of LabelChoices that may collide 084 private boolean[][] candidates; 085 086 /** 087 * Creates a new instance of LabelOptimizer. 088 */ 089 public LabelOptimizer() { 090 } 091 092 /** 093 * Creates a new instance of LabelOptimizer for the given <tt>Themes</tt>. 094 * 095 * @param themes 096 */ 097 public LabelOptimizer( Theme[] themes ) { 098 099 // collect all LabelDisplayElements from all Themes 100 for ( int i = 0; i < themes.length; i++ ) { 101 addTheme( themes[i] ); 102 } 103 } 104 105 /** 106 * Adds a <tt>Theme</tt> that the <tt>Optimizer</tt> should consider. 107 * 108 * @param theme 109 */ 110 public void addTheme( Theme theme ) { 111 if ( !themes.contains( theme ) ) { 112 List<DisplayElement> themeElements = theme.getDisplayElements(); 113 for ( int i = 0; i < themeElements.size(); i++ ) { 114 Object o = themeElements.get( i ); 115 if ( o instanceof LabelDisplayElement ) { 116 LabelDisplayElement element = (LabelDisplayElement) o; 117 TextSymbolizer symbolizer = (TextSymbolizer) element.getSymbolizer(); 118 // only add element if "auto" is set 119 if ( symbolizer.getLabelPlacement() != null ) { 120 if ( symbolizer.getLabelPlacement().getPointPlacement() != null 121 && symbolizer.getLabelPlacement().getPointPlacement().isAuto() ) { 122 displayElements.add( (LabelDisplayElement) o ); 123 // } else if (symbolizer.getLabelPlacement().getLinePlacement() != null) 124 // { 125 // displayElements.add (o); 126 } 127 } 128 } 129 } 130 themes.add( theme ); 131 } 132 } 133 134 /** 135 * Finds optimized <tt>Label</tt> representations for the registered 136 * <tt>LabelDisplayElement</tt>s. 137 * <p> 138 * 139 * @param g 140 */ 141 public void optimize( Graphics2D g ) 142 throws Exception { 143 144 choices.clear(); 145 double scale = mapView.getScale( g ); 146 GeoTransform projection = mapView.getProjection(); 147 148 // used to signal the LabelDisplayElement that it should 149 // not create Labels itself (in any case) 150 Label[] dummyLabels = new Label[0]; 151 152 // collect LabelChoices for all LabelDisplayElements 153 for ( int i = 0; i < displayElements.size(); i++ ) { 154 LabelDisplayElement element = (LabelDisplayElement) displayElements.get( i ); 155 if ( !element.doesScaleConstraintApply( scale ) ) 156 continue; 157 158 element.setLabels( dummyLabels ); 159 ((ScaledFeature)element.getFeature()).setScale( scale / 0.00028 ); 160 choices.addAll( LabelChoiceFactory.createLabelChoices( element, g, projection ) ); 161 } 162 163 buildCollisionMatrix(); 164 165 // do the magic 166 try { 167 anneal(); 168 } catch ( Exception e ) { 169 e.printStackTrace(); 170 } 171 172 // sets the optimized labels to the LabelDisplayElements, so 173 // they are considered when the DisplayElements are painted the next 174 // time 175 176 for ( int i = 0; i < choices.size(); i++ ) { 177 LabelChoice choice = choices.get( i ); 178 choice.getElement().addLabel( choice.getSelectedLabel() ); 179 } 180 } 181 182 /** 183 * Builds the collision matrix for all <tt>LabelChoice</tt>s. 184 */ 185 private void buildCollisionMatrix() { 186 long now = System.currentTimeMillis(); 187 candidates = new boolean[choices.size()][choices.size()]; 188 for ( int i = 0; i < choices.size(); i++ ) { 189 LabelChoice choice1 = choices.get( i ); 190 for ( int j = i + 1; j < choices.size(); j++ ) { 191 LabelChoice choice2 = choices.get( j ); 192 if ( choice1.intersects( choice2 ) ) { 193 candidates[i][j] = true; 194 } 195 } 196 } 197 LOG.logDebug( "Building of collision matrix took: " + ( System.currentTimeMillis() - now ) + " millis." ); 198 } 199 200 /** 201 * Performs the "Simulated Annealing" for the <tt>LabelChoices</tt>. 202 */ 203 private void anneal() { 204 double currentValue = objectiveFunction(); 205 206 double temperature = 1.0; 207 int counter = 0; 208 int successCounter = 0; 209 int failCounter = 0; 210 211 int n = choices.size(); 212 213 LOG.logDebug( "Starting Annealing with value: " + currentValue ); 214 long now = System.currentTimeMillis(); 215 while ( counter <= 2500 && currentValue > ( n + 0.8 * 40 ) ) { 216 217 counter++; 218 if ( successCounter % 5 == 0 ) { 219 temperature *= 0.9; 220 } 221 222 // choose one Label from one LabelChoice randomly 223 int choiceIndex = (int) ( Math.random() * ( n - 1 ) + 0.5 ); 224 LabelChoice choice = choices.get( choiceIndex ); 225 int oldPos = choice.getSelected(); 226 choice.selectLabelRandomly(); 227 228 double value = objectiveFunction(); 229 230 // does the new placement imply an improvement? 231 if ( value < currentValue ) { 232 // yes -> keep it 233 currentValue = value; 234 successCounter++; 235 failCounter = 0; 236 } else { 237 // no -> only keep it with a certain probability 238 if ( Math.random() < temperature ) { 239 currentValue = value; 240 failCounter = 0; 241 } else { 242 // change it back to the old placement 243 choice.setSelected( oldPos ); 244 failCounter++; 245 } 246 } 247 } 248 LOG.logDebug( "Final value: " + currentValue ); 249 LOG.logDebug( "Annealing took: " + ( System.currentTimeMillis() - now ) + " millis." ); 250 } 251 252 /** 253 * Calculates a quality value for the currently selected combination of <tt>Label</tt>s. 254 * <p> 255 * 256 * @return 257 */ 258 private double objectiveFunction() { 259 float value = 0.0f; 260 261 for ( int i = 0; i < choices.size(); i++ ) { 262 LabelChoice choice1 = choices.get( i ); 263 Label label1 = choice1.getSelectedLabel(); 264 value += choice1.getQuality() + 1.0f; 265 266 for ( int j = i + 1; j < choices.size(); j++ ) { 267 if ( candidates[i][j] ) { 268 LabelChoice choice2 = choices.get( j ); 269 Label label2 = choice2.getSelectedLabel(); 270 if ( label1.intersects( label2 ) ) { 271 value += 40.0f; 272 } 273 } 274 } 275 } 276 return value; 277 } 278 }