001    //$HeadURL: http://svn.wald.intevation.org/svn/deegree/base/trunk/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: 29952 $ $Date: 2011-03-09 13:14:53 +0100 (Wed, 09 Mar 2011) $
085     */
086    public class LabelFactory {
087    
088        private static final ILogger LOG = getLogger( LabelFactory.class );
089    
090    
091        /**
092         * @param caption
093         * @param font
094         * @param color
095         * @param metrics
096         * @param feature
097         * @param halo
098         * @param x
099         * @param y
100         * @param w
101         * @param h
102         * @param rotation
103         * @param anchorPointX
104         * @param anchorPointY
105         * @param displacementX
106         * @param displacementY
107         * @param opacity
108         * @return label representations
109         */
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, double opacity ) {
113            if ( rotation == 0.0 ) {
114                return new HorizontalLabel( caption, font, color, metrics, feature, halo, x, y, w, h,
115                                            new double[] { anchorPointX, anchorPointY }, new double[] { displacementX,
116                                                                                                       displacementY },
117                                            opacity );
118            }
119            return new RotatedLabel( caption, font, color, metrics, feature, halo, x, y, w, h, rotation,
120                                     new double[] { anchorPointX, anchorPointY }, new double[] { displacementX,
121                                                                                                 displacementY }, opacity );
122    
123        }
124    
125        /**
126         * @param caption
127         * @param font
128         * @param color
129         * @param metrics
130         * @param feature
131         * @param halo
132         * @param x
133         * @param y
134         * @param w
135         * @param h
136         * @param rotation
137         * @param anchorPoint
138         * @param displacement
139         * @param opacity
140         * @return the label
141         */
142        public static Label createLabel( String caption, Font font, Color color, LineMetrics metrics, Feature feature,
143                                         Halo halo, int x, int y, int w, int h, double rotation, double[] anchorPoint,
144                                         double[] displacement, double opacity ) {
145            if ( rotation == 0.0 ) {
146                return new HorizontalLabel( caption, font, color, metrics, feature, halo, x, y, w, h, anchorPoint,
147                                            displacement, opacity );
148            }
149            return new RotatedLabel( caption, font, color, metrics, feature, halo, x, y, w, h, rotation, anchorPoint,
150                                     displacement, opacity );
151    
152        }
153    
154        /**
155         * Generates label-representations for a given <tt>LabelDisplayElement</tt>.
156         * <p>
157         *
158         * @param element
159         * @param projection
160         * @param g
161         * @return label-representations
162         * @throws Exception
163         */
164        public static Label[] createLabels( LabelDisplayElement element, GeoTransform projection, Graphics2D g )
165                                throws Exception {
166    
167            Label[] labels = new Label[0];
168            Feature feature = element.getFeature();
169            String caption = element.getLabel().evaluate( feature );
170    
171            // sanity check: empty labels are ignored
172            if ( caption == null || caption.trim().equals( "" ) ) {
173                return labels;
174            }
175    
176            Geometry geometry = element.getGeometry();
177            TextSymbolizer symbolizer = (TextSymbolizer) element.getSymbolizer();
178    
179            // gather font information
180            org.deegree.graphics.sld.Font sldFont = symbolizer.getFont();
181            java.awt.Font font = new java.awt.Font( sldFont.getFamily( feature ), sldFont.getStyle( feature )
182                                                                                  | sldFont.getWeight( feature ),
183                                                    sldFont.getSize( feature ) );
184            g.setFont( font );
185    
186            // the bboxing is not a good solution, the geometry should be used instead (rectangle or
187            // something)
188            // on the other hand, we'd need a flag to symbolize "use the surface as bounding box"
189            // anyway, so... TODO
190            ParameterValueType[] bbox = symbolizer.getBoundingBox();
191            if ( bbox != null ) {
192                try {
193                    double x = parseDouble( bbox[0].evaluate( feature ) );
194                    double y = parseDouble( bbox[1].evaluate( feature ) );
195                    double maxx = parseDouble( bbox[2].evaluate( feature ) );
196                    double maxy = parseDouble( bbox[3].evaluate( feature ) );
197                    double opacity = symbolizer.getFill() == null ? 1 : symbolizer.getFill().getOpacity( feature );
198                    return new Label[] { createLabelInABox( caption, font, sldFont.getColor( feature ),
199                                                            symbolizer.getHalo(), x, y, maxx - x, maxy - y, feature,
200                                                            projection, opacity ) };
201                } catch ( NumberFormatException nfe ) {
202                    LOG.logWarning( "Could not render text because of missing bbox attributes." );
203                    return labels; // empty array
204                }
205            }
206    
207            FontRenderContext frc = g.getFontRenderContext();
208            Rectangle2D bounds = font.getStringBounds( caption, frc );
209            LineMetrics metrics = font.getLineMetrics( caption, frc );
210            int w = (int) bounds.getWidth();
211            int h = (int) bounds.getHeight();
212            // int descent = (int) metrics.getDescent ();
213    
214            if ( geometry instanceof Point || geometry instanceof MultiPoint ) {
215    
216                // get screen coordinates
217                int[] coords = calcScreenCoordinates( projection, geometry );
218                int x = coords[0];
219                int y = coords[1];
220    
221                // default placement information
222                double rotation = 0.0;
223                double[] anchorPoint = { 0.0, 0.5 };
224                double[] displacement = { 0.0, 0.0 };
225    
226                // use placement information from SLD
227                LabelPlacement lPlacement = symbolizer.getLabelPlacement();
228    
229                if ( lPlacement != null ) {
230                    PointPlacement pPlacement = lPlacement.getPointPlacement();
231                    anchorPoint = pPlacement.getAnchorPoint( feature );
232                    displacement = pPlacement.getDisplacement( feature );
233                    rotation = pPlacement.getRotation( feature );
234                }
235    
236                double opacity = symbolizer.getFill() == null ? 1 : symbolizer.getFill().getOpacity( feature );
237                labels = new Label[] { createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
238                                                    symbolizer.getHalo(), x, y, w, h, rotation, anchorPoint, displacement,
239                                                    opacity ) };
240            } else if ( geometry instanceof Surface || geometry instanceof MultiSurface ) {
241    
242                // get screen coordinates
243                int[] coords = calcScreenCoordinates( projection, geometry );
244                int x = coords[0];
245                int y = coords[1];
246    
247                // default placement information
248                double rotation = 0.0;
249                double[] anchorPoint = { 0.5, 0.5 };
250                double[] displacement = { 0.0, 0.0 };
251    
252                // use placement information from SLD
253                LabelPlacement lPlacement = symbolizer.getLabelPlacement();
254    
255                if ( lPlacement != null ) {
256                    PointPlacement pPlacement = lPlacement.getPointPlacement();
257    
258                    // check if the label is to be centered within the intersection
259                    // of
260                    // the screen surface and the polygon geometry
261                    if ( pPlacement.isAuto() ) {
262                        Surface screenSurface = GeometryFactory.createSurface( projection.getSourceRect(), null );
263                        Geometry intersection = screenSurface.intersection( geometry );
264                        if ( intersection != null ) {
265                            Position source = intersection.getCentroid().getPosition();
266                            x = (int) ( projection.getDestX( source.getX() ) + 0.5 );
267                            y = (int) ( projection.getDestY( source.getY() ) + 0.5 );
268                        }
269                    }
270                    anchorPoint = pPlacement.getAnchorPoint( feature );
271                    displacement = pPlacement.getDisplacement( feature );
272                    rotation = pPlacement.getRotation( feature );
273                }
274    
275                double opacity = symbolizer.getFill() == null ? 1 : symbolizer.getFill().getOpacity( feature );
276                labels = new Label[] { createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
277                                                    symbolizer.getHalo(), x, y, w, h, rotation, anchorPoint, displacement,
278                                                    opacity )
279    
280                };
281            } else if ( geometry instanceof Curve || geometry instanceof MultiCurve ) {
282                Surface screenSurface = GeometryFactory.createSurface( projection.getSourceRect(), null );
283                Geometry intersection = screenSurface.intersection( geometry );
284                if ( intersection != null ) {
285                    List<Label> list = null;
286                    if ( intersection instanceof Curve ) {
287                        list = createLabels( (Curve) intersection, element, g, projection );
288                    } else if ( intersection instanceof MultiCurve ) {
289                        list = createLabels( (MultiCurve) intersection, element, g, projection );
290                    } else {
291                        throw new Exception( "Intersection produced unexpected " + "geometry type: '"
292                                             + intersection.getClass().getName() + "'!" );
293                    }
294                    labels = new Label[list.size()];
295                    for ( int i = 0; i < labels.length; i++ ) {
296                        Label label = list.get( i );
297                        labels[i] = label;
298                    }
299                }
300            } else {
301                throw new Exception( "LabelFactory does not implement generation " + "of Labels from geometries of type: '"
302                                     + geometry.getClass().getName() + "'!" );
303            }
304            return labels;
305        }
306    
307        /**
308         * Determines positions on the given <tt>MultiCurve</tt> where a caption could be drawn. For each of this positons,
309         * three candidates are produced; one on the line, one above of it and one below.
310         * <p>
311         *
312         * @param multiCurve
313         * @param element
314         * @param g
315         * @param projection
316         * @return ArrayList containing Arrays of Label-objects
317         * @throws FilterEvaluationException
318         */
319        public static List<Label> createLabels( MultiCurve multiCurve, LabelDisplayElement element, Graphics2D g,
320                                                GeoTransform projection )
321                                throws FilterEvaluationException {
322    
323            List<Label> placements = Collections.synchronizedList( new ArrayList<Label>( 10 ) );
324            for ( int i = 0; i < multiCurve.getSize(); i++ ) {
325                Curve curve = multiCurve.getCurveAt( i );
326                placements.addAll( createLabels( curve, element, g, projection ) );
327            }
328            return placements;
329        }
330    
331        /**
332         * Determines positions on the given <tt>Curve</tt> where a caption could be drawn. For each of this positons, three
333         * candidates are produced; one on the line, one above of it and one below.
334         * <p>
335         *
336         * @param curve
337         * @param element
338         * @param g
339         * @param projection
340         * @return ArrayList containing Arrays of Label-objects
341         * @throws FilterEvaluationException
342         */
343        public static ArrayList<Label> createLabels( Curve curve, LabelDisplayElement element, Graphics2D g,
344                                                     GeoTransform projection )
345                                throws FilterEvaluationException {
346    
347            Feature feature = element.getFeature();
348    
349            // determine the placement type and parameters from the TextSymbolizer
350            double perpendicularOffset = 0.0;
351            int placementType = LinePlacement.TYPE_ABSOLUTE;
352            double lineWidth = 3.0;
353            int gap = 6;
354            TextSymbolizer symbolizer = ( (TextSymbolizer) element.getSymbolizer() );
355            if ( symbolizer.getLabelPlacement() != null ) {
356                LinePlacement linePlacement = symbolizer.getLabelPlacement().getLinePlacement();
357                if ( linePlacement != null ) {
358                    placementType = linePlacement.getPlacementType( feature );
359                    perpendicularOffset = linePlacement.getPerpendicularOffset( feature );
360                    lineWidth = linePlacement.getLineWidth( feature );
361                    gap = linePlacement.getGap( feature );
362                }
363            }
364    
365            // get width & height of the caption
366            String caption = element.getLabel().evaluate( feature );
367            org.deegree.graphics.sld.Font sldFont = symbolizer.getFont();
368            java.awt.Font font = new java.awt.Font( sldFont.getFamily( feature ), sldFont.getStyle( feature )
369                                                                                  | sldFont.getWeight( feature ),
370                                                    sldFont.getSize( feature ) );
371            g.setFont( font );
372            FontRenderContext frc = g.getFontRenderContext();
373            Rectangle2D bounds = font.getStringBounds( caption, frc );
374            LineMetrics metrics = font.getLineMetrics( caption, frc );
375            double width = bounds.getWidth();
376            double height = bounds.getHeight();
377    
378            // get screen coordinates of the line
379            int[][] pos = calcScreenCoordinates( projection, curve );
380    
381            // ideal distance from the line
382            double delta = height / 2.0 + lineWidth / 2.0;
383    
384            // walk along the linestring and "collect" possible placement positions
385            int w = (int) width;
386            int lastX = pos[0][0];
387            int lastY = pos[1][0];
388            int count = pos[2][0];
389            int boxStartX = lastX;
390            int boxStartY = lastY;
391    
392            ArrayList<Label> labels = new ArrayList<Label>( 100 );
393            List<int[]> eCandidates = Collections.synchronizedList( new ArrayList<int[]>( 100 ) );
394            int i = 1;
395            int kk = 0;
396            while ( i < count && kk < 100 ) {
397                kk++;
398                int x = pos[0][i];
399                int y = pos[1][i];
400    
401                // segment found where endpoint of box should be located?
402                if ( getDistance( boxStartX, boxStartY, x, y ) >= w ) {
403    
404                    double offx = 0, offy = 0;
405    
406                    if ( abs( perpendicularOffset ) > 1E-10 ) {
407                        double dx = lastX - x;
408                        double dy = lastY - y;
409    
410                        double len = sqrt( dx * dx + dy * dy );
411                        dx /= len;
412                        dy /= len;
413    
414                        offx += perpendicularOffset * dy;
415                        offy += -perpendicularOffset * dx;
416                    }
417    
418                    int[] p0 = new int[] { boxStartX, boxStartY };
419                    int[] p1 = new int[] { lastX, lastY };
420                    int[] p2 = new int[] { x, y };
421    
422                    int[] p = findPointWithDistance( p0, p1, p2, w );
423                    x = p[0];
424                    y = p[1];
425    
426                    lastX = x;
427                    lastY = y;
428                    int boxEndX = x;
429                    int boxEndY = y;
430    
431                    // does the linesegment run from right to left?
432                    if ( x <= boxStartX ) {
433                        boxEndX = boxStartX;
434                        boxEndY = boxStartY;
435                        boxStartX = x;
436                        boxStartY = y;
437                        x = boxEndX;
438                        y = boxEndY;
439                    }
440    
441                    double rotation = getRotation( boxStartX, boxStartY, x, y );
442                    double[] deviation = calcDeviation( new int[] { boxStartX, boxStartY }, new int[] { boxEndX, boxEndY },
443                                                        eCandidates );
444    
445                    Label label = null;
446    
447                    boxStartX += offx;
448                    boxStartY += offy;
449                    boxEndX += offx;
450                    boxEndY += offy;
451    
452                    double opacity = symbolizer.getFill() == null ? 1 : symbolizer.getFill().getOpacity( feature );
453    
454                    switch ( placementType ) {
455                    case LinePlacement.TYPE_ABSOLUTE: {
456                        label = createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
457                                             symbolizer.getHalo(), boxStartX, boxStartY, (int) width, (int) height,
458                                             rotation, 0.0, 0.5, ( w - width ) / 2, 0, opacity );
459                        break;
460                    }
461                    case LinePlacement.TYPE_ABOVE: {
462                        label = createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
463                                             symbolizer.getHalo(), boxStartX, boxStartY, (int) width, (int) height,
464                                             rotation, 0.0, 0.5, ( w - width ) / 2, delta + deviation[0], opacity );
465                        break;
466                    }
467                    case LinePlacement.TYPE_BELOW:
468                    case LinePlacement.TYPE_AUTO: {
469                        label = createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
470                                             symbolizer.getHalo(), boxStartX, boxStartY, (int) width, (int) height,
471                                             rotation, 0.0, 0.5, ( w - width ) / 2, -delta - deviation[1], opacity );
472                        break;
473                    }
474                    case LinePlacement.TYPE_CENTER: {
475                        label = createLabel( caption, font, sldFont.getColor( feature ), metrics, feature,
476                                             symbolizer.getHalo(), boxStartX, boxStartY, (int) width, (int) height,
477                                             rotation, 0.0, 0.5, ( w - width ) / 2, 0.0, opacity );
478                        break;
479                    }
480                    default: {
481                        // nothing?
482                    }
483                    }
484                    labels.add( label );
485                    boxStartX = lastX;
486                    boxStartY = lastY;
487                    eCandidates.clear();
488                } else {
489                    eCandidates.add( new int[] { x, y } );
490                    lastX = x;
491                    lastY = y;
492                    i++;
493                }
494            }
495    
496            // pick lists of boxes on the linestring
497            ArrayList<Label> pick = new ArrayList<Label>( 100 );
498            int n = labels.size();
499            for ( int j = n / 2; j < labels.size(); j += ( gap + 1 ) ) {
500                pick.add( labels.get( j ) );
501            }
502            for ( int j = n / 2 - ( gap + 1 ); j > 0; j -= ( gap + 1 ) ) {
503                pick.add( labels.get( j ) );
504            }
505            return pick;
506        }
507    
508        /**
509         * Calculates the maximum deviation that points on a linestring have to the ideal line between the starting point
510         * and the end point.
511         * <p>
512         * The ideal line is thought to be running from left to right, the left deviation value generally is above the line,
513         * the right value is below.
514         * <p>
515         *
516         * @param start
517         *            starting point of the linestring
518         * @param end
519         *            end point of the linestring
520         * @param points
521         *            points in between
522         * @return maximum deviation
523         */
524        public static double[] calcDeviation( int[] start, int[] end, List<int[]> points ) {
525    
526            // extreme deviation to the left
527            double d1 = 0.0;
528            // extreme deviation to the right
529            double d2 = 0.0;
530            Iterator<int[]> it = points.iterator();
531    
532            // eventually swap start and end point
533            if ( start[0] > end[0] ) {
534                int[] tmp = start;
535                start = end;
536                end = tmp;
537            }
538    
539            if ( start[0] != end[0] ) {
540                // label orientation is not completly vertical
541                if ( start[1] != end[1] ) {
542                    // label orientation is not completly horizontal
543                    while ( it.hasNext() ) {
544                        int[] point = it.next();
545                        double u = ( (double) end[1] - (double) start[1] ) / ( (double) end[0] - (double) start[0] );
546                        double x = ( u * u * start[0] - u * ( (double) start[1] - (double) point[1] ) + point[0] )
547                                   / ( 1.0 + u * u );
548                        double y = ( x - start[0] ) * u + start[1];
549                        double d = getDistance( point, new int[] { (int) ( x + 0.5 ), (int) ( y + 0.5 ) } );
550                        if ( y >= point[1] ) {
551                            // candidate for left extreme value
552                            if ( d > d1 ) {
553                                d1 = d;
554                            }
555                        } else if ( d > d2 ) {
556                            // candidate for right extreme value
557                            d2 = d;
558                        }
559                    }
560                } else {
561                    // label orientation is completly horizontal
562                    while ( it.hasNext() ) {
563                        int[] point = it.next();
564                        double d = point[1] - start[1];
565                        if ( d < 0 ) {
566                            // candidate for left extreme value
567                            if ( -d > d1 ) {
568                                d1 = -d;
569                            }
570                        } else if ( d > d2 ) {
571                            // candidate for left extreme value
572                            d2 = d;
573                        }
574                    }
575                }
576            } else {
577                // label orientation is completly vertical
578                while ( it.hasNext() ) {
579                    int[] point = it.next();
580                    double d = point[0] - start[0];
581                    if ( d < 0 ) {
582                        // candidate for left extreme value
583                        if ( -d > d1 ) {
584                            d1 = -d;
585                        }
586                    } else if ( d > d2 ) {
587                        // candidate for right extreme value
588                        d2 = d;
589                    }
590                }
591            }
592            return new double[] { d1, d2 };
593        }
594    
595        /**
596         * Finds a point on the line between p1 and p2 that has a certain distance from point p0 (provided that there is
597         * such a point).
598         * <p>
599         *
600         * @param p0
601         *            point that is used as reference point for the distance
602         * @param p1
603         *            starting point of the line
604         * @param p2
605         *            end point of the line
606         * @param d
607         *            distance
608         * @return point on the line between p1 and p2 that has a certain distance from point p0
609         */
610        public static int[] findPointWithDistance( int[] p0, int[] p1, int[] p2, int d ) {
611    
612            double x, y;
613            double x0 = p0[0];
614            double y0 = p0[1];
615            double x1 = p1[0];
616            double y1 = p1[1];
617            double x2 = p2[0];
618            double y2 = p2[1];
619    
620            if ( x1 != x2 ) {
621                // line segment does not run vertical
622                double u = ( y2 - y1 ) / ( x2 - x1 );
623                double p = -2 * ( x0 + u * u * x1 - u * ( y1 - y0 ) ) / ( u * u + 1 );
624                double q = ( ( y1 - y0 ) * ( y1 - y0 ) + u * u * x1 * x1 + x0 * x0 - 2 * u * x1 * ( y1 - y0 ) - d * d )
625                           / ( u * u + 1 );
626                double minX = x1;
627                double maxX = x2;
628                double minY = y1;
629                double maxY = y2;
630                if ( minX > maxX ) {
631                    minX = x2;
632                    maxX = x1;
633                }
634                if ( minY > maxY ) {
635                    minY = y2;
636                    maxY = y1;
637                }
638                x = -p / 2 - Math.sqrt( ( p / 2 ) * ( p / 2 ) - q );
639                if ( x < minX || x > maxX ) {
640                    x = -p / 2 + Math.sqrt( ( p / 2 ) * ( p / 2 ) - q );
641                }
642                y = ( x - x1 ) * u + y1;
643            } else {
644                // vertical line segment
645                x = x1;
646                double minY = y1;
647                double maxY = y2;
648    
649                if ( minY > maxY ) {
650                    minY = y2;
651                    maxY = y1;
652                }
653    
654                double p = -2 * y0;
655                double q = y0 * y0 + ( x1 - x0 ) * ( x1 - x0 ) - d * d;
656    
657                y = -p / 2 - Math.sqrt( ( p / 2 ) * ( p / 2 ) - q );
658                if ( y < minY || y > maxY ) {
659                    y = -p / 2 + Math.sqrt( ( p / 2 ) * ( p / 2 ) - q );
660                }
661            }
662            return new int[] { (int) ( x + 0.5 ), (int) ( y + 0.5 ) };
663        }
664    
665        /**
666         * @param text
667         * @param font
668         * @param color
669         * @param halo
670         * @param x
671         * @param y
672         * @param w
673         * @param h
674         * @param feature
675         * @param projection
676         * @param opacity
677         * @return the label
678         */
679        public static HorizontalLabel createLabelInABox( String text, Font font, Color color, Halo halo, double x,
680                                                         double y, double w, double h, Feature feature,
681                                                         GeoTransform projection, double opacity ) {
682    
683            // one could probably get a significant speedup if no new fonts would be created (ie, use
684            // another method to find out the text extent of a given string and font size)
685            FontRenderContext frc = new FontRenderContext( null, false, false );
686            font = font.deriveFont( 128f ); // TODO hardcoded max
687            Rectangle2D rect = font.getStringBounds( text, frc );
688            int ix = (int) projection.getDestX( x );
689            int iy = (int) projection.getDestY( y );
690            int iw = abs( ( (int) projection.getDestX( x + w ) ) - ix );
691            int ih = abs( ( (int) projection.getDestY( y + h ) ) - iy );
692            while ( rect.getWidth() > iw && rect.getHeight() > ih && font.getSize2D() > 5 ) {
693                font = font.deriveFont( font.getSize2D() - 4 );
694                rect = font.getStringBounds( text, frc );
695            }
696    
697            LOG.logDebug( "Determined font size from bounding box", font.getSize2D() );
698    
699            return new HorizontalLabel( text, font, color, font.getLineMetrics( text, frc ), feature, halo, ix, iy, iw, ih,
700                                        new double[] { 0, 0 }, new double[] { 0, 0 }, opacity );
701        }
702    
703        /**
704         * @param x1
705         * @param y1
706         * @param x2
707         * @param y2
708         * @return rotation (degree) of the line between two passed coordinates
709         */
710        public static double getRotation( double x1, double y1, double x2, double y2 ) {
711            double dx = x2 - x1;
712            double dy = y2 - y1;
713    
714            return Math.toDegrees( Math.atan( dy / dx ) );
715        }
716    
717        /**
718         * @param p1
719         * @param p2
720         * @return distance between two passed coordinates
721         */
722        public static double getDistance( int[] p1, int[] p2 ) {
723            double dx = p1[0] - p2[0];
724            double dy = p1[1] - p2[1];
725            return Math.sqrt( dx * dx + dy * dy );
726        }
727    
728        /**
729         * @param x1
730         * @param y1
731         * @param x2
732         * @param y2
733         * @return distance between two passed coordinates
734         */
735        public static double getDistance( double x1, double y1, double x2, double y2 ) {
736            double dx = x2 - x1;
737            double dy = y2 - y1;
738            return Math.sqrt( dx * dx + dy * dy );
739        }
740    
741        /**
742         * Calculates the screen coordinates of the given <tt>Curve</tt>. physical screen coordinates
743         *
744         * @param projection
745         * @param curve
746         * @return the coordinates
747         */
748        public static int[][] calcScreenCoordinates( GeoTransform projection, Curve curve ) {
749    
750            LineString lineString = null;
751            try {
752                lineString = curve.getAsLineString();
753            } catch ( GeometryException e ) {
754                // ignored, assumed to be valid
755            }
756    
757            int count = lineString.getNumberOfPoints();
758            int[][] pos = new int[3][];
759            pos[0] = new int[count];
760            pos[1] = new int[count];
761            pos[2] = new int[1];
762    
763            int k = 0;
764            for ( int i = 0; i < count; i++ ) {
765                Position position = lineString.getPositionAt( i );
766                double tx = projection.getDestX( position.getX() );
767                double ty = projection.getDestY( position.getY() );
768                if ( i > 0 ) {
769                    if ( getDistance( tx, ty, pos[0][k - 1], pos[1][k - 1] ) > 1 ) {
770                        pos[0][k] = (int) ( tx + 0.5 );
771                        pos[1][k] = (int) ( ty + 0.5 );
772                        k++;
773                    }
774                } else {
775                    pos[0][k] = (int) ( tx + 0.5 );
776                    pos[1][k] = (int) ( ty + 0.5 );
777                    k++;
778                }
779            }
780            pos[2][0] = k;
781    
782            return pos;
783        }
784    
785        /**
786         * Returns the physical (screen) coordinates.
787         *
788         * @param projection
789         * @param geometry
790         *
791         * @return physical screen coordinates
792         */
793        public static int[] calcScreenCoordinates( GeoTransform projection, Geometry geometry ) {
794    
795            int[] coords = new int[2];
796    
797            Position source = null;
798            if ( geometry instanceof Point ) {
799                source = ( (Point) geometry ).getPosition();
800            } else if ( geometry instanceof Curve || geometry instanceof MultiCurve ) {
801                source = geometry.getCentroid().getPosition();
802            } else {
803                source = geometry.getCentroid().getPosition();
804            }
805    
806            coords[0] = (int) ( projection.getDestX( source.getX() ) + 0.5 );
807            coords[1] = (int) ( projection.getDestY( source.getY() ) + 0.5 );
808            return coords;
809        }
810    }