| Current File : /home/jvzmxxx/wiki1/vendor/data-values/geo/src/Formatters/GeoCoordinateFormatter.php |
<?php
namespace DataValues\Geo\Formatters;
use DataValues\Geo\Values\LatLongValue;
use InvalidArgumentException;
use ValueFormatters\FormatterOptions;
use ValueFormatters\ValueFormatterBase;
/**
* Geographical coordinates formatter.
* Formats LatLongValue objects.
*
* Supports the following notations:
* - Degree minute second
* - Decimal degrees
* - Decimal minutes
* - Float
*
* Some code in this class has been borrowed from the
* MapsCoordinateParser class of the Maps extension for MediaWiki.
*
* @since 0.1
*
* @license GPL-2.0+
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
* @author Addshore
* @author Thiemo Mättig
*/
class GeoCoordinateFormatter extends ValueFormatterBase {
/**
* Output formats for use with the self::OPT_FORMAT option.
*/
const TYPE_FLOAT = 'float';
const TYPE_DMS = 'dms';
const TYPE_DM = 'dm';
const TYPE_DD = 'dd';
/**
* The symbols representing the different directions for usage in directional notation.
* @since 0.1
*/
const OPT_NORTH_SYMBOL = 'north';
const OPT_EAST_SYMBOL = 'east';
const OPT_SOUTH_SYMBOL = 'south';
const OPT_WEST_SYMBOL = 'west';
/**
* The symbols representing degrees, minutes and seconds.
* @since 0.1
*/
const OPT_DEGREE_SYMBOL = 'degree';
const OPT_MINUTE_SYMBOL = 'minute';
const OPT_SECOND_SYMBOL = 'second';
/**
* Flags for use with the self::OPT_SPACING_LEVEL option.
*/
const OPT_SPACE_LATLONG = 'latlong';
const OPT_SPACE_DIRECTION = 'direction';
const OPT_SPACE_COORDPARTS = 'coordparts';
/**
* Option specifying the output format (also referred to as output type). Must be one of the
* self::TYPE_… constants.
*/
const OPT_FORMAT = 'geoformat';
/**
* Boolean option specifying if negative coordinates should have minus signs, e.g. "-1°, -2°"
* (false) or cardinal directions, e.g. "1° S, 2° W" (true). Default is false.
*/
const OPT_DIRECTIONAL = 'directional';
/**
* Option for the separator character between latitude and longitude. Defaults to a comma.
*/
const OPT_SEPARATOR_SYMBOL = 'separator';
/**
* Option specifying the amount and position of space characters in the output. Must be an array
* containing zero or more of the self::OPT_SPACE_… flags.
*/
const OPT_SPACING_LEVEL = 'spacing';
/**
* Option specifying the precision in fractional degrees. Must be a number or numeric string.
*/
const OPT_PRECISION = 'precision';
/**
* @since 0.1
*
* @param FormatterOptions|null $options
*/
public function __construct( FormatterOptions $options = null ) {
parent::__construct( $options );
$this->defaultOption( self::OPT_NORTH_SYMBOL, 'N' );
$this->defaultOption( self::OPT_EAST_SYMBOL, 'E' );
$this->defaultOption( self::OPT_SOUTH_SYMBOL, 'S' );
$this->defaultOption( self::OPT_WEST_SYMBOL, 'W' );
$this->defaultOption( self::OPT_DEGREE_SYMBOL, '°' );
$this->defaultOption( self::OPT_MINUTE_SYMBOL, "'" );
$this->defaultOption( self::OPT_SECOND_SYMBOL, '"' );
$this->defaultOption( self::OPT_FORMAT, self::TYPE_FLOAT );
$this->defaultOption( self::OPT_DIRECTIONAL, false );
$this->defaultOption( self::OPT_SEPARATOR_SYMBOL, ',' );
$this->defaultOption( self::OPT_SPACING_LEVEL, array(
self::OPT_SPACE_LATLONG,
self::OPT_SPACE_DIRECTION,
self::OPT_SPACE_COORDPARTS,
) );
$this->defaultOption( self::OPT_PRECISION, 0 );
}
/**
* @see ValueFormatter::format
*
* Calls formatLatLongValue() using OPT_PRECISION for the $precision parameter.
*
* @param LatLongValue $value
*
* @return string Plain text
* @throws InvalidArgumentException
*/
public function format( $value ) {
if ( !( $value instanceof LatLongValue ) ) {
throw new InvalidArgumentException( 'Data value type mismatch. Expected a LatLongValue.' );
}
$precision = $this->options->getOption( self::OPT_PRECISION );
return $this->formatLatLongValue( $value, $precision );
}
/**
* Formats a LatLongValue with the desired precision.
*
* @since 0.5
*
* @param LatLongValue $value
* @param float|int $precision The desired precision, given as fractional degrees.
*
* @return string Plain text
* @throws InvalidArgumentException
*/
public function formatLatLongValue( LatLongValue $value, $precision ) {
if ( $precision <= 0 || !is_finite( $precision ) ) {
$precision = 1 / 3600;
}
$formatted = implode(
$this->getOption( self::OPT_SEPARATOR_SYMBOL ) . $this->getSpacing( self::OPT_SPACE_LATLONG ),
array(
$this->formatLatitude( $value->getLatitude(), $precision ),
$this->formatLongitude( $value->getLongitude(), $precision )
)
);
return $formatted;
}
/**
* @param string $spacingLevel One of the self::OPT_SPACE_… constants
*
* @return string
*/
private function getSpacing( $spacingLevel ) {
if ( in_array( $spacingLevel, $this->getOption( self::OPT_SPACING_LEVEL ) ) ) {
return ' ';
}
return '';
}
/**
* @param float $latitude
* @param float|int $precision
*
* @return string
*/
private function formatLatitude( $latitude, $precision ) {
return $this->makeDirectionalIfNeeded(
$this->formatCoordinate( $latitude, $precision ),
$this->options->getOption( self::OPT_NORTH_SYMBOL ),
$this->options->getOption( self::OPT_SOUTH_SYMBOL )
);
}
/**
* @param float $longitude
* @param float|int $precision
*
* @return string
*/
private function formatLongitude( $longitude, $precision ) {
return $this->makeDirectionalIfNeeded(
$this->formatCoordinate( $longitude, $precision ),
$this->options->getOption( self::OPT_EAST_SYMBOL ),
$this->options->getOption( self::OPT_WEST_SYMBOL )
);
}
/**
* @param string $coordinate
* @param string $positiveSymbol
* @param string $negativeSymbol
*
* @return string
*/
private function makeDirectionalIfNeeded( $coordinate, $positiveSymbol, $negativeSymbol ) {
if ( $this->options->getOption( self::OPT_DIRECTIONAL ) ) {
return $this->makeDirectional( $coordinate, $positiveSymbol, $negativeSymbol );
}
return $coordinate;
}
/**
* @param string $coordinate
* @param string $positiveSymbol
* @param string $negativeSymbol
*
* @return string
*/
private function makeDirectional( $coordinate, $positiveSymbol, $negativeSymbol ) {
$isNegative = substr( $coordinate, 0, 1 ) === '-';
if ( $isNegative ) {
$coordinate = substr( $coordinate, 1 );
}
$symbol = $isNegative ? $negativeSymbol : $positiveSymbol;
return $coordinate . $this->getSpacing( self::OPT_SPACE_DIRECTION ) . $symbol;
}
/**
* @param float $degrees
* @param float|int $precision
*
* @return string
*/
private function formatCoordinate( $degrees, $precision ) {
// Remove insignificant detail
$degrees = $this->roundDegrees( $degrees, $precision );
switch ( $this->getOption( self::OPT_FORMAT ) ) {
case self::TYPE_FLOAT:
return $this->getInFloatFormat( $degrees );
case self::TYPE_DMS:
return $this->getInDegreeMinuteSecondFormat( $degrees, $precision );
case self::TYPE_DD:
return $this->getInDecimalDegreeFormat( $degrees, $precision );
case self::TYPE_DM:
return $this->getInDecimalMinuteFormat( $degrees, $precision );
default:
throw new InvalidArgumentException( 'Invalid coordinate format specified in the options' );
}
}
/**
* Round degrees according to OPT_PRECISION
*
* @param float $degrees
* @param float|int $precision
*
* @return float
*/
private function roundDegrees( $degrees, $precision ) {
$sign = $degrees > 0 ? 1 : -1;
$reduced = round( abs( $degrees ) / $precision );
$expanded = $reduced * $precision;
return $sign * $expanded;
}
/**
* @param float $floatDegrees
*
* @return string
*/
private function getInFloatFormat( $floatDegrees ) {
$stringDegrees = (string)$floatDegrees;
// Floats are fun...
if ( $stringDegrees === '-0' ) {
$stringDegrees = '0';
}
return $stringDegrees;
}
/**
* @param float $floatDegrees
* @param float|int $precision
*
* @return string
*/
private function getInDecimalDegreeFormat( $floatDegrees, $precision ) {
$degreeDigits = $this->getSignificantDigits( 1, $precision );
$stringDegrees = $this->formatNumber( $floatDegrees, $degreeDigits );
return $stringDegrees . $this->options->getOption( self::OPT_DEGREE_SYMBOL );
}
/**
* @param float $floatDegrees
* @param float|int $precision
*
* @return string
*/
private function getInDegreeMinuteSecondFormat( $floatDegrees, $precision ) {
$isNegative = $floatDegrees < 0;
$floatDegrees = abs( $floatDegrees );
$degrees = (int)$floatDegrees;
$minutes = (int)( ( $floatDegrees - $degrees ) * 60 );
$seconds = ( $floatDegrees - ( $degrees + $minutes / 60 ) ) * 3600;
$result = $this->formatNumber( $degrees )
. $this->options->getOption( self::OPT_DEGREE_SYMBOL );
if ( $precision < 1 ) {
$result .= $this->getSpacing( self::OPT_SPACE_COORDPARTS )
. $this->formatNumber( $minutes )
. $this->options->getOption( self::OPT_MINUTE_SYMBOL );
}
if ( $precision < 1 / 60 ) {
$secondDigits = $this->getSignificantDigits( 60 * 60, $precision );
$result .= $this->getSpacing( self::OPT_SPACE_COORDPARTS )
. $this->formatNumber( $seconds, $secondDigits )
. $this->options->getOption( self::OPT_SECOND_SYMBOL );
}
if ( $isNegative && ( $degrees + $minutes + $seconds ) > 0 ) {
$result = '-' . $result;
}
return $result;
}
/**
* @param float $floatDegrees
* @param float|int $precision
*
* @return string
*/
private function getInDecimalMinuteFormat( $floatDegrees, $precision ) {
$isNegative = $floatDegrees < 0;
$floatDegrees = abs( $floatDegrees );
$degrees = (int)$floatDegrees;
$minutes = ( $floatDegrees - $degrees ) * 60;
$result = $this->formatNumber( $degrees )
. $this->options->getOption( self::OPT_DEGREE_SYMBOL );
if ( $precision < 1 ) {
$minuteDigits = $this->getSignificantDigits( 60, $precision );
$result .= $this->getSpacing( self::OPT_SPACE_COORDPARTS )
. $this->formatNumber( $minutes, $minuteDigits )
. $this->options->getOption( self::OPT_MINUTE_SYMBOL );
}
if ( $isNegative && ( $degrees + $minutes ) > 0 ) {
$result = '-' . $result;
}
return $result;
}
/**
* @param float|int $unitsPerDegree The number of target units per degree
* (60 for minutes, 3600 for seconds)
* @param float|int $degreePrecision
*
* @return int The number of digits to show after the decimal point
* (resp. before, if the result is negative).
*/
private function getSignificantDigits( $unitsPerDegree, $degreePrecision ) {
return (int)ceil( -log10( $unitsPerDegree * $degreePrecision ) );
}
/**
* @param float $number
* @param int $digits The number of digits after the decimal point.
*
* @return string
*/
private function formatNumber( $number, $digits = 0 ) {
// TODO: use NumberLocalizer
return sprintf( '%.' . ( $digits > 0 ? $digits : 0 ) . 'F', $number );
}
}