blob: 87aee3d626c7c09828633debdb3b9371f7ebe505 [file] [log] [blame]
/*
* Copyright 2005 Google Inc.
*
* 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 com.google.common.geometry;
import com.google.common.base.Preconditions;
/**
* An S2LatLngRect represents a latitude-longitude rectangle. It is capable of
* representing the empty and full rectangles as well as single points.
*
*/
public strictfp class S2LatLngRect implements S2Region {
private final R1Interval lat;
private final S1Interval lng;
/**
* Construct a rectangle from minimum and maximum latitudes and longitudes. If
* lo.lng() > hi.lng(), the rectangle spans the 180 degree longitude line.
*/
public S2LatLngRect(final S2LatLng lo, final S2LatLng hi) {
lat = new R1Interval(lo.lat().radians(), hi.lat().radians());
lng = new S1Interval(lo.lng().radians(), hi.lng().radians());
// assert (isValid());
}
/** Construct a rectangle from latitude and longitude intervals. */
public S2LatLngRect(R1Interval lat, S1Interval lng) {
this.lat = lat;
this.lng = lng;
// assert (isValid());
}
/** The canonical empty rectangle */
public static S2LatLngRect empty() {
return new S2LatLngRect(R1Interval.empty(), S1Interval.empty());
}
/** The canonical full rectangle. */
public static S2LatLngRect full() {
return new S2LatLngRect(fullLat(), fullLng());
}
/** The full allowable range of latitudes. */
public static R1Interval fullLat() {
return new R1Interval(-S2.M_PI_2, S2.M_PI_2);
}
/**
* The full allowable range of longitudes.
*/
public static S1Interval fullLng() {
return S1Interval.full();
}
/**
* Construct a rectangle from a center point (in lat-lng space) and size in
* each dimension. If size.lng() is greater than 360 degrees it is clamped,
* and latitudes greater than +/- 90 degrees are also clamped. So for example,
* FromCenterSize((80,170),(20,20)) -> (lo=(60,150),hi=(90,-170)).
*/
public static S2LatLngRect fromCenterSize(S2LatLng center, S2LatLng size) {
return fromPoint(center).expanded(size.mul(0.5));
}
/** Convenience method to construct a rectangle containing a single point. */
public static S2LatLngRect fromPoint(S2LatLng p) {
// assert (p.isValid());
return new S2LatLngRect(p, p);
}
/**
* Convenience method to construct the minimal bounding rectangle containing
* the two given points. This is equivalent to starting with an empty
* rectangle and calling AddPoint() twice. Note that it is different than the
* S2LatLngRect(lo, hi) constructor, where the first point is always used as
* the lower-left corner of the resulting rectangle.
*/
public static S2LatLngRect fromPointPair(S2LatLng p1, S2LatLng p2) {
// assert (p1.isValid() && p2.isValid());
return new S2LatLngRect(R1Interval.fromPointPair(p1.lat().radians(), p2
.lat().radians()), S1Interval.fromPointPair(p1.lng().radians(), p2.lng()
.radians()));
}
/**
* Return a latitude-longitude rectangle that contains the edge from "a" to
* "b". Both points must be unit-length. Note that the bounding rectangle of
* an edge can be larger than the bounding rectangle of its endpoints.
*/
public static S2LatLngRect fromEdge(S2Point a, S2Point b) {
// assert (S2.isUnitLength(a) && S2.isUnitLength(b));
S2LatLngRect r = fromPointPair(new S2LatLng(a), new S2LatLng(b));
// Check whether the min/max latitude occurs in the edge interior.
// We find the normal to the plane containing AB, and then a vector "dir" in
// this plane that also passes through the equator. We use RobustCrossProd
// to ensure that the edge normal is accurate even when the two points are
// very close together.
S2Point ab = S2.robustCrossProd(a, b);
S2Point dir = S2Point.crossProd(ab, new S2Point(0, 0, 1));
double da = dir.dotProd(a);
double db = dir.dotProd(b);
if (da * db >= 0) {
// Minimum and maximum latitude are attained at the vertices.
return r;
}
// Minimum/maximum latitude occurs in the edge interior. This affects the
// latitude bounds but not the longitude bounds.
double absLat = Math.acos(Math.abs(ab.z / ab.norm()));
if (da < 0) {
return new S2LatLngRect(new R1Interval(r.lat().lo(), absLat), r.lng());
} else {
return new S2LatLngRect(new R1Interval(-absLat, r.lat().hi()), r.lng());
}
}
/**
* Return true if the rectangle is valid, which essentially just means that
* the latitude bounds do not exceed Pi/2 in absolute value and the longitude
* bounds do not exceed Pi in absolute value.
*
*/
public boolean isValid() {
// The lat/lng ranges must either be both empty or both non-empty.
return (Math.abs(lat.lo()) <= S2.M_PI_2 && Math.abs(lat.hi()) <= S2.M_PI_2
&& lng.isValid() && lat.isEmpty() == lng.isEmpty());
}
// Accessor methods.
public S1Angle latLo() {
return S1Angle.radians(lat.lo());
}
public S1Angle latHi() {
return S1Angle.radians(lat.hi());
}
public S1Angle lngLo() {
return S1Angle.radians(lng.lo());
}
public S1Angle lngHi() {
return S1Angle.radians(lng.hi());
}
public R1Interval lat() {
return lat;
}
public S1Interval lng() {
return lng;
}
public S2LatLng lo() {
return new S2LatLng(latLo(), lngLo());
}
public S2LatLng hi() {
return new S2LatLng(latHi(), lngHi());
}
/**
* Return true if the rectangle is empty, i.e. it contains no points at all.
*/
public boolean isEmpty() {
return lat.isEmpty();
}
// Return true if the rectangle is full, i.e. it contains all points.
public boolean isFull() {
return lat.equals(fullLat()) && lng.isFull();
}
/**
* Return true if lng_.lo() > lng_.hi(), i.e. the rectangle crosses the 180
* degree latitude line.
*/
public boolean isInverted() {
return lng.isInverted();
}
/** Return the k-th vertex of the rectangle (k = 0,1,2,3) in CCW order. */
public S2LatLng getVertex(int k) {
// Return the points in CCW order (SW, SE, NE, NW).
switch (k) {
case 0:
return S2LatLng.fromRadians(lat.lo(), lng.lo());
case 1:
return S2LatLng.fromRadians(lat.lo(), lng.hi());
case 2:
return S2LatLng.fromRadians(lat.hi(), lng.hi());
case 3:
return S2LatLng.fromRadians(lat.hi(), lng.lo());
default:
throw new IllegalArgumentException("Invalid vertex index.");
}
}
/**
* Return the center of the rectangle in latitude-longitude space (in general
* this is not the center of the region on the sphere).
*/
public S2LatLng getCenter() {
return S2LatLng.fromRadians(lat.getCenter(), lng.getCenter());
}
/**
* Return the minimum distance (measured along the surface of the sphere)
* from a given point to the rectangle (both its boundary and its interior).
* The latLng must be valid.
*/
public S1Angle getDistance(S2LatLng p) {
// The algorithm here is the same as in getDistance(S2LagLngRect), only
// with simplified calculations.
S2LatLngRect a = this;
Preconditions.checkState(!a.isEmpty());
Preconditions.checkArgument(p.isValid());
if (a.lng().contains(p.lng().radians())) {
return S1Angle.radians(Math.max(0.0, Math.max(p.lat().radians() - a.lat().hi(),
a.lat().lo() - p.lat().radians())));
}
S1Interval interval = new S1Interval(a.lng().hi(), a.lng().complement().getCenter());
double aLng = a.lng().lo();
if (interval.contains(p.lng().radians())) {
aLng = a.lng().hi();
}
S2Point lo = S2LatLng.fromRadians(a.lat().lo(), aLng).toPoint();
S2Point hi = S2LatLng.fromRadians(a.lat().hi(), aLng).toPoint();
S2Point loCrossHi =
S2LatLng.fromRadians(0, aLng - S2.M_PI_2).normalized().toPoint();
return S2EdgeUtil.getDistance(p.toPoint(), lo, hi, loCrossHi);
}
/**
* Return the minimum distance (measured along the surface of the sphere) to
* the given S2LatLngRect. Both S2LatLngRects must be non-empty.
*/
public S1Angle getDistance(S2LatLngRect other) {
S2LatLngRect a = this;
S2LatLngRect b = other;
Preconditions.checkState(!a.isEmpty());
Preconditions.checkArgument(!b.isEmpty());
// First, handle the trivial cases where the longitude intervals overlap.
if (a.lng().intersects(b.lng())) {
if (a.lat().intersects(b.lat())) {
return S1Angle.radians(0); // Intersection between a and b.
}
// We found an overlap in the longitude interval, but not in the latitude
// interval. This means the shortest path travels along some line of
// longitude connecting the high-latitude of the lower rect with the
// low-latitude of the higher rect.
S1Angle lo, hi;
if (a.lat().lo() > b.lat().hi()) {
lo = b.latHi();
hi = a.latLo();
} else {
lo = a.latHi();
hi = b.latLo();
}
return S1Angle.radians(hi.radians() - lo.radians());
}
// The longitude intervals don't overlap. In this case, the closest points
// occur somewhere on the pair of longitudinal edges which are nearest in
// longitude-space.
S1Angle aLng, bLng;
S1Interval loHi = S1Interval.fromPointPair(a.lng().lo(), b.lng().hi());
S1Interval hiLo = S1Interval.fromPointPair(a.lng().hi(), b.lng().lo());
if (loHi.getLength() < hiLo.getLength()) {
aLng = a.lngLo();
bLng = b.lngHi();
} else {
aLng = a.lngHi();
bLng = b.lngLo();
}
// The shortest distance between the two longitudinal segments will include
// at least one segment endpoint. We could probably narrow this down further
// to a single point-edge distance by comparing the relative latitudes of the
// endpoints, but for the sake of clarity, we'll do all four point-edge
// distance tests.
S2Point aLo = new S2LatLng(a.latLo(), aLng).toPoint();
S2Point aHi = new S2LatLng(a.latHi(), aLng).toPoint();
S2Point aLoCrossHi =
S2LatLng.fromRadians(0, aLng.radians() - S2.M_PI_2).normalized().toPoint();
S2Point bLo = new S2LatLng(b.latLo(), bLng).toPoint();
S2Point bHi = new S2LatLng(b.latHi(), bLng).toPoint();
S2Point bLoCrossHi =
S2LatLng.fromRadians(0, bLng.radians() - S2.M_PI_2).normalized().toPoint();
return S1Angle.min(S2EdgeUtil.getDistance(aLo, bLo, bHi, bLoCrossHi),
S1Angle.min(S2EdgeUtil.getDistance(aHi, bLo, bHi, bLoCrossHi),
S1Angle.min(S2EdgeUtil.getDistance(bLo, aLo, aHi, aLoCrossHi),
S2EdgeUtil.getDistance(bHi, aLo, aHi, aLoCrossHi))));
}
/**
* Return the width and height of this rectangle in latitude-longitude space.
* Empty rectangles have a negative width and height.
*/
public S2LatLng getSize() {
return S2LatLng.fromRadians(lat.getLength(), lng.getLength());
}
/**
* More efficient version of Contains() that accepts a S2LatLng rather than an
* S2Point.
*/
public boolean contains(S2LatLng ll) {
// assert (ll.isValid());
return (lat.contains(ll.lat().radians()) && lng.contains(ll.lng()
.radians()));
}
/**
* Return true if and only if the given point is contained in the interior of
* the region (i.e. the region excluding its boundary). The point 'p' does not
* need to be normalized.
*/
public boolean interiorContains(S2Point p) {
return interiorContains(new S2LatLng(p));
}
/**
* More efficient version of InteriorContains() that accepts a S2LatLng rather
* than an S2Point.
*/
public boolean interiorContains(S2LatLng ll) {
// assert (ll.isValid());
return (lat.interiorContains(ll.lat().radians()) && lng
.interiorContains(ll.lng().radians()));
}
/**
* Return true if and only if the rectangle contains the given other
* rectangle.
*/
public boolean contains(S2LatLngRect other) {
return lat.contains(other.lat) && lng.contains(other.lng);
}
/**
* Return true if and only if the interior of this rectangle contains all
* points of the given other rectangle (including its boundary).
*/
public boolean interiorContains(S2LatLngRect other) {
return (lat.interiorContains(other.lat) && lng
.interiorContains(other.lng));
}
/** Return true if this rectangle and the given other rectangle have any
points in common. */
public boolean intersects(S2LatLngRect other) {
return lat.intersects(other.lat) && lng.intersects(other.lng);
}
/**
* Returns true if this rectangle intersects the given cell. (This is an exact
* test and may be fairly expensive, see also MayIntersect below.)
*/
public boolean intersects(S2Cell cell) {
// First we eliminate the cases where one region completely contains the
// other. Once these are disposed of, then the regions will intersect
// if and only if their boundaries intersect.
if (isEmpty()) {
return false;
}
if (contains(cell.getCenter())) {
return true;
}
if (cell.contains(getCenter().toPoint())) {
return true;
}
// Quick rejection test (not required for correctness).
if (!intersects(cell.getRectBound())) {
return false;
}
// Now check whether the boundaries intersect. Unfortunately, a
// latitude-longitude rectangle does not have straight edges -- two edges
// are curved, and at least one of them is concave.
// Precompute the cell vertices as points and latitude-longitudes.
S2Point[] cellV = new S2Point[4];
S2LatLng[] cellLl = new S2LatLng[4];
for (int i = 0; i < 4; ++i) {
cellV[i] = cell.getVertex(i); // Must be normalized.
cellLl[i] = new S2LatLng(cellV[i]);
if (contains(cellLl[i])) {
return true; // Quick acceptance test.
}
}
for (int i = 0; i < 4; ++i) {
S1Interval edgeLng = S1Interval.fromPointPair(
cellLl[i].lng().radians(), cellLl[(i + 1) & 3].lng().radians());
if (!lng.intersects(edgeLng)) {
continue;
}
final S2Point a = cellV[i];
final S2Point b = cellV[(i + 1) & 3];
if (edgeLng.contains(lng.lo())) {
if (intersectsLngEdge(a, b, lat, lng.lo())) {
return true;
}
}
if (edgeLng.contains(lng.hi())) {
if (intersectsLngEdge(a, b, lat, lng.hi())) {
return true;
}
}
if (intersectsLatEdge(a, b, lat.lo(), lng)) {
return true;
}
if (intersectsLatEdge(a, b, lat.hi(), lng)) {
return true;
}
}
return false;
}
/**
* Return true if and only if the interior of this rectangle intersects any
* point (including the boundary) of the given other rectangle.
*/
public boolean interiorIntersects(S2LatLngRect other) {
return (lat.interiorIntersects(other.lat) && lng
.interiorIntersects(other.lng));
}
public S2LatLngRect addPoint(S2Point p) {
return addPoint(new S2LatLng(p));
}
// Increase the size of the bounding rectangle to include the given point.
// The rectangle is expanded by the minimum amount possible.
public S2LatLngRect addPoint(S2LatLng ll) {
// assert (ll.isValid());
R1Interval newLat = lat.addPoint(ll.lat().radians());
S1Interval newLng = lng.addPoint(ll.lng().radians());
return new S2LatLngRect(newLat, newLng);
}
/**
* Return a rectangle that contains all points whose latitude distance from
* this rectangle is at most margin.lat(), and whose longitude distance from
* this rectangle is at most margin.lng(). In particular, latitudes are
* clamped while longitudes are wrapped. Note that any expansion of an empty
* interval remains empty, and both components of the given margin must be
* non-negative.
*
* NOTE: If you are trying to grow a rectangle by a certain *distance* on the
* sphere (e.g. 5km), use the ConvolveWithCap() method instead.
*/
public S2LatLngRect expanded(S2LatLng margin) {
// assert (margin.lat().radians() >= 0 && margin.lng().radians() >= 0);
if (isEmpty()) {
return this;
}
return new S2LatLngRect(lat.expanded(margin.lat().radians()).intersection(
fullLat()), lng.expanded(margin.lng().radians()));
}
/**
* Return the smallest rectangle containing the union of this rectangle and
* the given rectangle.
*/
public S2LatLngRect union(S2LatLngRect other) {
return new S2LatLngRect(lat.union(other.lat), lng.union(other.lng));
}
/**
* Return the smallest rectangle containing the intersection of this rectangle
* and the given rectangle. Note that the region of intersection may consist
* of two disjoint rectangles, in which case a single rectangle spanning both
* of them is returned.
*/
public S2LatLngRect intersection(S2LatLngRect other) {
R1Interval intersectLat = lat.intersection(other.lat);
S1Interval intersectLng = lng.intersection(other.lng);
if (intersectLat.isEmpty() || intersectLng.isEmpty()) {
// The lat/lng ranges must either be both empty or both non-empty.
return empty();
}
return new S2LatLngRect(intersectLat, intersectLng);
}
/**
* Return a rectangle that contains the convolution of this rectangle with a
* cap of the given angle. This expands the rectangle by a fixed distance (as
* opposed to growing the rectangle in latitude-longitude space). The returned
* rectangle includes all points whose minimum distance to the original
* rectangle is at most the given angle.
*/
public S2LatLngRect convolveWithCap(S1Angle angle) {
// The most straightforward approach is to build a cap centered on each
// vertex and take the union of all the bounding rectangles (including the
// original rectangle; this is necessary for very large rectangles).
// Optimization: convert the angle to a height exactly once.
S2Cap cap = S2Cap.fromAxisAngle(new S2Point(1, 0, 0), angle);
S2LatLngRect r = this;
for (int k = 0; k < 4; ++k) {
S2Cap vertexCap = S2Cap.fromAxisHeight(getVertex(k).toPoint(), cap
.height());
r = r.union(vertexCap.getRectBound());
}
return r;
}
/** Return the surface area of this rectangle on the unit sphere. */
public double area() {
if (isEmpty()) {
return 0;
}
// This is the size difference of the two spherical caps, multiplied by
// the longitude ratio.
return lng().getLength() * Math.abs(Math.sin(latHi().radians()) - Math.sin(latLo().radians()));
}
/** Return true if two rectangles contains the same set of points. */
@Override
public boolean equals(Object that) {
if (!(that instanceof S2LatLngRect)) {
return false;
}
S2LatLngRect otherRect = (S2LatLngRect) that;
return lat().equals(otherRect.lat()) && lng().equals(otherRect.lng());
}
/**
* Return true if the latitude and longitude intervals of the two rectangles
* are the same up to the given tolerance (see r1interval.h and s1interval.h
* for details).
*/
public boolean approxEquals(S2LatLngRect other, double maxError) {
return (lat.approxEquals(other.lat, maxError) && lng.approxEquals(
other.lng, maxError));
}
public boolean approxEquals(S2LatLngRect other) {
return approxEquals(other, 1e-15);
}
@Override
public int hashCode() {
int value = 17;
value = 37 * value + lat.hashCode();
return (37 * value + lng.hashCode());
}
// //////////////////////////////////////////////////////////////////////
// S2Region interface (see {@code S2Region} for details):
@Override
public S2Region clone() {
return new S2LatLngRect(this.lo(), this.hi());
}
@Override
public S2Cap getCapBound() {
// We consider two possible bounding caps, one whose axis passes
// through the center of the lat-long rectangle and one whose axis
// is the north or south pole. We return the smaller of the two caps.
if (isEmpty()) {
return S2Cap.empty();
}
double poleZ, poleAngle;
if (lat.lo() + lat.hi() < 0) {
// South pole axis yields smaller cap.
poleZ = -1;
poleAngle = S2.M_PI_2 + lat.hi();
} else {
poleZ = 1;
poleAngle = S2.M_PI_2 - lat.lo();
}
S2Cap poleCap = S2Cap.fromAxisAngle(new S2Point(0, 0, poleZ), S1Angle
.radians(poleAngle));
// For bounding rectangles that span 180 degrees or less in longitude, the
// maximum cap size is achieved at one of the rectangle vertices. For
// rectangles that are larger than 180 degrees, we punt and always return a
// bounding cap centered at one of the two poles.
double lngSpan = lng.hi() - lng.lo();
if (Math.IEEEremainder(lngSpan, 2 * S2.M_PI) >= 0) {
if (lngSpan < 2 * S2.M_PI) {
S2Cap midCap = S2Cap.fromAxisAngle(getCenter().toPoint(), S1Angle
.radians(0));
for (int k = 0; k < 4; ++k) {
midCap = midCap.addPoint(getVertex(k).toPoint());
}
if (midCap.height() < poleCap.height()) {
return midCap;
}
}
}
return poleCap;
}
@Override
public S2LatLngRect getRectBound() {
return this;
}
@Override
public boolean contains(S2Cell cell) {
// A latitude-longitude rectangle contains a cell if and only if it contains
// the cell's bounding rectangle. (This is an exact test.)
return contains(cell.getRectBound());
}
/**
* This test is cheap but is NOT exact. Use Intersects() if you want a more
* accurate and more expensive test. Note that when this method is used by an
* S2RegionCoverer, the accuracy isn't all that important since if a cell may
* intersect the region then it is subdivided, and the accuracy of this method
* goes up as the cells get smaller.
*/
@Override
public boolean mayIntersect(S2Cell cell) {
// This test is cheap but is NOT exact (see s2latlngrect.h).
return intersects(cell.getRectBound());
}
/** The point 'p' does not need to be normalized. */
public boolean contains(S2Point p) {
return contains(new S2LatLng(p));
}
/**
* Return true if the edge AB intersects the given edge of constant longitude.
*/
private static boolean intersectsLngEdge(S2Point a, S2Point b,
R1Interval lat, double lng) {
// Return true if the segment AB intersects the given edge of constant
// longitude. The nice thing about edges of constant longitude is that
// they are straight lines on the sphere (geodesics).
return S2.simpleCrossing(a, b, S2LatLng.fromRadians(lat.lo(), lng)
.toPoint(), S2LatLng.fromRadians(lat.hi(), lng).toPoint());
}
/**
* Return true if the edge AB intersects the given edge of constant latitude.
*/
private static boolean intersectsLatEdge(S2Point a, S2Point b, double lat,
S1Interval lng) {
// Return true if the segment AB intersects the given edge of constant
// latitude. Unfortunately, lines of constant latitude are curves on
// the sphere. They can intersect a straight edge in 0, 1, or 2 points.
// assert (S2.isUnitLength(a) && S2.isUnitLength(b));
// First, compute the normal to the plane AB that points vaguely north.
S2Point z = S2Point.normalize(S2.robustCrossProd(a, b));
if (z.z < 0) {
z = S2Point.neg(z);
}
// Extend this to an orthonormal frame (x,y,z) where x is the direction
// where the great circle through AB achieves its maximium latitude.
S2Point y = S2Point.normalize(S2.robustCrossProd(z, new S2Point(0, 0, 1)));
S2Point x = S2Point.crossProd(y, z);
// assert (S2.isUnitLength(x) && x.z >= 0);
// Compute the angle "theta" from the x-axis (in the x-y plane defined
// above) where the great circle intersects the given line of latitude.
double sinLat = Math.sin(lat);
if (Math.abs(sinLat) >= x.z) {
return false; // The great circle does not reach the given latitude.
}
// assert (x.z > 0);
double cosTheta = sinLat / x.z;
double sinTheta = Math.sqrt(1 - cosTheta * cosTheta);
double theta = Math.atan2(sinTheta, cosTheta);
// The candidate intersection points are located +/- theta in the x-y
// plane. For an intersection to be valid, we need to check that the
// intersection point is contained in the interior of the edge AB and
// also that it is contained within the given longitude interval "lng".
// Compute the range of theta values spanned by the edge AB.
S1Interval abTheta = S1Interval.fromPointPair(Math.atan2(
a.dotProd(y), a.dotProd(x)), Math.atan2(b.dotProd(y), b.dotProd(x)));
if (abTheta.contains(theta)) {
// Check if the intersection point is also in the given "lng" interval.
S2Point isect = S2Point.add(S2Point.mul(x, cosTheta), S2Point.mul(y,
sinTheta));
if (lng.contains(Math.atan2(isect.y, isect.x))) {
return true;
}
}
if (abTheta.contains(-theta)) {
// Check if the intersection point is also in the given "lng" interval.
S2Point intersection = S2Point.sub(S2Point.mul(x, cosTheta), S2Point.mul(y, sinTheta));
if (lng.contains(Math.atan2(intersection.y, intersection.x))) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "[Lo=" + lo() + ", Hi=" + hi() + "]";
}
}