All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arakelian.jackson.model.Coordinate Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 com.arakelian.jackson.model;

import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Iterator;
import java.util.regex.Pattern;

import org.immutables.value.Value;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;

@Value.Immutable(copy = false)
@JsonSerialize(using = Coordinate.CoordinateSerializer.class)
@JsonDeserialize(using = Coordinate.CoordinateDeserializer.class)
@JsonPropertyOrder({ "lat", "lon" })
public abstract class Coordinate implements Serializable, Comparable {
    public static class CoordinateDeserializer extends JsonDeserializer {
        @Override
        public Coordinate deserialize(final JsonParser p, final DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            final JsonNode node = ctxt.readValue(p, JsonNode.class);
            if (node instanceof ArrayNode) {
                final ArrayNode arr = (ArrayNode) node;
                final int size = arr.size();
                if (size == 2) {
                    return ImmutableCoordinate.builder() //
                            .x(arr.get(0).asDouble()) //
                            .y(arr.get(1).asDouble()) //
                            .build();
                } else if (size == 3) {
                    return ImmutableCoordinate.builder() //
                            .x(arr.get(0).asDouble()) //
                            .y(arr.get(1).asDouble()) //
                            .z(arr.get(2).asDouble()) //
                            .build();
                }

                // always throws exception
                ctxt.reportInputMismatch(
                        this,
                        "Expecting array with 2 or 3 elements but found %s elements",
                        size);
                return null;

            }

            if (node instanceof TextNode) {
                final TextNode text = (TextNode) node;
                final String v = text.asText("");
                return of(v);
            }

            // always throws exception
            ctxt.reportInputMismatch(this, "Expecting array, object or text node");
            return null;
        }
    }

    public static class CoordinateSerializer extends JsonSerializer {
        @Override
        public void serialize(
                final Coordinate value,
                final JsonGenerator gen,
                final SerializerProvider serializers) throws IOException, JsonProcessingException {
            gen.writeStartArray();
            gen.writeNumber(value.getX());
            gen.writeNumber(value.getY());

            final double z = value.getZ();
            if (!Double.isNaN(z)) {
                gen.writeNumber(z);
            }

            gen.writeEndArray();
        }
    }

    private static final Splitter COMMA_SPLITTER = Splitter.on(Pattern.compile("\\s*,\\s*"));

    /**
     * Default number of decimals places that we round to
     */
    public static final int DEFAULT_PLACES = 6;

    /**
     * Error margin when comparison x/y/z values
     */
    public static final double DEFAULT_ERROR = 0.000001d;

    /**
     * The value used to indicate a null or missing ordinate value. In particular, used for the
     * value of ordinates for dimensions greater than the defined dimension of a coordinate.
     */
    public static final double NULL_ORDINATE = Double.NaN;

    private static boolean equalsWithTolerance(final double x1, final double x2, final double tolerance) {
        return Math.abs(x1 - x2) <= tolerance;
    }

    public static Coordinate of(final double x, final double y) {
        return ImmutableCoordinate.builder() //
                .x(x) //
                .y(y) //
                .build();
    }

    public static Coordinate of(final double x, final double y, final double z) {
        return ImmutableCoordinate.builder() //
                .x(x) //
                .y(y) //
                .z(z) //
                .build();
    }

    public static Coordinate of(final String value) {
        final Iterator it = COMMA_SPLITTER.split(value).iterator();

        Preconditions.checkState(it.hasNext(), "Expected coordinate in x,y,z format but have: %s", value);
        final double x = Double.parseDouble(it.next());

        Preconditions.checkState(it.hasNext(), "Expected coordinate in x,y,z format but have: %s", value);
        final double y = Double.parseDouble(it.next());

        final double z;
        if (it.hasNext()) {
            z = Double.parseDouble(it.next());
            Preconditions
                    .checkState(!it.hasNext(), "Expected coordinate in x,y,z format but have: %s", value);
        } else {
            z = NULL_ORDINATE;
        }

        return of(x, y, z);
    }

    public static double round(final double value) {
        return round(value, DEFAULT_PLACES);
    }

    public static double round(final double value, final int places) {
        Preconditions.checkArgument(places >= 0, "places must be >= 0");

        if (Double.isNaN(value)) {
            return value;
        }

        return new BigDecimal(Double.toString(value)) //
                .setScale(places, RoundingMode.HALF_UP) //
                .doubleValue();
    }

    /**
     * Compares this {@link Coordinate} with the specified {@link Coordinate} for order. This method
     * ignores the z value when making the comparison. Returns:
     * 
    *
  • -1 : this.x < other.x || ((this.x == other.x) && (this.y < other.y)) *
  • 0 : this.x == other.x && this.y = other.y *
  • 1 : this.x > other.x || ((this.x == other.x) && (this.y > other.y)) * *
* Note: This method assumes that ordinate values are valid numbers. NaN values are not handled * correctly. * * @param other * the Coordinate with which this Coordinate is being * compared * @return -1, zero, or 1 as this Coordinate is less than, equal to, or greater * than the specified Coordinate */ @Override public int compareTo(final Coordinate other) { if (getX() < other.getX()) { return -1; } if (getX() > other.getX()) { return 1; } if (getY() < other.getY()) { return -1; } if (getY() > other.getY()) { return 1; } return 0; } /** * Computes the 2-dimensional Euclidean distance to another location. The Z-ordinate is ignored. * * @param c * a point * @return the 2-dimensional Euclidean distance between the locations */ public double distance(final Coordinate c) { final double dx = getX() - c.getX(); final double dy = getY() - c.getY(); return Math.sqrt(dx * dx + dy * dy); } /** * Computes the 3-dimensional Euclidean distance to another location. * * @param c * a coordinate * @return the 3-dimensional Euclidean distance between the locations */ public double distance3D(final Coordinate c) { final double dx = getX() - c.getX(); final double dy = getY() - c.getY(); final double dz = getZ() - c.getZ(); return Math.sqrt(dx * dx + dy * dy + dz * dz); } /** * Tests if another coordinate has the same value for Z, within a tolerance. * * @param c * a coordinate * @param tolerance * the tolerance value * @return true if the Z ordinates are within the given tolerance */ public boolean equalInZ(final Coordinate c, final double tolerance) { return equalsWithTolerance(this.getZ(), c.getZ(), tolerance); } /** * Tests if another coordinate has the same values for the X and Y ordinates. The Z ordinate is * ignored. * * @param c * a Coordinate with which to do the 2D comparison. * @param tolerance * margin of error * @return true if other is a Coordinate with the same values for X * and Y. */ public boolean equals2D(final Coordinate c, final double tolerance) { if (!equalsWithTolerance(this.getX(), c.getX(), tolerance)) { return false; } if (!equalsWithTolerance(this.getY(), c.getY(), tolerance)) { return false; } return true; } /** * Tests if another coordinate has the same values for the X, Y and Z ordinates. * * @param other * a Coordinate with which to do the 3D comparison. * @return true if other is a Coordinate with the same values for X, Y * and Z. */ public boolean equals3D(final Coordinate other) { return getX() == other.getX() && getY() == other.getY() && (getZ() == other.getZ() || Double.isNaN(getZ()) && Double.isNaN(other.getZ())); } public abstract double getX(); public abstract double getY(); @Value.Default public double getZ() { return NULL_ORDINATE; } @Value.Check protected Coordinate normalize() { final double x = getX(); if (Double.isNaN(x)) { throw new IllegalArgumentException("invalid x: " + x); } final double y = getY(); if (Double.isNaN(y)) { throw new IllegalArgumentException("invalid y: " + y); } // round decimals return round(DEFAULT_PLACES); } public Coordinate round(final int places) { final double x = getX(); final double newX = round(x, places); final double y = getY(); final double newY = round(y, places); final double z = getZ(); final double newZ = round(z, places); if (Double.doubleToLongBits(x) == Double.doubleToLongBits(newX) && Double.doubleToLongBits(y) == Double.doubleToLongBits(newY) && Double.doubleToLongBits(z) == Double.doubleToLongBits(newZ)) { return this; } return of(newX, newY, newZ); } /** * Returns a String of the form (x,y,z) . * * @return a String of the form (x,y,z) */ @Override public String toString() { return "(" + getX() + ", " + getY() + ", " + getZ() + ")"; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy