dk.dma.dmiweather.grib.AbstractDataProvider.java Source code

Java tutorial

Introduction

Here is the source code for dk.dma.dmiweather.grib.AbstractDataProvider.java

Source

/* Copyright (c) 2011 Danish Maritime Authority.
 *
 * 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 dk.dma.dmiweather.grib;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.math.DoubleMath;
import dk.dma.common.dto.GeoCoordinate;
import dk.dma.common.exception.APIException;
import dk.dma.common.exception.ErrorMessage;
import dk.dma.common.util.MathUtil;
import lombok.Getter;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import ucar.grib.grib1.*;

import java.io.IOException;
import java.lang.ref.WeakReference;

import static dk.dma.dmiweather.service.GribFileWrapper.GRIB_NOT_DEFINED;

/**
 * Abstract class for providing data. Handles the request validation, and selecting the data points that match the request
 * subclasses provides the data.
 * This class does a simple WeakReference caching to void loading data from disk on every request
 *
 * @author Klaus Groenbaek
 *         Created 04/04/17.
 */
@Slf4j
@Getter
public abstract class AbstractDataProvider implements DataProvider {
    private final int dataRounding;
    private final double dx;
    private final double dy;
    private final int Ny;
    private final int Nx;
    private final double lo1;
    private final double la2;
    private final double la1;
    private final double lo2;
    private WeakReference<float[]> cachedData; // todo consider using a GUAVA cache, and share it between all dataProviders to get LRU behaviour

    AbstractDataProvider(ParameterAndRecord parameterAndRecord, int dataRounding) {
        this.dataRounding = dataRounding;
        // we have to recalculate dx, dy because the GRIB format is not precise enough (it only has two bytes, and then divides with 1000)
        Grib1GDSVariables vars = parameterAndRecord.record.getGDS().getGdsVars();
        Ny = vars.getNy();
        Nx = vars.getNx();
        dy = (vars.getLa2() - vars.getLa1()) / (Ny - 1);
        dx = (vars.getLo2() - vars.getLo1()) / (Nx - 1);
        lo1 = vars.getLo1();
        la2 = vars.getLa2();
        la1 = vars.getLa1();
        lo2 = vars.getLo2();
    }

    /**
     * Test constructor allowing you to set the grid metrics directly without needing a grib file
     */
    @VisibleForTesting
    protected AbstractDataProvider(int dataRounding, float dx, float dy, int ny, int nx, float lo1, float la2,
            float la1, float lo2) {
        this.dataRounding = dataRounding;
        this.dx = dx;
        this.dy = dy;
        Ny = ny;
        Nx = nx;
        this.lo1 = lo1;
        this.la2 = la2;
        this.la1 = la1;
        this.lo2 = lo2;
    }

    @Override
    public float[] getData(GeoCoordinate northWest, GeoCoordinate southEast, int Nx, int Ny, double lonSpacing,
            double latSpacing) {

        validate(northWest, southEast);

        int startY = (int) Math.round((southEast.getLat() - this.la1) / this.dy);
        int startX = (int) Math.round((northWest.getLon() - this.lo1) / this.dx);

        float[] grid = new float[Ny * Nx];

        double tolerance = 0.00001;
        if (DoubleMath.fuzzyEquals(this.dx, lonSpacing, tolerance)
                && DoubleMath.fuzzyEquals(this.dy, latSpacing, tolerance)) {
            // native resolution
            float[] data = roundAndCache();
            for (int row = 0; row < Ny; row++) {
                System.arraycopy(data, (startY + row) * this.Nx + startX, grid, row * Nx, Nx);
            }
        } else {
            if (Nx > this.Nx && Ny < this.Ny || Ny > this.Ny && Nx < this.Nx) {
                throw new APIException(ErrorMessage.INVALID_SCALING,
                        String.format("Native Nx:%s, Ny:%s, desired Nx:%s, Ny:%s", this.Nx, this.Ny, Nx, Ny));
            }

            if (Nx > this.Nx) {
                // scaling up
                float[] data = roundAndCache();
                for (int row = 0; row < Ny; row++) {
                    int y = startY + (int) Math.round(Math.floor(row * latSpacing));
                    for (int col = 0; col < Nx; col++) {
                        int x = startX + (int) Math.round(Math.floor(col * lonSpacing));

                        float datum = data[y * this.Nx + x];
                        grid[row * Nx + col] = datum;
                    }
                }

            } else {

                float[] data = roundAndCache();

                for (int row = 0; row < Ny; row++) {
                    int y = startY + (int) Math.round(row * latSpacing / this.dy);
                    for (int col = 0; col < Nx; col++) {

                        int x = startX + (int) Math.round(col * lonSpacing / this.dx);
                        float datum = data[y * this.Nx + x];

                        if (datum == GRIB_NOT_DEFINED) {
                            // when we down-sample we may end up selecting only NOT_DEFINED, so we look around to see if there is a defined value close by
                            int xSearchDistance = (int) Math.round(lonSpacing / 2);
                            int ySearchDistance = (int) Math.round(latSpacing / 2); // we search the space between points

                            float[] candidate = new float[] { this.Nx + this.Ny, GRIB_NOT_DEFINED }; // to hold the closest distance and the value

                            searchRow(data, y, x, xSearchDistance, candidate); // search this row

                            for (int i = 1; i <= ySearchDistance && i < candidate[0]; i++) {
                                if (y - i > 0) {
                                    // there is a row before
                                    searchRow(data, y - i, x, xSearchDistance, candidate);
                                } else if (y + i < this.Ny) {
                                    //there is a row after
                                    searchRow(data, y + i, x, xSearchDistance, candidate);
                                } else {
                                    break;
                                }
                            }
                            datum = candidate[1];
                        }

                        grid[row * Nx + col] = datum;
                    }
                }
            }
        }

        return grid;
    }

    private void searchRow(float[] data, int y, int x, int xSearchDistance, float[] candidate) {
        float current = data[y * this.Nx + x];
        if (current != GRIB_NOT_DEFINED) {
            candidate[0] = 1; // may not actually be distance 1, but it is the closest, because this is the first point in a new row
            candidate[1] = current;
            return;
        }
        // first search the current row left then right, taking care not to run outside the grid
        for (int i = 1; i <= xSearchDistance && i < candidate[0]; i++) {

            if (x - i > 0) {
                // we can go left
                current = data[y * this.Nx + x - i];
            } else if (x + i < this.Nx) {
                // we can go right
                current = data[y * this.Nx + x + i];
            } else {
                break;
            }
            if (current != GRIB_NOT_DEFINED) {
                candidate[0] = i; // distance
                candidate[1] = current; // value
            }
        }
    }

    @Synchronized
    private float[] roundAndCache() {
        if (cachedData == null) {
            float[] data = loadAndRound();
            cachedData = new WeakReference<>(data);

            return data;
        } else {
            float[] data = cachedData.get();
            if (data == null) {
                data = loadAndRound();
                cachedData = new WeakReference<>(data);
            }
            return data;
        }
    }

    private float[] loadAndRound() {
        float[] data = getData();
        if (dataRounding != -1) {
            for (int i = 0; i < data.length; i++) {
                if (data[i] != GRIB_NOT_DEFINED) {
                    data[i] = MathUtil.round(data[i], dataRounding);
                }
            }
        }
        return data;
    }

    public abstract float[] getData();

    @Override
    public void validate(GeoCoordinate northWest, GeoCoordinate southEast) {
        if (northWest.getLon() < lo1 || northWest.getLat() > la2 || southEast.getLon() > lo2
                || southEast.getLat() < la1) {
            throw new APIException(ErrorMessage.OUTSIDE_GRID,
                    String.format("Query is outside data grid, grid corners northWest:(%s), southEast:(%s)",
                            new GeoCoordinate(lo1, la1), new GeoCoordinate(lo2, la2)));
        }

        if (northWest.getLon() > southEast.getLon()) {
            throw new APIException(ErrorMessage.INVALID_GRID_LOT);
        }
        if (northWest.getLat() < southEast.getLat()) {
            throw new APIException(ErrorMessage.INVALID_GRID_LAT);
        }
    }

    protected float[] getData(Grib1ProductDefinitionSection pds, Grib1Data gd,
            ParameterAndRecord parameterAndRecord) throws IOException {
        return gd.getData(parameterAndRecord.record.getDataOffset(), pds.getDecimalScale(), pds.bmsExists());
    }
}