/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.constellation.coverage;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.coverage.*;
import org.geotoolkit.coverage.filestore.FileCoverageStoreFactory;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.grid.GridGeometry2D;
import org.geotoolkit.coverage.io.GridCoverageReadParam;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.coverage.memory.MPCoverageStore;
import org.geotoolkit.coverage.postgresql.PGCoverageStoreFactory;
import org.geotoolkit.coverage.xmlstore.XMLCoverageStoreFactory;
import org.geotoolkit.image.interpolation.InterpolationCase;
import org.geotoolkit.parameter.Parameters;
import org.geotoolkit.process.ProcessListener;
import org.geotoolkit.referencing.adapters.NetcdfCRS;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import org.constellation.admin.SpringHelper;
import org.geotoolkit.util.NamesExt;
import org.geotoolkit.storage.coverage.CoverageReference;
import org.geotoolkit.storage.coverage.CoverageStore;
import org.geotoolkit.storage.coverage.CoverageStoreFinder;
import org.geotoolkit.storage.coverage.PyramidCoverageBuilder;
import org.opengis.util.GenericName;
/**
* Helper class to ease the pyramid process.
*
* @author olivier.nouguier@geomatys.com
*/
public class PyramidCoverageHelper {
public interface IBuilder {
WithInput fromImage(String string) throws MalformedURLException;
IBuilder withInterpolation(InterpolationCase bilinear);
IBuilder inputFormat(String inputFormat);
IBuilder withDeeps(double[] deeps);
IBuilder withEnvelope(Envelope deeps);
IBuilder withTile(int width, int height);
IBuilder withBaseCoverageNamer(CoverageNamer coverageNamer);
}
private final CoverageStore store;
private CoverageStore outputCoverageStore;
private PyramidCoverageBuilder pyramidCoverageBuilder;
private String baseCoverageName;
private CoverageNamer coverageNamer;
private List<GridCoverage2D> coveragesPyramid;
private double[] depth;
private Envelope envelope;
public PyramidCoverageHelper(Builder builder) throws DataStoreException {
SpringHelper.injectDependencies(this);
this.store = builder.buildInputStore();
final List<GridCoverage2D> coverages = buildCoverages();
if (coverages.size() > 0) {
coveragesPyramid = coverages;
this.outputCoverageStore = builder.buildOutputStore();
this.pyramidCoverageBuilder = new PyramidCoverageBuilder(
new Dimension(builder.tileWidth, builder.tileHeight),
builder.interpolation, 1);
this.coverageNamer = builder.coverageNamer;
this.baseCoverageName = builder.baseCoverageName;
this.depth = builder.depth;
this.envelope = builder.envelope;
}
}
/**
* Builder factory with a base name for coverage.
*
* @param name
* of coverage
* @return Builder instance
*/
public static IBuilder builder(String name) {
Builder builder = new Builder(name);
return builder;
}
public CoverageStore getCoverageStore() {
return outputCoverageStore;
}
/**
* Build coverage list which can be pyramided
*
* @return a {@link org.geotoolkit.coverage.grid.GridCoverage2D}
* {@link java.util.List}
* @throws CancellationException
* @throws DataStoreException
*/
private List<GridCoverage2D> buildCoverages() throws CancellationException,
DataStoreException {
final List<GridCoverage2D> coverages = new ArrayList<>(0);
for (GenericName name : store.getNames()) {
final CoverageReference ref = store.getCoverageReference(name);
final GridCoverageReader reader = ref.acquireReader();
final GridCoverageReadParam param = new GridCoverageReadParam();
param.setDeferred(true);
final GridCoverage coverage = reader.read(ref.getImageIndex(), param);
ref.recycle(reader);
if (coverage instanceof GridCoverageStack) {
// TODO handle stack
} else {
final GridCoverage2D coverage2D = (GridCoverage2D) coverage;
final GridGeometry2D gridGeometry = coverage2D.getGridGeometry();
if ((gridGeometry.getCoordinateReferenceSystem() instanceof NetcdfCRS)) {
//FIXME normalize CRS ?
break;
}
// Always pyramid coverage for WMTS
coverages.add(coverage2D);
}
}
return coverages;
}
public Map<Envelope, double[]> getResolutionPerEnvelope() {
Map<Envelope, double[]> map = new HashMap<>();
if (envelope != null) {
map.put(envelope, depth);
}
return map;
}
public PyramidCoverageBuilder getPyramidCoverageBuilder() {
return pyramidCoverageBuilder;
}
public List<GridCoverage2D> getCoveragesPyramid() {
return coveragesPyramid;
}
/**
* Build pyramid and give a {@link org.geotoolkit.process.ProcessListener}
* to
*
* @param listener
* @throws DataStoreException
* @throws TransformException
* @throws FactoryException
*/
public void buildPyramid(final ProcessListener listener)
throws DataStoreException, TransformException, FactoryException, IOException {
List<GridCoverage2D> coverages = getCoveragesPyramid();
int coverageCount = 1;
for (GridCoverage2D coverage : coverages) {
final Map<Envelope, double[]> resolution_Per_Envelope = getResolutionPerEnvelope();
if (resolution_Per_Envelope.isEmpty()) {
final Envelope coverageEnv = coverage.getEnvelope();
final GridGeometry2D gg = coverage.getGridGeometry();
int gridspan = gg.getExtent2D().getSpan(0);
//calculate scales
final double spanX = coverageEnv.getSpan(0);
final double baseScale = spanX / gridspan;
double scale = spanX / 256;
double[] scales = new double[0];
while (true) {
if (scale <= baseScale) {
//fit to exact match to preserve base quality.
scale = baseScale;
}
scales = ArraysExt.insert(scales, scales.length, 1);
scales[scales.length - 1] = scale;
if (scale <= baseScale) {
break;
}
scale = scale / 2;
}
resolution_Per_Envelope.put(coverageEnv, scales);
}
pyramidCoverageBuilder.create(coverage, getCoverageStore(),
coverageNamer.getName(baseCoverageName, coverageCount),
resolution_Per_Envelope, null, listener, null);
coverageCount++;
}
}
/**
* Command that produce coverage names during the pyramid generation.
*
* @author olivier.nouguier@geomatys.com
*/
public interface CoverageNamer {
/**
* @param baseName
* @param n
* @return
*/
GenericName getName(String baseName, int n);
}
/**
* Exposed builder when output is set.
*
* @author olivier.nouguier@geomatys.com
*/
public static interface WithOutput {
WithOutput outputFormat(String output);
PyramidCoverageHelper build() throws DataStoreException;
}
/**
* Exposed when PGStorage is set.
*
* @author olivier.nouguier@geomatys.com
*/
public interface WithPGOutput extends WithOutput {
WithPGOutput withHostname(String hostname);
WithPGOutput withPgPort(int port);
WithPGOutput withSchema(String schema);
}
public static interface WithInput {
WithOutput toFileStore(String path) throws MalformedURLException;
WithOutput toMemoryStore();
WithPGOutput toPostGisStore(String databaseName, String login,
String password);
}
/**
* Non exposed builder.
*
* @author olivier.nouguier@geomatys.com
*/
private static abstract class WithOutputImpl implements WithOutput {
Builder builder;
public WithOutputImpl(Builder builder) {
this.builder = builder;
}
@Override
public WithOutput outputFormat(String output) {
builder.outputFormat = output;
return this;
}
abstract CoverageStore createOutputStore() throws DataStoreException;
@Override
public PyramidCoverageHelper build() throws DataStoreException {
PyramidCoverageHelper helper = new PyramidCoverageHelper(builder);
return helper;
}
}
/**
* Only exposed to this classes.
*
* @author olivier.nouguier@geomatys.com
*/
public static abstract class WithInputImpl implements WithInput {
Builder builder;
public WithInputImpl(final Builder builder) {
this.builder = builder;
}
@Override
public WithOutput toFileStore(final String path) throws MalformedURLException {
final WithFileOutput fileOutput = new WithFileOutput(builder);
fileOutput.tileFolder = new File(path, "tiles").toURI().toURL();
builder.output = fileOutput;
return fileOutput;
}
@Override
public WithOutput toMemoryStore() {
final WithMemoryOutput memoryOutput = new WithMemoryOutput(builder);
builder.output = memoryOutput;
return memoryOutput;
}
@Override
public WithPGOutput toPostGisStore(String databaseName, String login,
String password) {
final WithPGOutputImpl pgOutput = new WithPGOutputImpl(builder);
pgOutput.pgDatabaseName = databaseName;
pgOutput.pgLogin = login;
pgOutput.pgPassword = password;
builder.output = pgOutput;
return pgOutput;
}
protected abstract CoverageStore buildInputStore(String inputFormat)
throws DataStoreException;
/**
* Inner builder when output is set to file.
*
* @author olivier.nouguier@geomatys.com
*/
private static class WithFileOutput extends WithOutputImpl {
public URL tileFolder;
public WithFileOutput(Builder builder) {
super(builder);
}
@Override
protected CoverageStore createOutputStore() throws DataStoreException {
final XMLCoverageStoreFactory factory = new XMLCoverageStoreFactory();
Map<String, Serializable> parameters = new HashMap<>();
parameters.put("path", tileFolder);
parameters.put("type", builder.outputFormat);
return factory.create(parameters);
}
}
private static class WithMemoryOutput extends WithOutputImpl {
public WithMemoryOutput(Builder builder) {
super(builder);
}
@Override
public CoverageStore createOutputStore() {
return new MPCoverageStore();
}
}
public static class WithPGOutputImpl extends WithOutputImpl implements WithPGOutput {
private String pgPassword;
private String pgLogin;
private String pgDatabaseName;
private String pgHostname = "localhost";
private int pgPort = 5432;
private String pgSchema = "pgcoverage";
public WithPGOutputImpl(Builder builder) {
super(builder);
}
@Override
public WithPGOutput withHostname(String hostname) {
this.pgHostname = hostname;
return this;
}
@Override
public WithPGOutput withPgPort(int port) {
this.pgPort = port;
return this;
}
@Override
public WithPGOutput withSchema(String schema) {
this.pgSchema = schema;
return this;
}
@Override
protected CoverageStore createOutputStore()
throws DataStoreException {
PGCoverageStoreFactory coverageStoreFactory = new PGCoverageStoreFactory();
ParameterDescriptorGroup parametersDescriptor = coverageStoreFactory
.getParametersDescriptor();
ParameterValueGroup params = parametersDescriptor.createValue();
params.parameter("host").setValue(pgHostname);
params.parameter("port").setValue(pgPort);
params.parameter("database").setValue(pgDatabaseName);
params.parameter("user").setValue(pgLogin);
params.parameter("password").setValue(pgPassword);
params.parameter("schema").setValue(pgSchema);
return coverageStoreFactory.create(params);
}
}
}
/**
* Inner builder when input is set.
*
* @author olivier.nouguier@geomatys.com
*/
private static class WithFileInput extends WithInputImpl {
public URL imageFile;
public WithFileInput(final Builder builder) {
super(builder);
}
@Override
protected CoverageStore buildInputStore(final String inputFormat)
throws DataStoreException {
final ParameterValueGroup params = FileCoverageStoreFactory.PARAMETERS_DESCRIPTOR
.createValue();
Parameters.getOrCreate(FileCoverageStoreFactory.PATH, params)
.setValue(imageFile);
Parameters.getOrCreate(FileCoverageStoreFactory.TYPE, params)
.setValue(inputFormat);
return CoverageStoreFinder.open(params);
}
}
/**
* External builder.
*
* @author Olivier NOUGUIER
*/
public static class Builder implements IBuilder {
/**
* Mandatory property, it is the base name used to define coverage(s)
* name: eg: name="forest_" then coverages names are "forest_1",
* "forest_2"
*/
// General inputs.
final private String baseCoverageName;
private int tileWidth = 256;
private int tileHeight = 256;
private String outputFormat = "PNG";
private String inputFormat = "geotiff";
private double[] depth = new double[] { 1, 0.5, 0.25, 0.125 };
private Envelope envelope = null;
private InterpolationCase interpolation = InterpolationCase.BILINEAR;
private CoverageNamer coverageNamer = new CoverageNamer() {
@Override
public GenericName getName(String baseName, int n) {
return NamesExt.create(baseName + n);
}
};
private WithInputImpl input;
private WithOutputImpl output;
private Builder(String baseCoverageName) {
this.baseCoverageName = baseCoverageName;
}
@Override
public IBuilder inputFormat(String inputFormat) {
this.inputFormat = inputFormat;
return this;
}
@Override
public IBuilder withDeeps(double[] deeps) {
this.depth = deeps;
return this;
}
@Override
public IBuilder withEnvelope(Envelope envelope) {
this.envelope = envelope;
return this;
}
@Override
public IBuilder withTile(int width, int height) {
this.tileWidth = width;
this.tileHeight = height;
return this;
}
@Override
public IBuilder withInterpolation(InterpolationCase interpolation) {
this.interpolation = interpolation;
return this;
}
@Override
public WithInput fromImage(String path) throws MalformedURLException {
WithFileInput fileInput = new WithFileInput(this);
fileInput.imageFile = new File(path).toURI().toURL();
input = fileInput;
return input;
}
@Override
public IBuilder withBaseCoverageNamer(CoverageNamer coverageNamer) {
this.coverageNamer = coverageNamer;
return this;
}
public CoverageStore buildOutputStore() throws DataStoreException {
return output.createOutputStore();
}
private CoverageStore buildInputStore() throws DataStoreException {
return input.buildInputStore(inputFormat);
}
}
}