...

Source file src/github.com/golang/geo/s2/textformat_test.go

Documentation: github.com/golang/geo/s2

     1  // Copyright 2017 Google Inc. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package s2
    16  
    17  // s2textformat_test contains a collection of functions for converting geometry
    18  // to and from a human-readable format. It is intended for testing and debugging.
    19  // Be aware that the human-readable format is *NOT* designed to preserve the full
    20  // precision of the original object, so it should not be used for data storage.
    21  //
    22  // Most functions here use the same format for inputs, a comma separated set of
    23  // latitude-longitude coordinates in degrees. Functions that expect a different
    24  // input document the values in the function comment.
    25  //
    26  // Examples of the input format:
    27  //     ""                                 // no points
    28  //     "-20:150"                          // one point
    29  //     "-20:150, 10:-120, 0.123:-170.652" // three points
    30  
    31  import (
    32  	"bytes"
    33  	"fmt"
    34  	"io"
    35  	"strconv"
    36  	"strings"
    37  
    38  	"github.com/golang/geo/r3"
    39  )
    40  
    41  // writePoint formats the point and writes it to the given writer.
    42  func writePoint(w io.Writer, p Point) {
    43  	ll := LatLngFromPoint(p)
    44  	fmt.Fprintf(w, "%.15g:%.15g", ll.Lat.Degrees(), ll.Lng.Degrees())
    45  }
    46  
    47  // writePoints formats the given points in debug format and writes them to the given writer.
    48  func writePoints(w io.Writer, pts []Point) {
    49  	for i, pt := range pts {
    50  		if i > 0 {
    51  			fmt.Fprint(w, ", ")
    52  		}
    53  		writePoint(w, pt)
    54  	}
    55  }
    56  
    57  // parsePoint returns a Point from the given string, or the origin if the
    58  // string was invalid. If more than one value is given, only the first is used.
    59  func parsePoint(s string) Point {
    60  	p := parsePoints(s)
    61  	if len(p) > 0 {
    62  		return p[0]
    63  	}
    64  
    65  	return Point{r3.Vector{0, 0, 0}}
    66  }
    67  
    68  // pointToString returns a string representation suitable for reconstruction
    69  // by the parsePoint method.
    70  func pointToString(point Point) string {
    71  	var buf bytes.Buffer
    72  	writePoint(&buf, point)
    73  	return buf.String()
    74  }
    75  
    76  // parsePoints returns the values in the input string as Points.
    77  func parsePoints(s string) []Point {
    78  	lls := parseLatLngs(s)
    79  	if len(lls) == 0 {
    80  		return nil
    81  	}
    82  	points := make([]Point, len(lls))
    83  	for i, ll := range lls {
    84  		points[i] = PointFromLatLng(ll)
    85  	}
    86  	return points
    87  }
    88  
    89  // pointsToString returns a string representation suitable for reconstruction
    90  // by the parsePoints method.
    91  func pointsToString(points []Point) string {
    92  	var buf bytes.Buffer
    93  	writePoints(&buf, points)
    94  	return buf.String()
    95  }
    96  
    97  // parseLatLngs returns the values in the input string as LatLngs.
    98  func parseLatLngs(s string) []LatLng {
    99  	var lls []LatLng
   100  	if s == "" {
   101  		return lls
   102  	}
   103  
   104  	for _, piece := range strings.Split(s, ",") {
   105  		piece = strings.TrimSpace(piece)
   106  
   107  		// Skip empty strings.
   108  		if piece == "" {
   109  			continue
   110  		}
   111  
   112  		p := strings.Split(piece, ":")
   113  		if len(p) != 2 {
   114  			continue
   115  		}
   116  
   117  		lat, err := strconv.ParseFloat(p[0], 64)
   118  		if err != nil {
   119  			panic(fmt.Sprintf("invalid float in parseLatLngs: %q, err: %v", p[0], err))
   120  		}
   121  
   122  		lng, err := strconv.ParseFloat(p[1], 64)
   123  		if err != nil {
   124  			panic(fmt.Sprintf("invalid float in parseLatLngs: %q, err: %v", p[1], err))
   125  		}
   126  
   127  		lls = append(lls, LatLngFromDegrees(lat, lng))
   128  	}
   129  	return lls
   130  }
   131  
   132  // makeRect returns the minimal bounding Rect that contains the values in the input string.
   133  func makeRect(s string) Rect {
   134  	var rect Rect
   135  	lls := parseLatLngs(s)
   136  	if len(lls) > 0 {
   137  		rect = RectFromLatLng(lls[0])
   138  	}
   139  
   140  	for _, ll := range lls[1:] {
   141  		rect = rect.AddPoint(ll)
   142  	}
   143  
   144  	return rect
   145  }
   146  
   147  // makeCellUnion returns a CellUnion from the given CellID token strings.
   148  func makeCellUnion(tokens ...string) CellUnion {
   149  	var cu CellUnion
   150  
   151  	for _, t := range tokens {
   152  		cu = append(cu, CellIDFromString(t))
   153  	}
   154  	return cu
   155  }
   156  
   157  // makeLoop constructs a Loop from the input string.
   158  // The strings "empty" or "full" create an empty or full loop respectively.
   159  func makeLoop(s string) *Loop {
   160  	if s == "full" {
   161  		return FullLoop()
   162  	}
   163  	if s == "empty" {
   164  		return EmptyLoop()
   165  	}
   166  
   167  	return LoopFromPoints(parsePoints(s))
   168  }
   169  
   170  // makePolygon constructs a polygon from the sequence of loops in the input
   171  // string. Loops are automatically normalized by inverting them if necessary
   172  // so that they enclose at most half of the unit sphere. (Historically this was
   173  // once a requirement of polygon loops. It also hides the problem that if the
   174  // user thinks of the coordinates as X:Y rather than LAT:LNG, it yields a loop
   175  // with the opposite orientation.)
   176  //
   177  // Loops are semicolon separated in the input string with each loop using the
   178  // same format as above.
   179  //
   180  // Examples of the input format:
   181  //
   182  //	"10:20, 90:0, 20:30"                                  // one loop
   183  //	"10:20, 90:0, 20:30; 5.5:6.5, -90:-180, -15.2:20.3"   // two loops
   184  //	""       // the empty polygon (consisting of no loops)
   185  //	"empty"  // the empty polygon (consisting of no loops)
   186  //	"full"   // the full polygon (consisting of one full loop)
   187  func makePolygon(s string, normalize bool) *Polygon {
   188  	var loops []*Loop
   189  	// Avoid the case where strings.Split on empty string will still return
   190  	// one empty value, where we want no values.
   191  	if s == "empty" || s == "" {
   192  		return PolygonFromLoops(loops)
   193  	}
   194  
   195  	for _, str := range strings.Split(s, ";") {
   196  		// The polygon test strings mostly have a trailing semicolon
   197  		// (to make concatenating them for tests easy). The C++
   198  		// SplitString doesn't return empty elements where as Go does,
   199  		// so we need to check before using it.
   200  		if str == "" {
   201  			continue
   202  		}
   203  		loop := makeLoop(strings.TrimSpace(str))
   204  		if normalize && !loop.IsFull() {
   205  			loop.Normalize()
   206  		}
   207  		loops = append(loops, loop)
   208  	}
   209  	return PolygonFromLoops(loops)
   210  }
   211  
   212  // makePolyline constructs a Polyline from the given string.
   213  func makePolyline(s string) *Polyline {
   214  	p := Polyline(parsePoints(s))
   215  	return &p
   216  }
   217  
   218  // makeLaxPolyline constructs a LaxPolyline from the given string.
   219  func makeLaxPolyline(s string) *LaxPolyline {
   220  	return LaxPolylineFromPoints(parsePoints(s))
   221  }
   222  
   223  // laxPolylineToString returns a string representation suitable for reconstruction
   224  // by the makeLaxPolyline method.
   225  func laxPolylineToString(l *LaxPolyline) string {
   226  	var buf bytes.Buffer
   227  	writePoints(&buf, l.vertices)
   228  	return buf.String()
   229  
   230  }
   231  
   232  // makeLaxPolygon creates a LaxPolygon from the given debug formatted string.
   233  // Similar to makePolygon, except that loops must be oriented so that the
   234  // interior of the loop is always on the left, and polygons with degeneracies
   235  // are supported. As with makePolygon, "full" denotes the full polygon and "empty"
   236  // is not allowed (instead, simply create a LaxPolygon with no loops).
   237  func makeLaxPolygon(s string) *LaxPolygon {
   238  	var points [][]Point
   239  	if s == "" {
   240  		return LaxPolygonFromPoints(points)
   241  	}
   242  	for _, l := range strings.Split(s, ";") {
   243  		if l == "full" {
   244  			points = append(points, []Point{})
   245  		} else if l != "empty" {
   246  			points = append(points, parsePoints(l))
   247  		}
   248  	}
   249  	return LaxPolygonFromPoints(points)
   250  }
   251  
   252  // makeShapeIndex builds a ShapeIndex from the given debug string containing
   253  // the points, polylines, and loops (in the form of a single polygon)
   254  // described by the following format:
   255  //
   256  //	point1|point2|... # line1|line2|... # polygon1|polygon2|...
   257  //
   258  // Examples:
   259  //
   260  //	1:2 | 2:3 # #                     // Two points
   261  //	# 0:0, 1:1, 2:2 | 3:3, 4:4 #      // Two polylines
   262  //	# # 0:0, 0:3, 3:0; 1:1, 2:1, 1:2  // Two nested loops (one polygon)
   263  //	5:5 # 6:6, 7:7 # 0:0, 0:1, 1:0    // One of each
   264  //	# # empty                         // One empty polygon
   265  //	# # empty | full                  // One empty polygon, one full polygon
   266  //
   267  // Loops should be directed so that the region's interior is on the left.
   268  // Loops can be degenerate (they do not need to meet Loop requirements).
   269  //
   270  // Note: Because whitespace is ignored, empty polygons must be specified
   271  // as the string "empty" rather than as the empty string ("").
   272  func makeShapeIndex(s string) *ShapeIndex {
   273  	fields := strings.Split(s, "#")
   274  	if len(fields) != 3 {
   275  		panic("shapeIndex debug string must contain 2 '#' characters")
   276  	}
   277  
   278  	index := NewShapeIndex()
   279  
   280  	var points []Point
   281  	for _, p := range strings.Split(fields[0], "|") {
   282  		p = strings.TrimSpace(p)
   283  		if p == "" {
   284  			continue
   285  		}
   286  		points = append(points, parsePoint(p))
   287  	}
   288  	if len(points) > 0 {
   289  		p := PointVector(points)
   290  		index.Add(&p)
   291  	}
   292  
   293  	for _, p := range strings.Split(fields[1], "|") {
   294  		p = strings.TrimSpace(p)
   295  		if p == "" {
   296  			continue
   297  		}
   298  		if polyline := makeLaxPolyline(p); polyline != nil {
   299  			index.Add(polyline)
   300  		}
   301  	}
   302  
   303  	for _, p := range strings.Split(fields[2], "|") {
   304  		p = strings.TrimSpace(p)
   305  		if p == "" {
   306  			continue
   307  		}
   308  		if polygon := makeLaxPolygon(p); polygon != nil {
   309  			index.Add(polygon)
   310  		}
   311  	}
   312  
   313  	return index
   314  }
   315  
   316  // shapeIndexDebugString outputs the contents of this ShapeIndex in debug
   317  // format. The index may contain Shapes of any type. Shapes are reordered
   318  // if necessary so that all point geometry (shapes of dimension 0) are first,
   319  // followed by all polyline geometry, followed by all polygon geometry.
   320  func shapeIndexDebugString(index *ShapeIndex) string {
   321  	var buf bytes.Buffer
   322  
   323  	for dim := 0; dim <= 2; dim++ {
   324  		if dim > 0 {
   325  			buf.WriteByte('#')
   326  		}
   327  
   328  		var count int
   329  
   330  		// Use shapes ordered by id rather than ranging over the
   331  		// index.shapes map to ensure that the ordering of shapes in the
   332  		// generated string matches the C++ generated strings.
   333  		for i := int32(0); i < index.nextID; i++ {
   334  			shape := index.Shape(i)
   335  			// Only use shapes that are still in the index and at the
   336  			// current geometry level we are outputting.
   337  			if shape == nil || shape.Dimension() != dim {
   338  				continue
   339  			}
   340  			if count > 0 {
   341  				buf.WriteString(" | ")
   342  			} else {
   343  				if dim > 0 {
   344  					buf.WriteByte(' ')
   345  				}
   346  			}
   347  
   348  			for c := 0; c < shape.NumChains(); c++ {
   349  				if c > 0 {
   350  					if dim == 2 {
   351  						buf.WriteString("; ")
   352  					} else {
   353  						buf.WriteString(" | ")
   354  					}
   355  				}
   356  				chain := shape.Chain(c)
   357  				pts := []Point{shape.Edge(chain.Start).V0}
   358  				limit := chain.Start + chain.Length
   359  				if dim != 1 {
   360  					limit--
   361  				}
   362  
   363  				for e := chain.Start; e < limit; e++ {
   364  					pts = append(pts, shape.Edge(e).V1)
   365  				}
   366  				writePoints(&buf, pts)
   367  				count++
   368  			}
   369  		}
   370  
   371  		if dim == 1 || (dim == 0 && count > 0) {
   372  			buf.WriteByte(' ')
   373  		}
   374  	}
   375  
   376  	return buf.String()
   377  }
   378  
   379  // TODO(roberts): Remaining C++ textformat related methods
   380  // make$S2TYPE methods for missing types.
   381  // to debug string for many types
   382  

View as plain text