# -*- coding: UTF-8 -*-
"""
:filename: sppas.src.anndata.annlocation.point.py
:author: Brigitte Bigi
:contact: develop@sppas.org
:summary: Represent a point made of a midpoint and a radius.
.. _This file is part of SPPAS: http://www.sppas.org/
..
-------------------------------------------------------------------------
___ __ __ __ ___
/ | \ | \ | \ / the automatic
\__ |__/ |__/ |___| \__ annotation and
\ | | | | \ analysis
___/ | | | | ___/ of speech
Copyright (C) 2011-2021 Brigitte Bigi
Laboratoire Parole et Langage, Aix-en-Provence, France
Use of this software is governed by the GNU Public License, version 3.
SPPAS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
SPPAS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SPPAS. If not, see <http://www.gnu.org/licenses/>.
This banner notice must not be removed.
-------------------------------------------------------------------------
"""
import logging
from sppas.src.config.makeunicode import text_type, binary_type
from ...anndataexc import AnnDataTypeError
from ...anndataexc import AnnDataNegValueError
from .localization import sppasBaseLocalization
from .duration import sppasDuration
# ---------------------------------------------------------------------------
[docs]class sppasPoint(sppasBaseLocalization):
"""Localization of a point for any numerical representation.
Represents a point identified by a midpoint value and a radius value.
Generally, time is represented in seconds, as a float value ; frames
are represented by integers like ranks.
In this class, the 3 relations <, = and > take into account a radius
value, that represents the uncertainty of the localization. For a point
x, with a radius value of rx, and a point y with a radius value of ry,
these relations are defined as:
- x = y iff |x - y| <= rx + ry
- x < y iff not(x = y) and x < y
- x > y iff not(x = y) and x > y
:Example 1: Strictly equals:
- x = 1.000, rx=0.
- y = 1.000, ry=0.
- x = y is true
- x = 1.00000000000, rx=0.
- y = 0.99999999675, ry=0.
- x = y is false
:Example 2: Using the radius:
- x = 1.0000000000, rx=0.0005
- y = 1.0000987653, ry=0.0005
- x = y is true (accepts a margin of 1ms between x and y)
- x = 1.0000000, rx=0.0005
- y = 1.0011235, ry=0.0005
- x = y is false
So... an overlap of the vagueness "area" makes the two points equals:
|------------rx----------X-----ry===rx----Y--------ry------|
"""
[docs] def __init__(self, midpoint, radius=None):
"""Create a sppasPoint instance.
:param midpoint: (float, int) midpoint value.
:param radius: (float, int) represents the vagueness of the point.
Radius must be of the same type as midpoint.
"""
super(sppasPoint, self).__init__()
self.__midpoint = 0
self.__radius = None
self.set_midpoint(midpoint)
self.set_radius(radius)
# -----------------------------------------------------------------------
[docs] def set(self, other):
"""Set self members from another sppasPoint instance.
:param other: (sppasPoint)
"""
if isinstance(other, sppasPoint) is False:
raise AnnDataTypeError(other, "sppasPoint")
self.set_midpoint(other.get_midpoint())
self.set_radius(other.get_radius())
# -----------------------------------------------------------------------
[docs] def is_point(self):
"""Override. Return True, because self represents a point."""
return True
# -----------------------------------------------------------------------
[docs] def copy(self):
"""Return a deep copy of self."""
return sppasPoint(self.__midpoint, self.__radius)
# -----------------------------------------------------------------------
[docs] def get_midpoint(self):
"""Return the midpoint value."""
return self.__midpoint
# -----------------------------------------------------------------------
[docs] def set_midpoint(self, midpoint):
"""Set the midpoint value.
In versions < 1.9.8, it was required that midpoint >= 0.
Negative values are now accepted because some annotations are not
properly synchronized and then some of them can be negative.
:param midpoint: (float, int) is the new midpoint value.
:raise: AnnDataTypeError
"""
if isinstance(midpoint, (int, float, text_type, binary_type)) is False:
raise AnnDataTypeError(midpoint, "float, int")
if isinstance(midpoint, (int, text_type, binary_type)) is True:
try:
self.__midpoint = int(midpoint)
if self.__midpoint < 0:
logging.warning('Midpoint is negative: {:d}'
''.format(midpoint))
# self.__midpoint = 0
# raise AnnDataNegValueError(midpoint)
return
except ValueError:
pass # will try with float...
try:
self.__midpoint = float(midpoint)
except ValueError:
raise AnnDataTypeError(midpoint, "float, int")
if self.__midpoint < 0.:
logging.warning('Midpoint is negative: {:f}'
''.format(midpoint))
# self.__midpoint = 0.
# raise AnnDataNegValueError(midpoint)
# -----------------------------------------------------------------------
[docs] def get_radius(self):
"""Return the radius value (float or None)."""
return self.__radius
# -----------------------------------------------------------------------
[docs] def set_radius(self, radius=None):
"""Fix the radius value, ie. the vagueness of the point.
The midpoint value must be set first.
:param radius: (float, int, None) the radius value
:raise: AnnDataTypeError, AnnDataNegValueError
"""
if radius is not None:
if sppasPoint.check_types(self.__midpoint, radius) is False:
raise AnnDataTypeError(radius, str(type(self.__midpoint)))
if isinstance(radius, float):
try:
radius = float(radius)
if radius < 0.:
raise AnnDataNegValueError(radius)
except TypeError:
raise AnnDataTypeError(radius, "float")
elif isinstance(radius, int):
try:
radius = int(radius)
if radius < 0:
raise AnnDataNegValueError(radius)
except TypeError:
raise AnnDataTypeError(radius, "int")
if self.__midpoint < radius:
radius = self.__midpoint
self.__radius = radius
# -----------------------------------------------------------------------
[docs] def shift(self, delay):
"""Shift the point to a given delay.
:param delay: (int, float) delay to shift midpoint
:raise: AnnDataTypeError
"""
if sppasPoint.check_types(self.__midpoint, delay) is False:
raise AnnDataTypeError(delay, str(type(self.__midpoint)))
self.__midpoint += delay
# -----------------------------------------------------------------------
[docs] def duration(self):
"""Overrides. Return the duration of the point.
:returns: (sppasDuration) Duration and its vagueness.
"""
if self.__radius is None:
return sppasDuration(0., 0.)
return sppasDuration(0., 2.0*self.get_radius())
# -----------------------------------------------------------------------
[docs] def is_int(self):
"""Return True if the value of the point is an integer."""
return isinstance(self.__midpoint, int)
# -----------------------------------------------------------------------
[docs] def is_float(self):
"""Return True if the value of the point is a float."""
return isinstance(self.__midpoint, float)
# -----------------------------------------------------------------------
[docs] @staticmethod
def check_types(x, y):
"""True only if midpoint and radius are both of the same types.
:param x: any kind of data
:param y: any kind of data
:returns: Boolean
"""
return isinstance(x, type(y))
# -----------------------------------------------------------------------
# overloads
# -----------------------------------------------------------------------
def __format__(self, fmt):
return str(self).__format__(fmt)
# -----------------------------------------------------------------------
def __repr__(self):
if self.__radius is None:
return "sppasPoint: {!s:s}".format(self.__midpoint)
return "sppasPoint: {!s:s}, {!s:s}".format(self.__midpoint,
self.__radius)
# -----------------------------------------------------------------------
def __str__(self):
if self.__radius is None:
return "{!s:s}".format(self.__midpoint)
return "({!s:s}, {!s:s})".format(self.__midpoint, self.__radius)
# -----------------------------------------------------------------------
def __eq__(self, other):
"""Equal is required to use '=='.
Used between 2 sppasPoint instances or
between a sppasPoint and an other object representing time.
This relationship takes into account the radius.
:param other: (sppasPoint, float, int) the other point to compare
"""
if isinstance(other, sppasPoint) is True:
delta = abs(self.__midpoint - other.get_midpoint())
radius = 0
if self.__radius is not None:
radius += self.__radius
if other.get_radius() is not None:
radius += other.get_radius()
return delta <= radius
if isinstance(other, (int, float)):
if self.__radius is None:
return self.__midpoint == other
delta = abs(self.__midpoint - other)
radius = self.__radius
return delta <= radius
return False
# -----------------------------------------------------------------------
def __lt__(self, other):
"""LowerThan is required to use '<'.
Used between 2 sppasPoint instances
or between a sppasPoint and an other time object.
:param other: (sppasPoint, float, int) the other point to compare
"""
if isinstance(other, sppasPoint) is True:
return self != other and self.__midpoint < other.get_midpoint()
return (self != other) and (self.__midpoint < other)
# -----------------------------------------------------------------------
def __gt__(self, other):
"""GreaterThan is required to use '>'.
Used between 2 sppasPoint instances
or between a sppasPoint and an other time object.
:param other: (sppasPoint, float, int) the other point to compare
"""
if isinstance(other, sppasPoint) is True:
return self != other and self.__midpoint > other.get_midpoint()
return (self != other) and (self.__midpoint > other)