/* * Copyright (c) 2016 Metron, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Metron, Inc. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL METRON, INC. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.metsci.glimpse.charts.raster; import static com.metsci.glimpse.util.geo.datum.Datum.wgs84sphere; import java.awt.Color; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.IndexColorModel; import java.awt.image.Raster; import java.awt.image.SampleModel; import java.awt.image.SinglePixelPackedSampleModel; import java.awt.image.WritableRaster; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.HashSet; import java.util.Scanner; import java.util.Set; import java.util.Vector; import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.metsci.glimpse.gl.texture.ColorTexture1D; import com.metsci.glimpse.gl.texture.ColorTexture1D.MutatorColor1D; import com.metsci.glimpse.support.projection.FlatProjection; import com.metsci.glimpse.support.projection.GenericProjection; import com.metsci.glimpse.support.projection.Projection; import com.metsci.glimpse.support.texture.ByteTextureProjected2D; import com.metsci.glimpse.support.texture.ByteTextureProjected2D.MutatorByte2D; import com.metsci.glimpse.util.Pair; import com.metsci.glimpse.util.geo.LatLonGeo; import com.metsci.glimpse.util.geo.LatLonRect; import com.metsci.glimpse.util.geo.projection.GeoProjection; import com.metsci.glimpse.util.geo.projection.MercatorProjection; import com.metsci.glimpse.util.math.stat.StatCollectorNDim; import com.metsci.glimpse.util.vector.Vector2d; /** * Data structures and data IO utilities for displaying Electronic Navigation Chart * raster images available in the BSB Raster format.<p> * * @author osborn * @see com.metsci.glimpse.examples.charts.rnc.RasterNavigationChartExample */ public final class BsbRasterData { private final String _imageName; private final String _header; private final Set<Pair<IntPoint2d, LatLonGeo>> _registrationPoints; private final byte[] _imageData; private final IndexColorModel _colorModel; private final int _width_PIXELS; private final int _height_PIXELS; private BsbRasterData( String imageName, String header, int width_PIXELS, int height_PIXELS, byte[] imageData, IndexColorModel colorModel, Set<Pair<IntPoint2d, LatLonGeo>> registrationPoints ) { this._imageName = imageName; this._header = header; this._imageData = imageData; this._colorModel = colorModel; this._width_PIXELS = width_PIXELS; this._height_PIXELS = height_PIXELS; this._registrationPoints = registrationPoints; } public static BsbRasterData readImage( InputStream in ) throws IOException { BufferedInputStream bis = new BufferedInputStream( in ); DataInputStream dis = new DataInputStream( bis ); String header = extractAsciiHeader( dis ); int[] dim = extractDimension( header ); IndexColorModel icm = extractColorModel( header ); byte colorDepth = dis.readByte( ); byte[] ucData = new byte[dim[0] * dim[1]]; decodeImageData( dis, ucData, colorDepth, dim[1] ); return new BsbRasterData( extractImageName( header ), header, dim[0], dim[1], ucData, icm, extractRegistrationPoints( header ) ); } public IndexColorModel getColorModel( ) { return _colorModel; } private static String extractAsciiHeader( DataInputStream stream ) throws IOException { StringBuilder builder = new StringBuilder( ); byte thisByte = 0; byte lastByte = 0; while ( true ) { if ( lastByte == 26 && thisByte == 0 ) { break; } else { lastByte = thisByte; thisByte = stream.readByte( ); builder.append( ( char ) thisByte ); } } return builder.toString( ); } private static Vector<Pair<String, String>> extractTokenData( String header, String tokenPattern ) { Pattern pattern = Pattern.compile( "^\\w{3,}/", Pattern.MULTILINE ); Matcher matcher = pattern.matcher( header ); String[] items = pattern.split( header ); Vector<Pair<String, String>> results = new Vector<Pair<String, String>>( ); for ( int i = 1; i < items.length; i++ ) { matcher.find( ); if ( matcher.group( ).replace( "/", "" ).matches( tokenPattern ) ) { results.add( new Pair<String, String>( matcher.group( ).replace( "/", "" ), items[i] ) ); } } return results; } private static int[] extractDimension( String header ) { Vector<Pair<String, String>> allTokenData = extractTokenData( header, "BSB" ); String tokenData = allTokenData.get( 0 ).second( ); Scanner s = new Scanner( tokenData ); s.findWithinHorizon( "RA\\s*=\\s*(\\d+)\\s*,\\s*(\\d+)", tokenData.length( ) ); MatchResult results = s.match( ); int width_PIXELS = Integer.parseInt( results.group( 1 ) ); int height_PIXELS = Integer.parseInt( results.group( 2 ) ); s.close( ); return new int[] { width_PIXELS, height_PIXELS }; } private static String extractImageName( String header ) { Vector<Pair<String, String>> allTokenData = extractTokenData( header, "BSB" ); String tokenData = allTokenData.get( 0 ).second( ); Scanner s = new Scanner( tokenData ); s.findWithinHorizon( "NA=([\\w;\\s]+)", tokenData.length( ) ); String imageName = s.match( ).group( 1 ); s.close( ); return imageName; } private static Set<Pair<IntPoint2d, LatLonGeo>> extractRegistrationPoints( String header ) { Set<Pair<IntPoint2d, LatLonGeo>> refPoints = new HashSet<Pair<IntPoint2d, LatLonGeo>>( ); Vector<Pair<String, String>> allTokenData = extractTokenData( header, "REF" ); for ( Pair<String, String> tokenData : allTokenData ) { Scanner s = new Scanner( tokenData.second( ).replaceAll( "\r\n", "" ) ).useDelimiter( "," ); s.nextInt( ); int x = s.nextInt( ); int y = s.nextInt( ); double lat_DEG = s.nextDouble( ); double lon_DEG = s.nextDouble( ); refPoints.add( new Pair<IntPoint2d, LatLonGeo>( new IntPoint2d( x, y ), new LatLonGeo( lat_DEG, lon_DEG ) ) ); s.close( ); } return refPoints; } private static IndexColorModel extractColorModel( String header ) { Vector<Pair<String, String>> allTokenData = extractTokenData( header, "DAY" ); byte[] r = new byte[16]; byte[] g = new byte[16]; byte[] b = new byte[16]; for ( Pair<String, String> tokenData : allTokenData ) { Scanner s = new Scanner( tokenData.second( ) ); s.findWithinHorizon( "\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+),\\s*(\\d+)", tokenData.second( ).length( ) ); MatchResult results = s.match( ); int i = Integer.parseInt( results.group( 1 ) ); int rd = Integer.parseInt( results.group( 2 ) ); int gn = Integer.parseInt( results.group( 3 ) ); int bl = Integer.parseInt( results.group( 4 ) ); Color color = transform( new Color( rd, gn, bl ), 1.0f ); r[i] = ( byte ) color.getRed( ); g[i] = ( byte ) color.getGreen( ); b[i] = ( byte ) color.getBlue( ); s.close( ); } return new IndexColorModel( 8, 16, r, g, b ); } private static final Color transform( Color color, double v ) { int rd = ( int ) ( color.getRed( ) * v + ( 1 - v ) * 0 ); int gn = ( int ) ( color.getGreen( ) * v + ( 1 - v ) * 0 ); int bl = ( int ) ( color.getBlue( ) * v + ( 1 - v ) * 0 ); return new Color( rd, gn, bl ); } private static void decodeImageData( DataInputStream stream, byte[] cData, int numColorBits, int numRows ) throws IOException { byte colorMask = ( byte ) ( ( ( ( 1 << numColorBits ) ) - 1 ) << 7 - numColorBits ); byte countMask = ( byte ) ( ( 1 << 7 - numColorBits ) - 1 ); int nextByte = 0; int iPix = 0; for ( int i = 0; i < numRows; i++ ) { readRowNumber( stream ); while ( ( nextByte = stream.readUnsignedByte( ) ) != 0 ) { byte colorValue = ( byte ) ( ( nextByte & colorMask ) >> ( 7 - numColorBits ) ); int runLength = ( nextByte & countMask ); while ( ( nextByte & 0x80 ) != 0 ) { nextByte = stream.readUnsignedByte( ); runLength = runLength * 128 + ( nextByte & 0x7f ); } for ( int j = 0; j < runLength + 1; j++ ) { cData[iPix++] = colorValue; } } } } private static int readRowNumber( DataInputStream stream ) throws IOException { int nextByte; int lineNumber = 0; do { nextByte = stream.readUnsignedByte( ); lineNumber = lineNumber * 128 + ( nextByte & 0x7f ); } while ( ( nextByte & 0x80 ) != 0 ); return lineNumber; } private double distance( double x1, double y1, double x2, double y2 ) { double dx = x1 - x2; double dy = y1 - y2; return Math.sqrt( dx * dx + dy * dy ); } public Projection getProjection( GeoProjection plane, MercatorProjection projection, int resolution ) { FlatProjection flatProjection = getProjection( projection ); double sizeX = flatProjection.getMaxX( ) - flatProjection.getMinX( ); double sizeY = flatProjection.getMaxY( ) - flatProjection.getMinY( ); double minX = flatProjection.getMinX( ); double minY = flatProjection.getMinY( ); double[][] coordsX = new double[resolution][resolution]; double[][] coordsY = new double[resolution][resolution]; for ( int x = 0; x < resolution; x++ ) { for ( int y = 0; y < resolution; y++ ) { double fracX = ( ( double ) x / ( double ) ( resolution - 1 ) ); double fracY = ( ( double ) y / ( double ) ( resolution - 1 ) ); double valX = minX + fracX * sizeX; double valY = minY + fracY * sizeY; LatLonGeo geo = projection.unproject( valX, valY ); Vector2d planePoint = plane.project( geo ); coordsX[x][y] = planePoint.getX( ); coordsY[x][y] = planePoint.getY( ); } } return new GenericProjection( coordsX, coordsY ); } public LatLonGeo estimateCenterLatLon( ) { if ( _registrationPoints.size( ) == 0 ) return null; StatCollectorNDim center = new StatCollectorNDim( 3 ); for ( Pair<IntPoint2d, LatLonGeo> pair : _registrationPoints ) { LatLonRect posit = pair.second( ).toLatLonRect( wgs84sphere ); center.addElement( new double[] { posit.getX( ), posit.getY( ), posit.getZ( ) } ); } double[] centerXYZ = center.getMean( ); LatLonRect centerLLR = LatLonRect.fromXyz( centerXYZ[0], centerXYZ[1], centerXYZ[2] ); return centerLLR.toLatLonGeo( wgs84sphere ); } public FlatProjection getProjection( MercatorProjection projection ) { if ( _registrationPoints == null || _registrationPoints.isEmpty( ) ) return null; Pair<IntPoint2d, LatLonGeo> point1 = _registrationPoints.iterator( ).next( ); Pair<IntPoint2d, LatLonGeo> point2 = null; double maxDistance = Double.NEGATIVE_INFINITY; for ( Pair<IntPoint2d, LatLonGeo> pair : _registrationPoints ) { if ( pair.first( ).x != point1.first( ).x && pair.first( ).y != point1.first( ).y ) { double distance = distance( pair.first( ).x, pair.first( ).y, point1.first( ).x, point1.first( ).y ); if ( distance > maxDistance ) { maxDistance = distance; point2 = pair; break; } } } if ( point2 == null ) return null; LatLonGeo latlon1 = point1.second( ); LatLonGeo latlon2 = point2.second( ); double x1 = point1.first( ).x; double x2 = point2.first( ).x; double y1 = point1.first( ).y; double y2 = point2.first( ).y; Vector2d projected1 = projection.project( latlon1 ); Vector2d projected2 = projection.project( latlon2 ); double pixelDiffX = Math.abs( x1 - x2 ); double projDiffX = Math.abs( projected1.getX( ) - projected2.getX( ) ); double pixelToProjX = pixelDiffX / projDiffX; double pixelDiffY = Math.abs( y1 - y2 ); double projDiffY = Math.abs( projected1.getY( ) - projected2.getY( ) ); double pixelToProjY = pixelDiffY / projDiffY; double minX = projected1.getX( ) - x1 / pixelToProjX; double maxX = projected1.getX( ) + ( _width_PIXELS - x1 ) / pixelToProjX; double minY = projected1.getY( ) + y1 / pixelToProjY; double maxY = projected1.getY( ) - ( _height_PIXELS - y1 ) / pixelToProjY; return new FlatProjection( minX, maxX, minY, maxY ); } public final BufferedImage generateBufferedImage( ) { // Create a data buffer using the byte buffer of pixel data. // The pixel data is not copied; the data buffer uses the byte buffer array. DataBuffer dbuf = new DataBufferByte( _imageData, _width_PIXELS * _height_PIXELS, 0 ); // The number of banks should be 1 dbuf.getNumBanks( ); // 1 // Prepare a sample model that specifies a storage 4-bits of // pixel datavd in an 8-bit data element int[] bitMasks = new int[] { ( byte ) 0xf }; SampleModel sampleModel = new SinglePixelPackedSampleModel( DataBuffer.TYPE_BYTE, _width_PIXELS, _height_PIXELS, bitMasks ); // Create a raster using the sample model and data buffer WritableRaster raster = Raster.createWritableRaster( sampleModel, dbuf, null ); // Combine the color model and raster into a buffered image return new BufferedImage( _colorModel, raster, true, null ); } public final String getName( ) { return _imageName; } public final String getHeader( ) { return _header; } public final Set<Pair<IntPoint2d, LatLonGeo>> getRegistrationPoints( ) { return _registrationPoints; } public final ColorTexture1D getColorTexture( ) { ColorTexture1D texture = new ColorTexture1D( 16 ); texture.mutate( new MutatorColor1D( ) { @Override public void mutate( FloatBuffer floatBuffer, int dim ) { for ( int i = 0; i < dim; i++ ) { floatBuffer.put( _colorModel.getRed( i ) / 255.0f ); floatBuffer.put( _colorModel.getGreen( i ) / 255.0f ); floatBuffer.put( _colorModel.getBlue( i ) / 255.0f ); floatBuffer.put( _colorModel.getAlpha( i ) / 255.0f ); } } } ); return texture; } public final ByteTextureProjected2D getDataTexture( ) { ByteTextureProjected2D texture = new ByteTextureProjected2D( _width_PIXELS, _height_PIXELS ); texture.mutate( new MutatorByte2D( ) { @Override public void mutate( ByteBuffer data, int dataSizeX, int dataSizeY ) { for ( int y = 0; y < dataSizeY; y++ ) { for ( int x = 0; x < dataSizeX; x++ ) { data.put( _imageData[x + dataSizeX * y] ); } } } } ); return texture; } }