Permalink
Browse files

Hillshading: filtered upscaling on Android, smooth tile borders (#997)

  • Loading branch information...
usrusr authored and devemux86 committed Oct 4, 2017
1 parent fefae41 commit c691e7197c2daa3cf0663e11c7e7e5dd4147d428
Showing with 1,838 additions and 388 deletions.
  1. +5 −0 mapsforge-core/src/main/java/org/mapsforge/core/graphics/GraphicContext.java
  2. +3 −2 mapsforge-core/src/main/java/org/mapsforge/core/graphics/GraphicFactory.java
  3. +39 −0 mapsforge-core/src/main/java/org/mapsforge/core/graphics/HillshadingBitmap.java
  4. +207 −9 mapsforge-map-android/src/main/java/org/mapsforge/map/android/graphics/AndroidCanvas.java
  5. +3 −2 mapsforge-map-android/src/main/java/org/mapsforge/map/android/graphics/AndroidGraphicFactory.java
  6. +40 −0 mapsforge-map-android/src/main/java/org/mapsforge/map/android/graphics/AndroidHillshadingBitmap.java
  7. +35 −7 mapsforge-map-awt/src/main/java/org/mapsforge/map/awt/graphics/AwtCanvas.java
  8. +4 −4 mapsforge-map-awt/src/main/java/org/mapsforge/map/awt/graphics/AwtGraphicFactory.java
  9. +42 −0 mapsforge-map-awt/src/main/java/org/mapsforge/map/awt/graphics/AwtHillshadingBitmap.java
  10. +12 −103 mapsforge-map-awt/src/main/java/org/mapsforge/map/awt/graphics/AwtLuminanceShadingComposite.java
  11. +152 −0 mapsforge-map-awt/src/test/java/org/mapsforge/map/layer/hills/HgtCacheTest.java
  12. +527 −0 mapsforge-map/src/main/java/org/mapsforge/map/layer/hills/HgtCache.java
  13. +98 −190 mapsforge-map/src/main/java/org/mapsforge/map/layer/hills/HillsRenderConfig.java
  14. +158 −0 mapsforge-map/src/main/java/org/mapsforge/map/layer/hills/LazyFuture.java
  15. +121 −8 mapsforge-map/src/main/java/org/mapsforge/map/layer/hills/ShadingAlgorithm.java
  16. +22 −15 ...n/java/org/mapsforge/map/layer/hills/{SimpleShadingAlgortithm.java → SimpleShadingAlgorithm.java}
  17. +3 −2 mapsforge-map/src/main/java/org/mapsforge/map/layer/renderer/HillshadingContainer.java
  18. +52 −22 mapsforge-map/src/main/java/org/mapsforge/map/rendertheme/renderinstruction/Hillshading.java
  19. +5 −2 mapsforge-map/src/main/java/org/mapsforge/map/rendertheme/rule/RenderThemeHandler.java
  20. +160 −0 mapsforge-map/src/test/java/org/mapsforge/map/layer/hills/RawShadingResultTest.java
  21. +18 −0 mapsforge-samples-android/AndroidManifest.xml
  22. +0 −1 mapsforge-samples-android/res/values/strings.xml
  23. +48 −7 mapsforge-samples-android/src/main/java/org/mapsforge/samples/android/HillshadingMapViewer.java
  24. +25 −0 ...forge-samples-android/src/main/java/org/mapsforge/samples/android/HillshadingMapViewerFaster.java
  25. +22 −6 mapsforge-samples-android/src/main/java/org/mapsforge/samples/android/Samples.java
  26. +20 −3 mapsforge-samples-android/src/main/java/org/mapsforge/samples/android/SamplesBaseActivity.java
  27. +9 −5 mapsforge-samples-awt/src/main/java/org/mapsforge/samples/awt/Samples.java
  28. +8 −0 resources/renderTheme.xsd
@@ -48,5 +48,10 @@
void setClipDifference(int left, int top, int width, int height);
/**
* Shade whole map tile when tileRect is null (and bitmap, shadeRect are null).
* Shade tileRect neutral if bitmap is null (and shadeRect).
* Shade tileRect with bitmap otherwise.
*/
void shadeBitmap(Bitmap bitmap, Rectangle shadeRect, Rectangle tileRect, float magnitude);
}
@@ -18,6 +18,7 @@
import org.mapsforge.core.mapelements.PointTextContainer;
import org.mapsforge.core.mapelements.SymbolContainer;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.Point;
import java.io.IOException;
@@ -37,9 +38,9 @@
Matrix createMatrix();
/**
* Create a single channel bitmap, e.g. for hillshading.
* Create a single channel bitmap for hillshading, may include a buffer.
*/
Bitmap createMonoBitmap(int width, int height, byte[] buffer);
HillshadingBitmap createMonoBitmap(int width, int height, byte[] buffer, int padding, BoundingBox area);
Paint createPaint();
@@ -0,0 +1,39 @@
/*
* Copyright 2017 usrusr
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapsforge.core.graphics;
import org.mapsforge.core.model.BoundingBox;
public interface HillshadingBitmap extends Bitmap {
enum Border {
WEST(true), NORTH(false), EAST(true), SOUTH(false);
public final boolean vertical;
Border(boolean vertical) {
this.vertical = vertical;
}
}
/**
* Return geo bounds of the area within the padding.
*/
BoundingBox getAreaRect();
/**
* Optional padding (lies outside of areaRect).
*/
int getPadding();
}
@@ -44,18 +44,19 @@
android.graphics.Canvas canvas;
private final android.graphics.Paint bitmapPaint = new android.graphics.Paint();
private final android.graphics.Paint shadePaint = new android.graphics.Paint();
private ColorFilter grayscaleFilter, grayscaleInvertFilter, invertFilter;
/**
* A set of reusable temporaries that is not needed when hillshading is inactive.
*/
private HilshadingTemps hillshadingTemps = null;
AndroidCanvas() {
this.canvas = new android.graphics.Canvas();
this.bitmapPaint.setAntiAlias(true);
this.bitmapPaint.setFilterBitmap(true);
this.shadePaint.setAntiAlias(true);
this.shadePaint.setFilterBitmap(true);
createFilters();
}
@@ -102,7 +103,12 @@ public void destroy() {
@Override
public void drawBitmap(Bitmap bitmap, int left, int top) {
this.canvas.drawBitmap(AndroidGraphicFactory.getBitmap(bitmap), left, top, bitmapPaint);
android.graphics.Bitmap androidBitmap = AndroidGraphicFactory.getBitmap(bitmap);
if (AndroidGraphicFactory.MONO_ALPHA_BITMAP.equals(androidBitmap.getConfig())) {
// we need to clear the existing alpha to get a clean overwrite
canvas.drawColor(android.graphics.Color.argb(0, 0, 0, 0), PorterDuff.Mode.SRC);
}
this.canvas.drawBitmap(androidBitmap, left, top, bitmapPaint);
}
@Override
@@ -231,9 +237,201 @@ public void setClipInternal(int left, int top, int width, int height, Region.Op
@Override
public void shadeBitmap(Bitmap bitmap, Rectangle hillRect, Rectangle tileRect, float magnitude) {
shadePaint.setAlpha((int) (255 * magnitude));
Rect atr = new Rect((int) hillRect.left, (int) hillRect.top, (int) hillRect.right, (int) hillRect.bottom);
Rect asr = new Rect((int) tileRect.left, (int) tileRect.top, (int) tileRect.right, (int) tileRect.bottom);
this.canvas.drawBitmap(AndroidGraphicFactory.getBitmap(bitmap), atr, asr, shadePaint);
canvas.save();
final HilshadingTemps temps;
if (this.hillshadingTemps == null) {
this.hillshadingTemps = new HilshadingTemps();
}
temps = this.hillshadingTemps;
android.graphics.Paint shadePaint = hillshadingTemps.useAlphaPaint((int) (255 * magnitude));
;
if (bitmap == null) {
if (tileRect != null) {
this.canvas.clipRect((float) tileRect.left, (float) tileRect.top, (float) tileRect.right, (float) tileRect.bottom, Region.Op.REPLACE);
}
// scale a dummy pixel over the canvas - just drawing a paint would probably be faster, but the resulting colors can be inconsistent with the bitmap draw (maybe only on some devices?)
this.canvas.drawBitmap(hillshadingTemps.useNeutralShadingPixel(), hillshadingTemps.useAsr(0, 0, 1, 1), hillshadingTemps.useAdr(0, 0, canvas.getWidth(), canvas.getHeight()), shadePaint);
canvas.restore();
return;
}
android.graphics.Bitmap hillsBitmap = AndroidGraphicFactory.getBitmap(bitmap);
double horizontalScale = tileRect.getWidth() / hillRect.getWidth();
double verticalScale = tileRect.getHeight() / hillRect.getHeight();
if (horizontalScale < 1 && verticalScale < 1) {
// fast path for wide zoom (downscaling)
this.canvas.clipRect((float) tileRect.left, (float) tileRect.top, (float) tileRect.right, (float) tileRect.bottom, Region.Op.REPLACE);
android.graphics.Matrix transform = new android.graphics.Matrix();
transform.preTranslate((float) tileRect.left, (float) tileRect.top);
transform.preScale((float) horizontalScale, (float) verticalScale);
transform.preTranslate((float) -hillRect.left, (float) -hillRect.top);
this.canvas.drawBitmap(hillsBitmap, transform, shadePaint);
} else {
double leftRestUnlimited = 1 + (hillRect.left - Math.floor(hillRect.left));
double leftRest = Math.min(hillRect.left, leftRestUnlimited);
double leftExtra = horizontalScale * leftRest;
double rightRestUnlimited = Math.floor(hillRect.right) + 2 - hillRect.right;
double rightRest = Math.min(bitmap.getWidth() - hillRect.right, rightRestUnlimited);
double rightExtra = horizontalScale * rightRest;
double tempWidthDouble = rightExtra + leftExtra + (hillRect.right - hillRect.left) * horizontalScale;
int tempWidth = (int) Math.ceil(tempWidthDouble);
double topRestUnlimited = 1 + (hillRect.top - Math.floor(hillRect.top));
double topRest = Math.min(hillRect.top, topRestUnlimited);
double topExtra = verticalScale * topRest;
double bottomRestUnlimited = Math.floor(hillRect.bottom) + 2 - hillRect.bottom;
double bottomRest = Math.min(bitmap.getHeight() - hillRect.bottom, bottomRestUnlimited);
double bottomExtra = verticalScale * bottomRest;
double tempHeightDouble = bottomExtra + topExtra + (hillRect.bottom - hillRect.top) * verticalScale;
int tempHeight = (int) Math.ceil(tempHeightDouble);
int srcLeft = (int) Math.round(hillRect.left - leftRest);
int srcTop = (int) Math.round(hillRect.top - topRest);
int srcRight = (int) Math.round(hillRect.right + rightRest);
int srcBottom = (int) Math.round(hillRect.bottom + bottomRest);
android.graphics.Canvas tempCanvas = temps.useCanvas();
final android.graphics.Bitmap sourceImage;
if (srcLeft == 0 && srcTop == 0) {
// special handling for an inconsistency in android where rect->rect drawImage upscaling is unfiltered if source top,left is 0,0
// (seems to be shortcutting to a different implementation, observed on sony)
android.graphics.Bitmap shiftedTemp = android.graphics.Bitmap.createBitmap(srcRight + 1, srcBottom, hillsBitmap.getConfig());
tempCanvas.setBitmap(shiftedTemp);
tempCanvas.drawBitmap(hillsBitmap, 1, 0, null);
sourceImage = shiftedTemp;
srcLeft += 1;
srcRight += 1;
} else {
sourceImage = hillsBitmap;
}
Rect asr = temps.useAsr(
srcLeft,
srcTop,
srcRight,
srcBottom
);
Rect adr = temps.useAdr(
0,
0,
tempWidth,
tempHeight
);
android.graphics.Bitmap scaleTemp = temps.useScaleBitmap(tempWidth, tempHeight, hillsBitmap.getConfig());
tempCanvas.setBitmap(scaleTemp);
tempCanvas.drawBitmap(sourceImage, asr, adr, bitmapPaint);
this.canvas.clipRect((float) tileRect.left, (float) tileRect.top, (float) tileRect.right, (float) tileRect.bottom);
int drawOffsetLeft = (int) Math.round((tileRect.left - leftExtra));
int drawOffsetTop = (int) Math.round((tileRect.top - topExtra));
this.canvas.drawBitmap(scaleTemp, drawOffsetLeft, drawOffsetTop, shadePaint);
}
this.canvas.restore();
}
private static class HilshadingTemps {
private final Rect asr = new Rect(0, 0, 0, 0);
private final Rect adr = new Rect(0, 0, 0, 0);
private final android.graphics.Canvas tmpCanvas = new android.graphics.Canvas();
private android.graphics.Bitmap scaleTemp;
private android.graphics.Bitmap shiftTemp;
private final android.graphics.Paint shadePaint;
private android.graphics.Bitmap neutralShadingPixel = AndroidGraphicFactory.INSTANCE.createMonoBitmap(1, 1, new byte[]{(byte) (127 & 0xFF)}, 0, null).bitmap;
private HilshadingTemps() {
shadePaint = new android.graphics.Paint();
//shadePaint.setColor( android.graphics.Color.rgb(127,127,127));
//shadePaint.setColor( android.graphics.Color.WHITE);
this.shadePaint.setAntiAlias(true);
this.shadePaint.setFilterBitmap(true);
}
Rect useAsr(int srcLeft, int srcTop, int srcRight, int srcBottom) {
asr.left = srcLeft;
asr.top = srcTop;
asr.right = srcRight;
asr.bottom = srcBottom;
return asr;
}
Rect useAdr(int destLeft, int destTop, int destRight, int destBottom) {
adr.left = destLeft;
adr.top = destTop;
adr.right = destRight;
adr.bottom = destBottom;
return adr;
}
/**
* returns a temporary canvas that may be used in useScaleBitmap
*/
android.graphics.Canvas useCanvas() {
return tmpCanvas;
}
/**
* returns a reuseable bitmap of size or larger and sets it for the temp canvas
* (some internal operations use the canvas, setting it all the time makes this more uniform)
*/
android.graphics.Bitmap useScaleBitmap(int tempWidth, int tempHeight, android.graphics.Bitmap.Config config) {
scaleTemp = internalUseBitmap(scaleTemp, tempWidth, tempHeight, config);
return scaleTemp;
}
/**
* returns a reuseable bitmap of size or larger and sets it for the temp canvas
* (some internal operations use the canvas, setting it all the time makes this more uniform)
*/
android.graphics.Bitmap useShiftBitmap(int tempWidth, int tempHeight, android.graphics.Bitmap.Config config) {
shiftTemp = internalUseBitmap(shiftTemp, tempWidth, tempHeight, config);
return shiftTemp;
}
private android.graphics.Bitmap internalUseBitmap(android.graphics.Bitmap tmpBitmap, int tempWidth, int tempHeight, android.graphics.Bitmap.Config config) {
if (tmpBitmap == null) {
tmpBitmap = android.graphics.Bitmap.createBitmap(tempWidth, tempHeight, config);
tmpCanvas.setBitmap(tmpBitmap);
} else {
if (tmpBitmap.getWidth() < tempWidth || tmpBitmap.getHeight() < tempHeight || !tmpBitmap.getConfig().equals(config)) {
tmpBitmap.recycle();
tmpBitmap = android.graphics.Bitmap.createBitmap(tempWidth, tempHeight, config);
tmpCanvas.setBitmap(tmpBitmap);
} else {
tmpCanvas.setBitmap(tmpBitmap);
tmpCanvas.drawColor(android.graphics.Color.argb(0, 0, 0, 0), PorterDuff.Mode.SRC);
}
}
return tmpBitmap;
}
public android.graphics.Paint useAlphaPaint(int alpha) {
shadePaint.setAlpha(alpha);
return shadePaint;
}
public android.graphics.Bitmap useNeutralShadingPixel() {
return neutralShadingPixel;
}
}
}
@@ -38,6 +38,7 @@
import org.mapsforge.core.graphics.TileBitmap;
import org.mapsforge.core.mapelements.PointTextContainer;
import org.mapsforge.core.mapelements.SymbolContainer;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.Point;
import org.mapsforge.map.model.DisplayModel;
@@ -225,8 +226,8 @@ public Matrix createMatrix() {
}
@Override
public Bitmap createMonoBitmap(int width, int height, byte[] buffer) {
AndroidBitmap androidBitmap = new AndroidBitmap(width, height, MONO_ALPHA_BITMAP);
public AndroidHillshadingBitmap createMonoBitmap(int width, int height, byte[] buffer, int padding, BoundingBox area) {
AndroidHillshadingBitmap androidBitmap = new AndroidHillshadingBitmap(width + 2 * padding, height + 2 * padding, padding, area);
if (buffer != null) {
Buffer b = ByteBuffer.wrap(buffer);
androidBitmap.bitmap.copyPixelsFromBuffer(b);
@@ -0,0 +1,40 @@
/*
* Copyright 2017 usrusr
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapsforge.map.android.graphics;
import org.mapsforge.core.graphics.HillshadingBitmap;
import org.mapsforge.core.model.BoundingBox;
public class AndroidHillshadingBitmap extends AndroidBitmap implements HillshadingBitmap {
private final int padding;
private final BoundingBox areaRect;
public AndroidHillshadingBitmap(int width, int height, int padding, BoundingBox areaRect) {
super(width, height, AndroidGraphicFactory.MONO_ALPHA_BITMAP);
this.padding = padding;
this.areaRect = areaRect;
}
@Override
public BoundingBox getAreaRect() {
return areaRect;
}
@Override
public int getPadding() {
return padding;
}
}
Oops, something went wrong.

0 comments on commit c691e71

Please sign in to comment.