Skip to content

Commit

Permalink
Merge pull request #1423 from gritGmbH/enhancement/graphic-stroke-wit…
Browse files Browse the repository at this point in the history
…h-point-199-1204

Optimization of vendor-specific rendering of a graphic on a line
  • Loading branch information
copierrj authored Nov 16, 2022
2 parents de33f5f + 8ce350a commit ac2832b
Show file tree
Hide file tree
Showing 14 changed files with 1,529 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Occam Labs UG (haftungsbeschränkt)
import static java.awt.BasicStroke.JOIN_BEVEL;
import static java.awt.BasicStroke.JOIN_MITER;
import static java.awt.BasicStroke.JOIN_ROUND;
import static org.deegree.commons.utils.TunableParameter.get;
import static org.deegree.commons.utils.math.MathUtils.isZero;
import static org.deegree.style.utils.ShapeHelper.getShapeFromMark;
import static org.deegree.style.utils.ShapeHelper.getShapeFromSvg;
Expand All @@ -56,8 +57,8 @@ Occam Labs UG (haftungsbeschränkt)
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;

import org.deegree.commons.utils.TunableParameter;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import org.deegree.rendering.r2d.strokes.OffsetStroke;
import org.deegree.rendering.r2d.strokes.ShapeStroke;
import org.deegree.style.styling.components.PerpendicularOffsetType;
Expand All @@ -78,16 +79,21 @@ class Java2DStrokeRenderer {

private static final Logger LOG = getLogger( Java2DStrokeRenderer.class );

private static final boolean SVG_AS_MARK = get( "deegree.rendering.graphicstroke.svg-as-mark", false );

private Graphics2D graphics;

private UomCalculator uomCalculator;

private Java2DFillRenderer fillRenderer;

Java2DStrokeRenderer( Graphics2D graphics, UomCalculator uomCalculator, Java2DFillRenderer fillRenderer ) {
private RendererContext rendererContext;

Java2DStrokeRenderer( Graphics2D graphics, UomCalculator uomCalculator, Java2DFillRenderer fillRenderer, RendererContext rendererContext ) {
this.graphics = graphics;
this.uomCalculator = uomCalculator;
this.fillRenderer = fillRenderer;
this.rendererContext = rendererContext;
}

void applyStroke( Stroke stroke, UOM uom, Shape object, double perpendicularOffset, PerpendicularOffsetType type ) {
Expand All @@ -113,46 +119,76 @@ void applyStroke( Stroke stroke, UOM uom, Shape object, double perpendicularOffs

private boolean applyGraphicStroke( Stroke stroke, UOM uom, Shape object, double perpendicularOffset,
PerpendicularOffsetType type ) {
if ( stroke.stroke.image == null && stroke.stroke.imageURL != null ) {
Shape shape = getShapeFromSvg( stroke.stroke.imageURL,
uomCalculator.considerUOM( stroke.stroke.size, uom ), stroke.stroke.rotation );
graphics.setStroke( new ShapeStroke( shape, uomCalculator.considerUOM( stroke.strokeGap
+ stroke.stroke.size, uom ),
stroke.positionPercentage, stroke.strokeInitialGap ) );
} else if ( stroke.stroke.mark != null ) {
double poff = uomCalculator.considerUOM( perpendicularOffset, uom );
Shape transed = object;
if ( !isZero( poff ) ) {
transed = new OffsetStroke( poff, null, type ).createStrokedShape( transed );
}
double sz = stroke.stroke.size;
Shape shape = getShapeFromMark( stroke.stroke.mark, sz <= 0 ? 6 : uomCalculator.considerUOM( sz, uom ),
stroke.stroke.rotation );
if ( sz <= 0 ) {
sz = 6;
double strokeSizeUOM = stroke.stroke.size <= 0 ? 6 : uomCalculator.considerUOM( stroke.stroke.size, uom );
double poff = uomCalculator.considerUOM( perpendicularOffset, uom );
Shape transed = object;
if ( !isZero( poff ) ) {
transed = new OffsetStroke( poff, null, type ).createStrokedShape( transed );
}

Rectangle2D.Double rect = fillRenderer.getGraphicBounds( stroke.stroke, 0,0, uom );
BufferedImage img = null;
Shape[] shapes;
if ( stroke.stroke.image != null ) {
shapes = new Shape[0];
img = stroke.stroke.image;
} else if ( stroke.stroke.imageURL != null && SVG_AS_MARK ) {
// Render SVG like mark
Shape shape = getShapeFromSvg( stroke.stroke.imageURL, uomCalculator.considerUOM( stroke.stroke.size, uom ),
stroke.stroke.rotation );
shapes = new Shape[]{ shape };
} else if ( stroke.stroke.imageURL != null ) {
// render SVG like image
img = rendererContext.svgRenderer.prepareSvg( rect, stroke.stroke );
if ( img == null ) {
// fallback to regular rendering if no image can be produced
return false;
}
ShapeStroke s = new ShapeStroke( shape, uomCalculator.considerUOM( stroke.strokeGap + sz, uom ),
stroke.positionPercentage, stroke.strokeInitialGap );
shapes = new Shape[0];
} else if ( stroke.stroke.mark != null ) {
Shape shape = getShapeFromMark( stroke.stroke.mark, strokeSizeUOM, stroke.stroke.rotation );
shapes = new Shape[]{ shape };
} else {
LOG.warn("Only images, SVGs and Mark are currently supported as GraphicStroke.");
return true;
}

ShapeStroke s = new ShapeStroke( shapes,
uomCalculator.considerUOM( stroke.strokeGap, uom ) + strokeSizeUOM,
stroke.positionPercentage,
uomCalculator.considerUOM( stroke.strokeInitialGap, uom ),
stroke.stroke.anchorPointX, stroke.stroke.anchorPointY,
uomCalculator.considerUOM( stroke.stroke.displacementX, uom ),
uomCalculator.considerUOM( stroke.stroke.displacementY, uom ),
stroke.positionRotation );


if ( img != null ) {
s.renderStroke( transed, graphics, img, stroke.stroke, rect );
return true;
} else if ( stroke.stroke.image == null && stroke.stroke.imageURL != null ) {
graphics.setStroke( s );
graphics.draw( transed );
return true;
} else if ( stroke.stroke.mark != null ) {
transed = s.createStrokedShape( transed );
if ( stroke.stroke.mark.fill != null ) {
if ( stroke.stroke.mark.fill != null && !stroke.stroke.mark.fill.isInvisible() ) {
fillRenderer.applyFill( stroke.stroke.mark.fill, uom );
graphics.fill( transed );
}
if ( stroke.stroke.mark.stroke != null ) {
if ( stroke.stroke.mark.stroke != null && !stroke.stroke.mark.stroke.isInvisible()) {
applyStroke( stroke.stroke.mark.stroke, uom, transed, 0, null );
graphics.draw( transed );
}
return true;
} else {
LOG.warn( "Rendering of raster images along lines is not supported yet." );
}
return false;
}

private void applyNormalStroke( Stroke stroke, UOM uom, Shape object, double perpendicularOffset,
PerpendicularOffsetType type ) {
int linecap = getLinecap( stroke );
float miterLimit = TunableParameter.get( "deegree.rendering.stroke.miterlimit", 10f );
float miterLimit = get( "deegree.rendering.stroke.miterlimit", 10f );
int linejoin = getLinejoin( stroke );
float dashoffset = (float) uomCalculator.considerUOM( stroke.dashoffset, uom );
float[] dasharray = stroke.dasharray == null ? null : new float[stroke.dasharray.length];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class RendererContext {
}
uomCalculator = new UomCalculator( pixelSize, res );
fillRenderer = new Java2DFillRenderer( uomCalculator, graphics );
strokeRenderer = new Java2DStrokeRenderer( graphics, uomCalculator, fillRenderer );
strokeRenderer = new Java2DStrokeRenderer( graphics, uomCalculator, fillRenderer, this );
svgRenderer = new SvgRenderer();
polygonRenderer = new PolygonRenderer( geomHelper, fillRenderer, strokeRenderer, graphics, renderer );
curveRenderer = new CurveRenderer( renderer );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,23 @@
package org.deegree.rendering.r2d.strokes;

import static java.lang.Math.sqrt;
import static java.lang.Math.toRadians;
import static org.deegree.commons.utils.math.MathUtils.isZero;
import static org.deegree.commons.utils.math.MathUtils.round;

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import org.deegree.style.styling.components.Graphic;

/**
* <code>ShapeStroke</code>
Expand All @@ -76,6 +85,8 @@ public class ShapeStroke implements Stroke {

private boolean repeat = true;

private boolean rotate = true;

private AffineTransform t = new AffineTransform();

private double positionPercentage;
Expand All @@ -84,6 +95,14 @@ public class ShapeStroke implements Stroke {

private static final float FLATNESS = 1;

private class Counter {
private int value = 0;
}

private interface StrokeResult {
boolean append(float x, float y, double rotation);
}

/**
* @param shapes
* @param advance
Expand All @@ -101,15 +120,29 @@ public ShapeStroke( Shape shapes, double advance, double positionPercentage, dou
* @param initialGap
*/
public ShapeStroke( Shape shapes[], double advance, double positionPercentage, double initialGap ) {
this( shapes, advance, positionPercentage, initialGap, 0.5, 0.5, 0, 0, false );
}

public ShapeStroke( Shape shapes, double advance, double positionPercentage, double initialGap, double anchorPointX,
double anchorPointY, double displacementX, double displacementY, boolean rotate ) {
this( new Shape[] { shapes }, advance, positionPercentage, initialGap, anchorPointX, anchorPointY,
displacementX, displacementY, rotate );
}

public ShapeStroke( Shape shapes[], double advance, double positionPercentage, double initialGap,
double anchorPointX, double anchorPointY, double displacementX, double displacementY, boolean rotate ) {
this.advance = advance;
this.shapes = new Shape[shapes.length];
this.positionPercentage = positionPercentage;
this.repeat = positionPercentage < 0;
this.initialGap = initialGap;
this.rotate = rotate;

for ( int i = 0; i < this.shapes.length; i++ ) {
Rectangle2D bounds = shapes[i].getBounds2D();
t.setToTranslation( -bounds.getCenterX(), -bounds.getCenterY() );
double translateX = bounds.getX() + bounds.getWidth() * anchorPointX + displacementX;
double translateY = bounds.getY() + bounds.getHeight() * anchorPointY + displacementY;
t.setToTranslation( -translateX, -translateY );
this.shapes[i] = t.createTransformedShape( shapes[i] );
}
}
Expand All @@ -133,29 +166,85 @@ public Shape createStrokedShape( Shape shape ) {
next = minLength;
}

float points[] = new float[6];
int currentShape = 0;
int length = shapes.length;
Counter currentShape = new Counter();
final int length = shapes.length;

createStrokedShape( result, it, next, minLength, points, currentShape, length );
createStrokedShape( ( x, y, angle ) -> {
t.setToTranslation( x, y );
if ( this.rotate ) {
t.rotate( angle );
}

result.append( t.createTransformedShape( shapes[currentShape.value] ), false );
currentShape.value++;
if ( currentShape.value >= length && repeat ) {
currentShape.value = 0;
return false;
} else {
return true;
}
}, it, next, minLength );

return result;
}

private void createStrokedShape( GeneralPath result, PathIterator it, float next, float minLength, float[] points,
int currentShape, int length ) {
/**
* Draw a {@code Image} along a {@code Shape}
*
* @param shape Shape to render along
* @param graphics Graphics context
* @param img The image
* @param g Graphics for anchor point and rotation
* @param rect Rectangle describing the image
*/
public void renderStroke( Shape shape, Graphics2D graphics, Image img, Graphic g, Rectangle2D.Double rect) {
PathIterator it = new FlatteningPathIterator( shape.getPathIterator( null ), FLATNESS );

// a little sub optimal to actually go through twice
double totalLength = 0;
if ( positionPercentage >= 0 ) {
totalLength = calculatePathLength( it );
it = new FlatteningPathIterator( shape.getPathIterator( null ), FLATNESS );
}

float next = 0;
float minLength = (float) initialGap;
if ( positionPercentage >= 0 ) {
minLength = (float) ( totalLength * ( positionPercentage / 100 ) );
next = minLength;
}
double graphicsRotation = toRadians( g.rotation );

createStrokedShape( ( x, y, angle ) -> {
double rotation = this.rotate ? graphicsRotation + angle : graphicsRotation;
AffineTransform t = graphics.getTransform();
if ( !isZero( rotation ) ) {
int rotationPointX = round( x + rect.x + rect.getWidth() * g.anchorPointX );
int rotationPointY = round( y + rect.y + rect.getHeight() * g.anchorPointY );
graphics.rotate( rotation, rotationPointX, rotationPointY );
}
graphics.drawImage( img, round( x + rect.x ), round( y + rect.y ), round( rect.width ),
round( rect.height ), null );
graphics.setTransform( t );
return !repeat;
}, it, next, minLength );
}


private void createStrokedShape( StrokeResult result, PathIterator it, float next, float minLength ) {
int type = 0;
float moveX = 0, moveY = 0;
float lastX = 0, lastY = 0;
float thisX = 0, thisY = 0;
float points[] = new float[6];
boolean stop = false;

while ( currentShape < length && !it.isDone() ) {
while ( !stop && !it.isDone() ) {
type = it.currentSegment( points );
switch ( type ) {
case PathIterator.SEG_MOVETO:
moveX = lastX = points[0];
moveY = lastY = points[1];
result.moveTo( moveX, moveY );
next = minLength;
break;

Expand All @@ -172,17 +261,12 @@ private void createStrokedShape( GeneralPath result, PathIterator it, float next
float distance = (float) Math.sqrt( dx * dx + dy * dy );
if ( distance >= next ) {
float r = 1.0f / distance;
float angle = (float) Math.atan2( dy, dx );
while ( currentShape < length && distance >= next ) {
double angle = Math.atan2( dy, dx );
while ( !stop && distance >= next ) {
float x = lastX + next * dx * r;
float y = lastY + next * dy * r;
t.setToTranslation( x, y );
t.rotate( angle );
result.append( t.createTransformedShape( shapes[currentShape] ), false );
stop = result.append(x, y, angle);
next += advance;
currentShape++;
if ( repeat )
currentShape %= length;
}
}
next -= distance;
Expand Down Expand Up @@ -219,5 +303,4 @@ private double calculatePathLength( PathIterator it ) {
}
return totalLength;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<gml:FeatureCollection id="FC_1"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns="http://deegree.org/app"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/gml/3.2 https://schemas.opengis.net/gml/3.2.1/gml.xsd">
<gml:boundedBy>
<gml:Envelope srsName="EPSG:25832">
<gml:lowerCorner>0.0 0.0</gml:lowerCorner>
<gml:upperCorner>100.0 100.0</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<gml:featureMember><Object gml:id="FEATURE_1"><id>1</id><geom><gml:CompositeCurve><gml:curveMember><gml:LineString><gml:posList>20 20 60 70 85 70</gml:posList></gml:LineString></gml:curveMember></gml:CompositeCurve></geom></Object></gml:featureMember>
<gml:featureMember><Object gml:id="FEATURE_2"><id>2</id><geom><gml:CompositeCurve><gml:curveMember><gml:LineString><gml:posList>15 75 30 90</gml:posList></gml:LineString></gml:curveMember></gml:CompositeCurve></geom></Object></gml:featureMember>
<gml:featureMember><Object gml:id="FEATURE_3"><id>3</id><geom><gml:CompositeCurve><gml:curveMember><gml:LineString><gml:posList>65 15 80 30</gml:posList></gml:LineString></gml:curveMember></gml:CompositeCurve></geom></Object></gml:featureMember>
</gml:FeatureCollection>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit ac2832b

Please sign in to comment.