001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/graphics/displayelements/LabelFactory.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005       Department of Geography, University of Bonn
006     and
007       lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035    ----------------------------------------------------------------------------*/
036    package org.deegree.graphics.displayelements;
037    
038    import static java.lang.Double.parseDouble;
039    import static java.lang.Math.abs;
040    import static java.lang.Math.sqrt;
041    import static org.deegree.framework.log.LoggerFactory.getLogger;
042    
043    import java.awt.Color;
044    import java.awt.Font;
045    import java.awt.Graphics2D;
046    import java.awt.font.FontRenderContext;
047    import java.awt.font.LineMetrics;
048    import java.awt.geom.Rectangle2D;
049    import java.util.ArrayList;
050    import java.util.Collections;
051    import java.util.Iterator;
052    import java.util.List;
053    
054    import org.deegree.framework.log.ILogger;
055    import org.deegree.graphics.sld.Halo;
056    import org.deegree.graphics.sld.LabelPlacement;
057    import org.deegree.graphics.sld.LinePlacement;
058    import org.deegree.graphics.sld.ParameterValueType;
059    import org.deegree.graphics.sld.PointPlacement;
060    import org.deegree.graphics.sld.TextSymbolizer;
061    import org.deegree.graphics.transformation.GeoTransform;
062    import org.deegree.model.feature.Feature;
063    import org.deegree.model.filterencoding.FilterEvaluationException;
064    import org.deegree.model.spatialschema.Curve;
065    import org.deegree.model.spatialschema.Geometry;
066    import org.deegree.model.spatialschema.GeometryException;
067    import org.deegree.model.spatialschema.GeometryFactory;
068    import org.deegree.model.spatialschema.LineString;
069    import org.deegree.model.spatialschema.MultiCurve;
070    import org.deegree.model.spatialschema.MultiPoint;
071    import org.deegree.model.spatialschema.MultiSurface;
072    import org.deegree.model.spatialschema.Point;
073    import org.deegree.model.spatialschema.Position;
074    import org.deegree.model.spatialschema.Surface;
075    
076    /**
077     * Does the labeling, i.e. creates (screen) <tt>Label</tt> representations from <tt>LabelDisplayElement</tt>s.
078     * <p>
079     * Different geometry-types (of the LabelDisplayElement) imply different strategies concerning the way the
080     * <tt>Labels</tt> are generated.
081     * <p>
082     *
083     * @author <a href="mailto:mschneider@lat-lon.de">Markus Schneider </a>
084     * @version $Revision: 18195 $ $Date: 2009-06-18 17:55:39 +0200 (Do, 18. Jun 2009) $
085     */
086    public class LabelFactory {
087    
088        private static final ILogger LOG = getLogger( LabelFactory.class );
089    
090        /**
091         * @param caption
092         * @param font
093         * @param color
094         * @param metrics
095         * @param feature
096         * @param halo
097         * @param x
098         * @param y
099         * @param w
100         * @param h
101         * @param rotation
102         * @param anchorPointX
103         * @param anchorPointY
104         * @param displacementX
105         * @param displacementY
106         * @return label-representations
107         * @deprecated use the one with opacity instead
108         */
109        @Deprecated
110        public static Label createLabel( String caption, Font font, Color color, LineMetrics metrics, Feature feature,
111                                         Halo halo, int x, int y, int w, int h, double rotation, double anchorPointX,
112                                         double anchorPointY, double displacementX, double displacementY ) {
113            return createLabel( caption, font, color, metrics, feature, halo, x, y, w, h, rotation, anchorPointX,
114                                anchorPointY, displacementX, displacementY, 1.0 );
115        }
116    
117        /**
118         * @param caption
119         * @param font
120         * @param color
121         * @param metrics
122         * @param feature
123         * @param halo
124         * @param x
125         * @param y
126         * @param w
127         * @param h
128         * @param rotation
129         * @param anchorPointX
130         * @param anchorPointY
131         * @param displacementX
132         * @param displacementY
133         * @param opacity
134         * @return label representations
135         */
136        public static Label createLabel( String caption, Font font, Color color, LineMetrics metrics, Feature feature,
137                                         Halo halo, int x, int y, int w, int h, double rotation, double anchorPointX,
138                                         double anchorPointY, double displacementX, double displacementY, double opacity ) {
139            if ( rotation == 0.0 ) {
140                return new HorizontalLabel( caption, font, color, metrics, feature, halo, x, y, w, h,
141                                            new double[] { anchorPointX, anchorPointY }, new double[] { displacementX,
142                                                                                                       displacementY },
143                                            opacity );
144            }
145            return new RotatedLabel( caption, font, color, metrics, feature, halo, x, y, w, h, rotation,
146                                     new double[] { anchorPointX, anchorPointY }, opacity );
147    
148        }
149    
150        /**
151         * @param caption
152         * @param font
153         * @param color
154         * @param metrics
155         * @param feature
156         * @param halo
157         * @param x
158         * @param y
159         * @param w
160         * @param h
161         * @param rotation
162         * @param anchorPoint
163         * @param displacement
164         * @param opacity
165         * @return the label
166         */
167        public static Label createLabel( String caption, Font font, Color color, LineMetrics metrics, Feature feature,
168                                         Halo halo, int x, int y, int w, int h, double rotation, double[] anchorPoint,
169                                         double[] displacement, double opacity ) {
170            if ( rotation == 0.0 ) {
171                return new HorizontalLabel( caption, font, color, metrics, feature, halo, x, y, w, h, anchorPoint,
172                                            displacement, opacity );
173            }
174            return new RotatedLabel( caption, font, color, metrics, feature, halo, x, y, w, h, rotation, anchorPoint,
175                                     opacity );
176    
177        }
178    
179        /**
180         * @param caption
181         * @param font
182         * @param color
183         * @param metrics
184         * @param feature
185         * @param halo
186         * @param x
187         * @param y
188         * @param w
189         * @param h
190         * @param rotation
191         * @param anchorPoint
192         * @param displacement
193         * @return label-representations
194         * @deprecated use the one with opacity instead
195         */
196        @Deprecated
197        public static Label createLabel( String caption, Font font, Color color, LineMetrics metrics, Feature feature,
198                                         Halo halo, int x, int y, int w, int h, double rotation, double[] anchorPoint,
199                                         double[] displacement ) {
200            return createLabel( caption, font, color, metrics, feature, halo, x, y, w, h, rotation, anchorPoint,
201                                displacement, 1.0 );
202        }
203    
204        /**
205         * Generates label-representations for a given <tt>LabelDisplayElement</tt>.
206         * <p>
207         *
208         * @param element
209         * @param projection
210         * @param g
211         * @return label-representations
212         * @throws Exception
213         */
214        public static Label[] createLabels( LabelDisplayElement element, GeoTransform projection, Graphics2D g )
215                                throws Exception {
216    
217            Label[] labels = new Label[0];
218            Feature feature = element.getFeature();
219            String caption = element.getLabel().evaluate( feature );
220    
221            // sanity check: empty labels are ignored
222            if ( caption == null || caption.trim().equals( "" ) ) {
223                return labels;
224            }
225    
226            Geometry geometry = element.getGeometry();
227            TextSymbolizer symbolizer = (TextSymbolizer) element.getSymbolizer();
228    
229            // gather font information
230            org.deegree.graphics.sld.Font sldFont = symbolizer.getFont();
231            java.awt.Font font = new java.awt.Font( sldFont.getFamily( feature ), sldFont.getStyle( feature )
232                                                                                  | sldFont.getWeight( feature ),
233                                                    sldFont.getSize( feature ) );
234            g.setFont( font );
235    
236            // the bboxing is not a good solution, the geometry should be used instead (rectangle or
237            // something)
238            // on the other hand, we'd need a flag to symbolize "use the surface as bounding box"
239            // anyway, so... TODO
240            ParameterValueType[] bbox = symbolizer.getBoundingBox();
241            if ( bbox != null ) {
242                try {
243                    double x = parseDouble( bbox[0].evaluate( feature ) );
244                    double y = parseDouble( bbox[1].evaluate( feature ) );
245                    double maxx = parseDouble( bbox[2].evaluate( feature ) );
246                    double maxy = parseDouble( bbox[3].evaluate( feature ) );
247                    double opacity = symbolizer.getFill() == null ? 1 : symbolizer.getFill().getOpacity( feature );
248                    return new Label[] { createLabelInABox( caption, font, sldFont.getColor( feature ),
249                                                            symbolizer.getHalo(), x, y, maxx - x, maxy - y, feature,
250                                                            projection, opacity ) };
251                } catch ( NumberFormatException nfe ) {
252                    LOG.logWarning( "Could not render text because of missing bbox attributes." );
253                    return labels; // empty array
254                }
255            }
256    
257            FontRenderContext frc = g.getFontRenderContext();
258            Rectangle2D bounds = font.getStringBounds( caption, frc );
259            LineMetrics metrics = font.getLineMetrics( caption, frc );
260            int w = (int) bounds.getWidth();
261            int h = (int) bounds.getHeight();
262            // int descent = (int) metrics.getDescent ();
263    
264            if ( geometry instanceof Point || geometry instanceof MultiPoint ) {
265    
266                // get screen coordinates
267                int[] coords = calcScreenCoordinates( projection, geometry );
268                int x = coords[0];
269                int y = coords[1];
270    
271                // default placement information
272                double rotation = 0.0;
273                double[] anchorPoint = { 0.0, 0.5 };
274                double[] displacement = { 0.0, 0.0 };
275    
276                // use placement information from SLD
277                LabelPlacement lPlacement = symbolizer.getLabelPlacement();
278    
279                if ( lPlacement != null ) {
280                    PointPlacement pPlacement = lPlacement.getPointPlacement();
281                    anchorPoint = pPlacement.getAnchorPoint( feature );
282                    displacement = pPlacement.getDisplacement( feature );
283                    rotation = pPlacement.getRotation( feature );
284                }
285    
286                double opacity = symbolizer.getFill() == null ? 1 : symbolizer.getFill().getOpacity( feature );
287                labels = new Label[] { createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
288                                                    symbolizer.getHalo(), x, y, w, h, rotation, anchorPoint, displacement,
289                                                    opacity ) };
290            } else if ( geometry instanceof Surface || geometry instanceof MultiSurface ) {
291    
292                // get screen coordinates
293                int[] coords = calcScreenCoordinates( projection, geometry );
294                int x = coords[0];
295                int y = coords[1];
296    
297                // default placement information
298                double rotation = 0.0;
299                double[] anchorPoint = { 0.5, 0.5 };
300                double[] displacement = { 0.0, 0.0 };
301    
302                // use placement information from SLD
303                LabelPlacement lPlacement = symbolizer.getLabelPlacement();
304    
305                if ( lPlacement != null ) {
306                    PointPlacement pPlacement = lPlacement.getPointPlacement();
307    
308                    // check if the label is to be centered within the intersection
309                    // of
310                    // the screen surface and the polygon geometry
311                    if ( pPlacement.isAuto() ) {
312                        Surface screenSurface = GeometryFactory.createSurface( projection.getSourceRect(), null );
313                        Geometry intersection = screenSurface.intersection( geometry );
314                        if ( intersection != null ) {
315                            Position source = intersection.getCentroid().getPosition();
316                            x = (int) ( projection.getDestX( source.getX() ) + 0.5 );
317                            y = (int) ( projection.getDestY( source.getY() ) + 0.5 );
318                        }
319                    }
320                    anchorPoint = pPlacement.getAnchorPoint( feature );
321                    displacement = pPlacement.getDisplacement( feature );
322                    rotation = pPlacement.getRotation( feature );
323                }
324    
325                double opacity = symbolizer.getFill() == null ? 1 : symbolizer.getFill().getOpacity( feature );
326                labels = new Label[] { createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
327                                                    symbolizer.getHalo(), x, y, w, h, rotation, anchorPoint, displacement,
328                                                    opacity )
329    
330                };
331            } else if ( geometry instanceof Curve || geometry instanceof MultiCurve ) {
332                Surface screenSurface = GeometryFactory.createSurface( projection.getSourceRect(), null );
333                Geometry intersection = screenSurface.intersection( geometry );
334                if ( intersection != null ) {
335                    List<Label> list = null;
336                    if ( intersection instanceof Curve ) {
337                        list = createLabels( (Curve) intersection, element, g, projection );
338                    } else if ( intersection instanceof MultiCurve ) {
339                        list = createLabels( (MultiCurve) intersection, element, g, projection );
340                    } else {
341                        throw new Exception( "Intersection produced unexpected " + "geometry type: '"
342                                             + intersection.getClass().getName() + "'!" );
343                    }
344                    labels = new Label[list.size()];
345                    for ( int i = 0; i < labels.length; i++ ) {
346                        Label label = list.get( i );
347                        labels[i] = label;
348                    }
349                }
350            } else {
351                throw new Exception( "LabelFactory does not implement generation " + "of Labels from geometries of type: '"
352                                     + geometry.getClass().getName() + "'!" );
353            }
354            return labels;
355        }
356    
357        /**
358         * Determines positions on the given <tt>MultiCurve</tt> where a caption could be drawn. For each of this positons,
359         * three candidates are produced; one on the line, one above of it and one below.
360         * <p>
361         *
362         * @param multiCurve
363         * @param element
364         * @param g
365         * @param projection
366         * @return ArrayList containing Arrays of Label-objects
367         * @throws FilterEvaluationException
368         */
369        public static List<Label> createLabels( MultiCurve multiCurve, LabelDisplayElement element, Graphics2D g,
370                                                GeoTransform projection )
371                                throws FilterEvaluationException {
372    
373            List<Label> placements = Collections.synchronizedList( new ArrayList<Label>( 10 ) );
374            for ( int i = 0; i < multiCurve.getSize(); i++ ) {
375                Curve curve = multiCurve.getCurveAt( i );
376                placements.addAll( createLabels( curve, element, g, projection ) );
377            }
378            return placements;
379        }
380    
381        /**
382         * Determines positions on the given <tt>Curve</tt> where a caption could be drawn. For each of this positons, three
383         * candidates are produced; one on the line, one above of it and one below.
384         * <p>
385         *
386         * @param curve
387         * @param element
388         * @param g
389         * @param projection
390         * @return ArrayList containing Arrays of Label-objects
391         * @throws FilterEvaluationException
392         */
393        public static ArrayList<Label> createLabels( Curve curve, LabelDisplayElement element, Graphics2D g,
394                                                     GeoTransform projection )
395                                throws FilterEvaluationException {
396    
397            Feature feature = element.getFeature();
398    
399            // determine the placement type and parameters from the TextSymbolizer
400            double perpendicularOffset = 0.0;
401            int placementType = LinePlacement.TYPE_ABSOLUTE;
402            double lineWidth = 3.0;
403            int gap = 6;
404            TextSymbolizer symbolizer = ( (TextSymbolizer) element.getSymbolizer() );
405            if ( symbolizer.getLabelPlacement() != null ) {
406                LinePlacement linePlacement = symbolizer.getLabelPlacement().getLinePlacement();
407                if ( linePlacement != null ) {
408                    placementType = linePlacement.getPlacementType( feature );
409                    perpendicularOffset = linePlacement.getPerpendicularOffset( feature );
410                    lineWidth = linePlacement.getLineWidth( feature );
411                    gap = linePlacement.getGap( feature );
412                }
413            }
414    
415            // get width & height of the caption
416            String caption = element.getLabel().evaluate( feature );
417            org.deegree.graphics.sld.Font sldFont = symbolizer.getFont();
418            java.awt.Font font = new java.awt.Font( sldFont.getFamily( feature ), sldFont.getStyle( feature )
419                                                                                  | sldFont.getWeight( feature ),
420                                                    sldFont.getSize( feature ) );
421            g.setFont( font );
422            FontRenderContext frc = g.getFontRenderContext();
423            Rectangle2D bounds = font.getStringBounds( caption, frc );
424            LineMetrics metrics = font.getLineMetrics( caption, frc );
425            double width = bounds.getWidth();
426            double height = bounds.getHeight();
427    
428            // get screen coordinates of the line
429            int[][] pos = calcScreenCoordinates( projection, curve );
430    
431            // ideal distance from the line
432            double delta = height / 2.0 + lineWidth / 2.0;
433    
434            // walk along the linestring and "collect" possible placement positions
435            int w = (int) width;
436            int lastX = pos[0][0];
437            int lastY = pos[1][0];
438            int count = pos[2][0];
439            int boxStartX = lastX;
440            int boxStartY = lastY;
441    
442            ArrayList<Label> labels = new ArrayList<Label>( 100 );
443            List<int[]> eCandidates = Collections.synchronizedList( new ArrayList<int[]>( 100 ) );
444            int i = 1;
445            int kk = 0;
446            while ( i < count && kk < 100 ) {
447                kk++;
448                int x = pos[0][i];
449                int y = pos[1][i];
450    
451                // segment found where endpoint of box should be located?
452                if ( getDistance( boxStartX, boxStartY, x, y ) >= w ) {
453    
454                    double offx = 0, offy = 0;
455    
456                    if ( abs( perpendicularOffset ) > 1E-10 ) {
457                        double dx = lastX - x;
458                        double dy = lastY - y;
459    
460                        double len = sqrt( dx * dx + dy * dy );
461                        dx /= len;
462                        dy /= len;
463    
464                        offx += perpendicularOffset * dy;
465                        offy += -perpendicularOffset * dx;
466                    }
467    
468                    int[] p0 = new int[] { boxStartX, boxStartY };
469                    int[] p1 = new int[] { lastX, lastY };
470                    int[] p2 = new int[] { x, y };
471    
472                    int[] p = findPointWithDistance( p0, p1, p2, w );
473                    x = p[0];
474                    y = p[1];
475    
476                    lastX = x;
477                    lastY = y;
478                    int boxEndX = x;
479                    int boxEndY = y;
480    
481                    // does the linesegment run from right to left?
482                    if ( x <= boxStartX ) {
483                        boxEndX = boxStartX;
484                        boxEndY = boxStartY;
485                        boxStartX = x;
486                        boxStartY = y;
487                        x = boxEndX;
488                        y = boxEndY;
489                    }
490    
491                    double rotation = getRotation( boxStartX, boxStartY, x, y );
492                    double[] deviation = calcDeviation( new int[] { boxStartX, boxStartY }, new int[] { boxEndX, boxEndY },
493                                                        eCandidates );
494    
495                    Label label = null;
496    
497                    boxStartX += offx;
498                    boxStartY += offy;
499                    boxEndX += offx;
500                    boxEndY += offy;
501    
502                    double opacity = symbolizer.getFill() == null ? 1 : symbolizer.getFill().getOpacity( feature );
503    
504                    switch ( placementType ) {
505                    case LinePlacement.TYPE_ABSOLUTE: {
506                        label = createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
507                                             symbolizer.getHalo(), boxStartX, boxStartY, (int) width, (int) height,
508                                             rotation, 0.0, 0.5, ( w - width ) / 2, 0, opacity );
509                        break;
510                    }
511                    case LinePlacement.TYPE_ABOVE: {
512                        label = createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
513                                             symbolizer.getHalo(), boxStartX, boxStartY, (int) width, (int) height,
514                                             rotation, 0.0, 0.5, ( w - width ) / 2, delta + deviation[0], opacity );
515                        break;
516                    }
517                    case LinePlacement.TYPE_BELOW:
518                    case LinePlacement.TYPE_AUTO: {
519                        label = createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
520                                             symbolizer.getHalo(), boxStartX, boxStartY, (int) width, (int) height,
521                                             rotation, 0.0, 0.5, ( w - width ) / 2, -delta - deviation[1], opacity );
522                        break;
523                    }
524                    case LinePlacement.TYPE_CENTER: {
525                        label = createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
526                                             symbolizer.getHalo(), boxStartX, boxStartY, (int) width, (int) height,
527                                             rotation, 0.0, 0.5, ( w - width ) / 2, 0.0, opacity );
528                        break;
529                    }
530                    default: {
531                        // nothing?
532                    }
533                    }
534                    labels.add( label );
535                    boxStartX = lastX;
536                    boxStartY = lastY;
537                    eCandidates.clear();
538                } else {
539                    eCandidates.add( new int[] { x, y } );
540                    lastX = x;
541                    lastY = y;
542                    i++;
543                }
544            }
545    
546            // pick lists of boxes on the linestring
547            ArrayList<Label> pick = new ArrayList<Label>( 100 );
548            int n = labels.size();
549            for ( int j = n / 2; j < labels.size(); j += ( gap + 1 ) ) {
550                pick.add( labels.get( j ) );
551            }
552            for ( int j = n / 2 - ( gap + 1 ); j > 0; j -= ( gap + 1 ) ) {
553                pick.add( labels.get( j ) );
554            }
555            return pick;
556        }
557    
558        /**
559         * Calculates the maximum deviation that points on a linestring have to the ideal line between the starting point
560         * and the end point.
561         * <p>
562         * The ideal line is thought to be running from left to right, the left deviation value generally is above the line,
563         * the right value is below.
564         * <p>
565         *
566         * @param start
567         *            starting point of the linestring
568         * @param end
569         *            end point of the linestring
570         * @param points
571         *            points in between
572         * @return maximum deviation
573         */
574        public static double[] calcDeviation( int[] start, int[] end, List<int[]> points ) {
575    
576            // extreme deviation to the left
577            double d1 = 0.0;
578            // extreme deviation to the right
579            double d2 = 0.0;
580            Iterator<int[]> it = points.iterator();
581    
582            // eventually swap start and end point
583            if ( start[0] > end[0] ) {
584                int[] tmp = start;
585                start = end;
586                end = tmp;
587            }
588    
589            if ( start[0] != end[0] ) {
590                // label orientation is not completly vertical
591                if ( start[1] != end[1] ) {
592                    // label orientation is not completly horizontal
593                    while ( it.hasNext() ) {
594                        int[] point = it.next();
595                        double u = ( (double) end[1] - (double) start[1] ) / ( (double) end[0] - (double) start[0] );
596                        double x = ( u * u * start[0] - u * ( (double) start[1] - (double) point[1] ) + point[0] )
597                                   / ( 1.0 + u * u );
598                        double y = ( x - start[0] ) * u + start[1];
599                        double d = getDistance( point, new int[] { (int) ( x + 0.5 ), (int) ( y + 0.5 ) } );
600                        if ( y >= point[1] ) {
601                            // candidate for left extreme value
602                            if ( d > d1 ) {
603                                d1 = d;
604                            }
605                        } else if ( d > d2 ) {
606                            // candidate for right extreme value
607                            d2 = d;
608                        }
609                    }
610                } else {
611                    // label orientation is completly horizontal
612                    while ( it.hasNext() ) {
613                        int[] point = it.next();
614                        double d = point[1] - start[1];
615                        if ( d < 0 ) {
616                            // candidate for left extreme value
617                            if ( -d > d1 ) {
618                                d1 = -d;
619                            }
620                        } else if ( d > d2 ) {
621                            // candidate for left extreme value
622                            d2 = d;
623                        }
624                    }
625                }
626            } else {
627                // label orientation is completly vertical
628                while ( it.hasNext() ) {
629                    int[] point = it.next();
630                    double d = point[0] - start[0];
631                    if ( d < 0 ) {
632                        // candidate for left extreme value
633                        if ( -d > d1 ) {
634                            d1 = -d;
635                        }
636                    } else if ( d > d2 ) {
637                        // candidate for right extreme value
638                        d2 = d;
639                    }
640                }
641            }
642            return new double[] { d1, d2 };
643        }
644    
645        /**
646         * Finds a point on the line between p1 and p2 that has a certain distance from point p0 (provided that there is
647         * such a point).
648         * <p>
649         *
650         * @param p0
651         *            point that is used as reference point for the distance
652         * @param p1
653         *            starting point of the line
654         * @param p2
655         *            end point of the line
656         * @param d
657         *            distance
658         * @return point on the line between p1 and p2 that has a certain distance from point p0
659         */
660        public static int[] findPointWithDistance( int[] p0, int[] p1, int[] p2, int d ) {
661    
662            double x, y;
663            double x0 = p0[0];
664            double y0 = p0[1];
665            double x1 = p1[0];
666            double y1 = p1[1];
667            double x2 = p2[0];
668            double y2 = p2[1];
669    
670            if ( x1 != x2 ) {
671                // line segment does not run vertical
672                double u = ( y2 - y1 ) / ( x2 - x1 );
673                double p = -2 * ( x0 + u * u * x1 - u * ( y1 - y0 ) ) / ( u * u + 1 );
674                double q = ( ( y1 - y0 ) * ( y1 - y0 ) + u * u * x1 * x1 + x0 * x0 - 2 * u * x1 * ( y1 - y0 ) - d * d )
675                           / ( u * u + 1 );
676                double minX = x1;
677                double maxX = x2;
678                double minY = y1;
679                double maxY = y2;
680                if ( minX > maxX ) {
681                    minX = x2;
682                    maxX = x1;
683                }
684                if ( minY > maxY ) {
685                    minY = y2;
686                    maxY = y1;
687                }
688                x = -p / 2 - Math.sqrt( ( p / 2 ) * ( p / 2 ) - q );
689                if ( x < minX || x > maxX ) {
690                    x = -p / 2 + Math.sqrt( ( p / 2 ) * ( p / 2 ) - q );
691                }
692                y = ( x - x1 ) * u + y1;
693            } else {
694                // vertical line segment
695                x = x1;
696                double minY = y1;
697                double maxY = y2;
698    
699                if ( minY > maxY ) {
700                    minY = y2;
701                    maxY = y1;
702                }
703    
704                double p = -2 * y0;
705                double q = y0 * y0 + ( x1 - x0 ) * ( x1 - x0 ) - d * d;
706    
707                y = -p / 2 - Math.sqrt( ( p / 2 ) * ( p / 2 ) - q );
708                if ( y < minY || y > maxY ) {
709                    y = -p / 2 + Math.sqrt( ( p / 2 ) * ( p / 2 ) - q );
710                }
711            }
712            return new int[] { (int) ( x + 0.5 ), (int) ( y + 0.5 ) };
713        }
714    
715        /**
716         * Creates a label that fits in the specified box.
717         *
718         * @param text
719         * @param font
720         * @param color
721         * @param halo
722         * @param x
723         * @param y
724         * @param w
725         * @param h
726         * @param feature
727         * @param projection
728         * @return the label
729         * @deprecated use the one with opacity instead
730         */
731        @Deprecated
732        public static HorizontalLabel createLabelInABox( String text, Font font, Color color, Halo halo, double x,
733                                                         double y, double w, double h, Feature feature,
734                                                         GeoTransform projection ) {
735            return createLabelInABox( text, font, color, halo, x, y, w, h, feature, projection, 1.0 );
736        }
737    
738        /**
739         * @param text
740         * @param font
741         * @param color
742         * @param halo
743         * @param x
744         * @param y
745         * @param w
746         * @param h
747         * @param feature
748         * @param projection
749         * @param opacity
750         * @return the label
751         */
752        public static HorizontalLabel createLabelInABox( String text, Font font, Color color, Halo halo, double x,
753                                                         double y, double w, double h, Feature feature,
754                                                         GeoTransform projection, double opacity ) {
755    
756            // one could probably get a significant speedup if no new fonts would be created (ie, use
757            // another method to find out the text extent of a given string and font size)
758            FontRenderContext frc = new FontRenderContext( null, false, false );
759            font = font.deriveFont( 128f ); // TODO hardcoded max
760            Rectangle2D rect = font.getStringBounds( text, frc );
761            int ix = (int) projection.getDestX( x );
762            int iy = (int) projection.getDestY( y );
763            int iw = abs( ( (int) projection.getDestX( x + w ) ) - ix );
764            int ih = abs( ( (int) projection.getDestY( y + h ) ) - iy );
765            while ( rect.getWidth() > iw && rect.getHeight() > ih && font.getSize2D() > 5 ) {
766                font = font.deriveFont( font.getSize2D() - 4 );
767                rect = font.getStringBounds( text, frc );
768            }
769    
770            LOG.logDebug( "Determined font size from bounding box", font.getSize2D() );
771    
772            return new HorizontalLabel( text, font, color, font.getLineMetrics( text, frc ), feature, halo, ix, iy, iw, ih,
773                                        new double[] { 0, 0 }, new double[] { 0, 0 }, opacity );
774        }
775    
776        /**
777         * @param x1
778         * @param y1
779         * @param x2
780         * @param y2
781         * @return rotation (degree) of the line between two passed coordinates
782         */
783        public static double getRotation( double x1, double y1, double x2, double y2 ) {
784            double dx = x2 - x1;
785            double dy = y2 - y1;
786    
787            return Math.toDegrees( Math.atan( dy / dx ) );
788        }
789    
790        /**
791         * @param p1
792         * @param p2
793         * @return distance between two passed coordinates
794         */
795        public static double getDistance( int[] p1, int[] p2 ) {
796            double dx = p1[0] - p2[0];
797            double dy = p1[1] - p2[1];
798            return Math.sqrt( dx * dx + dy * dy );
799        }
800    
801        /**
802         * @param x1
803         * @param y1
804         * @param x2
805         * @param y2
806         * @return distance between two passed coordinates
807         */
808        public static double getDistance( double x1, double y1, double x2, double y2 ) {
809            double dx = x2 - x1;
810            double dy = y2 - y1;
811            return Math.sqrt( dx * dx + dy * dy );
812        }
813    
814        /**
815         * Calculates the screen coordinates of the given <tt>Curve</tt>. physical screen coordinates
816         *
817         * @param projection
818         * @param curve
819         * @return the coordinates
820         */
821        public static int[][] calcScreenCoordinates( GeoTransform projection, Curve curve ) {
822    
823            LineString lineString = null;
824            try {
825                lineString = curve.getAsLineString();
826            } catch ( GeometryException e ) {
827                // ignored, assumed to be valid
828            }
829    
830            int count = lineString.getNumberOfPoints();
831            int[][] pos = new int[3][];
832            pos[0] = new int[count];
833            pos[1] = new int[count];
834            pos[2] = new int[1];
835    
836            int k = 0;
837            for ( int i = 0; i < count; i++ ) {
838                Position position = lineString.getPositionAt( i );
839                double tx = projection.getDestX( position.getX() );
840                double ty = projection.getDestY( position.getY() );
841                if ( i > 0 ) {
842                    if ( getDistance( tx, ty, pos[0][k - 1], pos[1][k - 1] ) > 1 ) {
843                        pos[0][k] = (int) ( tx + 0.5 );
844                        pos[1][k] = (int) ( ty + 0.5 );
845                        k++;
846                    }
847                } else {
848                    pos[0][k] = (int) ( tx + 0.5 );
849                    pos[1][k] = (int) ( ty + 0.5 );
850                    k++;
851                }
852            }
853            pos[2][0] = k;
854    
855            return pos;
856        }
857    
858        /**
859         * Returns the physical (screen) coordinates.
860         *
861         * @param projection
862         * @param geometry
863         *
864         * @return physical screen coordinates
865         */
866        public static int[] calcScreenCoordinates( GeoTransform projection, Geometry geometry ) {
867    
868            int[] coords = new int[2];
869    
870            Position source = null;
871            if ( geometry instanceof Point ) {
872                source = ( (Point) geometry ).getPosition();
873            } else if ( geometry instanceof Curve || geometry instanceof MultiCurve ) {
874                source = geometry.getCentroid().getPosition();
875            } else {
876                source = geometry.getCentroid().getPosition();
877            }
878    
879            coords[0] = (int) ( projection.getDestX( source.getX() ) + 0.5 );
880            coords[1] = (int) ( projection.getDestY( source.getY() ) + 0.5 );
881            return coords;
882        }
883    }